From 6dc8bd8935727eb1ba89b8f3e7e9cbc6a5a966bb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 20:45:12 +0100 Subject: [PATCH] fix(gateway): read active transcript history branch --- src/gateway/session-utils.fs.test.ts | 68 ++++++++++++++++++++++++++++ src/gateway/session-utils.fs.ts | 55 ++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index 4e395957b8a..f15e5dbf2a3 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -501,6 +501,74 @@ describe("readSessionMessages", () => { expect(typeof marker.timestamp).toBe("number"); }); + test("reads only the active branch when transcript rewrites abandon older entries", () => { + const sessionId = "test-session-active-branch"; + const sessionFile = path.join(tmpDir, `${sessionId}.jsonl`); + const lines = [ + { + type: "session", + version: 3, + id: sessionId, + cwd: tmpDir, + timestamp: "2026-04-27T00:00:00.000Z", + }, + { + type: "message", + id: "original", + parentId: null, + timestamp: "2026-04-27T00:00:01.000Z", + message: { + role: "user", + content: "Sender (untrusted metadata): webchat\n\noriginal wrapped prompt", + timestamp: 1, + }, + }, + { + type: "message", + id: "clean", + parentId: null, + timestamp: "2026-04-27T00:00:02.000Z", + message: { role: "user", content: "clean prompt", timestamp: 2 }, + }, + { + type: "message", + id: "answer", + parentId: "clean", + timestamp: "2026-04-27T00:00:03.000Z", + message: { + role: "assistant", + content: [{ type: "text", text: "clean answer" }], + api: "chat", + provider: "openclaw", + model: "test", + usage: {}, + stopReason: "stop", + timestamp: 3, + }, + }, + ]; + fs.writeFileSync(sessionFile, lines.map((line) => JSON.stringify(line)).join("\n"), "utf-8"); + const rawTranscript = fs.readFileSync(sessionFile, "utf-8"); + expect(rawTranscript).toContain("original wrapped prompt"); + expect(rawTranscript).toContain("clean prompt"); + + const out = readSessionMessages(sessionId, storePath, sessionFile); + expect(out).toHaveLength(2); + expect(out).toEqual([ + expect.objectContaining({ + role: "user", + content: "clean prompt", + __openclaw: expect.objectContaining({ seq: 1 }), + }), + expect.objectContaining({ + role: "assistant", + content: [{ type: "text", text: "clean answer" }], + __openclaw: expect.objectContaining({ seq: 2 }), + }), + ]); + expect(JSON.stringify(out)).not.toContain("original wrapped prompt"); + }); + test.each([ { sessionId: "cross-agent-default-root", diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index 1eb8cb85217..17fdf42e53e 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { SessionManager, type SessionEntry } from "@mariozechner/pi-coding-agent"; import { deriveSessionTotalTokens, hasNonzeroUsage, normalizeUsage } from "../agents/usage.js"; import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js"; import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js"; @@ -103,6 +104,60 @@ export function readSessionMessages( } const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/); + const hasTreeEntries = lines.some((line) => { + if (!line.trim()) { + return false; + } + try { + const parsed = JSON.parse(line) as { type?: unknown; id?: unknown; parentId?: unknown }; + return parsed.type !== "session" && typeof parsed.id === "string" && "parentId" in parsed; + } catch { + return false; + } + }); + let branchEntries: SessionEntry[] | null = null; + if (hasTreeEntries) { + try { + branchEntries = SessionManager.open(filePath).getBranch(); + } catch { + branchEntries = null; + } + } + + if (branchEntries) { + const messages: unknown[] = []; + let messageSeq = 0; + for (const entry of branchEntries) { + if (entry.type === "message" && entry.message) { + messageSeq += 1; + messages.push( + attachOpenClawTranscriptMeta(entry.message, { + ...(typeof entry.id === "string" ? { id: entry.id } : {}), + seq: messageSeq, + }), + ); + continue; + } + + if (entry.type === "compaction") { + const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN; + const timestamp = Number.isFinite(ts) ? ts : Date.now(); + messageSeq += 1; + messages.push({ + role: "system", + content: [{ type: "text", text: "Compaction" }], + timestamp, + __openclaw: { + kind: "compaction", + id: typeof entry.id === "string" ? entry.id : undefined, + seq: messageSeq, + }, + }); + } + } + return messages; + } + const messages: unknown[] = []; let messageSeq = 0; for (const line of lines) {