Files
openclaw/src/agents/pi-settings.ts
Brad Hallett 0bdba47a3e fix: disable Pi auto-compaction when safeguard mode is active (#73839)
Merged via squash.

Prepared head SHA: d554201343
Co-authored-by: bradhallett <53977268+bradhallett@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-05 19:35:47 -07:00

226 lines
8.7 KiB
TypeScript

import type { AgentCompactionMode } from "../config/types.agent-defaults.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { ContextEngineInfo } from "../context-engine/types.js";
import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./pi-compaction-constants.js";
import { resolveProviderEndpoint } from "./provider-attribution.js";
import { normalizeProviderId } from "./provider-id.js";
export const DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000;
type PiSettingsManagerLike = {
getCompactionReserveTokens: () => number;
getCompactionKeepRecentTokens: () => number;
applyOverrides: (overrides: {
compaction: {
reserveTokens?: number;
keepRecentTokens?: number;
};
}) => void;
setCompactionEnabled?: (enabled: boolean) => void;
};
/**
* Ensures the compaction reserve tokens are at least the specified minimum.
* Note: This function is not context-aware and uses an uncapped floor.
* If called for small-context models without threading `contextTokenBudget`,
* it may re-introduce context overflow issues.
*/
export function ensurePiCompactionReserveTokens(params: {
settingsManager: PiSettingsManagerLike;
minReserveTokens?: number;
}): { didOverride: boolean; reserveTokens: number } {
const minReserveTokens = params.minReserveTokens ?? DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR;
const current = params.settingsManager.getCompactionReserveTokens();
if (current >= minReserveTokens) {
return { didOverride: false, reserveTokens: current };
}
params.settingsManager.applyOverrides({
compaction: { reserveTokens: minReserveTokens },
});
return { didOverride: true, reserveTokens: minReserveTokens };
}
export function resolveCompactionReserveTokensFloor(cfg?: OpenClawConfig): number {
const raw = cfg?.agents?.defaults?.compaction?.reserveTokensFloor;
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
return Math.floor(raw);
}
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;
/** When known, the resolved context window budget for the current model. */
contextTokenBudget?: number;
}): {
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);
let reserveTokensFloor = resolveCompactionReserveTokensFloor(params.cfg);
// Cap the floor to a safe fraction of the context window so that
// small-context models (e.g. Ollama with 16 K tokens) are not starved of
// prompt budget. Without this cap the default floor of 20 000 can exceed
// the entire context window, causing every prompt to be classified as an
// overflow and triggering an infinite compaction loop.
const ctxBudget = params.contextTokenBudget;
if (typeof ctxBudget === "number" && Number.isFinite(ctxBudget) && ctxBudget > 0) {
const minPromptBudget = Math.min(
MIN_PROMPT_BUDGET_TOKENS,
Math.max(1, Math.floor(ctxBudget * MIN_PROMPT_BUDGET_RATIO)),
);
const maxReserve = Math.max(0, ctxBudget - minPromptBudget);
reserveTokensFloor = Math.min(reserveTokensFloor, maxReserve);
}
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,
},
};
}
/** Resolve the compaction mode after provider-backed safeguard promotion. */
export function resolveEffectiveCompactionMode(cfg?: OpenClawConfig): AgentCompactionMode {
const compaction = cfg?.agents?.defaults?.compaction;
if (compaction?.provider) {
return "safeguard";
}
return compaction?.mode === "safeguard" ? "safeguard" : "default";
}
/**
* Detect providers whose pi-ai `isContextOverflow` Case 2 (silent overflow)
* fires on a successful turn and triggers Pi's `_runAutoCompaction` from
* inside `Session.prompt()`, collapsing `agent.state.messages` before the
* provider call (openclaw#75799).
*
* True on any of: `zai-native` endpoint class, normalized provider id `zai`,
* a `z-ai/` / `openrouter/z-ai/` model-id namespace prefix, or a bare `glm-`
* model id (no namespace prefix) — the latter covers in-house gateways that
* expose Zhipu's GLM family directly without a `z-ai/` qualifier. Intentionally
* narrow: namespaced GLM ids that route through other providers (e.g.
* `ollama/glm-*`, `opencode-go/glm-*`) are NOT included because their hosts
* have their own overflow accounting and may not exhibit the z.ai silent-
* overflow shape. Other providers documented as silently truncating are not
* added without a reproducible repro.
*/
export function isSilentOverflowProneModel(model: {
provider?: string | null;
modelId?: string | null;
baseUrl?: string | null;
}): boolean {
const provider = normalizeProviderId(typeof model.provider === "string" ? model.provider : "");
if (provider === "zai") {
return true;
}
if (typeof model.baseUrl === "string" && model.baseUrl.length > 0) {
if (resolveProviderEndpoint(model.baseUrl).endpointClass === "zai-native") {
return true;
}
}
if (typeof model.modelId === "string" && model.modelId.length > 0) {
const normalized = model.modelId.toLowerCase();
if (
normalized.startsWith("z-ai/") ||
normalized.startsWith("openrouter/z-ai/") ||
normalized.startsWith("glm-")
) {
return true;
}
}
return false;
}
/**
* Disable Pi's `_checkCompaction → _runAutoCompaction` (which would otherwise
* fire from inside `Session.prompt()` and reassign `agent.state.messages`
* before the provider call) when OpenClaw or a plugin owns compaction:
* `contextEngineInfo.ownsCompaction === true`, effective safeguard compaction,
* or an active model that is silent-overflow-prone (openclaw#75799).
* Default-mode runs against ordinary providers keep Pi's auto-compaction as
* the existing baseline.
*/
export function shouldDisablePiAutoCompaction(params: {
contextEngineInfo?: ContextEngineInfo;
compactionMode?: AgentCompactionMode;
silentOverflowProneProvider?: boolean;
}): boolean {
return (
params.contextEngineInfo?.ownsCompaction === true ||
params.compactionMode === "safeguard" ||
params.silentOverflowProneProvider === true
);
}
/**
* Apply the auto-compaction guard. Callers that reload a `DefaultResourceLoader`
* MUST call this AGAIN after each `reload()` — `settingsManager.reload()`
* rehydrates `compaction.enabled` from disk and silently restores Pi's
* default-on behavior, undoing the guard. Mirrors the existing
* `applyPiCompactionSettingsFromConfig` re-call pattern at the same sites.
*/
export function applyPiAutoCompactionGuard(params: {
settingsManager: PiSettingsManagerLike;
contextEngineInfo?: ContextEngineInfo;
compactionMode?: AgentCompactionMode;
silentOverflowProneProvider?: boolean;
}): { supported: boolean; disabled: boolean } {
const disable = shouldDisablePiAutoCompaction({
contextEngineInfo: params.contextEngineInfo,
compactionMode: params.compactionMode,
silentOverflowProneProvider: params.silentOverflowProneProvider,
});
const hasMethod = typeof params.settingsManager.setCompactionEnabled === "function";
if (!disable || !hasMethod) {
return { supported: hasMethod, disabled: false };
}
params.settingsManager.setCompactionEnabled!(false);
return { supported: true, disabled: true };
}