From f8141da4a62ffe3e351272fc9e6c574a64dda308 Mon Sep 17 00:00:00 2001 From: vinhnguyenthanhdn <76728419+vinhnguyenthanhdn@users.noreply.github.com> Date: Sun, 3 May 2026 22:06:28 +0700 Subject: [PATCH] test(session): prevent regression where session repair trims final assistant response after tool usage (#76538) Summary: - Adds a Vitest regression case in `src/agents/session-file-repair.test.ts` for `repairSessionFileIfNeeded` preserving a final stop assistant message after a tool-call/tool-result pair. - Reproducibility: yes. The added JSONL sequence can be traced through current `repairSessionFileIfNeeded`: no ... ry, so the function returns `repaired: false`; I did not execute the test because this review is read-only. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for head ee3aa0e036f476270169b2fb6b19d4d7c0d1be1b. - Required merge gates passed before the squash merge. Prepared head SHA: ee3aa0e036f476270169b2fb6b19d4d7c0d1be1b Review: https://github.com/openclaw/openclaw/pull/76538#issuecomment-4365670008 Co-authored-by: Vinh Nguyen Co-authored-by: Claude Sonnet 4.6 --- src/agents/session-file-repair.test.ts | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/agents/session-file-repair.test.ts b/src/agents/session-file-repair.test.ts index f0151e96843..1efb6fc6c41 100644 --- a/src/agents/session-file-repair.test.ts +++ b/src/agents/session-file-repair.test.ts @@ -465,6 +465,60 @@ describe("repairSessionFileIfNeeded", () => { expect(after).toBe(original); }); + it("preserves final text assistant turn that follows a tool-call/tool-result pair", async () => { + // Regression: a trailing assistant message with stopReason "stop" that follows a + // tool-call turn and its matching tool-result must never be trimmed by the repair + // pass. This is the exact sequence produced by any agent run that calls at least + // one tool before returning a final text response, and it must survive intact so + // subsequent user messages are parented to the correct leaf node. + const { file } = await createTempSessionPath(); + const { header, message } = buildSessionHeaderAndMessage(); + const toolCallAssistant = { + type: "message", + id: "msg-asst-tc", + parentId: "msg-1", + timestamp: new Date().toISOString(), + message: { + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "get_tasks", input: {} }], + stopReason: "toolUse", + }, + }; + const toolResult = { + type: "message", + id: "msg-tool-result", + parentId: "msg-asst-tc", + timestamp: new Date().toISOString(), + message: { + role: "toolResult", + toolCallId: "call_1", + toolName: "get_tasks", + content: [{ type: "text", text: "Task A, Task B" }], + isError: false, + }, + }; + const finalAssistant = { + type: "message", + id: "msg-asst-final", + parentId: "msg-tool-result", + timestamp: new Date().toISOString(), + message: { + role: "assistant", + content: [{ type: "text", text: "Here are your tasks: Task A, Task B." }], + stopReason: "stop", + }, + }; + const original = `${JSON.stringify(header)}\n${JSON.stringify(message)}\n${JSON.stringify(toolCallAssistant)}\n${JSON.stringify(toolResult)}\n${JSON.stringify(finalAssistant)}\n`; + await fs.writeFile(file, original, "utf-8"); + + const result = await repairSessionFileIfNeeded({ sessionFile: file }); + + expect(result.repaired).toBe(false); + + const after = await fs.readFile(file, "utf-8"); + expect(after).toBe(original); + }); + it("preserves assistant-only session history after the header", async () => { const { file } = await createTempSessionPath(); const { header } = buildSessionHeaderAndMessage();