mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 14:12:51 +00:00
Opening a Codex agent whose timeline the daemon doesn't hold asked Codex for the whole thread in one thread/read reply. Past the 128 MiB provider line cap that reply killed the session; before the cap it crashed the daemon. Codex 0.145 and newer reject that read on paginated threads, so importing them failed. Paginated threads now load through thread/items/list pages. A legacy thread keeps its single read. A reply over the line cap fails only its own request: for a legacy read, a warning row names the archive, `codex migrate-rollouts`, unarchive steps; for a page, a warning row ends the history there. Items are capped as they are projected, and rewind's fork no longer returns the thread's turns. Co-authored-by: hunterricks <hr@savagestrategic.io> Co-authored-by: dundunHa <lixinpengde@gmail.com>
109 lines
3.8 KiB
TypeScript
109 lines
3.8 KiB
TypeScript
// Opt-in diagnostic for #3905. Run from the repository root:
|
|
// node --import tsx packages/server/scripts/reproduce-codex-history-string-limit.mts 520 [legacy|paginated]
|
|
// Requires Codex 0.148+ on PATH and approximately 2 GiB of available memory.
|
|
// `paginated` migrates the synthetic rollout with `codex migrate-rollouts` first.
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { randomUUID } from "node:crypto";
|
|
import { mkdtemp, mkdir, open, rm } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import pino from "pino";
|
|
import { CodexAppServerAgentSession } from "../src/server/agent/providers/codex-app-server-agent.js";
|
|
|
|
const megabytes = Number(process.argv[2] ?? "520");
|
|
if (!Number.isSafeInteger(megabytes) || megabytes < 1 || megabytes > 600) {
|
|
throw new Error("Expected an integer size between 1 and 600 MiB");
|
|
}
|
|
const historyMode = process.argv[3] ?? "legacy";
|
|
if (historyMode !== "legacy" && historyMode !== "paginated") {
|
|
throw new Error("Expected history mode legacy or paginated");
|
|
}
|
|
const root = await mkdtemp(path.join(process.cwd(), ".codex-history-repro-"));
|
|
const home = path.join(root, "codex-home");
|
|
const now = new Date();
|
|
const timestamp = now.toISOString();
|
|
const threadId = randomUUID();
|
|
const turnId = randomUUID();
|
|
const sessions = path.join(home, "sessions", ...timestamp.slice(0, 10).split("-"));
|
|
await mkdir(sessions, { recursive: true });
|
|
const rollout = await open(
|
|
path.join(sessions, `rollout-${timestamp.slice(0, 19).replaceAll(":", "-")}-${threadId}.jsonl`),
|
|
"w",
|
|
);
|
|
async function write(type: string, payload: unknown) {
|
|
await rollout.write(`${JSON.stringify({ timestamp, type, payload })}\n`);
|
|
}
|
|
await write("session_meta", {
|
|
id: threadId,
|
|
timestamp,
|
|
cwd: root,
|
|
originator: "codex_cli_rs",
|
|
cli_version: "0.159.0",
|
|
source: "cli",
|
|
model_provider: "openai",
|
|
history_mode: "legacy",
|
|
});
|
|
await write("event_msg", { type: "task_started", turn_id: turnId, model_context_window: 200000 });
|
|
await write("event_msg", {
|
|
type: "user_message",
|
|
message: "Synthetic oversized history",
|
|
images: [],
|
|
local_images: [],
|
|
text_elements: [],
|
|
});
|
|
for (let index = 0; index < megabytes; index++) {
|
|
await write("event_msg", {
|
|
type: "agent_message",
|
|
message: "x".repeat(1024 * 1024),
|
|
phase: "commentary",
|
|
});
|
|
}
|
|
await write("event_msg", { type: "task_complete", turn_id: turnId, last_agent_message: "done" });
|
|
await rollout.close();
|
|
if (historyMode === "paginated") {
|
|
execFileSync("codex", ["migrate-rollouts", "--apply", "--thread", threadId], {
|
|
env: { ...process.env, CODEX_HOME: home },
|
|
stdio: "ignore",
|
|
});
|
|
}
|
|
console.log(
|
|
`Opening synthetic ${megabytes} MiB ${historyMode} history through the real Codex app-server`,
|
|
);
|
|
const session = new CodexAppServerAgentSession(
|
|
{ provider: "codex", cwd: root },
|
|
{ sessionId: threadId },
|
|
pino({ level: "silent" }),
|
|
async () =>
|
|
spawn(
|
|
"codex",
|
|
["app-server", "--disable", "plugins", "--disable", "apps", "--disable", "remote_plugin"],
|
|
{
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
env: { ...process.env, CODEX_HOME: home },
|
|
},
|
|
),
|
|
{ codexHome: home },
|
|
false,
|
|
false,
|
|
false,
|
|
undefined,
|
|
"history",
|
|
);
|
|
try {
|
|
await session.connect();
|
|
let count = 0;
|
|
for await (const event of session.streamHistory()) {
|
|
if (event.type !== "timeline") continue;
|
|
count++;
|
|
if (event.item.type === "notification") console.log(event.item.message);
|
|
}
|
|
console.log(`History loaded: ${count} timeline rows`);
|
|
} catch (error) {
|
|
// A recoverable session error is acceptable for the crash-isolation assertion.
|
|
console.log(
|
|
`History rejected without crashing: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
} finally {
|
|
await session.close();
|
|
await rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
}
|