Skip to content

Commit 28f22f0

Browse files
committed
fix(agents): don't fail CLI turn when the native harness owns compaction
runCliTurnCompactionLifecycle threw on the Codex app-server's successful no-op (ok:true, compacted:false) when it owns automatic compaction, failing the agent run and jamming the session lane (endless 'typing' on large Codex threads). Treat the owned no-op as benign: skip the throw and the context-engine fallback so the turn proceeds and the harness self-compacts. Matches the existing 'Codex owns compaction' contract in the auto-reply preflight path.
1 parent d33c2ee commit 28f22f0

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

src/agents/command/cli-compaction.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,83 @@ describe("runCliTurnCompactionLifecycle", () => {
502502
expect(compactCalls).toHaveLength(0);
503503
});
504504

505+
it("does not throw when the native harness owns automatic compaction (ok:true no-op)", async () => {
506+
// Regression: Codex app-server returns a successful no-op (ok:true,
507+
// compacted:false) for budget-triggered compaction because it owns
508+
// automatic compaction. The lifecycle must treat that as benign and let the
509+
// turn proceed, not throw "CLI native harness compaction failed" — throwing
510+
// fails the agent run and jams the session lane (endless Discord "typing").
511+
const sessionKey = "agent:main:codex-owned-skip";
512+
const sessionId = "session-codex-owned-skip";
513+
const sessionFile = path.join(tmpDir, "session-codex-owned-skip.jsonl");
514+
const storePath = path.join(tmpDir, "sessions-codex-owned-skip.json");
515+
await writeSessionFile({ sessionFile, sessionId });
516+
517+
const sessionEntry: SessionEntry = {
518+
sessionId,
519+
updatedAt: Date.now(),
520+
sessionFile,
521+
contextTokens: 1_000,
522+
totalTokens: 950,
523+
totalTokensFresh: true,
524+
agentHarnessId: "codex",
525+
};
526+
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
527+
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
528+
529+
const compactCalls: Array<Parameters<ContextEngine["compact"]>[0]> = [];
530+
const compactAgentHarnessSession = vi.fn(async () => ({
531+
ok: true,
532+
compacted: false,
533+
reason: "codex app-server owns automatic compaction",
534+
}));
535+
const recordCliCompactionInStore = vi.fn(async () => ({
536+
...sessionEntry,
537+
compactionCount: 1,
538+
}));
539+
setCliCompactionTestDeps({
540+
resolveContextEngine: async () => buildContextEngine({ compactCalls }),
541+
ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined),
542+
maybeCompactAgentHarnessSession: compactAgentHarnessSession as never,
543+
createPreparedEmbeddedAgentSettingsManager: async () => ({
544+
getCompactionReserveTokens: () => 200,
545+
getCompactionKeepRecentTokens: () => 0,
546+
applyOverrides: () => {},
547+
}),
548+
shouldPreemptivelyCompactBeforePrompt: () => ({
549+
route: "fits",
550+
shouldCompact: false,
551+
estimatedPromptTokens: 600,
552+
promptBudgetBeforeReserve: 800,
553+
overflowTokens: 0,
554+
toolResultReducibleChars: 0,
555+
effectiveReserveTokens: 200,
556+
}),
557+
resolveLiveToolResultMaxChars: () => 20_000,
558+
recordCliCompactionInStore,
559+
});
560+
561+
const updatedEntry = await runCliTurnCompactionLifecycle({
562+
cfg: {} as OpenClawConfig,
563+
sessionId,
564+
sessionKey,
565+
sessionEntry,
566+
sessionStore,
567+
storePath,
568+
sessionAgentId: "main",
569+
workspaceDir: tmpDir,
570+
agentDir: tmpDir,
571+
provider: "codex",
572+
model: "gpt-5.5",
573+
});
574+
575+
expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1);
576+
// No context-engine fallback, no store rotation: the harness self-compacts.
577+
expect(compactCalls).toHaveLength(0);
578+
expect(recordCliCompactionInStore).not.toHaveBeenCalled();
579+
expect(updatedEntry).toBe(sessionEntry);
580+
});
581+
505582
it("passes owning context engines into native harness CLI compaction", async () => {
506583
const sessionKey = "agent:main:codex-owned-engine";
507584
const sessionId = "session-codex-owned-engine";

src/agents/command/cli-compaction.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ type NativeHarnessCliCompactionOutcome = {
6969
compacted: boolean;
7070
result?: EmbeddedAgentCompactResult;
7171
fallbackToContextEngine?: boolean;
72+
// Native harness reported a successful no-op because it owns automatic
73+
// compaction itself (e.g. the Codex app-server on non-manual/budget triggers).
74+
// Not a failure: the caller must skip the throw and let the harness compact
75+
// during the turn, otherwise the agent run errors and the session lane jams.
76+
ownedByNativeHarness?: boolean;
7277
failureReason?: string;
7378
};
7479
type CliTranscriptCompactionOutcome = {
@@ -403,12 +408,16 @@ async function compactNativeHarnessCliTranscript(params: {
403408
const fallbackToContextEngine =
404409
isUnsupportedNativeHarnessCompaction(result) ||
405410
isRecoverableNativeHarnessBindingFailure(result);
411+
// ok:true with no compaction is a successful no-op (the harness owns
412+
// automatic compaction), not a failure — do not throw on it downstream.
413+
const ownedByNativeHarness = result?.ok === true;
406414
log.warn(
407415
`CLI native harness compaction did not reduce context for ${params.provider}/${params.model}: ${result?.reason ?? "nothing to compact"}`,
408416
);
409417
return {
410418
compacted: false,
411419
fallbackToContextEngine,
420+
ownedByNativeHarness,
412421
failureReason: result?.reason ?? "native harness compaction did not reduce context",
413422
};
414423
}
@@ -520,6 +529,11 @@ export async function runCliTurnCompactionLifecycle(params: {
520529
compacted = true;
521530
nativeCompactionResult = nativeOutcome.result;
522531
useContextEngineCompaction = false;
532+
} else if (nativeOutcome.ownedByNativeHarness) {
533+
// Harness owns automatic compaction and returned a successful no-op. Skip
534+
// both the throw and the context-engine fallback so the turn proceeds and
535+
// the harness compacts itself; throwing here jams the session lane.
536+
useContextEngineCompaction = false;
523537
} else if (!nativeOutcome.fallbackToContextEngine) {
524538
throw new Error(
525539
`CLI native harness compaction failed for ${params.provider}/${params.model}: ${

0 commit comments

Comments
 (0)