openclaw/src/agents/pi-embedded-runner/transcript-rewrite.test.ts
Josh Lehman 751d5b7849
feat: add context engine transcript maintenance (#51191)
Merged via squash.

Prepared head SHA: b42a3c28b4395bd8a253c7728080f09100d02f42
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-03-20 16:28:27 -07:00

403 lines
12 KiB
TypeScript

import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { installSessionToolResultGuard } from "../session-tool-result-guard.js";
const acquireSessionWriteLockReleaseMock = vi.hoisted(() => vi.fn(async () => {}));
const acquireSessionWriteLockMock = vi.hoisted(() =>
vi.fn(async (_params?: unknown) => ({ release: acquireSessionWriteLockReleaseMock })),
);
vi.mock("../session-write-lock.js", () => ({
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
}));
import {
rewriteTranscriptEntriesInSessionFile,
rewriteTranscriptEntriesInSessionManager,
} from "./transcript-rewrite.js";
type AppendMessage = Parameters<SessionManager["appendMessage"]>[0];
function asAppendMessage(message: unknown): AppendMessage {
return message as AppendMessage;
}
function getBranchMessages(sessionManager: SessionManager): AgentMessage[] {
return sessionManager
.getBranch()
.filter((entry) => entry.type === "message")
.map((entry) => entry.message);
}
beforeEach(() => {
acquireSessionWriteLockMock.mockClear();
acquireSessionWriteLockReleaseMock.mockClear();
});
describe("rewriteTranscriptEntriesInSessionManager", () => {
it("branches from the first replaced message and re-appends the remaining suffix", () => {
const sessionManager = SessionManager.inMemory();
sessionManager.appendMessage(
asAppendMessage({
role: "user",
content: "read file",
timestamp: 1,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
timestamp: 2,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "x".repeat(8_000) }],
isError: false,
timestamp: 3,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "text", text: "summarized" }],
timestamp: 4,
}),
);
const toolResultEntry = sessionManager
.getBranch()
.find((entry) => entry.type === "message" && entry.message.role === "toolResult");
expect(toolResultEntry).toBeDefined();
const result = rewriteTranscriptEntriesInSessionManager({
sessionManager,
replacements: [
{
entryId: toolResultEntry!.id,
message: {
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "[externalized file_123]" }],
isError: false,
timestamp: 3,
},
},
],
});
expect(result).toMatchObject({
changed: true,
rewrittenEntries: 1,
});
expect(result.bytesFreed).toBeGreaterThan(0);
const branchMessages = getBranchMessages(sessionManager);
expect(branchMessages.map((message) => message.role)).toEqual([
"user",
"assistant",
"toolResult",
"assistant",
]);
const rewrittenToolResult = branchMessages[2] as Extract<AgentMessage, { role: "toolResult" }>;
expect(rewrittenToolResult.content).toEqual([
{ type: "text", text: "[externalized file_123]" },
]);
});
it("preserves active-branch labels after rewritten entries are re-appended", () => {
const sessionManager = SessionManager.inMemory();
sessionManager.appendMessage(
asAppendMessage({
role: "user",
content: "read file",
timestamp: 1,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
timestamp: 2,
}),
);
const toolResultEntryId = sessionManager.appendMessage(
asAppendMessage({
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "x".repeat(8_000) }],
isError: false,
timestamp: 3,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "text", text: "summarized" }],
timestamp: 4,
}),
);
const summaryEntry = sessionManager
.getBranch()
.find(
(entry) =>
entry.type === "message" &&
entry.message.role === "assistant" &&
Array.isArray(entry.message.content) &&
entry.message.content.some((part) => part.type === "text" && part.text === "summarized"),
);
expect(summaryEntry).toBeDefined();
sessionManager.appendLabelChange(summaryEntry!.id, "bookmark");
const result = rewriteTranscriptEntriesInSessionManager({
sessionManager,
replacements: [
{
entryId: toolResultEntryId,
message: {
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "[externalized file_123]" }],
isError: false,
timestamp: 3,
},
},
],
});
expect(result.changed).toBe(true);
const rewrittenSummaryEntry = sessionManager
.getBranch()
.find(
(entry) =>
entry.type === "message" &&
entry.message.role === "assistant" &&
Array.isArray(entry.message.content) &&
entry.message.content.some((part) => part.type === "text" && part.text === "summarized"),
);
expect(rewrittenSummaryEntry).toBeDefined();
expect(sessionManager.getLabel(rewrittenSummaryEntry!.id)).toBe("bookmark");
expect(sessionManager.getBranch().some((entry) => entry.type === "label")).toBe(true);
});
it("remaps compaction keep markers when rewritten entries change ids", () => {
const sessionManager = SessionManager.inMemory();
sessionManager.appendMessage(
asAppendMessage({
role: "user",
content: "read file",
timestamp: 1,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
timestamp: 2,
}),
);
const toolResultEntryId = sessionManager.appendMessage(
asAppendMessage({
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "x".repeat(8_000) }],
isError: false,
timestamp: 3,
}),
);
const keptAssistantEntryId = sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "text", text: "keep me" }],
timestamp: 4,
}),
);
sessionManager.appendCompaction("summary", keptAssistantEntryId, 123);
const result = rewriteTranscriptEntriesInSessionManager({
sessionManager,
replacements: [
{
entryId: toolResultEntryId,
message: {
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "[externalized file_123]" }],
isError: false,
timestamp: 3,
},
},
],
});
expect(result.changed).toBe(true);
const branch = sessionManager.getBranch();
const keptAssistantEntry = branch.find(
(entry) =>
entry.type === "message" &&
entry.message.role === "assistant" &&
Array.isArray(entry.message.content) &&
entry.message.content.some((part) => part.type === "text" && part.text === "keep me"),
);
const compactionEntry = branch.find((entry) => entry.type === "compaction");
expect(keptAssistantEntry).toBeDefined();
expect(compactionEntry).toBeDefined();
expect(compactionEntry?.firstKeptEntryId).toBe(keptAssistantEntry?.id);
expect(compactionEntry?.firstKeptEntryId).not.toBe(keptAssistantEntryId);
});
it("bypasses persistence hooks when replaying rewritten messages", () => {
const sessionManager = SessionManager.inMemory();
sessionManager.appendMessage(
asAppendMessage({
role: "user",
content: "run tool",
timestamp: 1,
}),
);
const toolResultEntryId = sessionManager.appendMessage(
asAppendMessage({
role: "toolResult",
toolCallId: "call_1",
toolName: "exec",
content: [{ type: "text", text: "before rewrite" }],
isError: false,
timestamp: 2,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "assistant",
content: [{ type: "text", text: "summarized" }],
timestamp: 3,
}),
);
installSessionToolResultGuard(sessionManager, {
transformToolResultForPersistence: (message) => ({
...(message as Extract<AgentMessage, { role: "toolResult" }>),
content: [{ type: "text", text: "[hook transformed]" }],
}),
beforeMessageWriteHook: ({ message }) =>
message.role === "assistant" ? { block: true } : undefined,
});
const result = rewriteTranscriptEntriesInSessionManager({
sessionManager,
replacements: [
{
entryId: toolResultEntryId,
message: {
role: "toolResult",
toolCallId: "call_1",
toolName: "exec",
content: [{ type: "text", text: "[exact replacement]" }],
isError: false,
timestamp: 2,
},
},
],
});
expect(result.changed).toBe(true);
const branchMessages = getBranchMessages(sessionManager);
expect(branchMessages.map((message) => message.role)).toEqual([
"user",
"toolResult",
"assistant",
]);
expect((branchMessages[1] as Extract<AgentMessage, { role: "toolResult" }>).content).toEqual([
{ type: "text", text: "[exact replacement]" },
]);
expect(branchMessages[2]).toMatchObject({
role: "assistant",
content: [{ type: "text", text: "summarized" }],
});
});
});
describe("rewriteTranscriptEntriesInSessionFile", () => {
it("emits transcript updates when the active branch changes", async () => {
const sessionFile = "/tmp/session.jsonl";
const sessionManager = SessionManager.inMemory();
sessionManager.appendMessage(
asAppendMessage({
role: "user",
content: "run tool",
timestamp: 1,
}),
);
sessionManager.appendMessage(
asAppendMessage({
role: "toolResult",
toolCallId: "call_1",
toolName: "exec",
content: [{ type: "text", text: "y".repeat(6_000) }],
isError: false,
timestamp: 2,
}),
);
const toolResultEntry = sessionManager
.getBranch()
.find((entry) => entry.type === "message" && entry.message.role === "toolResult");
expect(toolResultEntry).toBeDefined();
const openSpy = vi
.spyOn(SessionManager, "open")
.mockReturnValue(sessionManager as unknown as ReturnType<typeof SessionManager.open>);
const listener = vi.fn();
const cleanup = onSessionTranscriptUpdate(listener);
try {
const result = await rewriteTranscriptEntriesInSessionFile({
sessionFile,
sessionKey: "agent:main:test",
request: {
replacements: [
{
entryId: toolResultEntry!.id,
message: {
role: "toolResult",
toolCallId: "call_1",
toolName: "exec",
content: [{ type: "text", text: "[file_ref:file_abc]" }],
isError: false,
timestamp: 2,
},
},
],
},
});
expect(result.changed).toBe(true);
expect(acquireSessionWriteLockMock).toHaveBeenCalledWith({
sessionFile,
});
expect(acquireSessionWriteLockReleaseMock).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith({ sessionFile });
const rewrittenToolResult = getBranchMessages(sessionManager)[1] as Extract<
AgentMessage,
{ role: "toolResult" }
>;
expect(rewrittenToolResult.content).toEqual([{ type: "text", text: "[file_ref:file_abc]" }]);
} finally {
cleanup();
openSpy.mockRestore();
}
});
});