diff --git a/picoclaw-launcher b/picoclaw-launcher index bfb9ea89d..dc244cc99 100755 Binary files a/picoclaw-launcher and b/picoclaw-launcher differ diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index db9efa2cf..c04d3a4c5 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -571,7 +571,7 @@ func (al *AgentLoop) processSystemMessage( UserMessage: fmt.Sprintf("[System: %s] %s", msg.SenderID, msg.Content), DefaultResponse: "Background task completed.", EnableSummary: false, - SendResponse: true, + SendResponse: false, // Prevent duplicate responses caused by system message processing }) } @@ -1157,7 +1157,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) { newHistory = append(newHistory, history[len(history)-1]) // Last message // Update session - agent.Sessions.SetHistory(sessionKey, newHistory) + agent.Sessions.SetHistory(sessionKey, sanitizeToolPairs(newHistory)) agent.Sessions.Save(sessionKey) logger.WarnCF("agent", "Forced compression executed", map[string]any{ @@ -1332,6 +1332,9 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) { if finalSummary != "" { agent.Sessions.SetSummary(sessionKey, finalSummary) agent.Sessions.TruncateHistory(sessionKey, 4) + history := agent.Sessions.GetHistory(sessionKey) + cleanHistory := sanitizeToolPairs(history) + agent.Sessions.SetHistory(sessionKey, cleanHistory) agent.Sessions.Save(sessionKey) } } @@ -1502,3 +1505,40 @@ func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer { } return &routing.RoutePeer{Kind: parentKind, ID: parentID} } + +// sanitizeToolPairs removes malformed tool_call/tool_result pairs from message histories. It ensures +// that all tool_use IDs referenced in tool results have corresponding tool_use blocks in +// assistant messages, and removes orphaned tool_result messages that don't pair with a +// prior tool_call in the same session history. +func sanitizeToolPairs(messages []providers.Message) []providers.Message { + // Build map of expected tool call IDs + expectedToolCallIDs := make(map[string]bool) + var sanitized []providers.Message + + for _, msg := range messages { + if msg.Role == "assistant" && len(msg.ToolCalls) > 0 { + // Record all tool call IDs in assistant messages + for _, call := range msg.ToolCalls { + expectedToolCallIDs[call.ID] = true + } + sanitized = append(sanitized, msg) + } else if msg.Role == "tool" { + // Verify this tool result has a corresponding tool call + if expectedToolCallIDs[msg.ToolCallID] { + // Valid pairing: tool result has corresponding tool call + sanitized = append(sanitized, msg) + // Remove the ID after use to avoid reuse of stale ID + delete(expectedToolCallIDs, msg.ToolCallID) + } else { + // Invalid: orphaned tool result without matching tool call + // Skip this message rather than including it + continue + } + } else { + // Regular messages (user/assistant without tools/other) - always include + sanitized = append(sanitized, msg) + } + } + + return sanitized +} diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index f1cbc83a7..a3c316f26 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -20,6 +20,7 @@ type SubagentTask struct { Status string Result string Created int64 + announcedOnce sync.Once // Ensures the completion announcement is sent only once } type SubagentManager struct {