fix(gateway): read active transcript history branch

This commit is contained in:
Peter Steinberger
2026-04-27 20:45:12 +01:00
parent 9645fe72c6
commit 6dc8bd8935
2 changed files with 123 additions and 0 deletions

View File

@@ -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",

View File

@@ -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) {