fix(config): compaction.enabled is rejected as invalid config (#114118)

* fix(config): compaction.enabled is rejected as invalid config

Setting agents.defaults.compaction.enabled in openclaw.json made the
whole config fail to load with "agents.defaults.compaction: Invalid
input", so auto-compaction could not be turned off.

The runtime already reads the field (SettingsManager.getCompactionEnabled
returns settings.compaction?.enabled ?? true, and setCompactionEnabled
writes it), and the documented config example in
docs/reference/session-management-compaction.md already shows
"enabled: true". Only the zod schema was missing the key, and the
compaction object is .strict(), so the unknown key rejected the config.

Adds enabled to AgentDefaultsSchema and to AgentCompactionConfig.

Closes #110065

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): wire compaction enablement through runtime

Align the accepted compaction.enabled contract across runtime precedence, help, labels, docs, and generated schema baselines.

Co-authored-by: Zakaria Rahali <zakariarahali288@gmail.com>

* test(config): preserve omitted compaction setting

Document the reload lifecycle and lock in project-setting precedence when the OpenClaw config key is absent.

Co-authored-by: Zakaria Rahali <zakariarahali288@gmail.com>

* refactor(config): centralize compaction runtime override

* fix(config): preserve compaction safety guards

* fix(scripts): avoid ripgrep in merge fallback

* docs(config): refresh current main baseline

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Zakaria Rahali
2026-07-27 22:23:35 +01:00
committed by GitHub
parent 903d8bc9d5
commit a04ba3a468
15 changed files with 153 additions and 8 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{
"core": 2304,
"core": 2306,
"channel": 3652,
"plugin": 3575
"plugin": 3593
}
+3 -3
View File
@@ -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
+2
View File
@@ -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.
+2
View File
@@ -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.
@@ -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.
+1 -1
View File
@@ -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"
}
+73
View File
@@ -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<Parameters<typeof applyAgentAutoCompactionGuard>[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");
+14 -2
View File
@@ -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,
@@ -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,
@@ -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",
+2
View File
@@ -118,6 +118,8 @@ export const AGENT_FIELD_HELP: Record<string, string> = {
'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":
+1
View File
@@ -608,6 +608,7 @@ export const FIELD_LABELS: Record<string, string> = {
"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",
+2
View File
@@ -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. */
@@ -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: {
+1
View File
@@ -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(),