diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf4124c65f..4d0b9be450e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/agents/command/cli-compaction.ts b/src/agents/command/cli-compaction.ts index 3cd0c702b41..a3370e35e59 100644 --- a/src/agents/command/cli-compaction.ts +++ b/src/agents/command/cli-compaction.ts @@ -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({ diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/pi-embedded-runner/extensions.ts index c639d58ea14..fa88f47d934 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/pi-embedded-runner/extensions.ts @@ -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({ diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 30084da735e..d55caa0e407 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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, diff --git a/src/agents/pi-settings.test.ts b/src/agents/pi-settings.test.ts index 81c05e12c6c..bca472c9b14 100644 --- a/src/agents/pi-settings.test.ts +++ b/src/agents/pi-settings.test.ts @@ -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 diff --git a/src/agents/pi-settings.ts b/src/agents/pi-settings.ts index 4daedcad832..8beb54d94b0 100644 --- a/src/agents/pi-settings.ts +++ b/src/agents/pi-settings.ts @@ -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";