refactor(agents): remove stale file transcript rewrite

This commit is contained in:
Vincent Koc
2026-06-19 08:56:13 +08:00
parent c9605779ef
commit c33cec04d9
4 changed files with 1 additions and 393 deletions

View File

@@ -32,7 +32,6 @@ const legacyWriterNames = new Set([
const legacyTranscriptWriterNames = new Set([
"appendSessionTranscriptMessage",
"emitSessionTranscriptUpdate",
"rewriteTranscriptEntriesInSessionFile",
]);
const sessionCreateLifecycleWriterNames = new Set([
"applySessionStoreEntryPatch",

View File

@@ -20,7 +20,6 @@ vi.mock("../session-write-lock.js", () =>
),
);
let rewriteTranscriptEntriesInSessionFile: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionFile;
let rewriteTranscriptEntriesInSessionManager: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionManager;
let rewriteTranscriptEntriesInRuntimeTranscript: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInRuntimeTranscript;
let onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate;
@@ -50,10 +49,6 @@ function createTextContent(text: string) {
return [{ type: "text", text }];
}
function getMessageContent(message: AgentMessage): unknown {
return "content" in message ? message.content : undefined;
}
function createReadRewriteSession(options?: { tailAssistantText?: string }) {
// Read rewrite fixtures include a suffix assistant turn so branch rewrites
// must re-append downstream entries after replacing the tool result.
@@ -162,7 +157,6 @@ beforeAll(async () => {
({ installSessionToolResultGuard } = await import("../session-tool-result-guard.js"));
({
rewriteTranscriptEntriesInRuntimeTranscript,
rewriteTranscriptEntriesInSessionFile,
rewriteTranscriptEntriesInSessionManager,
} = await import("./transcript-rewrite.js"));
});
@@ -309,7 +303,7 @@ describe("rewriteTranscriptEntriesInSessionManager", () => {
});
});
describe("rewriteTranscriptEntriesInSessionFile", () => {
describe("rewriteTranscriptEntriesInRuntimeTranscript", () => {
it("does not create session metadata for missing runtime transcripts", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-runtime-"));
const storePath = path.join(dir, "sessions.json");
@@ -418,319 +412,4 @@ describe("rewriteTranscriptEntriesInSessionFile", () => {
}
});
it("aborts under the write lock when the active suffix contains an unexpected entry", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-guard-"));
const sessionManager = SessionManager.create(dir, dir);
const entryIds = appendSessionMessages(sessionManager, [
asAppendMessage({
role: "user",
content: "start",
timestamp: 1,
}),
asAppendMessage({
role: "assistant",
content: createTextContent("source reply media"),
timestamp: 2,
}),
asAppendMessage({
role: "assistant",
content: createTextContent("source reply text"),
timestamp: 3,
}),
asAppendMessage({
role: "user",
content: "concurrent append",
timestamp: 4,
}),
]);
const sessionFile = requireString(sessionManager.getSessionFile(), "persisted session file");
const mediaEntryId = entryIds[1];
const textEntryId = entryIds[2];
const listener = vi.fn();
const cleanup = onSessionTranscriptUpdate(listener);
try {
const result = await rewriteTranscriptEntriesInSessionFile({
sessionFile,
sessionKey: "agent:main:test",
request: {
allowedRewriteSuffixEntryIds: [mediaEntryId, textEntryId],
replacements: [
{
entryId: mediaEntryId,
message: asAppendMessage({
role: "assistant",
content: createTextContent("rewritten source reply media"),
timestamp: 2,
}) as AgentMessage,
},
],
},
});
expect(result).toMatchObject({
changed: false,
reason: "rewrite suffix guard failed",
});
expect(listener).not.toHaveBeenCalled();
const unchangedSession = SessionManager.open(sessionFile);
expect(getBranchMessages(unchangedSession).map(getMessageContent)).toEqual([
"start",
createTextContent("source reply media"),
createTextContent("source reply text"),
"concurrent append",
]);
} finally {
cleanup();
}
});
it("rewrites a guarded side branch and restores the active navigation state", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-side-"));
const sessionFile = path.join(dir, "session.jsonl");
await fs.writeFile(
sessionFile,
[
{
type: "session",
version: 3,
id: "session-side-rewrite",
timestamp: "2026-06-15T00:00:00.000Z",
cwd: dir,
},
{
type: "message",
id: "active-root",
parentId: null,
timestamp: "2026-06-15T00:00:01.000Z",
message: { role: "user", content: "active root", timestamp: 1 },
},
{
type: "message",
id: "side-mirror",
parentId: "active-root",
timestamp: "2026-06-15T00:00:02.000Z",
message: {
role: "assistant",
content: createTextContent("source reply before rewrite"),
timestamp: 2,
},
},
{
type: "leaf",
id: "active-leaf",
parentId: "side-mirror",
timestamp: "2026-06-15T00:00:03.000Z",
targetId: "active-root",
appendParentId: "side-mirror",
appendMode: "side",
},
]
.map((entry) => JSON.stringify(entry))
.join("\n") + "\n",
"utf-8",
);
const result = await rewriteTranscriptEntriesInSessionFile({
sessionFile,
sessionKey: "agent:main:test",
request: {
allowedRewriteSuffixEntryIds: ["side-mirror"],
replacements: [
{
entryId: "side-mirror",
message: asAppendMessage({
role: "assistant",
content: createTextContent("source reply after rewrite"),
timestamp: 2,
}) as AgentMessage,
},
],
},
});
expect(result).toMatchObject({ changed: true, rewrittenEntries: 1 });
const records = (await fs.readFile(sessionFile, "utf-8"))
.trim()
.split("\n")
.map(
(line) =>
JSON.parse(line) as {
type?: string;
id?: string;
parentId?: string | null;
targetId?: string | null;
appendParentId?: string | null;
appendMode?: "side";
message?: AgentMessage;
},
);
const rewrittenSideEntry = records.findLast(
(entry) =>
entry.type === "message" &&
JSON.stringify(entry.message).includes("source reply after rewrite"),
);
expect(rewrittenSideEntry).toMatchObject({ parentId: "active-root" });
expect(records.at(-1)).toMatchObject({
type: "leaf",
parentId: rewrittenSideEntry?.id,
targetId: "active-root",
appendParentId: "side-mirror",
appendMode: "side",
});
const reopened = SessionManager.open(sessionFile, dir, dir);
expect(getBranchMessages(reopened).map(getMessageContent)).toEqual(["active root"]);
const nextId = reopened.appendMessage(
asAppendMessage({ role: "user", content: "active continuation", timestamp: 3 }),
);
expect(reopened.getEntry(nextId)).toMatchObject({ parentId: "active-root" });
expect(reopened.getEntry(nextId)).not.toHaveProperty("appendMode");
});
it("rejects a rewrite batch split across active and side branches", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-mixed-"));
const sessionFile = path.join(dir, "session.jsonl");
const records = [
{
type: "session",
version: 3,
id: "session-mixed-rewrite",
timestamp: "2026-06-15T00:00:00.000Z",
cwd: dir,
},
{
type: "message",
id: "root",
parentId: null,
timestamp: "2026-06-15T00:00:01.000Z",
message: { role: "user", content: "root", timestamp: 1 },
},
{
type: "message",
id: "active-mirror",
parentId: "root",
timestamp: "2026-06-15T00:00:02.000Z",
message: { role: "assistant", content: createTextContent("active"), timestamp: 2 },
},
{
type: "message",
id: "side-mirror",
parentId: "root",
timestamp: "2026-06-15T00:00:03.000Z",
message: { role: "assistant", content: createTextContent("side"), timestamp: 3 },
},
{
type: "leaf",
id: "active-leaf",
parentId: "side-mirror",
timestamp: "2026-06-15T00:00:04.000Z",
targetId: "active-mirror",
},
];
const original = records.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
await fs.writeFile(sessionFile, original, "utf-8");
const result = await rewriteTranscriptEntriesInSessionFile({
sessionFile,
sessionKey: "agent:main:test",
request: {
allowedRewriteSuffixEntryIds: ["active-mirror", "side-mirror"],
replacements: [
{
entryId: "active-mirror",
message: asAppendMessage({
role: "assistant",
content: createTextContent("active rewritten"),
timestamp: 2,
}) as AgentMessage,
},
{
entryId: "side-mirror",
message: asAppendMessage({
role: "assistant",
content: createTextContent("side rewritten"),
timestamp: 3,
}) as AgentMessage,
},
],
},
});
expect(result).toMatchObject({
changed: false,
reason: "rewrite targets span multiple branches",
});
expect(await fs.readFile(sessionFile, "utf-8")).toBe(original);
});
it("emits transcript updates when the active branch changes without opening a manager", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-"));
const sessionManager = SessionManager.create(dir, dir);
const entryIds = appendSessionMessages(sessionManager, [
asAppendMessage({
role: "user",
content: "run tool",
timestamp: 1,
}),
asAppendMessage({
role: "toolResult",
toolCallId: "call_1",
toolName: "exec",
content: createTextContent("before rewrite"),
isError: false,
timestamp: 2,
}),
asAppendMessage({
role: "assistant",
content: createTextContent("summarized"),
timestamp: 3,
}),
]);
const sessionFile = requireString(sessionManager.getSessionFile(), "persisted session file");
const toolResultEntryId = entryIds[1];
const openSpy = vi.spyOn(SessionManager, "open").mockImplementation(() => {
throw new Error("SessionManager.open should not be used for file rewrites");
});
const listener = vi.fn();
const cleanup = onSessionTranscriptUpdate(listener);
try {
const result = await rewriteTranscriptEntriesInSessionFile({
sessionFile,
sessionKey: "agent:main:test",
request: {
replacements: [
{
entryId: toolResultEntryId,
message: createToolResultReplacement("exec", "[file_ref:file_abc]", 2),
},
],
},
});
expect(result.changed).toBe(true);
expect(acquireSessionWriteLockMock).toHaveBeenCalledWith({
sessionFile,
staleMs: 1_800_000,
timeoutMs: 60_000,
maxHoldMs: 300_000,
});
expect(acquireSessionWriteLockReleaseMock).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith({ sessionFile, sessionKey: "agent:main:test" });
openSpy.mockRestore();
const rewrittenSession = SessionManager.open(sessionFile);
const rewrittenToolResult = getBranchMessages(rewrittenSession)[1] as Extract<
AgentMessage,
{ role: "toolResult" }
>;
expect(rewrittenToolResult.content).toEqual([{ type: "text", text: "[file_ref:file_abc]" }]);
} finally {
cleanup();
openSpy.mockRestore();
}
});
});

View File

@@ -18,7 +18,6 @@ import {
import { SessionManager } from "../sessions/index.js";
import { log } from "./logger.js";
import {
persistTranscriptStateMutation,
readTranscriptFileState,
type TranscriptFileState,
type TranscriptPersistedEntry,
@@ -493,62 +492,3 @@ export async function rewriteTranscriptEntriesInRuntimeTranscript(params: {
await sessionLock?.release();
}
}
/**
* Rewrites a named transcript file artifact. Runtime callers should prefer
* rewriteTranscriptEntriesInRuntimeTranscript with agent/session scope.
*/
export async function rewriteTranscriptEntriesInSessionFile(params: {
sessionFile: string;
sessionId?: string;
sessionKey?: string;
agentId?: string;
request: TranscriptRewriteRequest;
config?: SessionWriteLockAcquireTimeoutConfig;
}): Promise<TranscriptRewriteResult> {
let sessionLock: Awaited<ReturnType<typeof acquireSessionWriteLock>> | undefined;
try {
sessionLock = await acquireSessionWriteLock({
sessionFile: params.sessionFile,
...resolveSessionWriteLockOptions(params.config),
});
const state = await readTranscriptFileState(params.sessionFile);
const result = rewriteTranscriptEntriesInState({
state,
replacements: params.request.replacements,
...(params.request.allowedRewriteSuffixEntryIds
? { allowedRewriteSuffixEntryIds: params.request.allowedRewriteSuffixEntryIds }
: {}),
});
if (result.changed) {
await persistTranscriptStateMutation({
sessionFile: params.sessionFile,
state,
appendedEntries: result.appendedEntries,
});
emitSessionTranscriptUpdate({
sessionFile: params.sessionFile,
sessionKey: params.sessionKey,
...(params.agentId ? { agentId: params.agentId } : {}),
});
log.info(
`[transcript-rewrite] rewrote ${result.rewrittenEntries} entr` +
`${result.rewrittenEntries === 1 ? "y" : "ies"} ` +
`bytesFreed=${result.bytesFreed} ` +
`sessionKey=${params.sessionKey ?? params.sessionId ?? "unknown"}`,
);
}
return result;
} catch (err) {
const reason = formatErrorMessage(err);
log.warn(`[transcript-rewrite] failed: ${reason}`);
return {
changed: false,
bytesFreed: 0,
rewrittenEntries: 0,
reason,
};
} finally {
await sessionLock?.release();
}
}

View File

@@ -228,15 +228,10 @@ describe("session accessor boundary guard", () => {
findTranscriptWriterBoundaryViolations(`
import { appendSessionTranscriptMessage } from "../config/sessions/transcript-append.js";
import { emitSessionTranscriptUpdate as emitUpdate } from "../sessions/transcript-events.js";
import { rewriteTranscriptEntriesInSessionFile } from "../agents/embedded-agent-runner/transcript-rewrite.js";
`),
).toEqual([
{ line: 2, reason: 'imports legacy transcript writer "appendSessionTranscriptMessage"' },
{ line: 3, reason: 'imports legacy transcript writer "emitSessionTranscriptUpdate"' },
{
line: 4,
reason: 'imports legacy transcript writer "rewriteTranscriptEntriesInSessionFile"',
},
]);
});
@@ -246,16 +241,11 @@ describe("session accessor boundary guard", () => {
appendSessionTranscriptMessage({ transcriptPath, message });
transcriptEvents.emitSessionTranscriptUpdate({ sessionFile });
transcriptAppend["appendSessionTranscriptMessage"]({ transcriptPath, message });
transcriptRewrite.rewriteTranscriptEntriesInSessionFile({ sessionFile, request });
`),
).toEqual([
{ line: 2, reason: 'calls legacy transcript writer "appendSessionTranscriptMessage"' },
{ line: 3, reason: 'references legacy transcript writer "emitSessionTranscriptUpdate"' },
{ line: 4, reason: 'references legacy transcript writer "appendSessionTranscriptMessage"' },
{
line: 5,
reason: 'references legacy transcript writer "rewriteTranscriptEntriesInSessionFile"',
},
]);
});