diff --git a/docs/.generated/config-baseline.counts.json b/docs/.generated/config-baseline.counts.json index c5c701bc2eae..932ec1c02c96 100644 --- a/docs/.generated/config-baseline.counts.json +++ b/docs/.generated/config-baseline.counts.json @@ -1,5 +1,5 @@ { - "core": 2304, + "core": 2306, "channel": 3652, - "plugin": 3575 + "plugin": 3593 } diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index bc3c7638a0ec..72d4c20e97fd 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -8f85e110ec2dc18850cba1b511969f072306c1b110d51be7774065d014f5faa9 config-baseline.json -49f9e5e72503e60b5e6b4a3e41ef642977ce2212e22f6977a1af30701105f419 config-baseline.core.json +cd399e4598294815541d0b6d60ec20bd7a93039da8c1bf62e7988ce66cd3c08f config-baseline.json +1f5e55a5e721ef17efb07fbc59158f7eec40aecf7a40d38a2addc03231e2162e config-baseline.core.json c66f0dde5a8d46f81158c67f9eb27b3889446927e71badaa07f076031a937016 config-baseline.channel.json -f8d5b7f36031c7029e61a017248f3cc752729f51bd700d39359f9aff72db6fbc config-baseline.plugin.json +c82313fac47ebb61e441242aa5821f994637e91ebabc8e088bce2faa6ceaa657 config-baseline.plugin.json diff --git a/docs/concepts/compaction.md b/docs/concepts/compaction.md index 761cd0712556..779ead45ba59 100644 --- a/docs/concepts/compaction.md +++ b/docs/concepts/compaction.md @@ -26,6 +26,8 @@ New configs default `agents.defaults.compaction.mode` to `"safeguard"` (stricter Auto-compaction is on by default. It runs when the session nears the context limit, or when the model returns a context-overflow error (in which case OpenClaw compacts and retries). +Set `agents.defaults.compaction.enabled: false` to disable the embedded runtime's proactive threshold compaction. OpenClaw's preflight and overflow-recovery compaction paths remain available, as does manual `/compact`. + You will see: - `embedded run auto-compaction start` / `complete` in normal Gateway logs. diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index a17da0ce0103..1e3e1c4ec5e3 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -597,6 +597,7 @@ Delegated consults with a requesting agent keep that requester as their owner. W agents: { defaults: { compaction: { + enabled: false, // disable embedded proactive auto-compaction (default: true) mode: "safeguard", // default | safeguard provider: "my-provider", // id of a registered compaction provider plugin (optional) thinkingLevel: "low", // optional compaction-only thinking override @@ -624,6 +625,7 @@ Delegated consults with a requesting agent keep that requester as their owner. W } ``` +- `enabled`: when `false`, disables threshold-driven auto-compaction inside the embedded agent runtime. OpenClaw's preflight and overflow-recovery compaction paths and manual `/compact` remain available. Default: `true`. - `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction). - `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction). - `thinkingLevel`: optional thinking level used only for embedded OpenClaw compaction summaries (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `adaptive`, `max`, or `ultra`). It overrides the session's current thinking level and is clamped to the selected compaction model/runtime. Leave unset to inherit the session level. Native Codex app-server compaction ignores this setting because the native compact request has no per-operation thinking override; OpenClaw logs a warning when configured. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index 8c21aca0078a..14c8017246c1 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -237,6 +237,8 @@ Two additional guards run outside these two triggers: OpenClaw enforces a built-in reserve for embedded runs and caps it against the active model context window so it cannot consume the whole prompt budget. This keeps small-context local models from entering compaction from the first token while leaving enough headroom for multi-turn housekeeping such as the memory flush. +Set `enabled: false` to disable threshold-driven auto-compaction inside the embedded agent runtime. OpenClaw's preflight and overflow-recovery compaction paths remain available, and manual `/compact` continues to work. + Manual `/compact` honors an explicit `agents.defaults.compaction.keepRecentTokens` and keeps the runtime's recent-tail cut point. Without an explicit keep budget, manual compaction is a hard checkpoint and rebuilt context starts from the new summary. When `truncateAfterCompaction` is enabled, OpenClaw rotates the active transcript to a compacted successor after compaction. Branch/restore checkpoint actions use that compacted successor; legacy pre-compaction checkpoint files remain readable while referenced. diff --git a/scripts/pr-lib/merge.sh b/scripts/pr-lib/merge.sh index cc7f8bc12ca6..0170a7cf4302 100644 --- a/scripts/pr-lib/merge.sh +++ b/scripts/pr-lib/merge.sh @@ -28,7 +28,7 @@ print_file_list_with_limit() { auto_merge_unavailable_error() { local log_file="$1" - rg -q -i \ + grep -Eqi -- \ 'auto[- ]merge.*(not allowed|not enabled|not available|unavailable|not configured|not supported|must be enabled)|(not allowed|not enabled|not available|unavailable|not configured|not supported).*auto[- ]merge' \ "$log_file" } diff --git a/src/agents/agent-settings.test.ts b/src/agents/agent-settings.test.ts index f64726c9489d..716cb9110719 100644 --- a/src/agents/agent-settings.test.ts +++ b/src/agents/agent-settings.test.ts @@ -11,6 +11,79 @@ import { shouldCompact } from "./sessions/compaction/compaction.js"; import { SettingsManager } from "./sessions/settings-manager.js"; describe("applyAgentCompactionSettingsFromConfig", () => { + it.each([false, true])( + "applies and preserves compaction.enabled=%s across a settings reload", + async (configuredEnabled) => { + const settingsManager = SettingsManager.inMemory({ + compaction: { enabled: !configuredEnabled, reserveTokens: 20_000 }, + }); + const cfg = { + agents: { defaults: { compaction: { enabled: configuredEnabled } } }, + }; + + const first = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg }); + await settingsManager.reload(); + expect(settingsManager.getCompactionEnabled()).toBe(configuredEnabled); + const afterReload = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg }); + + expect(first.didOverride).toBe(true); + expect(afterReload.didOverride).toBe(false); + expect(settingsManager.getCompactionEnabled()).toBe(configuredEnabled); + }, + ); + + const forcedDisableCases: Array< + [string, Omit[0], "settingsManager">] + > = [ + ["safeguard mode", { compactionMode: "safeguard" }], + [ + "context-engine ownership", + { + contextEngineInfo: { + id: "third-party", + name: "Third-party Context Engine", + version: "0.1.0", + ownsCompaction: true, + }, + }, + ], + ["silent-overflow protection", { silentOverflowProneProvider: true }], + ]; + + it.each(forcedDisableCases)( + "keeps the %s safety guard authoritative over explicit enabled=true", + (_label, guardParams) => { + const settingsManager = SettingsManager.inMemory({ + compaction: { enabled: false, reserveTokens: 20_000 }, + }); + + applyAgentCompactionSettingsFromConfig({ + settingsManager, + cfg: { agents: { defaults: { compaction: { enabled: true } } } }, + }); + const result = applyAgentAutoCompactionGuard({ settingsManager, ...guardParams }); + + expect(result).toEqual({ supported: true, disabled: true }); + expect(settingsManager.getCompactionEnabled()).toBe(false); + }, + ); + + it("preserves the embedded project setting when compaction.enabled is omitted", () => { + const settingsManager = SettingsManager.inMemory({ + compaction: { enabled: false, reserveTokens: 20_000 }, + }); + const setCompactionEnabled = vi.spyOn(settingsManager, "setCompactionEnabled"); + + const result = applyAgentCompactionSettingsFromConfig({ + settingsManager, + cfg: { agents: { defaults: { compaction: {} } } }, + }); + + expect(result.didOverride).toBe(false); + expect(settingsManager.getCompactionEnabled()).toBe(false); + expect(setCompactionEnabled).not.toHaveBeenCalled(); + }); + it("bumps reserveTokens when below floor", () => { const settingsManager = SettingsManager.inMemory(); const applyOverrides = vi.spyOn(settingsManager, "applyOverrides"); diff --git a/src/agents/agent-settings.ts b/src/agents/agent-settings.ts index 5937e404fc73..547918d2625f 100644 --- a/src/agents/agent-settings.ts +++ b/src/agents/agent-settings.ts @@ -9,6 +9,7 @@ import { resolveProviderEndpoint } from "./provider-attribution.js"; export const DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000; type AgentSettingsManagerLike = { + getCompactionEnabled?: () => boolean; getCompactionReserveTokens: () => number; getCompactionKeepRecentTokens: () => number; applyOverrides: (overrides: { @@ -40,6 +41,9 @@ export function applyAgentCompactionSettingsFromConfig(params: { const currentReserveTokens = params.settingsManager.getCompactionReserveTokens(); const currentKeepRecentTokens = params.settingsManager.getCompactionKeepRecentTokens(); const compactionCfg = params.cfg?.agents?.defaults?.compaction; + // Omission preserves embedded/project settings. OpenClaw config reloads create a new + // prepared manager; same-manager resource reloads reuse cfg and reapply explicit values. + const configuredEnabled = compactionCfg?.enabled; const configuredKeepRecentTokens = toPositiveInt(compactionCfg?.keepRecentTokens); let reserveTokensFloor = DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; @@ -76,10 +80,18 @@ export function applyAgentCompactionSettingsFromConfig(params: { overrides.keepRecentTokens = targetKeepRecentTokens; } - const didOverride = Object.keys(overrides).length > 0; - if (didOverride) { + const shouldApplyEnabled = + configuredEnabled !== undefined && + typeof params.settingsManager.setCompactionEnabled === "function" && + (typeof params.settingsManager.getCompactionEnabled !== "function" || + params.settingsManager.getCompactionEnabled() !== configuredEnabled); + if (shouldApplyEnabled) { + params.settingsManager.setCompactionEnabled!(configuredEnabled); + } + if (Object.keys(overrides).length > 0) { params.settingsManager.applyOverrides({ compaction: overrides }); } + const didOverride = shouldApplyEnabled || Object.keys(overrides).length > 0; return { didOverride, diff --git a/src/agents/embedded-agent-runner/run/attempt-session.test.ts b/src/agents/embedded-agent-runner/run/attempt-session.test.ts index c0647eaeed58..4755d450fa29 100644 --- a/src/agents/embedded-agent-runner/run/attempt-session.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt-session.test.ts @@ -202,6 +202,9 @@ describe("prepareEmbeddedAttemptAgentSession", () => { ]); expect(hoisted.applyAgentAutoCompactionGuard).toHaveBeenCalledTimes(2); expect(hoisted.applyAgentCompactionSettingsFromConfig).toHaveBeenCalledOnce(); + expect(hoisted.applyAgentCompactionSettingsFromConfig.mock.invocationCallOrder[0]).toBeLessThan( + hoisted.applyAgentAutoCompactionGuard.mock.invocationCallOrder[1] ?? 0, + ); expect(hoisted.createAgentSessionForEmbeddedRunner).toHaveBeenCalledWith( expect.objectContaining({ resourceLoader: fixture.resourceLoader, diff --git a/src/agents/sessions/agent-session-loop-correctness.test.ts b/src/agents/sessions/agent-session-loop-correctness.test.ts index 348deb365b44..078926632df3 100644 --- a/src/agents/sessions/agent-session-loop-correctness.test.ts +++ b/src/agents/sessions/agent-session-loop-correctness.test.ts @@ -286,6 +286,33 @@ describe("AgentSession loop correctness", () => { ); }); + it("skips threshold maintenance when embedded auto-compaction is disabled", async () => { + const settingsManager = SettingsManager.inMemory({ + compaction: { enabled: false, reserveTokens: 0, keepRecentTokens: 1 }, + retry: { enabled: false }, + }); + const compactionEvents: AgentSessionEvent[] = []; + streamMocks.streamSimple.mockImplementation((activeModel: Model) => + createAssistantResultStream( + createAssistant(activeModel, [{ type: "text", text: "complete answer" }], "stop", 100), + ), + ); + const { session } = await createTestSession({ + settingsManager, + resourceLoader: createResourceLoader(createCompactionHandlers()), + }); + session.subscribe((event) => { + if (event.type === "compaction_end") { + compactionEvents.push(event); + } + }); + + await session.prompt("new prompt"); + + expect(streamMocks.streamSimple).toHaveBeenCalledOnce(); + expect(compactionEvents).toEqual([]); + }); + it("does not retry a high-usage turn terminated by a tool result", async () => { const terminalTool: ToolDefinition = { name: "finish", diff --git a/src/config/schema.help.agents.ts b/src/config/schema.help.agents.ts index 6a938abff50a..ceb233541e13 100644 --- a/src/config/schema.help.agents.ts +++ b/src/config/schema.help.agents.ts @@ -118,6 +118,8 @@ export const AGENT_FIELD_HELP: Record = { 'Image-tool media compression preference: "auto" adapts to provider/model limits and image count, "efficient" saves tokens and bytes, "balanced" keeps the current middle ground, and "high" preserves more detail for screenshots and document images.', "agents.defaults.compaction": "Compaction behavior for when context nears token limits, including strategy and pre-compaction memory flush behavior. Use this when long-running sessions need stable continuity under tight context windows.", + "agents.defaults.compaction.enabled": + "Enable embedded proactive auto-compaction (default: true). Set false to stop threshold-driven embedded compaction while preserving OpenClaw overflow recovery, preflight compaction, and manual /compact.", "agents.defaults.compaction.mode": 'Compaction strategy mode: "default" uses baseline behavior, while "safeguard" applies stricter guardrails to preserve recent context. Keep "default" unless you observe aggressive history loss near limit boundaries.', "agents.defaults.compaction.provider": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 19447f4104f3..fdc54f75fd80 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -608,6 +608,7 @@ export const FIELD_LABELS: Record = { "Agent Sandbox Docker Allow Container Namespace Join", "agents.entries.*.sandbox.docker.gpus": "Agent Sandbox Docker GPUs", "agents.defaults.compaction": "Compaction", + "agents.defaults.compaction.enabled": "Embedded Auto-Compaction", "agents.defaults.compaction.mode": "Compaction Mode", "agents.defaults.compaction.provider": "Compaction Provider", "agents.defaults.compaction.thinkingLevel": "Compaction Thinking Level", diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index c583b8311dae..65047b4fd6f8 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -382,6 +382,8 @@ export type AgentCompactionMidTurnPrecheckConfig = { }; export type AgentCompactionConfig = { + /** Enable embedded proactive auto-compaction. Default: true. */ + enabled?: boolean; /** Compaction summarization mode. */ mode?: AgentCompactionMode; /** Override the session thinking level for embedded OpenClaw compaction summaries. */ diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 3784981de675..8d039327a233 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -410,6 +410,24 @@ describe("agent defaults schema", () => { expect(result.compaction?.midTurnPrecheck?.enabled).toBe(true); }); + it("accepts compaction.enabled so auto-compaction can be turned off", () => { + const result = AgentDefaultsSchema.parse({ + compaction: { + enabled: false, + }, + })!; + + expect(result.compaction?.enabled).toBe(false); + }); + + it("rejects a non-boolean compaction.enabled", () => { + expect( + AgentDefaultsSchema.safeParse({ + compaction: { enabled: "false" }, + }).success, + ).toBe(false); + }); + it("accepts focused contextLimits on defaults and agent entries", () => { const defaults = AgentDefaultsSchema.parse({ contextLimits: { diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 5ee49d8d273b..e861a0748445 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -139,6 +139,7 @@ export const AgentDefaultsSchema = z .optional(), compaction: z .object({ + enabled: z.boolean().optional(), mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(), provider: z.string().optional(), thinkingLevel: AgentThinkingLevelSchema.optional(),