From 873dbc4dfd1c35ee5c7142769fd4371db0e0f1e5 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Fri, 20 Mar 2026 16:22:37 +0200 Subject: [PATCH] fix(compaction): clarify skip reasons --- src/agents/pi-embedded-runner/compact.ts | 7 +++-- src/auto-reply/reply/commands-compact.ts | 3 ++ src/auto-reply/reply/commands.test.ts | 39 ++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 219ae9bc028..13cc4089703 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -64,7 +64,10 @@ import { validateAnthropicTurns, validateGeminiTurns, } from "../pi-embedded-helpers.js"; -import { consumeCompactionSafeguardCancelReason } from "../pi-extensions/compaction-safeguard-runtime.js"; +import { + consumeCompactionSafeguardCancelReason, + setCompactionSafeguardCancelReason, +} from "../pi-extensions/compaction-safeguard-runtime.js"; import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js"; import { createOpenClawCodingTools } from "../pi-tools.js"; import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js"; @@ -1122,7 +1125,7 @@ export async function compactEmbeddedPiSessionDirect( } const result = await compactWithSafetyTimeout( () => { - consumeCompactionSafeguardCancelReason(compactionSessionManager); + setCompactionSafeguardCancelReason(compactionSessionManager, undefined); return session.compact(params.customInstructions); }, compactionTimeoutMs, diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 14ba1fe7a16..fa4ff3b313e 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -62,6 +62,9 @@ function formatCompactionReason(reason?: string): string | undefined { const lower = text.toLowerCase(); if (lower.includes("nothing to compact")) { + return "nothing compactable in this session yet"; + } + if (lower.includes("below threshold")) { return "context is below the compaction threshold"; } if (lower.includes("already compacted")) { diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index e1399b19888..b65708cefaf 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -627,7 +627,7 @@ describe("/compact command", () => { ); }); - it("labels benign no-op compaction results as skipped", async () => { + it("labels nothing-to-compact results as skipped without calling them below-threshold", async () => { const cfg = { commands: { text: true }, channels: { whatsapp: { allowFrom: ["*"] } }, @@ -655,15 +655,48 @@ describe("/compact command", () => { expect(result).toEqual({ shouldContinue: false, reply: { - text: "⚙️ Compaction skipped: context is below the compaction threshold • Context 31k/?", + text: "⚙️ Compaction skipped: nothing compactable in this session yet • Context 31k/?", }, }); expect(vi.mocked(enqueueSystemEvent)).toHaveBeenCalledWith( - "Compaction skipped: context is below the compaction threshold • Context 31k/?", + "Compaction skipped: nothing compactable in this session yet • Context 31k/?", { sessionKey: params.sessionKey }, ); }); + it("formats below-threshold skip reasons with friendly copy", async () => { + const cfg = { + commands: { text: true }, + channels: { whatsapp: { allowFrom: ["*"] } }, + } as OpenClawConfig; + const params = buildParams("/compact", cfg); + vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + ok: false, + compacted: false, + reason: "Compaction skipped: below threshold for manual compaction", + }); + + const result = await handleCompactCommand( + { + ...params, + sessionEntry: { + sessionId: "session-1", + updatedAt: Date.now(), + totalTokens: 31_000, + contextTokens: 200_000, + }, + }, + true, + ); + + expect(result).toEqual({ + shouldContinue: false, + reply: { + text: "⚙️ Compaction skipped: context is below the compaction threshold • Context 31k/?", + }, + }); + }); + it("keeps true compaction errors labeled as failures", async () => { const cfg = { commands: { text: true },