mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 23:51:48 +00:00
fix: prevent synthetic toolResult for aborted/errored assistant messages
When an assistant message with toolCalls has stopReason 'aborted' or 'error', the guard should not add those tool call IDs to the pending map. Creating synthetic tool results for incomplete/aborted tool calls causes API 400 errors: 'unexpected tool_use_id found in tool_result blocks' This aligns the WRITE path (session-tool-result-guard.ts) with the READ path (session-transcript-repair.ts) which already skips aborted messages. Fixes: orphaned tool_result causing session corruption Tests added: - does NOT create synthetic toolResult for aborted assistant messages - does NOT create synthetic toolResult for errored assistant messages
This commit is contained in:
committed by
Peter Steinberger
parent
31b1b20b3c
commit
8db7ca8c02
@@ -357,4 +357,63 @@ describe("installSessionToolResultGuard", () => {
|
||||
sourceTool: "sessions_send",
|
||||
});
|
||||
});
|
||||
|
||||
// Regression test for orphaned tool_result bug
|
||||
// See: https://github.com/clawdbot/clawdbot/issues/XXXX
|
||||
// When an assistant message with toolCalls is aborted, no synthetic toolResult
|
||||
// should be created. Creating synthetic results for aborted/incomplete tool calls
|
||||
// causes API 400 errors: "unexpected tool_use_id found in tool_result blocks"
|
||||
it("does NOT create synthetic toolResult for aborted assistant messages with toolCalls", () => {
|
||||
const sm = SessionManager.inMemory();
|
||||
installSessionToolResultGuard(sm);
|
||||
|
||||
// Aborted assistant message with incomplete toolCall
|
||||
sm.appendMessage(
|
||||
asAppendMessage({
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call_aborted", name: "read", arguments: {} }],
|
||||
stopReason: "aborted",
|
||||
}),
|
||||
);
|
||||
|
||||
// Next message triggers flush of pending tool calls
|
||||
sm.appendMessage(
|
||||
asAppendMessage({
|
||||
role: "user",
|
||||
content: "are you stuck?",
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Should only have assistant + user, NO synthetic toolResult
|
||||
const messages = getPersistedMessages(sm);
|
||||
const roles = messages.map((m) => m.role);
|
||||
expect(roles).toEqual(["assistant", "user"]);
|
||||
expect(roles).not.toContain("toolResult");
|
||||
});
|
||||
|
||||
it("does NOT create synthetic toolResult for errored assistant messages with toolCalls", () => {
|
||||
const sm = SessionManager.inMemory();
|
||||
const guard = installSessionToolResultGuard(sm);
|
||||
|
||||
// Error assistant message with incomplete toolCall
|
||||
sm.appendMessage(
|
||||
asAppendMessage({
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call_error", name: "exec", arguments: {} }],
|
||||
stopReason: "error",
|
||||
}),
|
||||
);
|
||||
|
||||
// Explicit flush should NOT create synthetic result for errored messages
|
||||
guard.flushPendingToolResults();
|
||||
|
||||
const messages = getPersistedMessages(sm);
|
||||
const toolResults = messages.filter((m) => m.role === "toolResult");
|
||||
// No synthetic toolResults should exist for the errored call
|
||||
const syntheticForError = toolResults.filter(
|
||||
(m) => (m as { toolCallId?: string }).toolCallId === "call_error",
|
||||
);
|
||||
expect(syntheticForError).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,8 +166,15 @@ export function installSessionToolResultGuard(
|
||||
return originalAppend(persisted as never);
|
||||
}
|
||||
|
||||
// Skip tool call extraction for aborted/errored assistant messages.
|
||||
// When stopReason is "error" or "aborted", the tool_use blocks may be incomplete
|
||||
// and should not have synthetic tool_results created. Creating synthetic results
|
||||
// for incomplete tool calls causes API 400 errors:
|
||||
// "unexpected tool_use_id found in tool_result blocks"
|
||||
// This matches the behavior in repairToolUseResultPairing (session-transcript-repair.ts)
|
||||
const stopReason = (nextMessage as { stopReason?: string }).stopReason;
|
||||
const toolCalls =
|
||||
nextRole === "assistant"
|
||||
nextRole === "assistant" && stopReason !== "aborted" && stopReason !== "error"
|
||||
? extractToolCallsFromAssistant(nextMessage as Extract<AgentMessage, { role: "assistant" }>)
|
||||
: [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user