diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4691ff0fe..83ccc23fd61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai - Config/backups hardening: enforce owner-only (`0600`) permissions on rotated config backups and clean orphan `.bak.*` files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002. - WhatsApp/inbound self-message context: propagate inbound `fromMe` through the web inbox pipeline and annotate direct self messages as `(self)` in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob. - Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like `/usr/bin/g++` and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky. +- Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode. - Webchat/stream finalization: persist streamed assistant text when final events omit `message`, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin. - Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3. - Gateway/Heartbeat model reload: treat `models.*` and `agents.defaults.model` config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky. diff --git a/src/agents/session-tool-result-guard.test.ts b/src/agents/session-tool-result-guard.test.ts index 1e5b772c7d7..0499ca79a48 100644 --- a/src/agents/session-tool-result-guard.test.ts +++ b/src/agents/session-tool-result-guard.test.ts @@ -85,6 +85,25 @@ describe("installSessionToolResultGuard", () => { expectPersistedRoles(sm, ["assistant", "toolResult"]); }); + it("clears pending on user interruption when synthetic tool results are disabled", () => { + const sm = SessionManager.inMemory(); + const guard = installSessionToolResultGuard(sm, { + allowSyntheticToolResults: false, + }); + + sm.appendMessage(toolCallMessage); + sm.appendMessage( + asAppendMessage({ + role: "user", + content: "interrupt", + timestamp: Date.now(), + }), + ); + + expectPersistedRoles(sm, ["assistant", "user"]); + expect(guard.getPendingIds()).toEqual([]); + }); + it("does not add synthetic toolResult when a matching one exists", () => { const sm = SessionManager.inMemory(); installSessionToolResultGuard(sm); @@ -271,6 +290,54 @@ describe("installSessionToolResultGuard", () => { expectPersistedRoles(sm, ["assistant", "toolResult"]); }); + it("clears pending when a sanitized assistant message is dropped and synthetic results are disabled", () => { + const sm = SessionManager.inMemory(); + const guard = installSessionToolResultGuard(sm, { + allowSyntheticToolResults: false, + allowedToolNames: ["read"], + }); + + sm.appendMessage( + asAppendMessage({ + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], + }), + ); + + sm.appendMessage( + asAppendMessage({ + role: "assistant", + content: [{ type: "toolCall", id: "call_2", name: "write", arguments: {} }], + }), + ); + + expectPersistedRoles(sm, ["assistant"]); + expect(guard.getPendingIds()).toEqual([]); + }); + + it("drops older pending ids before new tool calls when synthetic results are disabled", () => { + const sm = SessionManager.inMemory(); + const guard = installSessionToolResultGuard(sm, { + allowSyntheticToolResults: false, + }); + + sm.appendMessage( + asAppendMessage({ + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], + }), + ); + sm.appendMessage( + asAppendMessage({ + role: "assistant", + content: [{ type: "toolCall", id: "call_2", name: "read", arguments: {} }], + }), + ); + + expectPersistedRoles(sm, ["assistant", "assistant"]); + expect(guard.getPendingIds()).toEqual(["call_2"]); + }); + it("caps oversized tool result text during persistence", () => { const sm = SessionManager.inMemory(); installSessionToolResultGuard(sm);