From 2c7d64232ead6d8b40039044aabe017e9b666671 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 22:23:03 +0100 Subject: [PATCH] feat: enable adaptive context pruning by default --- CHANGELOG.md | 1 + docs/concepts/session-pruning.md | 15 ++++++++--- docs/concepts/session.md | 4 +-- docs/gateway/configuration.md | 15 +++++++++-- src/config/config.test.ts | 46 ++++++++++++++++++++++++++++++-- src/config/defaults.ts | 19 +++++++++++++ src/config/io.ts | 17 ++++++++---- 7 files changed, 103 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64270e4737..7a3fe94cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Agent: enable adaptive context pruning by default for tool-result trimming. - Doctor: check config/state permissions and offer to tighten them. — thanks @steipete - Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete - Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c diff --git a/docs/concepts/session-pruning.md b/docs/concepts/session-pruning.md index d59b77b6e7..e5666d83fd 100644 --- a/docs/concepts/session-pruning.md +++ b/docs/concepts/session-pruning.md @@ -1,12 +1,12 @@ --- -summary: "Session pruning: opt-in tool-result trimming to reduce context bloat" +summary: "Session pruning: tool-result trimming to reduce context bloat" read_when: - You want to reduce LLM context growth from tool outputs - You are tuning agent.contextPruning --- # Session Pruning -Session pruning trims **old tool results** from the in-memory context right before each LLM call. It is **opt-in** and does **not** rewrite the on-disk session history (`*.jsonl`). +Session pruning trims **old tool results** from the in-memory context right before each LLM call. It does **not** rewrite the on-disk session history (`*.jsonl`). ## When it runs - Before each LLM request (context hook). @@ -59,7 +59,7 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz - `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }` ## Examples -Minimal (adaptive): +Default (adaptive): ```json5 { agent: { @@ -68,6 +68,15 @@ Minimal (adaptive): } ``` +To disable: +```json5 +{ + agent: { + contextPruning: { mode: "off" } + } +} +``` + Aggressive: ```json5 { diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 311015beaf..902c174a0a 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -21,8 +21,8 @@ All session state is **owned by the gateway** (the “master” Clawdbot). UI cl - Group entries may include `displayName`, `provider`, `subject`, `room`, and `space` to label sessions in UIs. - Clawdbot does **not** read legacy Pi/Tau session folders. -## Session pruning (optional) -Clawdbot can trim **old tool results** from the in-memory context right before LLM calls (opt-in). +## Session pruning +Clawdbot trims **old tool results** from the in-memory context right before LLM calls by default. This does **not** rewrite JSONL history. See [/concepts/session-pruning](/concepts/session-pruning). ## Mapping transports → session keys diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 88ca0361ae..86880352a0 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -994,7 +994,7 @@ If you configure the same alias name (case-insensitive) yourself, your value win } ``` -#### `agent.contextPruning` (opt-in tool-result pruning) +#### `agent.contextPruning` (tool-result pruning) `agent.contextPruning` prunes **old tool results** from the in-memory context right before a request is sent to the LLM. It does **not** modify the session history on disk (`*.jsonl` remains complete). @@ -1025,7 +1025,7 @@ Notes / current limitations: - If the session doesn’t contain at least `keepLastAssistants` assistant messages yet, pruning is skipped. - In `aggressive` mode, `hardClear.enabled` is ignored (eligible tool results are always replaced with `hardClear.placeholder`). -Example (minimal): +Default (adaptive): ```json5 { agent: { @@ -1036,6 +1036,17 @@ Example (minimal): } ``` +To disable: +```json5 +{ + agent: { + contextPruning: { + mode: "off" + } + } +} +``` + Defaults (when `mode` is `"adaptive"` or `"aggressive"`): - `keepLastAssistants`: `3` - `softTrimRatio`: `0.3` (adaptive only) diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 3125f93369..5f697a64cb 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -269,7 +269,7 @@ describe("config identity defaults", () => { }); }); - it("does not synthesize agent/session when absent", async () => { + it("does not synthesize session when absent", async () => { await withTempHome(async (home) => { const configDir = path.join(home, ".clawdbot"); await fs.mkdir(configDir, { recursive: true }); @@ -295,7 +295,7 @@ describe("config identity defaults", () => { expect(cfg.routing?.groupChat?.mentionPatterns).toEqual([ "\\b@?Samantha\\b", ]); - expect(cfg.agent).toBeUndefined(); + expect(cfg.agent?.contextPruning?.mode).toBe("adaptive"); expect(cfg.session).toBeUndefined(); }); }); @@ -327,6 +327,48 @@ describe("config identity defaults", () => { }); }); +describe("config pruning defaults", () => { + it("defaults contextPruning mode to adaptive", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdbot"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdbot.json"), + JSON.stringify({ agent: {} }, null, 2), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.agent?.contextPruning?.mode).toBe("adaptive"); + }); + }); + + it("does not override explicit contextPruning mode", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdbot"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdbot.json"), + JSON.stringify( + { agent: { contextPruning: { mode: "off" } } }, + null, + 2, + ), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.agent?.contextPruning?.mode).toBe("off"); + }); + }); +}); + describe("config discord", () => { let previousHome: string | undefined; diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 68d35bef83..5aa5fa1068 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -161,6 +161,25 @@ export function applyLoggingDefaults(cfg: ClawdbotConfig): ClawdbotConfig { }; } +export function applyContextPruningDefaults( + cfg: ClawdbotConfig, +): ClawdbotConfig { + const agent = cfg.agent; + const contextPruning = agent?.contextPruning; + if (contextPruning?.mode) return cfg; + + return { + ...cfg, + agent: { + ...agent, + contextPruning: { + ...contextPruning, + mode: "adaptive", + }, + }, + }; +} + export function resetSessionDefaultsWarningForTests() { defaultWarnState = { warned: false }; } diff --git a/src/config/io.ts b/src/config/io.ts index 2e2c7e275d..d361484e26 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -17,6 +17,7 @@ import { applyLoggingDefaults, applyMessageDefaults, applyModelDefaults, + applyContextPruningDefaults, applySessionDefaults, applyTalkApiKey, } from "./defaults.js"; @@ -135,10 +136,12 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { return {}; } const cfg = applyModelDefaults( - applySessionDefaults( - applyLoggingDefaults( - applyMessageDefaults( - applyIdentityDefaults(validated.data as ClawdbotConfig), + applyContextPruningDefaults( + applySessionDefaults( + applyLoggingDefaults( + applyMessageDefaults( + applyIdentityDefaults(validated.data as ClawdbotConfig), + ), ), ), ), @@ -182,7 +185,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { const exists = deps.fs.existsSync(configPath); if (!exists) { const config = applyTalkApiKey( - applyModelDefaults(applySessionDefaults(applyMessageDefaults({}))), + applyModelDefaults( + applyContextPruningDefaults( + applySessionDefaults(applyMessageDefaults({})), + ), + ), ); const legacyIssues: LegacyConfigIssue[] = []; return {