mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 11:02:26 +00:00
refactor(agents): remove stale file transcript rewrite
This commit is contained in:
@@ -32,7 +32,6 @@ const legacyWriterNames = new Set([
|
||||
const legacyTranscriptWriterNames = new Set([
|
||||
"appendSessionTranscriptMessage",
|
||||
"emitSessionTranscriptUpdate",
|
||||
"rewriteTranscriptEntriesInSessionFile",
|
||||
]);
|
||||
const sessionCreateLifecycleWriterNames = new Set([
|
||||
"applySessionStoreEntryPatch",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user