From 1c35ec6cd73ff84e4b65a1435c42438c479041eb Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 25 May 2026 18:23:34 +0100 Subject: [PATCH] fix: preserve chat send user turns on started failures --- src/agents/cli-runner.reliability.test.ts | 12 ++++---- .../chat.directive-tags.test.ts | 28 +++++++++++++++++++ src/gateway/server-methods/chat.ts | 8 ++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index db33e5eac95f..2cc29ac88990 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -10,6 +10,7 @@ import { } from "../auto-reply/reply/reply-run-registry.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; +import { buildPersistedUserTurnMessage } from "../sessions/user-turn-transcript.js"; import { runPreparedCliAgent } from "./cli-runner.js"; import { createManagedRun, @@ -900,12 +901,11 @@ describe("runCliAgent reliability", () => { workspaceDir: dir, prompt: "runtime image prompt", userTurnTranscript: { - message: { - role: "user", - content: "describe this", - MediaPath: "/tmp/image.png", - MediaType: "image/png", - }, + message: buildPersistedUserTurnMessage({ + text: "describe this", + media: [{ path: "/tmp/image.png", contentType: "image/png" }], + timestamp: 123, + }), }, onUserMessagePersisted, }, diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index bc5e184e0e42..60168908fccc 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -4422,6 +4422,34 @@ describe("chat directive tag stripping for non-streaming final payloads", () => expect(persistedUser?.content).toBe("hello before cli startup failure"); }); }); + + it("emits a user transcript update when a started agent returns an error before runtime persistence", async () => { + createTranscriptFixture("openclaw-chat-send-user-transcript-agent-error-no-runtime-persist-"); + mockState.triggerAgentRunStart = true; + mockState.finalPayload = { text: "agent failed before prompt append", isError: true }; + const respond = vi.fn(); + const context = createChatContext(); + + await runNonStreamingChatSend({ + context, + respond, + idempotencyKey: "idem-user-transcript-agent-error-no-runtime-persist", + message: "hello before agent error payload", + expectBroadcast: false, + }); + + await waitForAssertion(() => { + expect( + context.dedupe.get("chat:idem-user-transcript-agent-error-no-runtime-persist")?.ok, + ).toBe(false); + const userUpdate = findUserUpdate(); + const message = userUpdateMessage(userUpdate); + expect(userUpdate?.sessionFile.endsWith("sess.jsonl")).toBe(true); + expect(userUpdate?.sessionKey).toBe("main"); + expect(message?.role).toBe("user"); + expect(message?.content).toBe("hello before agent error payload"); + }); + }); }); describe("chat.send operator UI client sender context", () => { diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 9d1566feff44..e5de986bbb0a 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -2910,6 +2910,14 @@ export const chatHandlers: GatewayRequestHandlers = { .map((payload) => payload.text?.trim()) .filter((text): text is string => Boolean(text)) .join(" | ") || undefined; + if ( + agentRunStarted && + returnedAgentErrorPayloads.length > 0 && + !agentUserMessagePersisted && + !beforeAgentRunHooksRegistered + ) { + await persistGatewayUserTurnTranscript(); + } let broadcastedSourceReplyFinal = false; // WebChat persistence has two owners. Agent runs persist model-visible turns // through Pi's SessionManager; this dispatcher only owns live delivery payloads.