diff --git a/docs/concepts/compaction.md b/docs/concepts/compaction.md index db86470a0857..014aba71fa44 100644 --- a/docs/concepts/compaction.md +++ b/docs/concepts/compaction.md @@ -110,8 +110,11 @@ When `agents.defaults.compaction.maxActiveTranscriptBytes` is set, OpenClaw triggers normal local compaction before a run if transcript history reaches that size. This is useful for long-running sessions where provider-side context management may keep model context healthy while persisted transcript history -keeps growing. It does not split raw bytes; it asks the normal compaction -pipeline to create a semantic summary. +keeps growing. Set a positive byte count or size string such as `"20mb"` to opt +in; `0` or an unset value disables the guard. It does not split raw bytes; it +asks the normal compaction pipeline to create a semantic summary. For Codex +app-server sessions, the same threshold caps native rollout transcripts and +oversized native threads restart fresh. The byte guard applies to the active SQLite transcript history. Legacy JSONL @@ -120,7 +123,7 @@ checkpoint artifacts are not the active compaction target. ### Successor transcripts -When `agents.defaults.compaction.truncateAfterCompaction` is enabled, a context engine may return an explicit compacted successor session identity. OpenClaw adopts that successor and records checkpoint metadata against it. The built-in SQLite compactor keeps the current session identity and does not create a second runtime transcript. +A context engine may return an explicit compacted successor session identity. OpenClaw adopts that successor and records checkpoint metadata against it. The built-in SQLite compactor keeps the current session identity and does not create a second runtime transcript. OpenClaw no longer writes separate `.checkpoint.*.jsonl` copies for new compactions. Existing legacy checkpoint files can still be used while referenced diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index e2985f6ba46b..081c47d267fb 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -610,8 +610,7 @@ Delegated consults with a requesting agent keep that requester as their owner. W postIndexSync: "async", // off | async | await postCompactionSections: ["Session Startup", "Red Lines"], model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override - truncateAfterCompaction: true, // adopt a context-engine successor when one is returned - maxActiveTranscriptBytes: "20mb", // optional preflight local compaction trigger + maxActiveTranscriptBytes: "20mb", // opt in to preflight local compaction notifyUser: true, // notices when compaction starts/completes and on memory-flush degradation (default: false) memoryFlush: { enabled: true, @@ -638,8 +637,7 @@ Delegated consults with a requesting agent keep that requester as their owner. W - `postIndexSync`: post-compaction session-memory reindex mode. Default: `"async"`. Use `"await"` for strongest freshness, `"async"` for lower compaction latency, or `"off"` only when session-memory sync is handled elsewhere. - `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Leave unset or use `[]` to disable. - `model`: optional `provider/model-id` or bare alias from `agents.defaults.models` for compaction summarization only. Bare aliases resolve before dispatch; configured literal model IDs retain precedence on collisions. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model. -- `truncateAfterCompaction`: allows a context engine to advance the session to an explicit compacted successor identity. The built-in SQLite compactor keeps the current identity. Default: `false`. -- `maxActiveTranscriptBytes`: optional byte threshold (`number` or strings like `"20mb"`) that triggers normal local compaction before a run when transcript history grows past the threshold. Requires `truncateAfterCompaction`; durable size reduction also requires a context engine that returns a successor identity. Disabled when unset or `0`. +- `maxActiveTranscriptBytes`: byte threshold (`number` or strings like `"20mb"`) that opts in to normal local compaction before a run when transcript history reaches the threshold. For Codex app-server sessions, the same threshold caps native rollout transcripts and oversized native threads restart fresh. Disabled when unset or `0`. When a context engine returns an explicit compacted successor identity, OpenClaw adopts it; the built-in SQLite compactor keeps the current identity. - `notifyUser`: when `true`, sends brief context-maintenance notices to the user: when compaction starts and completes (for example, "Compacting context..." and "Compaction complete"), and when a pre-compaction memory flush is exhausted so the reply continues in a degraded state (for example, "Memory maintenance temporarily failed; continuing your reply."). Disabled by default to keep these notices silent. - `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Set `model` to an exact provider/model such as `ollama/qwen3:8b` when this housekeeping turn should stay on a local model; the override does not inherit the active session fallback chain. `forceFlushTranscriptBytes` forces the flush when transcript size reaches the threshold even if token counters are stale. Skipped when workspace is read-only. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index 5d8a7b864b4d..3ac9dcb7bd40 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -217,7 +217,7 @@ Two triggers in the embedded OpenClaw agent: Two additional guards run outside these two triggers: -- **Preflight local compaction**: set `agents.defaults.compaction.maxActiveTranscriptBytes` (bytes or a string like `"20mb"`) to trigger local compaction before opening the next run once the active transcript reaches that size. Normal semantic compaction still runs. Durable size reduction requires `truncateAfterCompaction` plus a context engine that returns an explicit successor identity. +- **Preflight local compaction**: set `agents.defaults.compaction.maxActiveTranscriptBytes` to a positive byte threshold (bytes or a string like `"20mb"`) to trigger local compaction before opening the next run once the active transcript reaches that size. Normal semantic compaction still runs. For Codex app-server sessions, the same threshold caps native rollout transcripts and oversized native threads restart fresh. Unset or `0` disables the guard. - **Mid-turn precheck**: set `agents.defaults.compaction.midTurnPrecheck.enabled: true` (default `false`) to add a tool-loop guard. After a tool result is appended and before the next model call, OpenClaw estimates prompt pressure using the same preflight budget logic used at turn start. If context no longer fits, the guard does not compact inline - it raises a structured mid-turn precheck signal, stops the current prompt submission, and lets the outer run loop use the existing recovery path (truncate oversized tool results when that is enough, or trigger the configured compaction mode and retry). Works with both `default` and `safeguard` compaction modes, including provider-backed safeguard compaction. Independent of `maxActiveTranscriptBytes`: the byte-size guard runs before a turn opens, mid-turn precheck runs later, after new tool results are appended. ## Compaction settings @@ -241,7 +241,7 @@ Set `enabled: false` to disable threshold-driven auto-compaction inside the embe Manual `/compact` uses `agents.defaults.compaction.keepRecentTokens` (default: `20000`) and keeps that recent-tail cut point. -When `truncateAfterCompaction` is enabled, OpenClaw adopts an explicit successor identity returned by a context engine. The built-in SQLite compactor keeps the current session identity. Branch/restore checkpoint actions use a returned successor when present; legacy pre-compaction checkpoint files remain readable while referenced. +OpenClaw adopts an explicit successor identity returned by a context engine. The built-in SQLite compactor keeps the current session identity. Branch/restore checkpoint actions use a returned successor when present; legacy pre-compaction checkpoint files remain readable while referenced. ## Pluggable compaction providers diff --git a/extensions/codex/src/app-server/run-attempt-connection.test.ts b/extensions/codex/src/app-server/run-attempt-connection.test.ts index c45ad2eb749a..2559246c0bca 100644 --- a/extensions/codex/src/app-server/run-attempt-connection.test.ts +++ b/extensions/codex/src/app-server/run-attempt-connection.test.ts @@ -68,7 +68,6 @@ describe("prepareCodexAttemptConnection", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, diff --git a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts index 167a8d0a1f6b..3a25680ed7b6 100644 --- a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts +++ b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts @@ -884,7 +884,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: 1_000, }, }, @@ -1054,7 +1053,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 9842314cf377..c38e1a82bc0e 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -4641,7 +4641,6 @@ describe("runCodexAppServerAttempt", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, @@ -4757,7 +4756,6 @@ describe("runCodexAppServerAttempt", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, diff --git a/extensions/codex/src/app-server/startup-binding.test.ts b/extensions/codex/src/app-server/startup-binding.test.ts index bfa5371c1ade..98b6c82bf3a9 100644 --- a/extensions/codex/src/app-server/startup-binding.test.ts +++ b/extensions/codex/src/app-server/startup-binding.test.ts @@ -76,15 +76,7 @@ describe("Codex app-server startup binding", () => { binding: await readCodexAppServerBinding(sessionFile), sessionFile, agentDir, - config: { - agents: { - defaults: { - compaction: { - truncateAfterCompaction: true, - }, - }, - }, - } as never, + config: undefined, }); expect(binding?.threadId).toBe("thread-existing"); @@ -122,7 +114,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1k", }, }, @@ -163,7 +154,7 @@ describe("Codex app-server startup binding", () => { config: { agents: { defaults: { - compaction: { truncateAfterCompaction: true, maxActiveTranscriptBytes: "1b" }, + compaction: { maxActiveTranscriptBytes: "1b" }, }, }, } as never, @@ -521,7 +512,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1k", }, }, @@ -553,7 +543,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1k", }, }, @@ -590,7 +579,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1k", }, }, @@ -628,7 +616,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1k", }, }, @@ -666,7 +653,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1k", }, }, @@ -698,7 +684,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: 1_000, }, }, @@ -744,7 +729,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, @@ -790,7 +774,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, @@ -843,7 +826,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, @@ -887,7 +869,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "1mb", }, }, @@ -990,7 +971,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: 1_000, }, }, @@ -1022,7 +1002,6 @@ describe("Codex app-server startup binding", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: 1_000, }, }, diff --git a/extensions/codex/src/app-server/startup-binding.ts b/extensions/codex/src/app-server/startup-binding.ts index 209e8d740907..daec59ee20a8 100644 --- a/extensions/codex/src/app-server/startup-binding.ts +++ b/extensions/codex/src/app-server/startup-binding.ts @@ -439,34 +439,42 @@ export async function rotateOversizedCodexAppServerStartupBinding(params: { binding.rolloutPath, ); const compaction = readCompactionConfig(params.config); + const maxBytes = parseCodexAppServerByteLimit(compaction?.maxActiveTranscriptBytes); const shouldDeferByteGuard = - compaction?.truncateAfterCompaction === true && + maxBytes !== undefined && 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) { - await Promise.all( - rolloutFiles.map(async (file) => { - await file.handle?.close(); - }), - ); - 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 params.bindingStore.mutate(params.identity, { - kind: "clear", + if (shouldDeferByteGuard) { + embeddedAgentLog.debug( + "codex app-server deferring native transcript byte guard for context-engine thread bootstrap", + { + threadId: binding.threadId, + engineId: binding.contextEngine?.engineId, + epoch: binding.contextEngine?.projection?.epoch, + fingerprint: binding.contextEngine?.projection?.fingerprint, + }, + ); + } else if (maxBytes !== undefined) { + const oversizedFiles = rolloutFiles.filter((file) => file.bytes >= maxBytes); + if (oversizedFiles.length > 0) { + await Promise.all( + rolloutFiles.map(async (file) => { + await file.handle?.close(); + }), + ); + embeddedAgentLog.warn( + "codex app-server native transcript exceeded active byte limit; starting a fresh thread", + { threadId: binding.threadId, - }); - return undefined; - } + maxBytes, + files: oversizedFiles.map((file) => ({ path: file.path, bytes: file.bytes })), + }, + ); + await params.bindingStore.mutate(params.identity, { + kind: "clear", + threadId: binding.threadId, + }); + return undefined; } } const nativeTokenSnapshots = await Promise.all( @@ -520,20 +528,5 @@ export async function rotateOversizedCodexAppServerStartupBinding(params: { }); return undefined; } - if (compaction?.truncateAfterCompaction !== true) { - return binding; - } - if (shouldDeferByteGuard) { - embeddedAgentLog.debug( - "codex app-server deferring native transcript byte guard for context-engine thread bootstrap", - { - threadId: binding.threadId, - engineId: binding.contextEngine?.engineId, - epoch: binding.contextEngine?.projection?.epoch, - fingerprint: binding.contextEngine?.projection?.fingerprint, - }, - ); - return binding; - } return binding; } diff --git a/src/agents/embedded-agent-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts index c25025f58428..b2765e983668 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -4178,19 +4178,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }, } as never); - const result = await compactEmbeddedAgentSession( - wrappedCompactionArgs({ - config: { - agents: { - defaults: { - compaction: { - truncateAfterCompaction: true, - }, - }, - }, - }, - }), - ); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(true); expect(result.result?.sessionId).toBe(delegatedSessionId); @@ -4543,19 +4531,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { sessionId: TEST_SESSION_ID, }, } as never); - const result = await compactEmbeddedAgentSession( - wrappedCompactionArgs({ - config: { - agents: { - defaults: { - compaction: { - truncateAfterCompaction: true, - }, - }, - }, - }, - }), - ); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(true); expect(rotateTranscriptAfterCompactionMock).not.toHaveBeenCalled(); diff --git a/src/agents/embedded-agent-runner/replay-history.ts b/src/agents/embedded-agent-runner/replay-history.ts index 585d287369d2..24bfcc22e539 100644 --- a/src/agents/embedded-agent-runner/replay-history.ts +++ b/src/agents/embedded-agent-runner/replay-history.ts @@ -810,7 +810,7 @@ export async function sanitizeSessionHistory(params: { // stripInvalidThinkingSignatures runs. Pre-compaction kept messages carry signatures // bound to the original prefix; after compaction the prefix changes and Anthropic // rejects them. Timestamp comparison with the latest compaction summary identifies - // the affected messages regardless of path (standard or truncateAfterCompaction). + // the affected messages regardless of which compaction path produced them. const compactionStaleStripped = signedThinkingProvider || policy.preserveSignatures ? stripStaleThinkingSignaturesForCompactionReplay(sanitizedImages) diff --git a/src/auto-reply/reply/agent-runner-memory.test.ts b/src/auto-reply/reply/agent-runner-memory.test.ts index c9202639e4be..8eb2e0723c01 100644 --- a/src/auto-reply/reply/agent-runner-memory.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.test.ts @@ -2597,7 +2597,6 @@ describe("runMemoryFlushIfNeeded", () => { defaults: { compaction: { memoryFlush: {}, - truncateAfterCompaction: true, maxActiveTranscriptBytes: "10mb", }, }, @@ -2652,7 +2651,6 @@ describe("runMemoryFlushIfNeeded", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "10b", }, }, @@ -2706,7 +2704,6 @@ describe("runMemoryFlushIfNeeded", () => { agents: { defaults: { compaction: { - truncateAfterCompaction: true, maxActiveTranscriptBytes: "10b", }, }, @@ -2921,7 +2918,6 @@ describe("runMemoryFlushIfNeeded", () => { defaults: { compaction: { notifyUser: true, - truncateAfterCompaction: true, maxActiveTranscriptBytes: "10b", }, }, @@ -2972,7 +2968,6 @@ describe("runMemoryFlushIfNeeded", () => { defaults: { compaction: { notifyUser: true, - truncateAfterCompaction: true, maxActiveTranscriptBytes: "10b", }, }, @@ -2999,50 +2994,6 @@ describe("runMemoryFlushIfNeeded", () => { expect(onCompactionNotice).toHaveBeenNthCalledWith(2, "incomplete"); }); - it("keeps the active transcript byte threshold inactive unless transcript rotation is enabled", async () => { - const sessionFile = path.join(rootDir, "large-session-no-rotation.jsonl"); - await fs.writeFile( - sessionFile, - `${JSON.stringify({ message: { role: "user", content: "x".repeat(256) } })}\n`, - "utf8", - ); - const sessionEntry: SessionEntry = { - sessionId: "session", - transcriptPath: sessionFile, - updatedAt: Date.now(), - totalTokens: 10, - totalTokensFresh: true, - compactionCount: 0, - }; - - const entry = await runPreflightCompactionIfNeeded({ - cfg: { - agents: { - defaults: { - compaction: { - maxActiveTranscriptBytes: "10b", - }, - }, - }, - }, - followupRun: createTestFollowupRun({ - sessionId: "session", - sessionFile, - sessionKey: "main", - }), - defaultModel: "anthropic/claude-opus-4-6", - agentCfgContextTokens: 100_000, - sessionEntry, - sessionStore: { main: sessionEntry }, - sessionKey: "main", - isHeartbeat: false, - replyOperation: createReplyOperation(), - }); - - expect(entry).toBe(sessionEntry); - expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled(); - }); - it("uses configured prompts and stored bootstrap warning signatures", async () => { const sessionEntry: SessionEntry = { sessionId: "session", diff --git a/src/auto-reply/reply/memory-flush.ts b/src/auto-reply/reply/memory-flush.ts index 90b644f4ea49..d9fd0446a77c 100644 --- a/src/auto-reply/reply/memory-flush.ts +++ b/src/auto-reply/reply/memory-flush.ts @@ -25,11 +25,9 @@ export function resolveMemoryFlushContextWindowTokens(params: { } export function resolveMaxActiveTranscriptBytes(cfg?: OpenClawConfig): number | undefined { - const compaction = cfg?.agents?.defaults?.compaction; - if (compaction?.truncateAfterCompaction !== true) { - return undefined; - } - const parsed = parseNonNegativeByteSize(compaction.maxActiveTranscriptBytes); + const parsed = parseNonNegativeByteSize( + cfg?.agents?.defaults?.compaction?.maxActiveTranscriptBytes, + ); return typeof parsed === "number" && parsed > 0 ? parsed : undefined; } diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts index 0a46f12f6512..b63db02e7ca7 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts @@ -44,6 +44,92 @@ function getPath(value: unknown, path: string): unknown { } describe("retired runtime config migrations", () => { + it("strips the retired compaction gate while keeping an enabled byte threshold", () => { + const migration = LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED.find( + (candidate) => candidate.id === "runtime.retired-config-keys", + ); + expect(migration).toBeDefined(); + const raw = { + agents: { + defaults: { + compaction: { truncateAfterCompaction: true, maxActiveTranscriptBytes: "20mb" }, + }, + }, + }; + const changes: string[] = []; + + migration?.apply(raw, changes); + + expect(raw.agents.defaults.compaction).toEqual({ maxActiveTranscriptBytes: "20mb" }); + expect(changes).toEqual([ + "Removed retired agents.defaults.compaction.truncateAfterCompaction.", + ]); + const retiredRule = migration?.legacyRules?.find( + (candidate) => + candidate.path.join(".") === "agents.defaults.compaction.truncateAfterCompaction", + ); + expect(retiredRule?.message).toBe( + 'agents.defaults.compaction.truncateAfterCompaction is retired; byte-triggered compaction now opts in via maxActiveTranscriptBytes alone. Run "openclaw doctor --fix".', + ); + }); + + it("preserves an explicit retired compaction opt-out", () => { + const migration = LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED.find( + (candidate) => candidate.id === "runtime.retired-config-keys", + ); + expect(migration).toBeDefined(); + const raw = { + agents: { + defaults: { + compaction: { truncateAfterCompaction: false, maxActiveTranscriptBytes: "20mb" }, + }, + }, + }; + const changes: string[] = []; + + migration?.apply(raw, changes); + + expect(raw.agents.defaults.compaction).toEqual({}); + expect(changes).toEqual([ + "Removed maxActiveTranscriptBytes to preserve truncateAfterCompaction: false.", + "Removed retired agents.defaults.compaction.truncateAfterCompaction.", + ]); + }); + + it("strips the retired compaction gate without adding a byte threshold", () => { + const migration = LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED.find( + (candidate) => candidate.id === "runtime.retired-config-keys", + ); + expect(migration).toBeDefined(); + const raw = { + agents: { defaults: { compaction: { truncateAfterCompaction: true } } }, + }; + const changes: string[] = []; + + migration?.apply(raw, changes); + + expect(raw.agents.defaults.compaction).toEqual({}); + expect(changes).toEqual([ + "Removed retired agents.defaults.compaction.truncateAfterCompaction.", + ]); + }); + + it("leaves compaction config without the retired gate untouched", () => { + const migration = LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED.find( + (candidate) => candidate.id === "runtime.retired-config-keys", + ); + expect(migration).toBeDefined(); + const raw = { + agents: { defaults: { compaction: { maxActiveTranscriptBytes: "20mb" } } }, + }; + const changes: string[] = []; + + migration?.apply(raw, changes); + + expect(raw.agents.defaults.compaction).toEqual({ maxActiveTranscriptBytes: "20mb" }); + expect(changes).toEqual([]); + }); + it("uses a dedicated, actionable migration for the retired device-auth bypass", () => { const migration = LEGACY_CONFIG_MIGRATIONS_RUNTIME_GATEWAY.find( (candidate) => candidate.id === "gateway.control-ui-device-auth-bypass->pairing-migration", diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts index d1f668a77c77..78477095ac83 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts @@ -53,6 +53,22 @@ function moveKey( delete owner[legacyKey]; } +function migrateTruncateAfterCompaction(raw: Record, changes: string[]): void { + const compaction = getRecord(getRecord(getRecord(raw.agents)?.defaults)?.compaction); + if (!compaction || !Object.hasOwn(compaction, "truncateAfterCompaction")) { + return; + } + if ( + compaction.truncateAfterCompaction === false && + Object.hasOwn(compaction, "maxActiveTranscriptBytes") + ) { + delete compaction.maxActiveTranscriptBytes; + changes.push("Removed maxActiveTranscriptBytes to preserve truncateAfterCompaction: false."); + } + delete compaction.truncateAfterCompaction; + changes.push("Removed retired agents.defaults.compaction.truncateAfterCompaction."); +} + function migrateFinalLayoutRenames(raw: Record, changes: string[]): void { const agents = getRecord(raw.agents); const defaults = getRecord(agents?.defaults); @@ -515,8 +531,13 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED: LegacyConfigMigrationSpec "Legacy Deepgram options moved to providerOptions.deepgram.", hasMediaDeepgram, ), + rule( + ["agents", "defaults", "compaction", "truncateAfterCompaction"], + "agents.defaults.compaction.truncateAfterCompaction is retired; byte-triggered compaction now opts in via maxActiveTranscriptBytes alone.", + ), ], apply: (raw, changes) => { + migrateTruncateAfterCompaction(raw, changes); if (Object.hasOwn(raw, "tui")) { delete raw.tui; changes.push("Removed retired tui config; the footer uses the default compact display."); diff --git a/src/config/config.schema-regressions.test.ts b/src/config/config.schema-regressions.test.ts index 28cd160e76a2..59d99818b95c 100644 --- a/src/config/config.schema-regressions.test.ts +++ b/src/config/config.schema-regressions.test.ts @@ -310,21 +310,6 @@ describe("config schema regressions", () => { expect(res.ok).toBe(true); }); - it("accepts agents.defaults.compaction.truncateAfterCompaction", () => { - const res = validateConfigObject({ - agents: { - defaults: { - compaction: { - truncateAfterCompaction: true, - maxActiveTranscriptBytes: "20mb", - }, - }, - }, - }); - - expect(res.ok).toBe(true); - }); - it("accepts Matrix queue byChannel overrides", () => { const res = validateConfigObject({ messages: { diff --git a/src/config/schema.help.agents.ts b/src/config/schema.help.agents.ts index ceb233541e13..a5a9fe1d2e57 100644 --- a/src/config/schema.help.agents.ts +++ b/src/config/schema.help.agents.ts @@ -150,10 +150,8 @@ export const AGENT_FIELD_HELP: Record = { "Maximum time in seconds allowed for a single compaction operation before it is aborted (default: 180). Increase this for very large sessions that need more time to summarize, or decrease it to fail faster on unresponsive models.", "agents.defaults.compaction.model": "Optional provider/model or configured bare alias used only for compaction summarization. Bare aliases resolve before dispatch; a configured literal model ID wins if it collides with an alias. Leave unset to keep using the primary agent model.", - "agents.defaults.compaction.truncateAfterCompaction": - "When enabled, rotates the active session transcript after compaction so future turns load only the summary and unsummarized tail while the previous full transcript remains archived. Prevents unbounded active transcript growth in long-running sessions. Default: false.", "agents.defaults.compaction.maxActiveTranscriptBytes": - 'Triggers normal local compaction when the active session transcript reaches this size (bytes or strings like "20mb"). Requires truncateAfterCompaction so successful compaction can rotate to a smaller successor transcript; set to 0 or leave unset to disable. This never splits raw transcript bytes.', + 'Byte threshold that triggers normal preflight local compaction when the active session transcript reaches this size (bytes or strings like "20mb"). Set to 0 or leave unset to disable. Also caps Codex app-server native rollout transcripts; oversized native threads restart fresh.', "agents.defaults.compaction.notifyUser": "When enabled, sends brief context-maintenance notices to the user: when compaction starts and completes (for example, '🧹 Compacting context...' and '🧹 Compaction complete'), and when a pre-compaction memory flush is exhausted so the reply continues in a degraded state (for example, '⚠️ Memory maintenance temporarily failed; continuing your reply.'). Disabled by default to keep context maintenance silent and non-intrusive.", "agents.defaults.compaction.memoryFlush": diff --git a/src/config/schema.help.quality.test-fixtures.ts b/src/config/schema.help.quality.test-fixtures.ts index 73047f32cc2c..0214e11e03ff 100644 --- a/src/config/schema.help.quality.test-fixtures.ts +++ b/src/config/schema.help.quality.test-fixtures.ts @@ -340,7 +340,6 @@ export const TARGET_KEYS = [ "agents.defaults.compaction.postCompactionSections", "agents.defaults.compaction.timeoutSeconds", "agents.defaults.compaction.model", - "agents.defaults.compaction.truncateAfterCompaction", "agents.defaults.compaction.maxActiveTranscriptBytes", "agents.defaults.compaction.memoryFlush", "agents.defaults.compaction.memoryFlush.enabled", diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index de91930cd8bb..c164c5432b8f 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -471,7 +471,7 @@ describe("config help copy quality", () => { 'FIELD_HELP["agents.defaults.compaction.maxActiveTranscriptBytes"] test invariant', ); expect(/transcript|bytes|compaction/i.test(transcriptBytes)).toBe(true); - expect(/never splits raw transcript bytes/i.test(transcriptBytes)).toBe(true); + expect(/Codex app-server|native rollout|restart fresh/i.test(transcriptBytes)).toBe(true); const flush = expectDefined( FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"], diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index d0b4bb252966..7b3935360537 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -621,7 +621,6 @@ export const FIELD_LABELS: Record = { "agents.defaults.compaction.postCompactionSections": "Post-Compaction Context Sections", "agents.defaults.compaction.timeoutSeconds": "Compaction Timeout (Seconds)", "agents.defaults.compaction.model": "Compaction Model Override", - "agents.defaults.compaction.truncateAfterCompaction": "Rotate Transcript After Compaction", "agents.defaults.compaction.maxActiveTranscriptBytes": "Compaction Active Transcript Size Threshold", "agents.defaults.compaction.notifyUser": "Compaction Notify User", diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 65047b4fd6f8..6cd20bf8a79f 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -417,18 +417,9 @@ export type AgentCompactionConfig = { */ provider?: string; /** - * Rotate the active session transcript after compaction so the next turn - * starts from the compaction summary and unsummarized tail while the old - * transcript stays archived. - * Default: false (existing behavior preserved). - */ - truncateAfterCompaction?: boolean; - /** - * Trigger a normal local compaction when the active session transcript reaches - * this size (bytes, or byte-size string like "20mb"). Set to 0/unset to - * disable. Requires truncateAfterCompaction so successful compaction can - * rotate to a smaller successor transcript. This does not split raw - * transcript bytes. + * Byte threshold for normal preflight local compaction (bytes, or a byte-size + * string like "20mb"). Set to 0 or leave unset to disable. Also caps Codex + * app-server native rollouts; oversized native threads restart fresh. */ maxActiveTranscriptBytes?: number | string; /** diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 8d039327a233..c5e21c834e51 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -350,17 +350,6 @@ describe("agent defaults schema", () => { expect(result.embeddedAgent?.projectSettingsPolicy).toBe("sanitize"); }); - it("accepts compaction.truncateAfterCompaction", () => { - const result = AgentDefaultsSchema.parse({ - compaction: { - truncateAfterCompaction: true, - maxActiveTranscriptBytes: "20mb", - }, - })!; - expect(result.compaction?.truncateAfterCompaction).toBe(true); - expect(result.compaction?.maxActiveTranscriptBytes).toBe("20mb"); - }); - it.each([ "off", "minimal", diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index e861a0748445..1d99f679597c 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -172,7 +172,6 @@ export const AgentDefaultsSchema = z }) .strict() .optional(), - truncateAfterCompaction: z.boolean().optional(), maxActiveTranscriptBytes: NonNegativeByteSizeSchema.optional(), notifyUser: z.boolean().optional(), })