Skip to content

Commit c429a3c

Browse files
committed
fix(codex): skip stale bootstrap history without engine
1 parent 444bdc4 commit c429a3c

2 files changed

Lines changed: 63 additions & 26 deletions

File tree

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,52 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
804804
await run;
805805
});
806806

807+
it("keeps mirrored history when an inactive per-turn context-engine binding starts fresh", async () => {
808+
const sessionFile = path.join(tempDir, "session.jsonl");
809+
const workspaceDir = path.join(tempDir, "workspace");
810+
const sessionManager = SessionManager.open(sessionFile);
811+
sessionManager.appendMessage(userMessage("previous per-turn request", 10) as never);
812+
sessionManager.appendMessage(assistantMessage("previous per-turn answer", 11) as never);
813+
await writeCodexAppServerBinding(sessionFile, {
814+
threadId: "thread-per-turn-context",
815+
cwd: workspaceDir,
816+
dynamicToolsFingerprint: "[]",
817+
contextEngine: {
818+
schemaVersion: 1,
819+
engineId: "lossless-claw",
820+
policyFingerprint:
821+
'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
822+
},
823+
});
824+
const harness = createStartedThreadHarness(async (method) => {
825+
if (method === "thread/start") {
826+
return threadStartResult("thread-fresh");
827+
}
828+
if (method === "thread/resume") {
829+
throw new Error("inactive context-engine bindings should start a fresh thread");
830+
}
831+
return undefined;
832+
});
833+
const params = createParams(sessionFile, workspaceDir);
834+
835+
const run = runCodexAppServerAttempt(params);
836+
await harness.waitForMethod("turn/start");
837+
838+
expect(harness.requests.map((request) => request.method)).toEqual([
839+
"thread/start",
840+
"turn/start",
841+
]);
842+
const inputText = getRequestInputText(harness);
843+
expect(inputText).toContain("OpenClaw assembled context for this turn:");
844+
expect(inputText).toContain("previous per-turn request");
845+
expect(inputText).toContain("previous per-turn answer");
846+
expect(inputText).toContain("Current user request:");
847+
expect(inputText).toContain("hello");
848+
849+
await harness.completeTurn("completed", "thread-fresh");
850+
await run;
851+
});
852+
807853
it("starts a fresh Codex thread and reprojects when context-engine epoch changes", async () => {
808854
const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined);
809855
const sessionFile = path.join(tempDir, "session.jsonl");

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

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -446,11 +446,13 @@ export async function runCodexAppServerAttempt(
446446
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
447447
? params.contextEngine
448448
: undefined;
449+
const isInactiveThreadBootstrapBinding = (binding: CodexAppServerThreadBinding | undefined) =>
450+
!activeContextEngine && binding?.contextEngine?.projection?.mode === "thread_bootstrap";
449451
let startupBinding = await readCodexAppServerBinding(params.sessionFile);
450452
preDynamicStartupStages.mark("read-binding");
451453
const startupBindingAuthProfileId = startupBinding?.authProfileId;
452-
const initialStartupBindingHadInactiveContextEngine =
453-
!activeContextEngine && Boolean(startupBinding?.contextEngine);
454+
const initialStartupBindingHadInactiveThreadBootstrap =
455+
isInactiveThreadBootstrapBinding(startupBinding);
454456
startupBinding = await rotateOversizedCodexAppServerStartupBinding({
455457
binding: startupBinding,
456458
sessionFile: params.sessionFile,
@@ -459,8 +461,8 @@ export async function runCodexAppServerAttempt(
459461
config: params.config,
460462
contextEngineActive: Boolean(activeContextEngine),
461463
});
462-
const initialInactiveContextEngineBindingForcedFreshStart =
463-
initialStartupBindingHadInactiveContextEngine && !startupBinding?.threadId;
464+
const initialInactiveThreadBootstrapBindingForcedFreshStart =
465+
initialStartupBindingHadInactiveThreadBootstrap && !startupBinding?.threadId;
464466
preDynamicStartupStages.mark("rotate-binding");
465467
const startupAuthProfileCandidate =
466468
params.runtimePlan?.auth.forwardedAuthProfileId ??
@@ -688,8 +690,8 @@ export async function runCodexAppServerAttempt(
688690
let contextEngineProjection: CodexContextEngineThreadBootstrapProjection | undefined;
689691
let precomputedStaleBindingContinuityProjectionApplied = false;
690692
let staleBindingContinuityForcedFreshStart = false;
691-
let inactiveContextEngineBindingForcedFreshStart =
692-
initialInactiveContextEngineBindingForcedFreshStart;
693+
let inactiveThreadBootstrapBindingForcedFreshStart =
694+
initialInactiveThreadBootstrapBindingForcedFreshStart;
693695
const applyFreshThreadContinuityProjection = () => {
694696
const projection = projectContextEngineAssemblyForCodex({
695697
assembledMessages: historyMessages,
@@ -881,8 +883,8 @@ export async function runCodexAppServerAttempt(
881883
if (activeContextEngine || !binding?.threadId) {
882884
return false;
883885
}
884-
if (binding.contextEngine) {
885-
inactiveContextEngineBindingForcedFreshStart = true;
886+
if (isInactiveThreadBootstrapBinding(binding)) {
887+
inactiveThreadBootstrapBindingForcedFreshStart = true;
886888
return false;
887889
}
888890
const projected = applyResumeStaleBindingContinuityProjection(binding);
@@ -892,7 +894,6 @@ export async function runCodexAppServerAttempt(
892894
const applyNoContextEngineContinuityProjection = (
893895
action: "started" | "resumed",
894896
binding?: CodexAppServerThreadBinding,
895-
rotatedContextEngineBinding = false,
896897
) => {
897898
if (activeContextEngine || !historyMessages.some((message) => message.role === "user")) {
898899
return false;
@@ -903,12 +904,9 @@ export async function runCodexAppServerAttempt(
903904
if (action === "started" && staleBindingContinuityForcedFreshStart) {
904905
return true;
905906
}
906-
if (
907-
action === "started" &&
908-
(inactiveContextEngineBindingForcedFreshStart || rotatedContextEngineBinding)
909-
) {
910-
// A retired or changed context-engine binding already forced Codex onto a
911-
// clean native thread; without the engine active, mirrored history would
907+
if (action === "started" && inactiveThreadBootstrapBindingForcedFreshStart) {
908+
// A retired thread-bootstrap context engine already forced Codex onto a
909+
// clean native thread; without that engine active, mirrored history would
912910
// re-inject stale bootstrap context as a new user turn.
913911
return false;
914912
}
@@ -929,8 +927,7 @@ export async function runCodexAppServerAttempt(
929927
return;
930928
}
931929
const previousThreadId = startupBinding.threadId;
932-
const hadInactiveContextEngineBinding =
933-
!activeContextEngine && Boolean(startupBinding.contextEngine);
930+
const hadInactiveThreadBootstrapBinding = isInactiveThreadBootstrapBinding(startupBinding);
934931
const projectedTurnTokens = estimateCodexAppServerProjectedTurnTokens({
935932
prompt: codexTurnPromptText,
936933
developerInstructions: buildRenderedCodexDeveloperInstructions(),
@@ -947,10 +944,10 @@ export async function runCodexAppServerAttempt(
947944
if (startupBinding?.threadId) {
948945
return;
949946
}
950-
inactiveContextEngineBindingForcedFreshStart = hadInactiveContextEngineBinding;
947+
inactiveThreadBootstrapBindingForcedFreshStart = hadInactiveThreadBootstrapBinding;
951948
staleBindingContinuityForcedFreshStart =
952949
precomputedStaleBindingContinuityProjectionApplied &&
953-
!inactiveContextEngineBindingForcedFreshStart;
950+
!inactiveThreadBootstrapBindingForcedFreshStart;
954951
if (activeContextEngine) {
955952
contextEngineProjection = undefined;
956953
try {
@@ -1110,13 +1107,7 @@ export async function runCodexAppServerAttempt(
11101107
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
11111108
throw error;
11121109
}
1113-
if (
1114-
applyNoContextEngineContinuityProjection(
1115-
thread.lifecycle.action,
1116-
thread,
1117-
thread.lifecycle.rotatedContextEngineBinding === true,
1118-
)
1119-
) {
1110+
if (applyNoContextEngineContinuityProjection(thread.lifecycle.action, thread)) {
11201111
await rebuildCodexTurnPromptTextFromCurrentProjection();
11211112
}
11221113
trajectoryRecorder?.recordEvent("session.started", {

0 commit comments

Comments
 (0)