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
This commit is contained in:
Brad Hallett
2026-05-05 22:35:47 -04:00
committed by GitHub
parent 2b8d91d9ee
commit 0bdba47a3e
6 changed files with 120 additions and 18 deletions

View File

@@ -392,7 +392,8 @@ Docs: https://docs.openclaw.ai
- Agents/sessions: after embedded Pi runs, append assistant-visible reply text to session JSONL only when Pi did not already persist an equivalent tail assistant entry, without re-mirroring the user prompt Pi owns. Fixes #77823. (#77839) Thanks @neeravmakwana.
- Plugins/CLI: load the install-records ledger when listing channel-catalog entries, so npm-installed third-party channel plugins resolve through `openclaw channels login`/`channels add` instead of failing with `Unsupported channel`. (#77269) Thanks @pumpkinxing1.
- Memory wiki/Security: enforce session visibility on shared-memory `wiki_search` and `wiki_get` so sandboxed subagents cannot read transcript content from sibling or parent sessions. Fixes GHSA-72fw-cqh5-f324. Thanks @zsxsoft.
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
- Agents/compaction: disable Pi auto-compaction whenever OpenClaw effectively owns safeguard compaction, including provider-backed safeguard mode, so Pi and OpenClaw no longer fight over long-session compaction. Fixes #73003. (#73839) Thanks @bradhallett.
## 2026.5.3-1

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