fix: disable Pi auto-compaction when safeguard mode is active (#73003)

When compaction.mode = "safeguard", OpenClaw's compaction is permanently
blocked by Pi SDK's internal auto-compaction. The two systems operate on
different thresholds and do not coordinate, causing consecutive
already_compacted_recently failures and uncontrolled session growth.

Extend shouldDisablePiAutoCompaction() to recognize safeguard mode in
addition to context engines that own compaction. Pass compactionMode from
both call sites (attempt.ts, cli-compaction.ts).

Fixes #73003
This commit is contained in:
Brad Hallett
2026-04-28 18:31:05 -04:00
committed by Josh Lehman
parent b9f711089a
commit 5e0d761480
5 changed files with 118 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { AgentCompactionMode } from "../../config/types.agent-defaults.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { resolveContextEngine as resolveContextEngineImpl } from "../../context-engine/registry.js";
import type { ContextEngine } from "../../context-engine/types.js";
@@ -10,7 +11,10 @@ import { runContextEngineMaintenance as runContextEngineMaintenanceImpl } from "
import { shouldPreemptivelyCompactBeforePrompt as shouldPreemptivelyCompactBeforePromptImpl } from "../pi-embedded-runner/run/preemptive-compaction.js";
import { resolveLiveToolResultMaxChars as resolveLiveToolResultMaxCharsImpl } from "../pi-embedded-runner/tool-result-truncation.js";
import { createPreparedEmbeddedPiSettingsManager as createPreparedEmbeddedPiSettingsManagerImpl } from "../pi-project-settings.js";
import { applyPiAutoCompactionGuard as applyPiAutoCompactionGuardImpl } from "../pi-settings.js";
import {
applyPiAutoCompactionGuard as applyPiAutoCompactionGuardImpl,
resolveEffectiveCompactionMode,
} from "../pi-settings.js";
import type { SkillSnapshot } from "../skills.js";
import { recordCliCompactionInStore as recordCliCompactionInStoreImpl } from "./session-store.js";
@@ -38,6 +42,7 @@ type CliCompactionDeps = {
applyPiAutoCompactionGuard: (params: {
settingsManager: SettingsManagerLike;
contextEngineInfo?: ContextEngine["info"];
compactionMode?: AgentCompactionMode;
}) => unknown;
shouldPreemptivelyCompactBeforePrompt: typeof shouldPreemptivelyCompactBeforePromptImpl;
resolveLiveToolResultMaxChars: typeof resolveLiveToolResultMaxCharsImpl;
@@ -207,6 +212,7 @@ export async function runCliTurnCompactionLifecycle(params: {
await cliCompactionDeps.applyPiAutoCompactionGuard({
settingsManager,
contextEngineInfo: contextEngine.info,
compactionMode: resolveEffectiveCompactionMode(params.cfg),
});
const preemptiveCompaction = cliCompactionDeps.shouldPreemptivelyCompactBeforePrompt({

View File

@@ -12,7 +12,7 @@ import contextPruningExtension from "../pi-hooks/context-pruning.js";
import { setContextPruningRuntime } from "../pi-hooks/context-pruning/runtime.js";
import { computeEffectiveSettings } from "../pi-hooks/context-pruning/settings.js";
import { makeToolPrunablePredicate } from "../pi-hooks/context-pruning/tools.js";
import { ensurePiCompactionReserveTokens } from "../pi-settings.js";
import { ensurePiCompactionReserveTokens, resolveEffectiveCompactionMode } from "../pi-settings.js";
import { resolveTranscriptPolicy } from "../transcript-policy.js";
import { isCacheTtlEligibleProvider, readLastCacheTtlTimestamp } from "./cache-ttl.js";
@@ -123,15 +123,6 @@ function buildContextPruningFactory(params: {
return contextPruningExtension;
}
function resolveCompactionMode(cfg?: OpenClawConfig): "default" | "safeguard" {
const compaction = cfg?.agents?.defaults?.compaction;
// A registered compaction provider requires the safeguard extension path
if (compaction?.provider) {
return "safeguard";
}
return compaction?.mode === "safeguard" ? "safeguard" : "default";
}
export function buildEmbeddedExtensionFactories(params: {
cfg: OpenClawConfig | undefined;
sessionManager: SessionManager;
@@ -140,7 +131,7 @@ export function buildEmbeddedExtensionFactories(params: {
model: ProviderRuntimeModel | undefined;
}): ExtensionFactory[] {
const factories: ExtensionFactory[] = [];
if (resolveCompactionMode(params.cfg) === "safeguard") {
if (resolveEffectiveCompactionMode(params.cfg) === "safeguard") {
const compactionCfg = params.cfg?.agents?.defaults?.compaction;
const qualityGuardCfg = compactionCfg?.qualityGuard;
const contextWindowInfo = resolveContextWindowInfo({

View File

@@ -107,6 +107,7 @@ import {
applyPiAutoCompactionGuard,
applyPiCompactionSettingsFromConfig,
isSilentOverflowProneModel,
resolveEffectiveCompactionMode,
} from "../../pi-settings.js";
import {
createClientToolNameConflictError,
@@ -1453,6 +1454,7 @@ export async function runEmbeddedAttempt(
const piAutoCompactionGuardArgs = {
settingsManager,
contextEngineInfo: activeContextEngine?.info,
compactionMode: resolveEffectiveCompactionMode(params.config),
silentOverflowProneProvider: isSilentOverflowProneModel({
provider: params.provider,
modelId: params.modelId,

View File

@@ -5,7 +5,9 @@ import {
applyPiCompactionSettingsFromConfig,
DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR,
isSilentOverflowProneModel,
resolveEffectiveCompactionMode,
resolveCompactionReserveTokensFloor,
shouldDisablePiAutoCompaction,
} from "./pi-settings.js";
describe("applyPiCompactionSettingsFromConfig", () => {
@@ -347,6 +349,40 @@ describe("resolveCompactionReserveTokensFloor", () => {
).toBe(0);
});
});
describe("resolveEffectiveCompactionMode", () => {
it("defaults to default compaction mode", () => {
expect(resolveEffectiveCompactionMode()).toBe("default");
expect(resolveEffectiveCompactionMode({ agents: { defaults: { compaction: {} } } })).toBe(
"default",
);
expect(
resolveEffectiveCompactionMode({
agents: { defaults: { compaction: { mode: "default" } } },
}),
).toBe("default");
});
it("returns safeguard for explicit safeguard mode", () => {
expect(
resolveEffectiveCompactionMode({
agents: { defaults: { compaction: { mode: "safeguard" } } },
}),
).toBe("safeguard");
});
it("returns safeguard when a compaction provider is configured", () => {
expect(
resolveEffectiveCompactionMode({
agents: { defaults: { compaction: { provider: "deepseek" } } },
}),
).toBe("safeguard");
expect(
resolveEffectiveCompactionMode({
agents: { defaults: { compaction: { mode: "default", provider: "deepseek" } } },
}),
).toBe("safeguard");
});
});
describe("isSilentOverflowProneModel", () => {
// Reporter's repro shape: openrouter routing to z-ai/glm. Both the bare
@@ -432,6 +468,36 @@ describe("isSilentOverflowProneModel", () => {
});
});
describe("shouldDisablePiAutoCompaction", () => {
it("returns false with no owner, default mode, and ordinary provider behavior", () => {
expect(shouldDisablePiAutoCompaction({})).toBe(false);
expect(shouldDisablePiAutoCompaction({ compactionMode: "default" })).toBe(false);
expect(
shouldDisablePiAutoCompaction({
contextEngineInfo: { id: "legacy", name: "Legacy", ownsCompaction: false },
compactionMode: "default",
silentOverflowProneProvider: false,
}),
).toBe(false);
});
it("returns true when a context engine owns compaction", () => {
expect(
shouldDisablePiAutoCompaction({
contextEngineInfo: { id: "third-party", name: "Third-party", ownsCompaction: true },
}),
).toBe(true);
});
it("returns true when effective compaction mode is safeguard", () => {
expect(shouldDisablePiAutoCompaction({ compactionMode: "safeguard" })).toBe(true);
});
it("returns true for silent-overflow-prone providers", () => {
expect(shouldDisablePiAutoCompaction({ silentOverflowProneProvider: true })).toBe(true);
});
});
describe("applyPiAutoCompactionGuard", () => {
// Direct repro of openclaw#75799: pi-ai's silent-overflow detection misfires
// on a successful turn against z.ai-style providers, triggering Pi's
@@ -481,6 +547,26 @@ describe("applyPiAutoCompactionGuard", () => {
expect(setCompactionEnabled).toHaveBeenCalledWith(false);
});
it("disables Pi auto-compaction when provider config forces safeguard mode", () => {
const setCompactionEnabled = vi.fn();
const settingsManager = {
getCompactionReserveTokens: () => 20_000,
getCompactionKeepRecentTokens: () => 4_000,
applyOverrides: () => {},
setCompactionEnabled,
};
const result = applyPiAutoCompactionGuard({
settingsManager,
compactionMode: resolveEffectiveCompactionMode({
agents: { defaults: { compaction: { provider: "deepseek" } } },
}),
});
expect(result).toEqual({ supported: true, disabled: true });
expect(setCompactionEnabled).toHaveBeenCalledWith(false);
});
// Default-mode runs against ordinary providers must keep Pi's auto-compaction
// enabled. Disabling it across the board would silently remove Pi's
// overflow-recovery path inside Session.prompt() for users who are not

View File

@@ -1,3 +1,4 @@
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";
@@ -124,6 +125,15 @@ export function applyPiCompactionSettingsFromConfig(params: {
};
}
/** 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
@@ -171,16 +181,20 @@ export function isSilentOverflowProneModel(model: {
* 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`, or the active model is
* silent-overflow-prone (openclaw#75799). Default-mode runs against ordinary
* providers keep Pi's auto-compaction as the existing baseline.
* `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.
*/
function shouldDisablePiAutoCompaction(params: {
export function shouldDisablePiAutoCompaction(params: {
contextEngineInfo?: ContextEngineInfo;
compactionMode?: AgentCompactionMode;
silentOverflowProneProvider?: boolean;
}): boolean {
return (
params.contextEngineInfo?.ownsCompaction === true || params.silentOverflowProneProvider === true
params.contextEngineInfo?.ownsCompaction === true ||
params.compactionMode === "safeguard" ||
params.silentOverflowProneProvider === true
);
}
@@ -194,10 +208,12 @@ function shouldDisablePiAutoCompaction(params: {
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";