mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
24a1139188
* Move raw transcript from system to user prompt to protect provenance. * Type fix. * fix(voice-call): harden transcript context handling * fix(voice-call): initialize inbound Twilio control state * test(voice-call): align runtime coordinator fixture --------- Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
1788 lines
61 KiB
TypeScript
1788 lines
61 KiB
TypeScript
// Codex tests cover transcript mirror plugin behavior.
|
|
import { Buffer } from "node:buffer";
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { embeddedAgentLog, type AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import {
|
|
initializeGlobalHookRunner,
|
|
resetGlobalHookRunner,
|
|
} from "openclaw/plugin-sdk/hook-runtime";
|
|
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
|
import { readSessionTranscriptEvents } from "openclaw/plugin-sdk/session-transcript-runtime";
|
|
import {
|
|
castAgentMessage,
|
|
makeAgentAssistantMessage,
|
|
makeAgentUserMessage,
|
|
} from "openclaw/plugin-sdk/test-fixtures";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { CodexThread } from "./protocol.js";
|
|
import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
|
|
import {
|
|
buildCodexUserPromptMessage,
|
|
codexTranscriptMirrorRuntime,
|
|
importCodexThreadHistoryToTranscript,
|
|
mirrorPromptAtTurnStartBestEffort,
|
|
projectBoundedCodexThreadHistory,
|
|
} from "./transcript-mirror.js";
|
|
import { attachCodexMirrorIdentity } from "./upstream-prompt-provenance.js";
|
|
|
|
const mirrorCodexAppServerTranscript = codexTranscriptMirrorRuntime.mirror;
|
|
const mirrorTranscriptBestEffort = codexTranscriptMirrorRuntime.mirrorBestEffort;
|
|
|
|
const publishSessionTranscriptUpdateByIdentityMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("openclaw/plugin-sdk/session-transcript-runtime", async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import("openclaw/plugin-sdk/session-transcript-runtime")>();
|
|
return {
|
|
...actual,
|
|
publishSessionTranscriptUpdateByIdentity: publishSessionTranscriptUpdateByIdentityMock,
|
|
};
|
|
});
|
|
|
|
type MirroredAgentMessage = Extract<AgentMessage, { role: "user" | "assistant" | "toolResult" }>;
|
|
|
|
// Mirrors transcript-mirror.ts's content fingerprint exactly so test
|
|
// expectations stay in sync without exposing the helper publicly.
|
|
function expectedFingerprint(message: MirroredAgentMessage): string {
|
|
const payload = JSON.stringify({ role: message.role, content: message.content });
|
|
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
}
|
|
|
|
function messageContent(message: AgentMessage | undefined) {
|
|
if (!message || !("content" in message)) {
|
|
throw new Error("expected transcript message content");
|
|
}
|
|
return message.content;
|
|
}
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
resetGlobalHookRunner();
|
|
publishSessionTranscriptUpdateByIdentityMock.mockReset();
|
|
for (const dir of tempDirs.splice(0)) {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function makeRoot(prefix: string): Promise<string> {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
tempDirs.push(root);
|
|
return root;
|
|
}
|
|
|
|
describe("buildCodexUserPromptMessage", () => {
|
|
it("uses transcriptPrompt when an embedded caller does not provide a recorder", () => {
|
|
const message = buildCodexUserPromptMessage({
|
|
prompt:
|
|
"[Audible call-opening context]\nAssistant: Welcome.\n[End audible call-opening context]\n\nCurrent caller message:\nHello",
|
|
transcriptPrompt: "Hello",
|
|
messageProvider: "voice",
|
|
inputProvenance: { kind: "external_user", sourceChannel: "voice" },
|
|
} as unknown as Parameters<typeof buildCodexUserPromptMessage>[0]);
|
|
|
|
expect(message).toMatchObject({
|
|
role: "user",
|
|
content: "Hello",
|
|
sourceChannel: "voice",
|
|
provenance: { kind: "external_user", sourceChannel: "voice" },
|
|
});
|
|
});
|
|
|
|
it("uses the prepared user transcript message for app-server prompt mirrors", () => {
|
|
const message = buildCodexUserPromptMessage({
|
|
prompt: "[Mon 2026-05-25 19:14 GMT+1] What is in this image?",
|
|
messageChannel: "webchat",
|
|
userTurnTranscriptRecorder: {
|
|
message: {
|
|
role: "user",
|
|
content: "What is in this image?",
|
|
timestamp: 1779732875151,
|
|
MediaPath: "/tmp/image.png",
|
|
MediaPaths: ["/tmp/image.png"],
|
|
MediaType: "image/png",
|
|
MediaTypes: ["image/png"],
|
|
},
|
|
},
|
|
} as unknown as Parameters<typeof buildCodexUserPromptMessage>[0]);
|
|
|
|
expect(message).toMatchObject({
|
|
role: "user",
|
|
content: "What is in this image?",
|
|
timestamp: 1779732875151,
|
|
sourceChannel: "webchat",
|
|
MediaPath: "/tmp/image.png",
|
|
MediaPaths: ["/tmp/image.png"],
|
|
MediaType: "image/png",
|
|
MediaTypes: ["image/png"],
|
|
});
|
|
});
|
|
});
|
|
|
|
function readEventMessages(events: unknown[]): Array<{ role?: string; text?: string }> {
|
|
return events
|
|
.map((event) =>
|
|
event && typeof event === "object" ? (event as { message?: unknown }).message : undefined,
|
|
)
|
|
.filter((message): message is { role?: string; content?: unknown } =>
|
|
Boolean(message && typeof message === "object"),
|
|
)
|
|
.map((message) => {
|
|
const content = Array.isArray(message.content)
|
|
? message.content.find((part): part is { text: string } =>
|
|
Boolean(part && typeof part === "object" && typeof part.text === "string"),
|
|
)?.text
|
|
: typeof message.content === "string"
|
|
? message.content
|
|
: undefined;
|
|
return { role: message.role, text: content };
|
|
});
|
|
}
|
|
|
|
async function createSqliteMirrorTarget(prefix: string, options: { sessionId?: string } = {}) {
|
|
const root = await makeRoot(prefix);
|
|
const agentId = "main";
|
|
const sessionId = options.sessionId ?? "session-1";
|
|
const sessionKey = `agent:${agentId}:${sessionId}`;
|
|
const storePath = path.join(root, "openclaw-agent.sqlite");
|
|
await upsertSessionEntry({
|
|
agentId,
|
|
sessionKey,
|
|
storePath,
|
|
entry: {
|
|
sessionFile: `sqlite:${agentId}:${sessionId}:${storePath}`,
|
|
sessionId,
|
|
updatedAt: 1,
|
|
},
|
|
});
|
|
return {
|
|
agentId,
|
|
sessionId,
|
|
sessionKey,
|
|
storePath,
|
|
bogusSessionFile: path.join(root, "should-not-be-created.jsonl"),
|
|
};
|
|
}
|
|
|
|
async function readMirrorEvents(target: {
|
|
agentId: string;
|
|
sessionId: string;
|
|
sessionKey: string;
|
|
storePath: string;
|
|
}): Promise<unknown[]> {
|
|
return await readSessionTranscriptEvents(target);
|
|
}
|
|
|
|
async function readMirrorRaw(target: {
|
|
agentId: string;
|
|
sessionId: string;
|
|
sessionKey: string;
|
|
storePath: string;
|
|
}): Promise<string> {
|
|
return (await readMirrorEvents(target)).map((event) => JSON.stringify(event)).join("\n");
|
|
}
|
|
|
|
async function readMirrorMessages(target: {
|
|
agentId: string;
|
|
sessionId: string;
|
|
sessionKey: string;
|
|
storePath: string;
|
|
}): Promise<Array<{ role?: string; text?: string }>> {
|
|
return readEventMessages(await readMirrorEvents(target));
|
|
}
|
|
|
|
describe("importCodexThreadHistoryToTranscript", () => {
|
|
it.each([
|
|
{
|
|
label: "remote audio-only input",
|
|
caseId: "remote",
|
|
content: [
|
|
{
|
|
type: "audio",
|
|
url: "https://private.example/secret-recording.wav?token=secret-audio-token",
|
|
},
|
|
],
|
|
expectedText: "[Audio attachment]",
|
|
privateValues: ["private.example", "secret-recording.wav", "secret-audio-token"],
|
|
},
|
|
{
|
|
label: "local audio-only input",
|
|
caseId: "local",
|
|
content: [{ type: "localAudio", path: "/private/codex/secret-local-recording.wav" }],
|
|
expectedText: "[Audio attachment]",
|
|
privateValues: ["/private/codex/secret-local-recording.wav"],
|
|
},
|
|
{
|
|
label: "legacy local audio-only input",
|
|
caseId: "legacy-local",
|
|
content: [{ type: "local_audio", path: "/private/codex/secret-legacy-recording.wav" }],
|
|
expectedText: "[Audio attachment]",
|
|
privateValues: ["/private/codex/secret-legacy-recording.wav"],
|
|
},
|
|
{
|
|
label: "mixed text, image, and audio input in source order",
|
|
caseId: "mixed",
|
|
content: [
|
|
{ type: "text", text: "Before the recording" },
|
|
{ type: "audio", url: "data:audio/wav;base64,c2VjcmV0LWF1ZGlv" },
|
|
{ type: "text", text: "After the recording" },
|
|
{ type: "image", url: "data:image/png;base64,c2VjcmV0LWltYWdl" },
|
|
{ type: "localAudio", path: "/private/codex/secret-mixed-recording.wav" },
|
|
{ type: "local_audio", path: "/private/codex/secret-mixed-legacy.wav" },
|
|
],
|
|
expectedText:
|
|
"Before the recording\n[Audio attachment]\nAfter the recording\n" +
|
|
"[Image attachment]\n[Audio attachment]\n[Audio attachment]",
|
|
privateValues: [
|
|
"data:audio/wav",
|
|
"c2VjcmV0LWF1ZGlv",
|
|
"data:image/png",
|
|
"c2VjcmV0LWltYWdl",
|
|
"/private/codex/secret-mixed-recording.wav",
|
|
"/private/codex/secret-mixed-legacy.wav",
|
|
],
|
|
},
|
|
])(
|
|
"preserves $label without leaking attachment contents or locations",
|
|
async ({ caseId, content, expectedText, privateValues }) => {
|
|
const target = await createSqliteMirrorTarget(`openclaw-codex-audio-history-${caseId}-`, {
|
|
sessionId: `session-audio-${caseId}`,
|
|
});
|
|
const thread = {
|
|
id: `thread-audio-${caseId}`,
|
|
turns: [
|
|
{
|
|
id: "turn-audio",
|
|
status: "completed",
|
|
items: [
|
|
{ id: "user-audio", type: "userMessage", content },
|
|
{
|
|
id: "assistant-audio",
|
|
type: "agentMessage",
|
|
text: "The recording was received.",
|
|
phase: "final_answer",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as unknown as CodexThread;
|
|
|
|
const projection = projectBoundedCodexThreadHistory({
|
|
thread,
|
|
throughTurnId: "turn-audio",
|
|
importedAt: 1_800_000_000_000,
|
|
});
|
|
expect(projection).toMatchObject({ importedMessages: 2, omittedMessages: 0 });
|
|
expect(projection.responseItems).toEqual([
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: expectedText }],
|
|
},
|
|
{
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "The recording was received." }],
|
|
phase: "final_answer",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
importCodexThreadHistoryToTranscript({
|
|
thread,
|
|
throughTurnId: "turn-audio",
|
|
storePath: target.storePath,
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
agentId: target.agentId,
|
|
}),
|
|
).resolves.toEqual({ importedMessages: 2, omittedMessages: 0 });
|
|
await expect(readMirrorMessages(target)).resolves.toEqual([
|
|
{ role: "user", text: expectedText },
|
|
{ role: "assistant", text: "The recording was received." },
|
|
]);
|
|
|
|
const responseArtifacts = JSON.stringify(projection.responseItems);
|
|
const transcriptArtifacts = await readMirrorRaw(target);
|
|
for (const privateValue of privateValues) {
|
|
expect(responseArtifacts).not.toContain(privateValue);
|
|
expect(transcriptArtifacts).not.toContain(privateValue);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("imports only bounded user-visible conversation items with stable identities", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-history-", {
|
|
sessionId: "session-history",
|
|
});
|
|
const sessionFile = `sqlite:${target.agentId}:${target.sessionId}:${target.storePath}`;
|
|
const thread = {
|
|
id: "thread-history",
|
|
cwd: "/workspace/project",
|
|
turns: [
|
|
{
|
|
id: "turn-1",
|
|
status: "completed",
|
|
startedAt: 1_700_000_000,
|
|
completedAt: 1_700_000_001,
|
|
items: [
|
|
{
|
|
id: "user-1",
|
|
type: "userMessage",
|
|
content: [
|
|
{ type: "text", text: "Review this image" },
|
|
{ type: "image", url: "data:image/png;base64,private" },
|
|
],
|
|
},
|
|
{
|
|
id: "reasoning-1",
|
|
type: "reasoning",
|
|
summary: ["private reasoning"],
|
|
content: ["private chain of thought"],
|
|
},
|
|
{
|
|
id: "command-1",
|
|
type: "commandExecution",
|
|
command: "print-secret",
|
|
aggregatedOutput: "private tool output",
|
|
},
|
|
{
|
|
id: "assistant-1",
|
|
type: "agentMessage",
|
|
text: "The visible answer",
|
|
phase: "final_answer",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as unknown as CodexThread;
|
|
|
|
const rawProjection = projectBoundedCodexThreadHistory({
|
|
thread,
|
|
throughTurnId: "turn-1",
|
|
importedAt: 1_800_000_000_000,
|
|
});
|
|
expect(rawProjection.responseItems).toEqual([
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "Review this image\n[Image attachment]" }],
|
|
},
|
|
{
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "The visible answer" }],
|
|
phase: "final_answer",
|
|
},
|
|
]);
|
|
expect(JSON.stringify(rawProjection.responseItems)).not.toContain("private");
|
|
expect(JSON.stringify(rawProjection.responseItems)).not.toContain("data:image");
|
|
|
|
await expect(
|
|
importCodexThreadHistoryToTranscript({
|
|
thread,
|
|
throughTurnId: "turn-1",
|
|
storePath: target.storePath,
|
|
sessionId: "session-history",
|
|
sessionKey: target.sessionKey,
|
|
agentId: target.agentId,
|
|
}),
|
|
).resolves.toEqual({ importedMessages: 2, omittedMessages: 0 });
|
|
|
|
const events = await readMirrorEvents(target);
|
|
const raw = events.map((event) => JSON.stringify(event)).join("\n");
|
|
const messages = (events as Array<{ message?: AgentMessage; type?: string }>)
|
|
.filter((event) => event.type === "message")
|
|
.map((event) => event.message);
|
|
expect(messages).toMatchObject([
|
|
{
|
|
role: "user",
|
|
content: "Review this image\n[Image attachment]",
|
|
timestamp: 1_700_000_000_000,
|
|
idempotencyKey: "codex-app-server:thread-history:history:turn-1:user-1",
|
|
},
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "The visible answer" }],
|
|
api: "openai-chatgpt-responses",
|
|
provider: "openai",
|
|
model: "native-history",
|
|
stopReason: "stop",
|
|
timestamp: 1_700_000_001_003,
|
|
idempotencyKey: "codex-app-server:thread-history:history:turn-1:assistant-1",
|
|
},
|
|
]);
|
|
expect(raw).not.toContain("private reasoning");
|
|
expect(raw).not.toContain("private chain of thought");
|
|
expect(raw).not.toContain("private tool output");
|
|
expect(raw).not.toContain("data:image");
|
|
await expect(
|
|
readCodexMirroredSessionHistoryMessages({
|
|
sessionFile,
|
|
sessionId: "session-history",
|
|
sessionKey: target.sessionKey,
|
|
agentId: target.agentId,
|
|
}),
|
|
).resolves.toMatchObject([
|
|
{ role: "user", content: "Review this image\n[Image attachment]" },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "The visible answer" }],
|
|
api: "openai-chatgpt-responses",
|
|
provider: "openai",
|
|
model: "native-history",
|
|
stopReason: "stop",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps the newest 200 visible messages and deduplicates a retried import", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-bounded-history-", {
|
|
sessionId: "session-bounded-history",
|
|
});
|
|
const thread = {
|
|
id: "thread-bounded-history",
|
|
turns: Array.from({ length: 205 }, (_, index) => ({
|
|
id: `turn-${index}`,
|
|
status: "completed",
|
|
startedAt: 1_700_000_000 + index,
|
|
completedAt: 1_700_000_000 + index,
|
|
items: [
|
|
{
|
|
id: `user-${index}`,
|
|
type: "userMessage",
|
|
content: [{ type: "text", text: `message-${index}` }],
|
|
},
|
|
],
|
|
})),
|
|
} as unknown as CodexThread;
|
|
const importParams = {
|
|
thread,
|
|
throughTurnId: "turn-204",
|
|
storePath: target.storePath,
|
|
sessionId: "session-bounded-history",
|
|
sessionKey: target.sessionKey,
|
|
agentId: target.agentId,
|
|
};
|
|
|
|
await expect(importCodexThreadHistoryToTranscript(importParams)).resolves.toEqual({
|
|
importedMessages: 200,
|
|
omittedMessages: 5,
|
|
});
|
|
await expect(importCodexThreadHistoryToTranscript(importParams)).resolves.toEqual({
|
|
importedMessages: 200,
|
|
omittedMessages: 5,
|
|
});
|
|
|
|
const events = await readMirrorEvents(target);
|
|
const messages = (events as Array<{ message?: AgentMessage; type?: string }>)
|
|
.filter((event) => event.type === "message")
|
|
.map((event) => event.message);
|
|
expect(messages).toHaveLength(200);
|
|
expect(messages[0]).toMatchObject({ content: "message-5" });
|
|
expect(messages.at(-1)).toMatchObject({ content: "message-204" });
|
|
});
|
|
|
|
it("assigns canonical assistant attribution and numeric fallback timestamps", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-fallback-history-", {
|
|
sessionId: "session-fallback-history",
|
|
});
|
|
const sessionFile = `sqlite:${target.agentId}:${target.sessionId}:${target.storePath}`;
|
|
const thread = {
|
|
id: "thread-fallback-history",
|
|
modelProvider: "source-provider",
|
|
turns: [
|
|
{
|
|
id: "turn-without-time",
|
|
status: "completed",
|
|
items: [
|
|
{
|
|
id: "user-without-time",
|
|
type: "userMessage",
|
|
content: [{ type: "text", text: "Earlier prompt" }],
|
|
},
|
|
{
|
|
id: "assistant-without-time",
|
|
type: "agentMessage",
|
|
text: "Earlier answer",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as unknown as CodexThread;
|
|
|
|
await importCodexThreadHistoryToTranscript({
|
|
thread,
|
|
throughTurnId: "turn-without-time",
|
|
storePath: target.storePath,
|
|
sessionId: "session-fallback-history",
|
|
sessionKey: target.sessionKey,
|
|
agentId: target.agentId,
|
|
});
|
|
|
|
const history = await readCodexMirroredSessionHistoryMessages({
|
|
sessionFile,
|
|
sessionId: "session-fallback-history",
|
|
sessionKey: target.sessionKey,
|
|
agentId: target.agentId,
|
|
});
|
|
expect(history).toMatchObject([
|
|
{ role: "user", content: "Earlier prompt", timestamp: expect.any(Number) },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "Earlier answer" }],
|
|
api: "openai-chatgpt-responses",
|
|
provider: "source-provider",
|
|
model: "native-history",
|
|
usage: { totalTokens: 0 },
|
|
stopReason: "stop",
|
|
timestamp: expect.any(Number),
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("projectBoundedCodexThreadHistory", () => {
|
|
const thread = {
|
|
id: "thread-prefix",
|
|
createdAt: 1_700_000_000,
|
|
turns: [
|
|
{
|
|
id: "turn-a",
|
|
status: "completed",
|
|
startedAt: 1_700_000_001,
|
|
completedAt: 1_700_000_002,
|
|
items: [
|
|
{
|
|
id: "user-a",
|
|
type: "userMessage",
|
|
content: [{ type: "text", text: "First question" }],
|
|
},
|
|
{
|
|
id: "assistant-a",
|
|
type: "agentMessage",
|
|
text: "First answer",
|
|
phase: "commentary",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "turn-b",
|
|
status: "completed",
|
|
startedAt: 1_700_000_003,
|
|
completedAt: 1_700_000_004,
|
|
items: [
|
|
{
|
|
id: "user-b",
|
|
type: "userMessage",
|
|
content: [{ type: "text", text: "Second question" }],
|
|
},
|
|
{
|
|
id: "assistant-b",
|
|
type: "agentMessage",
|
|
text: "Second answer",
|
|
phase: "final_answer",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "turn-active",
|
|
status: "inProgress",
|
|
items: [
|
|
{
|
|
id: "active-secret",
|
|
type: "agentMessage",
|
|
text: "Do not import the active tail",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "turn-failed",
|
|
status: "failed",
|
|
items: [
|
|
{
|
|
id: "failed-secret",
|
|
type: "agentMessage",
|
|
text: "Do not import the failed tail",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as unknown as CodexThread;
|
|
|
|
it("uses one inclusive completed-turn prefix for transcript and Responses API projection", () => {
|
|
const projection = projectBoundedCodexThreadHistory({
|
|
thread,
|
|
throughTurnId: "turn-b",
|
|
importedAt: 1_800_000_000_000,
|
|
modelProvider: "native-provider",
|
|
});
|
|
|
|
expect(projection).toMatchObject({ importedMessages: 4, omittedMessages: 0 });
|
|
expect(projection.transcriptMessages.map(messageContent)).toEqual([
|
|
"First question",
|
|
[{ type: "text", text: "First answer" }],
|
|
"Second question",
|
|
[{ type: "text", text: "Second answer" }],
|
|
]);
|
|
expect(projection.transcriptMessages[1]).toMatchObject({
|
|
role: "assistant",
|
|
api: "openai-chatgpt-responses",
|
|
provider: "native-provider",
|
|
model: "native-history",
|
|
});
|
|
expect(projection.responseItems).toEqual([
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "First question" }],
|
|
},
|
|
{
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "First answer" }],
|
|
phase: "commentary",
|
|
},
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "Second question" }],
|
|
},
|
|
{
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "Second answer" }],
|
|
phase: "final_answer",
|
|
},
|
|
]);
|
|
expect(JSON.stringify(projection)).not.toContain("active tail");
|
|
expect(JSON.stringify(projection)).not.toContain("failed tail");
|
|
});
|
|
|
|
it("accepts terminal boundaries", () => {
|
|
for (const [status, stopReason] of [
|
|
["completed", "stop"],
|
|
["interrupted", "aborted"],
|
|
["failed", "error"],
|
|
] as const) {
|
|
const terminalThread = {
|
|
...thread,
|
|
turns: [
|
|
...(thread.turns?.slice(0, 2) ?? []),
|
|
{
|
|
id: `turn-${status}`,
|
|
status,
|
|
...(status === "failed" ? { error: { message: "provider disconnected" } } : {}),
|
|
items: [
|
|
{
|
|
id: `user-${status}`,
|
|
type: "userMessage",
|
|
content: [{ type: "text", text: `${status} question` }],
|
|
},
|
|
{
|
|
id: `assistant-${status}`,
|
|
type: "agentMessage",
|
|
text: `${status} answer`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as unknown as CodexThread;
|
|
const projection = projectBoundedCodexThreadHistory({
|
|
thread: terminalThread,
|
|
throughTurnId: `turn-${status}`,
|
|
importedAt: 1_800_000_000_000,
|
|
});
|
|
expect(messageContent(projection.transcriptMessages.at(-2))).toBe(`${status} question`);
|
|
const assistant = projection.transcriptMessages.at(-1);
|
|
expect(messageContent(assistant)).toEqual([{ type: "text", text: `${status} answer` }]);
|
|
expect(assistant).toMatchObject({ role: "assistant", stopReason });
|
|
expect(projection.responseItems).toHaveLength(status === "completed" ? 6 : 5);
|
|
expect(projection.responseItems.at(-1)).toEqual(
|
|
status === "completed"
|
|
? {
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "completed answer" }],
|
|
}
|
|
: {
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: `${status} question` }],
|
|
},
|
|
);
|
|
if (status === "failed") {
|
|
expect(assistant).toMatchObject({ errorMessage: "provider disconnected" });
|
|
} else {
|
|
expect(assistant).not.toHaveProperty("errorMessage");
|
|
}
|
|
}
|
|
});
|
|
|
|
it("enforces UTF-8 byte limits without splitting multibyte text", () => {
|
|
const oversizedText = `prefix-${"🙂".repeat(20_000)}-suffix`;
|
|
const oversizedThread = {
|
|
id: "thread-byte-bounds",
|
|
turns: Array.from({ length: 9 }, (_, index) => ({
|
|
id: `turn-${index}`,
|
|
status: "completed",
|
|
items: [
|
|
{
|
|
id: `user-${index}`,
|
|
type: "userMessage",
|
|
content: [{ type: "text", text: `${index}:${oversizedText}` }],
|
|
},
|
|
],
|
|
})),
|
|
} as unknown as CodexThread;
|
|
|
|
const projection = projectBoundedCodexThreadHistory({
|
|
thread: oversizedThread,
|
|
throughTurnId: "turn-8",
|
|
importedAt: 1_800_000_000_000,
|
|
});
|
|
const texts = projection.transcriptMessages.map((message) => {
|
|
const content = messageContent(message);
|
|
return typeof content === "string" ? content : "";
|
|
});
|
|
|
|
expect(projection).toMatchObject({ importedMessages: 8, omittedMessages: 1 });
|
|
expect(texts[0]).toMatch(/^1:prefix-/u);
|
|
expect(texts.every((text) => Buffer.byteLength(text, "utf8") <= 64 * 1024)).toBe(true);
|
|
expect(
|
|
texts.reduce((bytes, text) => bytes + Buffer.byteLength(text, "utf8"), 0),
|
|
).toBeLessThanOrEqual(512 * 1024);
|
|
expect(texts.every((text) => !text.includes("�"))).toBe(true);
|
|
expect(
|
|
texts.every((text) => text.endsWith("[Message truncated during Codex history import.]")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects a non-terminal or missing boundary and projects no history without one", () => {
|
|
expect(() =>
|
|
projectBoundedCodexThreadHistory({
|
|
thread,
|
|
throughTurnId: "turn-active",
|
|
importedAt: 1_800_000_000_000,
|
|
}),
|
|
).toThrow("Codex history boundary turn is not terminal: turn-active");
|
|
expect(() =>
|
|
projectBoundedCodexThreadHistory({
|
|
thread,
|
|
throughTurnId: "turn-missing",
|
|
importedAt: 1_800_000_000_000,
|
|
}),
|
|
).toThrow("Codex history boundary turn not found: turn-missing");
|
|
expect(
|
|
projectBoundedCodexThreadHistory({
|
|
thread,
|
|
throughTurnId: null,
|
|
importedAt: 1_800_000_000_000,
|
|
}),
|
|
).toEqual({
|
|
importedMessages: 0,
|
|
omittedMessages: 0,
|
|
responseItems: [],
|
|
transcriptMessages: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("mirrorCodexAppServerTranscript", () => {
|
|
it("hides current memory-maintenance messages without hiding replayed turns", async () => {
|
|
initializeGlobalHookRunner(
|
|
createMockPluginRegistry([
|
|
{
|
|
hookName: "before_message_write",
|
|
handler: (event) => {
|
|
const { display: _display, ...message } = (
|
|
event as { message: Record<string, unknown> }
|
|
).message;
|
|
return { message: castAgentMessage(message) };
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-memory-");
|
|
const messages = [
|
|
attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "ordinary prior reply" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-prior:assistant",
|
|
),
|
|
attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "Pre-compaction memory flush" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-memory:prompt",
|
|
),
|
|
attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "toolCall", id: "call-1", name: "write", arguments: {} }],
|
|
timestamp: Date.now() + 2,
|
|
}),
|
|
"turn-memory:tool-call:call-1",
|
|
),
|
|
attachCodexMirrorIdentity(
|
|
castAgentMessage({
|
|
role: "toolResult",
|
|
toolCallId: "call-1",
|
|
toolName: "write",
|
|
content: [{ type: "toolResult", toolCallId: "call-1", content: "saved" }],
|
|
timestamp: Date.now() + 3,
|
|
}),
|
|
"turn-memory:tool-result:call-1",
|
|
),
|
|
attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "NO_REPLY" }],
|
|
timestamp: Date.now() + 4,
|
|
}),
|
|
"turn-memory:assistant",
|
|
),
|
|
];
|
|
for (const message of messages.slice(1)) {
|
|
Object.assign(message, { display: false });
|
|
}
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages,
|
|
idempotencyScope: "codex-app-server:memory",
|
|
});
|
|
|
|
const persistedMessages = (await readMirrorEvents(target))
|
|
.map((event) =>
|
|
event && typeof event === "object" ? (event as { message?: unknown }).message : undefined,
|
|
)
|
|
.filter((message): message is Record<string, unknown> =>
|
|
Boolean(message && typeof message === "object"),
|
|
);
|
|
expect(persistedMessages).toHaveLength(messages.length);
|
|
expect(persistedMessages[0]).not.toHaveProperty("display", false);
|
|
expect(persistedMessages.slice(1).every((message) => message.display === false)).toBe(true);
|
|
});
|
|
|
|
it("mirrors user, assistant, and tool result messages by SQLite identity", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-basic-");
|
|
const userMessage = makeAgentUserMessage({
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: Date.now(),
|
|
});
|
|
const assistantMessage = makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "hi there" }],
|
|
timestamp: Date.now() + 1,
|
|
});
|
|
const toolResultMessage = castAgentMessage({
|
|
role: "toolResult",
|
|
toolCallId: "call-1",
|
|
toolName: "read",
|
|
content: [{ type: "toolResult", toolCallId: "call-1", content: "read output" }],
|
|
timestamp: Date.now() + 2,
|
|
}) as MirroredAgentMessage;
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage, assistantMessage, toolResultMessage],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
const raw = await readMirrorRaw(target);
|
|
expect(raw).toContain('"role":"user"');
|
|
expect(raw).toContain('"content":[{"type":"text","text":"hello"}]');
|
|
expect(raw).toContain('"role":"assistant"');
|
|
expect(raw).toContain('"content":[{"type":"text","text":"hi there"}]');
|
|
expect(raw).toContain('"role":"toolResult"');
|
|
expect(raw).toContain('"toolCallId":"call-1"');
|
|
expect(raw).toContain('"content":"read output"');
|
|
expect(raw).toContain(`"idempotencyKey":"scope-1:user:${expectedFingerprint(userMessage)}"`);
|
|
expect(raw).toContain(
|
|
`"idempotencyKey":"scope-1:assistant:${expectedFingerprint(assistantMessage)}"`,
|
|
);
|
|
expect(raw).toContain(
|
|
`"idempotencyKey":"scope-1:toolResult:${expectedFingerprint(toolResultMessage)}"`,
|
|
);
|
|
await expect(fs.readFile(target.bogusSessionFile, "utf8")).rejects.toHaveProperty(
|
|
"code",
|
|
"ENOENT",
|
|
);
|
|
});
|
|
|
|
it("preserves gateway user-turn identity across Codex transcript mirroring", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-user-identity-");
|
|
const userMessage = castAgentMessage({
|
|
...makeAgentUserMessage({
|
|
content: [{ type: "text", text: "client prompt" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
idempotencyKey: "client-run:user",
|
|
}) as MirroredAgentMessage;
|
|
|
|
const first = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
const second = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
|
|
const raw = await readMirrorRaw(target);
|
|
expect(raw).toContain('"idempotencyKey":"client-run:user"');
|
|
expect(raw).toContain('"mirrorOrigin":"codex-app-server"');
|
|
expect(raw).not.toContain('"idempotencyKey":"codex-app-server:thread-1:');
|
|
expect(first.userMessagesPresent).toHaveLength(1);
|
|
expect(second.userMessagesPresent).toHaveLength(1);
|
|
expect(first.userMessageReceipts).toHaveLength(1);
|
|
expect(second.userMessageReceipts).toHaveLength(1);
|
|
expect(second.userMessageReceipts[0]?.anchor.entryId).toBe(
|
|
first.userMessageReceipts[0]?.anchor.entryId,
|
|
);
|
|
expect(
|
|
(await readMirrorMessages(target)).filter((message) => message.role === "user"),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("preserves mirror identity across redaction from prompt append through final snapshot", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-redacted-identity-");
|
|
const config = { logging: { redactPatterns: [String.raw`^codex-app-server:.*$`] } };
|
|
const userMessage = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "client prompt" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:prompt",
|
|
);
|
|
const assistantMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "final answer" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
const mirrorParams = {
|
|
...target,
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
config,
|
|
};
|
|
|
|
await mirrorCodexAppServerTranscript({ ...mirrorParams, messages: [userMessage] });
|
|
const finalMirror = await mirrorCodexAppServerTranscript({
|
|
...mirrorParams,
|
|
messages: [userMessage, assistantMessage],
|
|
});
|
|
|
|
expect(finalMirror.assistantMirrorIdentitiesOwned).toEqual(["turn-1:assistant"]);
|
|
expect(await readMirrorMessages(target)).toEqual([
|
|
{ role: "user", text: "client prompt" },
|
|
{ role: "assistant", text: "final answer" },
|
|
]);
|
|
const raw = await readMirrorRaw(target);
|
|
expect(raw).toContain('"idempotencyKey":"codex-app-server:thread-1:turn-1:prompt"');
|
|
expect(raw).toContain('"idempotencyKey":"codex-app-server:thread-1:turn-1:assistant"');
|
|
});
|
|
|
|
it("emits message-bearing updates for newly appended mirrored messages only", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-live-updates-");
|
|
const userMessage = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "show me live" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:prompt",
|
|
);
|
|
|
|
const firstMirror = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
const secondMirror = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
|
|
const updates = publishSessionTranscriptUpdateByIdentityMock.mock.calls.map(
|
|
([update]) => update as Record<string, unknown> & { update?: Record<string, unknown> },
|
|
);
|
|
expect(updates).toHaveLength(1);
|
|
expect(updates[0]?.sessionKey).toBe(target.sessionKey);
|
|
expect(updates[0]?.storePath).toBe(target.storePath);
|
|
expect(updates[0]?.update?.messageId).toEqual(expect.any(String));
|
|
expect(updates[0]?.update?.message).toMatchObject({
|
|
role: "user",
|
|
content: [{ type: "text", text: "show me live" }],
|
|
idempotencyKey: "codex-app-server:thread-1:turn-1:prompt",
|
|
});
|
|
expect(updates[0]?.update?.messageSeq).toBe(1);
|
|
expect(firstMirror.userMessagesPresent).toHaveLength(1);
|
|
expect(firstMirror.userMessagesPresent[0]).toMatchObject({
|
|
role: "user",
|
|
content: [{ type: "text", text: "show me live" }],
|
|
idempotencyKey: "codex-app-server:thread-1:turn-1:prompt",
|
|
});
|
|
expect(secondMirror.userMessagesPresent).toHaveLength(1);
|
|
expect(secondMirror.userMessagesPresent[0]).toMatchObject({
|
|
role: "user",
|
|
content: [{ type: "text", text: "show me live" }],
|
|
idempotencyKey: "codex-app-server:thread-1:turn-1:prompt",
|
|
});
|
|
});
|
|
|
|
it("emits stable sequence numbers for multi-message mirror batches", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-seq-");
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [
|
|
attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "first" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:prompt",
|
|
),
|
|
attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "second" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:assistant",
|
|
),
|
|
],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
|
|
const updates = publishSessionTranscriptUpdateByIdentityMock.mock.calls.map(
|
|
([update]) => update as Record<string, unknown> & { update?: Record<string, unknown> },
|
|
);
|
|
expect(updates.map((update) => update.update?.messageSeq)).toEqual([1, 2]);
|
|
expect(
|
|
updates.map((update) => {
|
|
const message = update.update?.message as { role?: string } | undefined;
|
|
return message?.role;
|
|
}),
|
|
).toEqual(["user", "assistant"]);
|
|
});
|
|
|
|
it("keeps assistant ownership when live update publication fails", async () => {
|
|
publishSessionTranscriptUpdateByIdentityMock.mockRejectedValueOnce(new Error("publish failed"));
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-publish-failure-");
|
|
const assistantMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "durably persisted" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
|
|
const result = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [assistantMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
|
|
expect(result.assistantMirrorIdentitiesOwned).toEqual(["turn-1:assistant"]);
|
|
expect(await readMirrorRaw(target)).toContain('"role":"assistant"');
|
|
});
|
|
|
|
it("rejects mirror writes without a runtime session identity", async () => {
|
|
await expect(
|
|
mirrorCodexAppServerTranscript({
|
|
sessionId: "session-1",
|
|
messages: [
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "no identity" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
],
|
|
}),
|
|
).rejects.toThrow("runtime session identity");
|
|
});
|
|
|
|
it("deduplicates app-server turn mirrors by idempotency scope", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-dedupe-");
|
|
const messages = [
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "hi there" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
] as const;
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [...messages],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [...messages],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
expect((await readMirrorMessages(target)).filter((message) => message.role)).toHaveLength(2);
|
|
});
|
|
|
|
it("serializes concurrent mirrors with the same supplied identity", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-concurrent-");
|
|
const message = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "append once" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:prompt",
|
|
);
|
|
|
|
const results = await Promise.all([
|
|
mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [message],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
}),
|
|
mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [message],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
}),
|
|
]);
|
|
|
|
expect((await readMirrorMessages(target)).filter((entry) => entry.role)).toEqual([
|
|
{ role: "user", text: "append once" },
|
|
]);
|
|
expect(results.map((result) => messageContent(result.userMessagesPresent[0]))).toEqual([
|
|
[{ type: "text", text: "append once" }],
|
|
[{ type: "text", text: "append once" }],
|
|
]);
|
|
});
|
|
|
|
it("reports final assistant ownership for new and idempotent mirrors", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-assistant-owned-");
|
|
const assistantMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "owned once" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
|
|
const firstMirror = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [assistantMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
const secondMirror = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [assistantMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
|
|
expect(firstMirror.assistantMirrorIdentitiesOwned).toEqual(["turn-1:assistant"]);
|
|
expect(secondMirror.assistantMirrorIdentitiesOwned).toEqual(["turn-1:assistant"]);
|
|
expect(
|
|
(await readMirrorMessages(target)).filter((message) => message.role === "assistant"),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("runs before_message_write before appending mirrored transcript messages", async () => {
|
|
initializeGlobalHookRunner(
|
|
createMockPluginRegistry([
|
|
{
|
|
hookName: "before_message_write",
|
|
handler: (event) => ({
|
|
message: castAgentMessage({
|
|
...((event as { message: unknown }).message as Record<string, unknown>),
|
|
content: [{ type: "text", text: "hello [hooked]" }],
|
|
}),
|
|
}),
|
|
},
|
|
]),
|
|
);
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-hook-");
|
|
const sourceMessage = makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [sourceMessage],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
const raw = await readMirrorRaw(target);
|
|
expect(raw).toContain('"content":[{"type":"text","text":"hello [hooked]"}]');
|
|
expect(raw).toContain(
|
|
`"idempotencyKey":"scope-1:assistant:${expectedFingerprint(sourceMessage)}"`,
|
|
);
|
|
});
|
|
|
|
it("returns the persisted user message for duplicate mirror hits", async () => {
|
|
initializeGlobalHookRunner(
|
|
createMockPluginRegistry([
|
|
{
|
|
hookName: "before_message_write",
|
|
handler: (event) => ({
|
|
message: castAgentMessage({
|
|
...((event as { message: unknown }).message as Record<string, unknown>),
|
|
content: [{ type: "text", text: "[redacted by hook]" }],
|
|
}),
|
|
}),
|
|
},
|
|
]),
|
|
);
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-duplicates-");
|
|
const sourceMessage = makeAgentUserMessage({
|
|
content: [{ type: "text", text: "secret prompt" }],
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
const first = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [sourceMessage],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
const second = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [sourceMessage],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
expect(first.userMessagesPresent[0]?.content).toEqual([
|
|
{ type: "text", text: "[redacted by hook]" },
|
|
]);
|
|
expect(second.userMessagesPresent[0]?.content).toEqual([
|
|
{ type: "text", text: "[redacted by hook]" },
|
|
]);
|
|
expect(JSON.stringify(second.userMessagesPresent)).not.toContain("secret prompt");
|
|
expect(
|
|
(await readMirrorMessages(target)).filter((message) => message.role === "user"),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("preserves the computed idempotency key when hooks rewrite message keys", async () => {
|
|
initializeGlobalHookRunner(
|
|
createMockPluginRegistry([
|
|
{
|
|
hookName: "before_message_write",
|
|
handler: (event) => ({
|
|
message: castAgentMessage({
|
|
...((event as { message: unknown }).message as Record<string, unknown>),
|
|
idempotencyKey: "hook-rewritten-key",
|
|
}),
|
|
}),
|
|
},
|
|
]),
|
|
);
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-key-hook-");
|
|
const sourceMessage = makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [sourceMessage],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
const raw = await readMirrorRaw(target);
|
|
expect(raw).toContain(
|
|
`"idempotencyKey":"scope-1:assistant:${expectedFingerprint(sourceMessage)}"`,
|
|
);
|
|
expect(raw).not.toContain("hook-rewritten-key");
|
|
});
|
|
|
|
it("respects before_message_write blocking decisions", async () => {
|
|
initializeGlobalHookRunner(
|
|
createMockPluginRegistry([
|
|
{ hookName: "before_message_write", handler: () => ({ block: true }) },
|
|
]),
|
|
);
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-blocked-");
|
|
|
|
const result = await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [
|
|
attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "should not persist" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
),
|
|
],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
expect(result.assistantMirrorIdentitiesOwned).toEqual(["turn-1:assistant"]);
|
|
expect(await readMirrorMessages(target)).toEqual([]);
|
|
});
|
|
|
|
it("skips transcript mirrors for sessionless embedded runs", async () => {
|
|
const root = await makeRoot("openclaw-codex-transcript-failure-");
|
|
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
|
|
const markRuntimePersistencePending = vi.fn();
|
|
const assistantMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "needs fallback persistence" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
|
|
const params = {
|
|
prompt: "sessionless prompt",
|
|
runId: "probe-setup-inference-sessionless",
|
|
sessionId: "session-1",
|
|
userTurnTranscriptRecorder: {
|
|
markRuntimePersistencePending,
|
|
resolveMessage: async () => undefined,
|
|
},
|
|
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"];
|
|
|
|
await mirrorPromptAtTurnStartBestEffort({
|
|
params,
|
|
sessionKey: "agent:main:setup-inference:incognito-session-1",
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: root,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
upstreamUserText: "sessionless prompt",
|
|
});
|
|
const mirrorOutcome = await mirrorTranscriptBestEffort({
|
|
params,
|
|
result: {
|
|
messagesSnapshot: [assistantMessage],
|
|
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
|
|
sessionKey: "agent:main:setup-inference:incognito-session-1",
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: root,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
expect(mirrorOutcome).toEqual({ assistantTranscriptOwned: false, mirroredMessages: [] });
|
|
expect(markRuntimePersistencePending).not.toHaveBeenCalled();
|
|
expect(warn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders normal-session mirror failures in structured warnings", async () => {
|
|
const root = await makeRoot("openclaw-codex-transcript-failure-");
|
|
const blockedParent = path.join(root, "not-a-directory");
|
|
await fs.writeFile(blockedParent, "blocked");
|
|
const storePath = path.join(blockedParent, "openclaw-agent.sqlite");
|
|
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
|
|
warn.mockClear();
|
|
const runId = "run-1";
|
|
const sessionId = "session-1";
|
|
const params = {
|
|
prompt: "persist me",
|
|
runId,
|
|
sessionId,
|
|
sessionTarget: { storePath },
|
|
} as unknown as Parameters<typeof mirrorPromptAtTurnStartBestEffort>[0]["params"];
|
|
|
|
await mirrorPromptAtTurnStartBestEffort({
|
|
params,
|
|
sessionKey: "agent:main:session-1",
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: storePath,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
upstreamUserText: "persist me",
|
|
});
|
|
|
|
expect(warn).toHaveBeenCalledWith("failed to mirror codex app-server prompt at turn start", {
|
|
error: expect.any(String),
|
|
runId,
|
|
sessionId,
|
|
});
|
|
const warning = warn.mock.calls.at(-1)?.[1] as { error?: string } | undefined;
|
|
expect(warning?.error).not.toBe("");
|
|
|
|
warn.mockClear();
|
|
await mirrorTranscriptBestEffort({
|
|
params,
|
|
result: {
|
|
messagesSnapshot: [
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "persist me too" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
],
|
|
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
|
|
sessionKey: "agent:main:session-1",
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: root,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
expect(warn).toHaveBeenCalledWith("failed to mirror codex app-server transcript", {
|
|
error: expect.any(String),
|
|
runId,
|
|
sessionId,
|
|
});
|
|
});
|
|
|
|
it("does not attest a stale idempotency hit with the same mirror identity", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-stale-identity-");
|
|
const staleMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "stale answer" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [staleMessage],
|
|
idempotencyScope: "codex-app-server:thread-1",
|
|
});
|
|
const currentMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "current answer" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
|
|
const mirrorOutcome = await mirrorTranscriptBestEffort({
|
|
params: {
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
sessionTarget: target,
|
|
suppressNextUserMessagePersistence: true,
|
|
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"],
|
|
result: {
|
|
messagesSnapshot: [currentMessage],
|
|
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
|
|
agentId: target.agentId,
|
|
sessionKey: target.sessionKey,
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: target.storePath,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
expect(mirrorOutcome.assistantTranscriptOwned).toBe(true);
|
|
expect(mirrorOutcome.mirroredMessages).toEqual([]);
|
|
});
|
|
|
|
it("attests the exact persisted payload after a message-write hook transforms it", async () => {
|
|
initializeGlobalHookRunner(
|
|
createMockPluginRegistry([
|
|
{
|
|
hookName: "before_message_write",
|
|
handler: () => ({
|
|
message: castAgentMessage({
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "[redacted by hook]" }],
|
|
}),
|
|
}),
|
|
},
|
|
]),
|
|
);
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-attested-hook-");
|
|
const sourceMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "sensitive answer" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
|
|
const mirrorOutcome = await mirrorTranscriptBestEffort({
|
|
params: {
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
sessionTarget: target,
|
|
suppressNextUserMessagePersistence: true,
|
|
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"],
|
|
result: {
|
|
messagesSnapshot: [sourceMessage],
|
|
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
|
|
agentId: target.agentId,
|
|
sessionKey: target.sessionKey,
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: target.storePath,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
expect(mirrorOutcome.assistantTranscriptOwned).toBe(true);
|
|
expect(mirrorOutcome.assistantTranscriptIdempotencyKey).toBe(
|
|
"codex-app-server:thread-1:turn-1:assistant",
|
|
);
|
|
expect(mirrorOutcome.mirroredMessages).toMatchObject([
|
|
{ role: "assistant", content: [{ type: "text", text: "[redacted by hook]" }] },
|
|
]);
|
|
expect(JSON.stringify(mirrorOutcome.mirroredMessages)).not.toContain("sensitive answer");
|
|
});
|
|
|
|
it("returns the final mirrored row as the terminal anchor", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-terminal-anchor-");
|
|
const assistantMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "toolCall", id: "call-1", name: "read", arguments: {} }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
const toolResultMessage = attachCodexMirrorIdentity(
|
|
castAgentMessage({
|
|
role: "toolResult",
|
|
toolCallId: "call-1",
|
|
toolName: "read",
|
|
content: [{ type: "toolResult", toolCallId: "call-1", content: "done" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:tool-result:call-1",
|
|
);
|
|
|
|
const mirrorOutcome = await mirrorTranscriptBestEffort({
|
|
params: {
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
sessionTarget: target,
|
|
suppressNextUserMessagePersistence: true,
|
|
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"],
|
|
result: {
|
|
messagesSnapshot: [assistantMessage, toolResultMessage],
|
|
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
|
|
agentId: target.agentId,
|
|
sessionKey: target.sessionKey,
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: target.storePath,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
const terminalEvent = (await readMirrorEvents(target)).find(
|
|
(event): event is { id: string; message: { role: string } } =>
|
|
Boolean(
|
|
event &&
|
|
typeof event === "object" &&
|
|
"id" in event &&
|
|
"message" in event &&
|
|
(event as { message?: { role?: unknown } }).message?.role === "toolResult",
|
|
),
|
|
);
|
|
|
|
expect(mirrorOutcome.assistantTranscriptOwned).toBe(true);
|
|
expect(mirrorOutcome.terminalAnchor?.entryId).toBe(terminalEvent?.id);
|
|
});
|
|
|
|
it("returns the user anchor for a turn without an assistant row", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-user-terminal-");
|
|
const userMessage = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "run silently" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:prompt",
|
|
);
|
|
|
|
const mirrorOutcome = await mirrorTranscriptBestEffort({
|
|
params: {
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
sessionTarget: target,
|
|
suppressNextUserMessagePersistence: true,
|
|
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"],
|
|
result: {
|
|
messagesSnapshot: [userMessage],
|
|
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
|
|
agentId: target.agentId,
|
|
sessionKey: target.sessionKey,
|
|
notifyUserMessagePersisted: () => undefined,
|
|
cwd: target.storePath,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
const terminalEvent = (await readMirrorEvents(target)).find(
|
|
(event): event is { id: string; message: { role: string } } =>
|
|
Boolean(
|
|
event &&
|
|
typeof event === "object" &&
|
|
"id" in event &&
|
|
"message" in event &&
|
|
(event as { message?: { role?: unknown } }).message?.role === "user",
|
|
),
|
|
);
|
|
|
|
expect(mirrorOutcome.assistantTranscriptOwned).toBe(false);
|
|
expect(mirrorOutcome.terminalAnchor?.entryId).toBe(terminalEvent?.id);
|
|
});
|
|
|
|
it("dedupes mirrored messages despite snapshot positional shifts", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-shift-");
|
|
const userMessage = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: Date.now(),
|
|
}),
|
|
"turn-1:prompt",
|
|
);
|
|
const assistantMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "hi there" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage, assistantMessage],
|
|
idempotencyScope: "codex-app-server:thread-X",
|
|
});
|
|
const reasoningMessage = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "[Codex reasoning] thinking" }],
|
|
timestamp: Date.now() + 2,
|
|
}),
|
|
"turn-1:reasoning",
|
|
);
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage, reasoningMessage, assistantMessage],
|
|
idempotencyScope: "codex-app-server:thread-X",
|
|
});
|
|
|
|
expect((await readMirrorMessages(target)).map((m) => m.text)).toEqual([
|
|
"hello",
|
|
"hi there",
|
|
"[Codex reasoning] thinking",
|
|
]);
|
|
});
|
|
|
|
it("keeps repeated same-content turns distinct", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-repeat-");
|
|
const userTurn1 = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({ content: [{ type: "text", text: "yes" }], timestamp: Date.now() }),
|
|
"turn-1:prompt",
|
|
);
|
|
const assistantTurn1 = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "ok 1" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
const userTurn2 = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({ content: [{ type: "text", text: "yes" }], timestamp: Date.now() + 2 }),
|
|
"turn-2:prompt",
|
|
);
|
|
const assistantTurn2 = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "ok 2" }],
|
|
timestamp: Date.now() + 3,
|
|
}),
|
|
"turn-2:assistant",
|
|
);
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userTurn1, assistantTurn1],
|
|
idempotencyScope: "codex-app-server:thread-X",
|
|
});
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userTurn2, assistantTurn2],
|
|
idempotencyScope: "codex-app-server:thread-X",
|
|
});
|
|
|
|
expect(await readMirrorMessages(target)).toEqual([
|
|
{ role: "user", text: "yes" },
|
|
{ role: "assistant", text: "ok 1" },
|
|
{ role: "user", text: "yes" },
|
|
{ role: "assistant", text: "ok 2" },
|
|
]);
|
|
});
|
|
|
|
it("dedupes prior-turn entries re-emitted into a later turn's snapshot", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-reemit-");
|
|
const userTurn1 = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({ content: [{ type: "text", text: "msg1" }], timestamp: Date.now() }),
|
|
"turn-1:prompt",
|
|
);
|
|
const assistantTurn1 = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "reply1" }],
|
|
timestamp: Date.now() + 1,
|
|
}),
|
|
"turn-1:assistant",
|
|
);
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userTurn1, assistantTurn1],
|
|
idempotencyScope: "codex-app-server:thread-X",
|
|
});
|
|
|
|
const userTurn2 = attachCodexMirrorIdentity(
|
|
makeAgentUserMessage({
|
|
content: [{ type: "text", text: "msg2" }],
|
|
timestamp: Date.now() + 2,
|
|
}),
|
|
"turn-2:prompt",
|
|
);
|
|
const assistantTurn2 = attachCodexMirrorIdentity(
|
|
makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "reply2" }],
|
|
timestamp: Date.now() + 3,
|
|
}),
|
|
"turn-2:assistant",
|
|
);
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userTurn1, assistantTurn1, userTurn2, assistantTurn2],
|
|
idempotencyScope: "codex-app-server:thread-X",
|
|
});
|
|
|
|
expect(await readMirrorMessages(target)).toEqual([
|
|
{ role: "user", text: "msg1" },
|
|
{ role: "assistant", text: "reply1" },
|
|
{ role: "user", text: "msg2" },
|
|
{ role: "assistant", text: "reply2" },
|
|
]);
|
|
});
|
|
|
|
it("uses the role+content fingerprint when no identity is attached", async () => {
|
|
const target = await createSqliteMirrorTarget("openclaw-codex-mirror-fingerprint-");
|
|
const userMessage = makeAgentUserMessage({
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: Date.now(),
|
|
});
|
|
const assistantMessage = makeAgentAssistantMessage({
|
|
content: [{ type: "text", text: "hi there" }],
|
|
timestamp: Date.now() + 1,
|
|
});
|
|
|
|
await mirrorCodexAppServerTranscript({
|
|
...target,
|
|
messages: [userMessage, assistantMessage],
|
|
idempotencyScope: "scope-1",
|
|
});
|
|
|
|
const raw = await readMirrorRaw(target);
|
|
expect(raw).toContain(`"idempotencyKey":"scope-1:user:${expectedFingerprint(userMessage)}"`);
|
|
expect(raw).toContain(
|
|
`"idempotencyKey":"scope-1:assistant:${expectedFingerprint(assistantMessage)}"`,
|
|
);
|
|
});
|
|
});
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|