From c1ac37a6410a9869406ab82a7d0883cbd26aacf0 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:41:09 -0600 Subject: [PATCH] Config: expose Pi compaction tuning values (openclaw#21568) thanks @Takhoffman Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/compact.ts | 9 +- src/agents/pi-embedded-runner/run/attempt.ts | 9 +- src/agents/pi-settings.e2e.test.ts | 106 ++++++++++++++++-- src/agents/pi-settings.ts | 65 ++++++++++- src/config/config.compaction-settings.test.ts | 31 +++++ src/config/types.agent-defaults.ts | 4 + src/config/zod-schema.agent-defaults.ts | 2 + 8 files changed, 204 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecee33b83..93ce8c4cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Config/Agents: expose Pi compaction tuning values `agents.defaults.compaction.reserveTokens` and `agents.defaults.compaction.keepRecentTokens` in config schema/types and apply them in embedded Pi runner settings overrides with floor enforcement via `reserveTokensFloor`. (#21568) Thanks @Takhoffman. - Auto-reply/WebChat: avoid defaulting inbound runtime channel labels to unrelated providers (for example `whatsapp`) for webchat sessions so channel-specific formatting guidance stays accurate. (#21534) Thanks @lbo728. - Status: include persisted `cacheRead`/`cacheWrite` in session summaries so compact `/status` output consistently shows cache hit percentages from real session data. - Heartbeat/Cron: restore interval heartbeat behavior so missing `HEARTBEAT.md` no longer suppresses runs (only effectively empty files skip), preserving prompt-driven and tagged-cron execution paths. diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 5dd39a53e..fc6548caa 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -37,10 +37,7 @@ import { validateAnthropicTurns, validateGeminiTurns, } from "../pi-embedded-helpers.js"; -import { - ensurePiCompactionReserveTokens, - resolveCompactionReserveTokensFloor, -} from "../pi-settings.js"; +import { applyPiCompactionSettingsFromConfig } from "../pi-settings.js"; import { createOpenClawCodingTools } from "../pi-tools.js"; import { resolveSandboxContext } from "../sandbox.js"; import { repairSessionFileIfNeeded } from "../session-file-repair.js"; @@ -532,9 +529,9 @@ export async function compactEmbeddedPiSessionDirect( }); trackSessionManagerAccess(params.sessionFile); const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); - ensurePiCompactionReserveTokens({ + applyPiCompactionSettingsFromConfig({ settingsManager, - minReserveTokens: resolveCompactionReserveTokensFloor(params.config), + cfg: params.config, }); // Call for side effects (sets compaction/pruning runtime state) buildEmbeddedExtensionPaths({ diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 364448eb8..fb808d56f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -45,10 +45,7 @@ import { validateGeminiTurns, } from "../../pi-embedded-helpers.js"; import { subscribeEmbeddedPiSession } from "../../pi-embedded-subscribe.js"; -import { - ensurePiCompactionReserveTokens, - resolveCompactionReserveTokensFloor, -} from "../../pi-settings.js"; +import { applyPiCompactionSettingsFromConfig } from "../../pi-settings.js"; import { toClientToolDefinitions } from "../../pi-tool-definition-adapter.js"; import { createOpenClawCodingTools, resolveToolLoopDetectionConfig } from "../../pi-tools.js"; import { resolveSandboxContext } from "../../sandbox.js"; @@ -531,9 +528,9 @@ export async function runEmbeddedAttempt( }); const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); - ensurePiCompactionReserveTokens({ + applyPiCompactionSettingsFromConfig({ settingsManager, - minReserveTokens: resolveCompactionReserveTokensFloor(params.config), + cfg: params.config, }); // Call for side effects (sets compaction/pruning runtime state) diff --git a/src/agents/pi-settings.e2e.test.ts b/src/agents/pi-settings.e2e.test.ts index dc0f03415..ac6efe829 100644 --- a/src/agents/pi-settings.e2e.test.ts +++ b/src/agents/pi-settings.e2e.test.ts @@ -1,37 +1,123 @@ import { describe, expect, it, vi } from "vitest"; import { + applyPiCompactionSettingsFromConfig, DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR, - ensurePiCompactionReserveTokens, resolveCompactionReserveTokensFloor, } from "./pi-settings.js"; -describe("ensurePiCompactionReserveTokens", () => { +describe("applyPiCompactionSettingsFromConfig", () => { it("bumps reserveTokens when below floor", () => { const settingsManager = { getCompactionReserveTokens: () => 16_384, + getCompactionKeepRecentTokens: () => 20_000, applyOverrides: vi.fn(), }; - const result = ensurePiCompactionReserveTokens({ settingsManager }); + const result = applyPiCompactionSettingsFromConfig({ settingsManager }); - expect(result).toEqual({ - didOverride: true, - reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR, - }); + expect(result.didOverride).toBe(true); + expect(result.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ compaction: { reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR }, }); }); - it("does not override when already above floor", () => { + it("does not override when already above floor and not in safeguard mode", () => { const settingsManager = { getCompactionReserveTokens: () => 32_000, + getCompactionKeepRecentTokens: () => 20_000, applyOverrides: vi.fn(), }; - const result = ensurePiCompactionReserveTokens({ settingsManager }); + const result = applyPiCompactionSettingsFromConfig({ + settingsManager, + cfg: { agents: { defaults: { compaction: { mode: "default" } } } }, + }); - expect(result).toEqual({ didOverride: false, reserveTokens: 32_000 }); + expect(result.didOverride).toBe(false); + expect(result.compaction.reserveTokens).toBe(32_000); + expect(settingsManager.applyOverrides).not.toHaveBeenCalled(); + }); + + it("applies explicit reserveTokens but still enforces floor", () => { + const settingsManager = { + getCompactionReserveTokens: () => 10_000, + getCompactionKeepRecentTokens: () => 20_000, + applyOverrides: vi.fn(), + }; + + const result = applyPiCompactionSettingsFromConfig({ + settingsManager, + cfg: { + agents: { + defaults: { + compaction: { reserveTokens: 12_000, reserveTokensFloor: 20_000 }, + }, + }, + }, + }); + + expect(result.compaction.reserveTokens).toBe(20_000); + expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ + compaction: { reserveTokens: 20_000 }, + }); + }); + + it("applies keepRecentTokens when explicitly configured", () => { + const settingsManager = { + getCompactionReserveTokens: () => 20_000, + getCompactionKeepRecentTokens: () => 20_000, + applyOverrides: vi.fn(), + }; + + const result = applyPiCompactionSettingsFromConfig({ + settingsManager, + cfg: { + agents: { + defaults: { + compaction: { + keepRecentTokens: 15_000, + }, + }, + }, + }, + }); + + expect(result.compaction.keepRecentTokens).toBe(15_000); + expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ + compaction: { keepRecentTokens: 15_000 }, + }); + }); + + it("preserves current keepRecentTokens when safeguard mode leaves it unset", () => { + const settingsManager = { + getCompactionReserveTokens: () => 25_000, + getCompactionKeepRecentTokens: () => 20_000, + applyOverrides: vi.fn(), + }; + + const result = applyPiCompactionSettingsFromConfig({ + settingsManager, + cfg: { agents: { defaults: { compaction: { mode: "safeguard" } } } }, + }); + + expect(result.compaction.keepRecentTokens).toBe(20_000); + expect(settingsManager.applyOverrides).not.toHaveBeenCalled(); + }); + + it("treats keepRecentTokens=0 as invalid and keeps the current setting", () => { + const settingsManager = { + getCompactionReserveTokens: () => 25_000, + getCompactionKeepRecentTokens: () => 20_000, + applyOverrides: vi.fn(), + }; + + const result = applyPiCompactionSettingsFromConfig({ + settingsManager, + cfg: { agents: { defaults: { compaction: { mode: "safeguard", keepRecentTokens: 0 } } } }, + }); + + expect(result.compaction.keepRecentTokens).toBe(20_000); expect(settingsManager.applyOverrides).not.toHaveBeenCalled(); }); }); diff --git a/src/agents/pi-settings.ts b/src/agents/pi-settings.ts index 955db01c6..3ea4c5d5b 100644 --- a/src/agents/pi-settings.ts +++ b/src/agents/pi-settings.ts @@ -4,7 +4,13 @@ export const DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000; type PiSettingsManagerLike = { getCompactionReserveTokens: () => number; - applyOverrides: (overrides: { compaction: { reserveTokens: number } }) => void; + getCompactionKeepRecentTokens: () => number; + applyOverrides: (overrides: { + compaction: { + reserveTokens?: number; + keepRecentTokens?: number; + }; + }) => void; }; export function ensurePiCompactionReserveTokens(params: { @@ -32,3 +38,60 @@ export function resolveCompactionReserveTokensFloor(cfg?: OpenClawConfig): numbe } return DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; } + +function toNonNegativeInt(value: unknown): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return undefined; + } + return Math.floor(value); +} + +function toPositiveInt(value: unknown): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { + return undefined; + } + return Math.floor(value); +} + +export function applyPiCompactionSettingsFromConfig(params: { + settingsManager: PiSettingsManagerLike; + cfg?: OpenClawConfig; +}): { + didOverride: boolean; + compaction: { reserveTokens: number; keepRecentTokens: number }; +} { + const currentReserveTokens = params.settingsManager.getCompactionReserveTokens(); + const currentKeepRecentTokens = params.settingsManager.getCompactionKeepRecentTokens(); + const compactionCfg = params.cfg?.agents?.defaults?.compaction; + + const configuredReserveTokens = toNonNegativeInt(compactionCfg?.reserveTokens); + const configuredKeepRecentTokens = toPositiveInt(compactionCfg?.keepRecentTokens); + const reserveTokensFloor = resolveCompactionReserveTokensFloor(params.cfg); + + const targetReserveTokens = Math.max( + configuredReserveTokens ?? currentReserveTokens, + reserveTokensFloor, + ); + const targetKeepRecentTokens = configuredKeepRecentTokens ?? currentKeepRecentTokens; + + const overrides: { reserveTokens?: number; keepRecentTokens?: number } = {}; + if (targetReserveTokens !== currentReserveTokens) { + overrides.reserveTokens = targetReserveTokens; + } + if (targetKeepRecentTokens !== currentKeepRecentTokens) { + overrides.keepRecentTokens = targetKeepRecentTokens; + } + + const didOverride = Object.keys(overrides).length > 0; + if (didOverride) { + params.settingsManager.applyOverrides({ compaction: overrides }); + } + + return { + didOverride, + compaction: { + reserveTokens: targetReserveTokens, + keepRecentTokens: targetKeepRecentTokens, + }, + }; +} diff --git a/src/config/config.compaction-settings.test.ts b/src/config/config.compaction-settings.test.ts index b6eada303..289748ccc 100644 --- a/src/config/config.compaction-settings.test.ts +++ b/src/config/config.compaction-settings.test.ts @@ -38,6 +38,8 @@ describe("config compaction settings", () => { expect(cfg.agents?.defaults?.compaction?.reserveTokensFloor).toBe(12_345); expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard"); + expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBeUndefined(); + expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBeUndefined(); expect(cfg.agents?.defaults?.compaction?.memoryFlush?.enabled).toBe(false); expect(cfg.agents?.defaults?.compaction?.memoryFlush?.softThresholdTokens).toBe(1234); expect(cfg.agents?.defaults?.compaction?.memoryFlush?.prompt).toBe("Write notes."); @@ -45,6 +47,35 @@ describe("config compaction settings", () => { }); }); + it("preserves pi compaction override values", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".openclaw"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "openclaw.json"), + JSON.stringify( + { + agents: { + defaults: { + compaction: { + reserveTokens: 15_000, + keepRecentTokens: 12_000, + }, + }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + const cfg = loadConfig(); + expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBe(15_000); + expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBe(12_000); + }); + }); + it("defaults compaction mode to safeguard", async () => { await withTempHome(async (home) => { const configDir = path.join(home, ".openclaw"); diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index c89d44261..aa3fbe419 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -289,6 +289,10 @@ export type AgentCompactionMode = "default" | "safeguard"; export type AgentCompactionConfig = { /** Compaction summarization mode. */ mode?: AgentCompactionMode; + /** Pi reserve tokens target before floor enforcement. */ + reserveTokens?: number; + /** Pi keepRecentTokens budget used for cut-point selection. */ + keepRecentTokens?: number; /** Minimum reserve tokens enforced for Pi compaction (0 disables the floor). */ reserveTokensFloor?: number; /** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */ diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index d99af6dc2..4ec06f66b 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -91,6 +91,8 @@ export const AgentDefaultsSchema = z compaction: z .object({ mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(), + reserveTokens: z.number().int().nonnegative().optional(), + keepRecentTokens: z.number().int().positive().optional(), reserveTokensFloor: z.number().int().nonnegative().optional(), maxHistoryShare: z.number().min(0.1).max(0.9).optional(), memoryFlush: z