mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"core": 2304,
|
||||
"core": 2306,
|
||||
"channel": 3652,
|
||||
"plugin": 3575
|
||||
"plugin": 3593
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user