import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "bun:test";
import { SessionManager } from "@earendil-works/pi-coding-agent";
import { DEFAULT_CODEX_NATIVE_SETTINGS } from "../src/contributions/xsettings.ts";
import {
PERSISTENT_ENTRY,
persistentHistory,
persistentInstructions,
persistentRequest,
} from "../src/provider/persistent-mode.ts";
import type { ResponsesBody } from "../src/provider/types.ts";
const tools = ["request_user_input_async", "send_message_to_user_async", "clock__sleep", "clock__curr_time"];
const body: ResponsesBody = {
model: "gpt-6-astra",
store: false,
stream: true,
input: [{ role: "user", content: "Inspect the repo" }],
instructions: "User-owned provider instructions",
tools: tools.map((name) => ({ type: "function", name })),
text: { verbosity: "low" },
include: [],
tool_choice: "auto",
parallel_tool_calls: false,
reasoning: { effort: "high", summary: "auto" },
};
const now = Date.parse("2026-09-13T12:00:00Z");
const settings = { reasoningMode: "persistent", currentTimeReminder: "auto" } as const;
const nextBody = {
...body,
input: [...body.input, { role: "assistant", content: "Checking." }, { role: "user", content: "Continue" }],
};
test("vendored prompts match the pinned Codex revision byte for byte", () => {
for (const [file, hash] of [
["persistent-mode.md", "03ff9da48abb85425514c42dad1b742312bc7e2d978d2941e9d2fd90560c2a27"],
["persistent-mode-astra.md", "30747e79e50efb64f91cfff3d5ef1882e347081e81448918d5f7d5358e98c523"],
])
expect(
createHash("sha256")
.update(readFileSync(new URL(`../assets/${file}`, import.meta.url)))
.digest("hex"),
).toBe(hash!);
});
test("ordinary mode preserves reasoning and async tools", () => {
const result = persistentRequest(body, DEFAULT_CODEX_NATIVE_SETTINGS, tools, now);
expect(result.body).toBe(body);
expect(result.added).toBeUndefined();
});
test("Astra catalog wins, with exact upstream text, and other models use fallback", () => {
const result = persistentRequest(body, settings, tools, now);
expect(result.body.reasoning).toEqual({ effort: "disabled", summary: "auto" });
expect(result.body.tools).toBe(body.tools);
expect(result.body.instructions).toBe(body.instructions);
expect(result.body.input[1]).toEqual({
role: "developer",
content: `\n${persistentInstructions("gpt-6-astra", true)}\n`,
});
expect(result.added?.instructions).toContain("Because a `final` answer immediately ends the turn");
expect(result.added?.instructions).not.toContain("replace all previously");
const other = persistentRequest({ ...body, model: "gpt-5.5" }, settings, tools, now);
expect(other.added?.instructions).toContain("After you've completed the user task");
expect(other.added?.instructions).toContain("via functions.send_user_message_async");
});
test("unchanged state preserves the request prefix; due clocks append after new messages", () => {
const first = persistentRequest(body, settings, tools, now);
const soon = persistentRequest(nextBody, settings, tools, now + 900, first.state);
expect(soon.added?.instructions).toBeUndefined();
expect(soon.added?.reminderTime).toBeUndefined();
expect(soon.body.input).toEqual([...first.body.input, ...nextBody.input.slice(1)]);
const later = persistentRequest(nextBody, settings, tools, now + 1000, soon.state);
expect(later.body.input.slice(0, -1)).toEqual(soon.body.input);
expect(later.body.input.at(-1)).toEqual({
role: "developer",
content: "It is 2026-09-13 12:00:01 UTC.",
});
expect(later.added?.instructions).toBeUndefined();
});
test("model and mode changes record replacements, a single removal, and fresh reactivation", () => {
const first = persistentRequest(body, settings, tools, now);
const changed = persistentRequest({ ...nextBody, model: "gpt-5.5" }, settings, tools, now, first.state);
expect(JSON.stringify(changed.body.input.at(-1))).toContain("replace all previously provided");
const off = persistentRequest(nextBody, DEFAULT_CODEX_NATIVE_SETTINGS, tools, now, changed.state);
expect(off.body.reasoning).toEqual(body.reasoning);
expect(off.body.input.at(-1)).toEqual({
role: "developer",
content:
"\nThe previously provided persistent-mode instructions no longer apply.\n",
});
const again = persistentRequest(nextBody, DEFAULT_CODEX_NATIVE_SETTINGS, tools, now + 1000, off.state);
expect(again.added).toBeUndefined();
expect(again.body).toEqual(off.body);
const on = persistentRequest(nextBody, settings, tools, now, again.state);
expect(JSON.stringify(on.body.input.at(-1))).not.toContain("replace all previously");
});
test("explicit clock settings override persistent defaults without removing recorded history", () => {
const off = persistentRequest(body, { ...settings, currentTimeReminder: "off" }, [], now);
expect(JSON.stringify(off.body.input)).not.toContain("current_time_reminder");
const ordinary = persistentRequest(body, { reasoningMode: "pi", currentTimeReminder: "on" }, tools, now);
expect(ordinary.body.input).toHaveLength(2);
expect(ordinary.body.reasoning).toEqual(body.reasoning);
});
test("journal resumes, follows branches, validates saved entries, and resets stale context", () => {
const session = SessionManager.inMemory();
const first = persistentRequest(body, settings, tools, now);
const fork = session.appendCustomEntry(PERSISTENT_ENTRY, first.added!);
const restored = persistentRequest(nextBody, settings, tools, now, persistentHistory(session.getBranch()));
expect(restored.added?.instructions).toBeUndefined();
expect(restored.added?.reminderTime).toBeUndefined();
expect(restored.body.input).toEqual([...first.body.input, ...nextBody.input.slice(1)]);
const off = persistentRequest(nextBody, DEFAULT_CODEX_NATIVE_SETTINGS, tools, now, restored.state);
session.appendCustomEntry(PERSISTENT_ENTRY, off.added!);
session.branch(fork);
expect(persistentHistory(session.getBranch())).toEqual(first.state);
session.appendCustomEntry(PERSISTENT_ENTRY, { version: 1, at: -1, prefix: "bad", instructions: 2 });
expect(persistentHistory(session.getBranch())).toEqual(first.state);
const reset = persistentRequest(
{ ...body, input: [{ role: "user", content: "Fresh context" }] },
settings,
tools,
now,
restored.state,
"new-window",
);
expect(reset.added?.reset).toBe(true);
expect(reset.body.input).toHaveLength(3);
session.appendCustomEntry(PERSISTENT_ENTRY, reset.added!);
expect(persistentHistory(session.getBranch())).toEqual(reset.state);
const fresh = persistentRequest(body, DEFAULT_CODEX_NATIVE_SETTINGS, tools, now, restored.state, "new-window");
expect(JSON.stringify(fresh.body.input)).not.toContain("persistent_mode");
});
test("developer context survives an actual Pi session file close and reopen", async () => {
const directory = await mkdtemp(join(tmpdir(), "pi-persistent-resume-"));
try {
const manager = SessionManager.create(directory, directory);
manager.appendMessage({ role: "user", content: "Inspect the repo", timestamp: now });
const first = persistentRequest(body, settings, tools, now);
manager.appendCustomEntry(PERSISTENT_ENTRY, first.added!);
manager.appendMessage({
role: "assistant",
content: [{ type: "text", text: "Done." }],
api: "openai-codex-responses",
provider: "openai-codex",
model: "gpt-6-astra",
timestamp: now,
stopReason: "stop",
usage: {
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
});
const restored = SessionManager.open(manager.getSessionFile()!);
const replay = persistentRequest(nextBody, settings, tools, now, persistentHistory(restored.getBranch()));
expect(replay.added?.instructions).toBeUndefined();
expect(replay.added?.reminderTime).toBeUndefined();
expect(replay.body.input).toEqual([...first.body.input, ...nextBody.input.slice(1)]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("native transport serializes configured Persistent and restores ordinary requests", async () => {
const { zstdDecompressSync } = await import("node:zlib");
const { CodexProviderRuntime } = await import("../src/provider/runtime.ts");
const { getCodexModels } = await import("../src/provider/models.ts");
const requests: string[] = [];
let mode: "pi" | "persistent" = "persistent";
const runtime = new CodexProviderRuntime({
getSettings: () => ({ ...DEFAULT_CODEX_NATIVE_SETTINGS, reasoningMode: mode }),
now: () => now,
});
const model = getCodexModels().find((model) => model.id === "gpt-6-astra")!;
const capture: typeof fetch = Object.assign(
async (input: Parameters[0], init?: Parameters[1]) => {
const request = new Request(input, init);
const bytes = Buffer.from(await request.arrayBuffer());
requests.push(
(request.headers.get("content-encoding") === "zstd" ? zstdDecompressSync(bytes) : bytes).toString(),
);
const events = [
{
type: "response.output_item.done",
output_index: 0,
item: {
type: "message",
id: "message-1",
role: "assistant",
content: [{ type: "output_text", text: "Done." }],
},
},
{
type: "response.completed",
response: {
id: `response-${requests.length}`,
status: "completed",
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
},
},
];
return new Response(events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), {
headers: { "content-type": "text/event-stream" },
});
},
{ preconnect: fetch.preconnect },
);
const account = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "local-test" } }),
).toString("base64url");
try {
for (const nextMode of ["persistent", "pi"] as const) {
mode = nextMode;
const stream = runtime.streamSimple(
model,
{ messages: [{ role: "user", content: "Hello", timestamp: now }] },
{
apiKey: `test.${account}.signature`,
sessionId: "persistent-test",
transport: "sse",
reasoning: "high",
fetch: capture,
},
);
const result = await stream.result();
expect(result.stopReason).toBe("stop");
}
expect(requests).toHaveLength(2);
expect(JSON.parse(requests[0]!)).toMatchObject({ reasoning: { effort: "disabled" } });
expect(requests[0]).toContain("current_time_reminder");
expect(JSON.parse(requests[1]!)).toMatchObject({ reasoning: { effort: "disabled" } });
expect(JSON.parse(requests[1]!).input.at(-1)).toEqual({
type: "configuration_update",
reasoning: { effort: "high" },
});
expect(requests[1]).toContain("no longer apply");
expect(requests[1].match(/current_time_reminder>/g)).toHaveLength(2);
} finally {
runtime.shutdown();
}
});
test("reminder delivery consumes a user boundary even when its interval suppresses the reminder", () => {
const clock = {
...settings,
currentTimeReminderIntervalSeconds: "2",
currentTimeReminderDelivery: "after_user_or_tool_output" as const,
};
const first = persistentRequest(body, clock, tools, now);
const early = persistentRequest(nextBody, clock, tools, now + 1000, first.state);
expect(early.added?.reminderTime).toBeUndefined();
const noBoundary = persistentRequest(nextBody, clock, tools, now + 3000, early.state);
expect(noBoundary.added).toBeUndefined();
const output = {
...nextBody,
input: [...nextBody.input, { type: "function_call_output", call_id: "call", output: "done" }],
};
const due = persistentRequest(output, clock, tools, now + 3000, noBoundary.state);
expect(due.added?.reminderTime).toBe(now + 3000);
const reset = persistentRequest(body, clock, tools, now + 3001, due.state, "fresh");
expect(reset.added?.reminderTime).toBe(now + 3001);
const zero = persistentRequest(
body,
{ ...settings, currentTimeReminderIntervalSeconds: "0" },
tools,
now,
first.state,
);
expect(zero.added?.reminderTime).toBe(now);
expect(() => persistentRequest(body, { ...settings, currentTimeReminderIntervalSeconds: "-1" }, tools, now)).toThrow(
"Time reminder interval",
);
const huge = persistentRequest(
body,
{ ...settings, currentTimeReminderIntervalSeconds: "18446744073709551615" },
tools,
now + 100000,
first.state,
);
expect(huge.added?.reminderTime).toBeUndefined();
});