Skip to content

Commit 0177987

Browse files
steipeteEva (agent)
andcommitted
fix(codex): preserve oversized native thread reuse
Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
1 parent 373b3bf commit 0177987

3 files changed

Lines changed: 349 additions & 29 deletions

File tree

extensions/codex/src/app-server/run-attempt.context-engine.test.ts

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,11 @@ function createStartedThreadHarness(
198198
async notify(notification: CodexServerNotification) {
199199
await notify(notification);
200200
},
201-
async completeTurn(status: "completed" | "failed" = "completed") {
201+
async completeTurn(status: "completed" | "failed" = "completed", threadId = "thread-1") {
202202
await notify({
203203
method: "turn/completed",
204204
params: {
205-
threadId: "thread-1",
205+
threadId,
206206
turnId: "turn-1",
207207
turn: {
208208
id: "turn-1",
@@ -545,6 +545,201 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
545545
await secondRun;
546546
});
547547

548+
it.each([
549+
[
550+
"token",
551+
`${JSON.stringify({
552+
payload: {
553+
type: "token_count",
554+
info: {
555+
last_token_usage: {
556+
total_tokens: 300_000,
557+
},
558+
},
559+
},
560+
})}\n`,
561+
"1mb",
562+
],
563+
["byte", "x".repeat(2_000), 1_000],
564+
] as const)(
565+
"resumes a matching thread-bootstrap binding even when the bootstrap turn exceeded the native %s guard",
566+
async (_guard, rolloutContent, maxActiveTranscriptBytes) => {
567+
const sessionFile = path.join(tempDir, "session.jsonl");
568+
const workspaceDir = path.join(tempDir, "workspace");
569+
const agentDir = path.join(tempDir, "agent");
570+
await writeCodexAppServerBinding(sessionFile, {
571+
threadId: "thread-bootstrapped",
572+
cwd: workspaceDir,
573+
dynamicToolsFingerprint: "[]",
574+
contextEngine: {
575+
schemaVersion: 1,
576+
engineId: "lossless-claw",
577+
policyFingerprint:
578+
'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
579+
projection: {
580+
schemaVersion: 1,
581+
mode: "thread_bootstrap",
582+
epoch: "epoch-1",
583+
},
584+
},
585+
});
586+
await fs.writeFile(
587+
path.join(path.dirname(sessionFile), "sessions.json"),
588+
JSON.stringify({
589+
"agent:main:session-1": {
590+
sessionFile,
591+
totalTokens: 12_000,
592+
},
593+
}),
594+
);
595+
const rolloutDir = path.join(agentDir, "codex-home", "sessions");
596+
await fs.mkdir(rolloutDir, { recursive: true });
597+
await fs.writeFile(
598+
path.join(rolloutDir, "rollout-thread-bootstrapped.jsonl"),
599+
rolloutContent,
600+
);
601+
const contextEngine = createContextEngine({
602+
assemble: vi.fn(async ({ prompt }) => ({
603+
messages: [
604+
assistantMessage("already bootstrapped context", 10),
605+
userMessage(prompt ?? "", 11),
606+
],
607+
estimatedTokens: 42,
608+
systemPromptAddition: "context-engine system",
609+
contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-1" },
610+
})),
611+
});
612+
const harness = createStartedThreadHarness(async (method) => {
613+
if (method === "thread/resume") {
614+
return threadStartResult("thread-bootstrapped");
615+
}
616+
if (method === "thread/start") {
617+
return threadStartResult("thread-fresh");
618+
}
619+
return undefined;
620+
});
621+
const params = createParams(sessionFile, workspaceDir);
622+
params.agentDir = agentDir;
623+
params.contextEngine = contextEngine;
624+
params.config = {
625+
agents: {
626+
defaults: {
627+
compaction: {
628+
truncateAfterCompaction: true,
629+
maxActiveTranscriptBytes,
630+
},
631+
},
632+
},
633+
} as EmbeddedRunAttemptParams["config"];
634+
635+
const run = runCodexAppServerAttempt(params);
636+
await harness.waitForMethod("turn/start");
637+
638+
expect(harness.requests.map((request) => request.method)).toEqual([
639+
"thread/resume",
640+
"turn/start",
641+
]);
642+
const inputText = getRequestInputText(harness);
643+
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
644+
expect(inputText).not.toContain("already bootstrapped context");
645+
expect(inputText).toBe("hello");
646+
647+
await harness.completeTurn("completed", "thread-bootstrapped");
648+
await run;
649+
},
650+
);
651+
652+
it("projects mirrored history when an oversized thread-bootstrap binding has no active context engine", async () => {
653+
const sessionFile = path.join(tempDir, "session.jsonl");
654+
const workspaceDir = path.join(tempDir, "workspace");
655+
const agentDir = path.join(tempDir, "agent");
656+
const sessionManager = SessionManager.open(sessionFile);
657+
sessionManager.appendMessage(
658+
userMessage("previous stale-bootstrap request", Date.now()) as never,
659+
);
660+
sessionManager.appendMessage(
661+
assistantMessage("previous stale-bootstrap answer", Date.now() + 1) as never,
662+
);
663+
await writeCodexAppServerBinding(sessionFile, {
664+
threadId: "thread-stale-bootstrap",
665+
cwd: workspaceDir,
666+
dynamicToolsFingerprint: "[]",
667+
contextEngine: {
668+
schemaVersion: 1,
669+
engineId: "lossless-claw",
670+
policyFingerprint:
671+
'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
672+
projection: {
673+
schemaVersion: 1,
674+
mode: "thread_bootstrap",
675+
epoch: "epoch-stale",
676+
},
677+
},
678+
});
679+
await fs.writeFile(
680+
path.join(path.dirname(sessionFile), "sessions.json"),
681+
JSON.stringify({
682+
"agent:main:session-1": {
683+
sessionFile,
684+
totalTokens: 12_000,
685+
},
686+
}),
687+
);
688+
const rolloutDir = path.join(agentDir, "codex-home", "sessions");
689+
await fs.mkdir(rolloutDir, { recursive: true });
690+
await fs.writeFile(
691+
path.join(rolloutDir, "rollout-thread-stale-bootstrap.jsonl"),
692+
`${JSON.stringify({
693+
payload: {
694+
type: "token_count",
695+
info: {
696+
last_token_usage: {
697+
total_tokens: 300_000,
698+
},
699+
},
700+
},
701+
})}\n`,
702+
);
703+
const harness = createStartedThreadHarness(async (method) => {
704+
if (method === "thread/resume") {
705+
return threadStartResult("thread-stale-bootstrap");
706+
}
707+
if (method === "thread/start") {
708+
return threadStartResult("thread-fresh");
709+
}
710+
return undefined;
711+
});
712+
const params = createParams(sessionFile, workspaceDir);
713+
params.agentDir = agentDir;
714+
params.config = {
715+
agents: {
716+
defaults: {
717+
compaction: {
718+
truncateAfterCompaction: true,
719+
maxActiveTranscriptBytes: "1mb",
720+
},
721+
},
722+
},
723+
} as EmbeddedRunAttemptParams["config"];
724+
725+
const run = runCodexAppServerAttempt(params);
726+
await harness.waitForMethod("turn/start");
727+
728+
expect(harness.requests.map((request) => request.method)).toEqual([
729+
"thread/start",
730+
"turn/start",
731+
]);
732+
const inputText = getRequestInputText(harness);
733+
expect(inputText).toContain("OpenClaw assembled context for this turn:");
734+
expect(inputText).toContain("previous stale-bootstrap request");
735+
expect(inputText).toContain("previous stale-bootstrap answer");
736+
expect(inputText).toContain("Current user request:");
737+
expect(inputText).toContain("hello");
738+
739+
await harness.completeTurn("completed", "thread-fresh");
740+
await run;
741+
});
742+
548743
it("starts a fresh Codex thread and reprojects when context-engine epoch changes", async () => {
549744
const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined);
550745
const sessionFile = path.join(tempDir, "session.jsonl");

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8656,7 +8656,7 @@ describe("runCodexAppServerAttempt", () => {
86568656
expect(resolved).toBe(false);
86578657
expect(
86588658
warn.mock.calls.some(([message]) =>
8659-
String(message).includes("turn/completed did not match active turn"),
8659+
message.includes("turn/completed did not match active turn"),
86608660
),
86618661
).toBe(false);
86628662

@@ -9727,7 +9727,7 @@ describe("runCodexAppServerAttempt", () => {
97279727
expect(resumeRequestParams?.developerInstructions).not.toContain(CODEX_GPT5_BEHAVIOR_CONTRACT);
97289728
});
97299729

9730-
it("starts a fresh Codex thread before resume when the native rollout is over budget", async () => {
9730+
it("starts a fresh Codex thread before resume when the native rollout reaches the fallback fuse", async () => {
97319731
const sessionFile = path.join(tempDir, "session.jsonl");
97329732
const workspaceDir = path.join(tempDir, "workspace");
97339733
const agentDir = path.join(tempDir, "agent");
@@ -9750,7 +9750,7 @@ describe("runCodexAppServerAttempt", () => {
97509750
type: "token_count",
97519751
info: {
97529752
total_token_usage: {
9753-
total_tokens: 70_000,
9753+
total_tokens: 300_000,
97549754
},
97559755
},
97569756
},
@@ -9783,7 +9783,7 @@ describe("runCodexAppServerAttempt", () => {
97839783
expect(savedBinding?.threadId).toBe("thread-1");
97849784
});
97859785

9786-
it("preserves bound auth when rotating an over-budget native rollout", async () => {
9786+
it("preserves bound auth when rotating a fallback-fuse native rollout", async () => {
97879787
const sessionFile = path.join(tempDir, "session.jsonl");
97889788
const workspaceDir = path.join(tempDir, "workspace");
97899789
const agentDir = path.join(tempDir, "agent");
@@ -9809,7 +9809,7 @@ describe("runCodexAppServerAttempt", () => {
98099809
type: "token_count",
98109810
info: {
98119811
total_token_usage: {
9812-
total_tokens: 70_000,
9812+
total_tokens: 300_000,
98139813
},
98149814
},
98159815
},
@@ -9997,7 +9997,7 @@ describe("runCodexAppServerAttempt", () => {
99979997
type: "token_count",
99989998
info: {
99999999
total_token_usage: {
10000-
total_tokens: 70_000,
10000+
total_tokens: 300_000,
1000110001
},
1000210002
last_token_usage: {
1000310003
total_tokens: 12_000,
@@ -10038,7 +10038,7 @@ describe("runCodexAppServerAttempt", () => {
1003810038
JSON.stringify({
1003910039
"agent:main:session-1": {
1004010040
sessionFile,
10041-
totalTokens: 70_000,
10041+
totalTokens: 300_000,
1004210042
totalTokensFresh: false,
1004310043
},
1004410044
}),
@@ -10080,7 +10080,7 @@ describe("runCodexAppServerAttempt", () => {
1008010080
expect(savedBinding?.threadId).toBe("thread-existing");
1008110081
});
1008210082

10083-
it("streams rollout token scans without reading the whole file", async () => {
10083+
it("clears native rollouts at Codex's reported model context window", async () => {
1008410084
const sessionFile = path.join(tempDir, "session.jsonl");
1008510085
const workspaceDir = path.join(tempDir, "workspace");
1008610086
const agentDir = path.join(tempDir, "agent");
@@ -10099,18 +10099,81 @@ describe("runCodexAppServerAttempt", () => {
1009910099
const rolloutFile = path.join(rolloutDir, "rollout-thread-existing.jsonl");
1010010100
await fs.writeFile(
1010110101
rolloutFile,
10102+
[
10103+
JSON.stringify({
10104+
payload: {
10105+
type: "token_count",
10106+
info: {
10107+
last_token_usage: {
10108+
total_tokens: 128_000,
10109+
},
10110+
},
10111+
},
10112+
}),
10113+
JSON.stringify({
10114+
payload: {
10115+
type: "token_count",
10116+
info: {
10117+
model_context_window: 128_000,
10118+
},
10119+
},
10120+
}),
10121+
].join("\n") + "\n",
10122+
);
10123+
const readFileSpy = vi.spyOn(fs, "readFile");
10124+
10125+
const binding = await testing.rotateOversizedCodexAppServerStartupBinding({
10126+
binding: await readCodexAppServerBinding(sessionFile),
10127+
sessionFile,
10128+
agentDir,
10129+
config: {
10130+
agents: {
10131+
defaults: {
10132+
compaction: {
10133+
truncateAfterCompaction: true,
10134+
maxActiveTranscriptBytes: "1mb",
10135+
},
10136+
},
10137+
},
10138+
} as never,
10139+
});
10140+
10141+
expect(binding).toBeUndefined();
10142+
expect(readFileSpy.mock.calls.some(([file]) => file === rolloutFile)).toBe(false);
10143+
const savedBinding = await readCodexAppServerBinding(sessionFile);
10144+
expect(savedBinding).toBeUndefined();
10145+
});
10146+
10147+
it("keeps native rollouts above the old guard when Codex still has context window headroom", async () => {
10148+
const sessionFile = path.join(tempDir, "session.jsonl");
10149+
const workspaceDir = path.join(tempDir, "workspace");
10150+
const agentDir = path.join(tempDir, "agent");
10151+
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
10152+
await fs.writeFile(
10153+
path.join(path.dirname(sessionFile), "sessions.json"),
10154+
JSON.stringify({
10155+
"agent:main:session-1": {
10156+
sessionFile,
10157+
totalTokens: 12_000,
10158+
},
10159+
}),
10160+
);
10161+
const rolloutDir = path.join(agentDir, "codex-home", "sessions");
10162+
await fs.mkdir(rolloutDir, { recursive: true });
10163+
await fs.writeFile(
10164+
path.join(rolloutDir, "rollout-thread-existing.jsonl"),
1010210165
`${JSON.stringify({
1010310166
payload: {
1010410167
type: "token_count",
1010510168
info: {
1010610169
last_token_usage: {
10107-
total_tokens: 70_000,
10170+
total_tokens: 86_000,
1010810171
},
10172+
model_context_window: 272_000,
1010910173
},
1011010174
},
1011110175
})}\n`,
1011210176
);
10113-
const readFileSpy = vi.spyOn(fs, "readFile");
1011410177

1011510178
const binding = await testing.rotateOversizedCodexAppServerStartupBinding({
1011610179
binding: await readCodexAppServerBinding(sessionFile),
@@ -10128,10 +10191,9 @@ describe("runCodexAppServerAttempt", () => {
1012810191
} as never,
1012910192
});
1013010193

10131-
expect(binding).toBeUndefined();
10132-
expect(readFileSpy.mock.calls.some(([file]) => file === rolloutFile)).toBe(false);
10194+
expect(binding?.threadId).toBe("thread-existing");
1013310195
const savedBinding = await readCodexAppServerBinding(sessionFile);
10134-
expect(savedBinding).toBeUndefined();
10196+
expect(savedBinding?.threadId).toBe("thread-existing");
1013510197
});
1013610198

1013710199
it("clears byte-oversized rollouts before reading their contents", async () => {

0 commit comments

Comments
 (0)