refactor(config): retire compaction truncateAfterCompaction gate (#115206)

This commit is contained in:
Peter Steinberger
2026-07-28 11:30:50 -04:00
committed by GitHub
parent 35b7d3f933
commit 2160b97cdc
22 changed files with 162 additions and 202 deletions
+6 -3
View File
@@ -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.
<Warning>
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
+2 -4
View File
@@ -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.
@@ -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
@@ -68,7 +68,6 @@ describe("prepareCodexAttemptConnection", () => {
agents: {
defaults: {
compaction: {
truncateAfterCompaction: true,
maxActiveTranscriptBytes: "1mb",
},
},
@@ -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",
},
},
@@ -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",
},
},
@@ -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,
},
},
@@ -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;
}
@@ -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();
@@ -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)
@@ -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",
+3 -5
View File
@@ -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;
}
@@ -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",
@@ -53,6 +53,22 @@ function moveKey(
delete owner[legacyKey];
}
function migrateTruncateAfterCompaction(raw: Record<string, unknown>, 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<string, unknown>, 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.");
@@ -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: {
+1 -3
View File
@@ -150,10 +150,8 @@ export const AGENT_FIELD_HELP: Record<string, string> = {
"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":
@@ -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",
+1 -1
View File
@@ -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"],
-1
View File
@@ -621,7 +621,6 @@ export const FIELD_LABELS: Record<string, string> = {
"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",
+3 -12
View File
@@ -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;
/**
@@ -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",
-1
View File
@@ -172,7 +172,6 @@ export const AgentDefaultsSchema = z
})
.strict()
.optional(),
truncateAfterCompaction: z.boolean().optional(),
maxActiveTranscriptBytes: NonNegativeByteSizeSchema.optional(),
notifyUser: z.boolean().optional(),
})