From 7ac8b48a08bfb873cbc556144c2e11e84361bf6e Mon Sep 17 00:00:00 2001 From: WhatsSkiLL Date: Sun, 28 Jun 2026 21:22:58 +0200 Subject: [PATCH] fix: flush embedded transcript before afterTurn (#97342) Fixes #97301 Co-authored-by: OpenClaw PR Agent --- ...mpt.spawn-workspace.context-engine.test.ts | 30 +++++++++++++++++++ .../attempt.spawn-workspace.test-support.ts | 3 ++ .../embedded-agent-runner/run/attempt.ts | 6 ++++ 3 files changed, 39 insertions(+) diff --git a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts index 3c2131477338..0b766a1d8d4b 100644 --- a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -2285,6 +2285,36 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { expect(runLlmInput).not.toHaveBeenCalled(); }); + it("flushes the embedded session transcript before afterTurn", async () => { + const events: string[] = []; + const afterTurn = vi.fn(async () => { + events.push("afterTurn"); + }); + hoisted.sessionManager.rewriteFile.mockImplementation(() => { + events.push("flush"); + }); + + await createContextEngineAttemptRunner({ + contextEngine: createTestContextEngine({ afterTurn }), + sessionKey, + tempPaths, + attemptOverrides: { + currentInboundEventKind: "room_event", + currentInboundContext: { text: "[OpenClaw room event]" }, + suppressNextUserMessagePersistence: true, + transcriptPrompt: "", + }, + sessionPrompt: async (session) => { + session.messages = [...session.messages, doneMessage]; + }, + }); + + const afterTurnIndex = events.indexOf("afterTurn"); + expect(afterTurn).toHaveBeenCalledTimes(1); + expect(afterTurnIndex).not.toBe(-1); + expect(events.slice(0, afterTurnIndex)).toContain("flush"); + }); + it("forwards sessionKey to bootstrap, assemble, and afterTurn", async () => { const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble(); const afterTurn = vi.fn(async (_params: { sessionKey?: string }) => {}); diff --git a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts index e4bf2661c603..dd5994d1c471 100644 --- a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts @@ -57,6 +57,7 @@ type SessionManagerMocks = { resetLeaf: UnknownMock; buildSessionContext: Mock<() => { messages: AgentMessage[] }>; appendCustomEntry: UnknownMock; + rewriteFile: UnknownMock; flushPendingToolResults: UnknownMock; clearPendingToolResults: UnknownMock; removeTrailingEntries: UnknownMock; @@ -207,6 +208,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { resetLeaf: vi.fn(), buildSessionContext: vi.fn<() => { messages: AgentMessage[] }>(() => ({ messages: [] })), appendCustomEntry: vi.fn(), + rewriteFile: vi.fn(), flushPendingToolResults: vi.fn(), clearPendingToolResults: vi.fn(), removeTrailingEntries: vi.fn(() => 0), @@ -1033,6 +1035,7 @@ export function resetEmbeddedAttemptHarness( .mockReset() .mockReturnValue({ messages: params.sessionMessages ?? [] }); hoisted.sessionManager.appendCustomEntry.mockReset(); + hoisted.sessionManager.rewriteFile.mockReset(); if (params.subscribeImpl) { hoisted.subscribeEmbeddedAgentSessionMock.mockImplementation(params.subscribeImpl); } diff --git a/src/agents/embedded-agent-runner/run/attempt.ts b/src/agents/embedded-agent-runner/run/attempt.ts index 9dd0ec3e2368..77463153f243 100644 --- a/src/agents/embedded-agent-runner/run/attempt.ts +++ b/src/agents/embedded-agent-runner/run/attempt.ts @@ -5110,6 +5110,12 @@ export async function runEmbeddedAttempt( log.warn(`failed to persist prompt error entry: ${String(entryErr)}`); } } + + if (activeContextEngine && !beforeAgentFinalizeRevisionReason) { + // Context-engine afterTurn hooks may reconcile against the jsonl, so + // materialize the active turn before finalization reads from disk. + flushSessionManagerFile(activeSessionManager); + } }); // Let the active context engine run its post-turn lifecycle. These hooks