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>
This commit is contained in:
Tak Hoffman
2026-02-19 21:41:09 -06:00
committed by GitHub
parent 10dab4f2c7
commit c1ac37a641
8 changed files with 204 additions and 23 deletions

View File

@@ -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.

View File

@@ -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({

View File

@@ -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)

View File

@@ -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();
});
});

View File

@@ -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,
},
};
}

View File

@@ -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");

View File

@@ -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.10.9, default 0.5). */

View File

@@ -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