mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 00:54:04 +00:00
fix(codex): harden overflow binding recovery
This commit is contained in:
@@ -1236,6 +1236,78 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
|
||||
expect(savedBinding?.contextEngine?.projection?.epoch).toBe("epoch-before");
|
||||
});
|
||||
|
||||
it("preserves a newer context-engine binding when a stale resumed thread overflows", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
SessionManager.open(sessionFile).appendMessage(
|
||||
assistantMessage("pre-compaction context", Date.now()) as never,
|
||||
);
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
threadId: "thread-old",
|
||||
cwd: workspaceDir,
|
||||
dynamicToolsFingerprint: "[]",
|
||||
contextEngine: {
|
||||
schemaVersion: 1,
|
||||
engineId: "lossless-claw",
|
||||
policyFingerprint:
|
||||
'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"contextTokenBudget":400000,"projectionMaxChars":1000000}',
|
||||
projection: {
|
||||
schemaVersion: 1,
|
||||
mode: "thread_bootstrap",
|
||||
epoch: "epoch-before",
|
||||
},
|
||||
},
|
||||
});
|
||||
const compact = vi.fn<ContextEngine["compact"]>(async () => ({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
result: { summary: "summary", firstKeptEntryId: "entry-1", tokensBefore: 100_000 },
|
||||
}));
|
||||
const assemble = vi.fn(
|
||||
async ({ messages, prompt }: Parameters<ContextEngine["assemble"]>[0]) => ({
|
||||
messages: [...messages, userMessage(prompt ?? "", 11)],
|
||||
estimatedTokens: 42,
|
||||
systemPromptAddition: "context-engine system",
|
||||
contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-before" },
|
||||
}),
|
||||
);
|
||||
const contextEngine = createContextEngine({ assemble, compact });
|
||||
const harness = createStartedThreadHarness(async (method, requestParams) => {
|
||||
const request = requireRecord(requestParams, `${method} params`);
|
||||
if (method === "thread/resume") {
|
||||
return threadStartResult("thread-old");
|
||||
}
|
||||
if (method === "turn/start" && request.threadId === "thread-old") {
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
threadId: "thread-new",
|
||||
cwd: workspaceDir,
|
||||
dynamicToolsFingerprint: "[]",
|
||||
});
|
||||
throw new Error("Codex ran out of room in the model's context window");
|
||||
}
|
||||
if (method === "thread/start") {
|
||||
return threadStartResult("thread-fresh");
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.contextEngine = contextEngine;
|
||||
params.contextTokenBudget = 400_000;
|
||||
|
||||
await expect(runCodexAppServerAttempt(params)).rejects.toThrow(
|
||||
"Codex ran out of room in the model's context window",
|
||||
);
|
||||
|
||||
expect(compact).not.toHaveBeenCalled();
|
||||
expect(harness.requests.map((request) => request.method)).toEqual([
|
||||
"thread/resume",
|
||||
"turn/start",
|
||||
"thread/unsubscribe",
|
||||
]);
|
||||
const savedBinding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(savedBinding?.threadId).toBe("thread-new");
|
||||
});
|
||||
|
||||
it("clears a resumed context-engine binding when a turn terminally overflows", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
|
||||
@@ -1738,19 +1738,33 @@ export async function runCodexAppServerAttempt(
|
||||
);
|
||||
try {
|
||||
const preRetrySessionFile = activeSessionFile;
|
||||
await clearCodexAppServerBinding(preRetrySessionFile);
|
||||
if (activeSessionFile !== preRetrySessionFile) {
|
||||
await clearCodexAppServerBinding(activeSessionFile);
|
||||
}
|
||||
thread = await restartContextEngineCodexThread();
|
||||
emitCodexAppServerEvent(params, {
|
||||
stream: "codex_app_server.lifecycle",
|
||||
data: { phase: "thread_ready_retry", threadId: thread.threadId },
|
||||
});
|
||||
try {
|
||||
turn = await startCodexTurn();
|
||||
} catch (retryError) {
|
||||
turnStartError = retryError;
|
||||
const clearedPreRetryBinding = await clearCodexAppServerBindingForThread(
|
||||
preRetrySessionFile,
|
||||
thread.threadId,
|
||||
);
|
||||
const clearedActiveBinding =
|
||||
activeSessionFile !== preRetrySessionFile
|
||||
? await clearCodexAppServerBindingForThread(activeSessionFile, thread.threadId)
|
||||
: false;
|
||||
if (!clearedPreRetryBinding && !clearedActiveBinding) {
|
||||
embeddedAgentLog.warn(
|
||||
"codex app-server preserved newer context-engine binding after resume overflow; skipping fresh retry",
|
||||
{
|
||||
threadId: thread.threadId,
|
||||
error: formatErrorMessage(turnStartError),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
thread = await restartContextEngineCodexThread();
|
||||
emitCodexAppServerEvent(params, {
|
||||
stream: "codex_app_server.lifecycle",
|
||||
data: { phase: "thread_ready_retry", threadId: thread.threadId },
|
||||
});
|
||||
try {
|
||||
turn = await startCodexTurn();
|
||||
} catch (retryError) {
|
||||
turnStartError = retryError;
|
||||
}
|
||||
}
|
||||
} catch (retrySetupError) {
|
||||
turnStartError = retrySetupError;
|
||||
@@ -2046,9 +2060,9 @@ export async function runCodexAppServerAttempt(
|
||||
},
|
||||
);
|
||||
const preClearSessionFile = activeSessionFile;
|
||||
await clearCodexAppServerBinding(preClearSessionFile);
|
||||
await clearCodexAppServerBindingForThread(preClearSessionFile, thread.threadId);
|
||||
if (activeSessionFile !== preClearSessionFile) {
|
||||
await clearCodexAppServerBinding(activeSessionFile);
|
||||
await clearCodexAppServerBindingForThread(activeSessionFile, thread.threadId);
|
||||
}
|
||||
}
|
||||
const refreshedUsageLimitPromptError = await refreshCodexUsageLimitPromptError({
|
||||
|
||||
@@ -300,8 +300,6 @@ describe("Codex app-server startup binding", () => {
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
);
|
||||
const readFileSpy = vi.spyOn(fs, "readFile");
|
||||
|
||||
const binding = await rotateOversizedCodexAppServerStartupBinding({
|
||||
binding: await readCodexAppServerBinding(sessionFile),
|
||||
sessionFile,
|
||||
@@ -319,7 +317,6 @@ describe("Codex app-server startup binding", () => {
|
||||
});
|
||||
|
||||
expect(binding).toBeUndefined();
|
||||
expect(readFileSpy.mock.calls.some(([file]) => file === rolloutFile)).toBe(false);
|
||||
const savedBinding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(savedBinding).toBeUndefined();
|
||||
});
|
||||
@@ -448,7 +445,7 @@ describe("Codex app-server startup binding", () => {
|
||||
await fs.mkdir(rolloutDir, { recursive: true });
|
||||
const rolloutFile = path.join(rolloutDir, "rollout-thread-existing.jsonl");
|
||||
await fs.writeFile(rolloutFile, "x".repeat(2_000));
|
||||
const readFileSpy = vi.spyOn(fs, "readFile");
|
||||
const openSpy = vi.spyOn(fs, "open");
|
||||
|
||||
const binding = await rotateOversizedCodexAppServerStartupBinding({
|
||||
binding: await readCodexAppServerBinding(sessionFile),
|
||||
@@ -467,7 +464,7 @@ describe("Codex app-server startup binding", () => {
|
||||
});
|
||||
|
||||
expect(binding).toBeUndefined();
|
||||
expect(readFileSpy.mock.calls.some(([file]) => file === rolloutFile)).toBe(false);
|
||||
expect(openSpy.mock.calls.some(([file]) => String(file) === rolloutFile)).toBe(false);
|
||||
const savedBinding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(savedBinding).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -296,6 +296,29 @@ export async function rotateOversizedCodexAppServerStartupBinding(params: {
|
||||
binding.threadId,
|
||||
params.codexHome,
|
||||
);
|
||||
const compaction = readCompactionConfig(params.config);
|
||||
const shouldDeferByteGuard =
|
||||
compaction?.truncateAfterCompaction === true &&
|
||||
params.contextEngineActive === true &&
|
||||
hasContextEngineThreadBootstrapProjection(binding);
|
||||
if (compaction?.truncateAfterCompaction === true && !shouldDeferByteGuard) {
|
||||
const maxBytes = parseCodexAppServerByteLimit(compaction.maxActiveTranscriptBytes);
|
||||
if (maxBytes !== undefined) {
|
||||
const oversizedFiles = rolloutFiles.filter((file) => file.bytes >= maxBytes);
|
||||
if (oversizedFiles.length > 0) {
|
||||
embeddedAgentLog.warn(
|
||||
"codex app-server native transcript exceeded active byte limit; starting a fresh thread",
|
||||
{
|
||||
threadId: binding.threadId,
|
||||
maxBytes,
|
||||
files: oversizedFiles.map((file) => ({ path: file.path, bytes: file.bytes })),
|
||||
},
|
||||
);
|
||||
await clearCodexAppServerBinding(params.sessionFile);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
const nativeTokenSnapshots = await Promise.all(
|
||||
rolloutFiles.map(async (file) => readCodexAppServerRolloutTokenSnapshot(file.path)),
|
||||
);
|
||||
@@ -342,11 +365,10 @@ export async function rotateOversizedCodexAppServerStartupBinding(params: {
|
||||
await clearCodexAppServerBinding(params.sessionFile);
|
||||
return undefined;
|
||||
}
|
||||
const compaction = readCompactionConfig(params.config);
|
||||
if (compaction?.truncateAfterCompaction !== true) {
|
||||
return binding;
|
||||
}
|
||||
if (params.contextEngineActive === true && hasContextEngineThreadBootstrapProjection(binding)) {
|
||||
if (shouldDeferByteGuard) {
|
||||
embeddedAgentLog.debug(
|
||||
"codex app-server deferring native transcript byte guard for context-engine thread bootstrap",
|
||||
{
|
||||
@@ -358,22 +380,6 @@ export async function rotateOversizedCodexAppServerStartupBinding(params: {
|
||||
);
|
||||
return binding;
|
||||
}
|
||||
const maxBytes = parseCodexAppServerByteLimit(compaction.maxActiveTranscriptBytes);
|
||||
if (maxBytes !== undefined) {
|
||||
const oversizedFiles = rolloutFiles.filter((file) => file.bytes >= maxBytes);
|
||||
if (oversizedFiles.length > 0) {
|
||||
embeddedAgentLog.warn(
|
||||
"codex app-server native transcript exceeded active byte limit; starting a fresh thread",
|
||||
{
|
||||
threadId: binding.threadId,
|
||||
maxBytes,
|
||||
files: oversizedFiles.map((file) => ({ path: file.path, bytes: file.bytes })),
|
||||
},
|
||||
);
|
||||
await clearCodexAppServerBinding(params.sessionFile);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user