diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f350bc3d1..b820465190c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -263,6 +263,7 @@ Docs: https://docs.openclaw.ai - Discord/model picker persistence check: add a short post-dispatch settle delay before reading back session model state so picker confirmations stop reporting false mismatch warnings after successful model switches. (#39105) Thanks @akropp. - Agents/OpenAI WS compat store flag: omit `store` from `response.create` payloads when model compat sets `supportsStore: false`, preventing strict OpenAI-compatible providers from rejecting websocket requests with unknown-field errors. (#39113) Thanks @scoootscooob. - Config/validation log sanitization: sanitize config-validation issue paths/messages before logging so control characters and ANSI escape sequences cannot inject misleading terminal output from crafted config content. (#39116) Thanks @powermaster888. +- Agents/compaction counter accuracy: count successful overflow-triggered auto-compactions (`willRetry=true`) in the compaction counter while still excluding aborted/no-result events, so `/status` reflects actual safeguard compaction activity. (#39123) Thanks @MumuTW. ## 2026.3.2 diff --git a/src/agents/pi-embedded-subscribe.handlers.compaction.ts b/src/agents/pi-embedded-subscribe.handlers.compaction.ts index f25d05f0065..705ffb7cf89 100644 --- a/src/agents/pi-embedded-subscribe.handlers.compaction.ts +++ b/src/agents/pi-embedded-subscribe.handlers.compaction.ts @@ -40,11 +40,17 @@ export function handleAutoCompactionStart(ctx: EmbeddedPiSubscribeContext) { export function handleAutoCompactionEnd( ctx: EmbeddedPiSubscribeContext, - evt: AgentEvent & { willRetry?: unknown }, + evt: AgentEvent & { willRetry?: unknown; result?: unknown; aborted?: unknown }, ) { ctx.state.compactionInFlight = false; const willRetry = Boolean(evt.willRetry); - if (!willRetry) { + // Increment counter whenever compaction actually produced a result, + // regardless of willRetry. Overflow-triggered compaction sets willRetry=true + // (the framework retries the LLM request), but the compaction itself succeeded + // and context was trimmed — the counter must reflect that. (#38905) + const hasResult = evt.result != null; + const wasAborted = Boolean(evt.aborted); + if (hasResult && !wasAborted) { ctx.incrementCompactionCount?.(); } if (willRetry) { diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts index 334839730f6..22d0a30bfde 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts @@ -38,11 +38,26 @@ describe("subscribeEmbeddedPiSession", () => { emit({ type: "auto_compaction_start" }); expect(subscription.getCompactionCount()).toBe(0); - emit({ type: "auto_compaction_end", willRetry: true }); + // willRetry with result — counter IS incremented (overflow compaction succeeded) + emit({ type: "auto_compaction_end", willRetry: true, result: { summary: "s" } }); + expect(subscription.getCompactionCount()).toBe(1); + + // willRetry=false with result — counter incremented again + emit({ type: "auto_compaction_end", willRetry: false, result: { summary: "s2" } }); + expect(subscription.getCompactionCount()).toBe(2); + }); + + it("does not count compaction when result is absent", async () => { + const { emit, subscription } = createSubscribedSessionHarness({ + runId: "run-compaction-no-result", + }); + + // No result (e.g. aborted or cancelled) — counter stays at 0 + emit({ type: "auto_compaction_end", willRetry: false, result: undefined }); expect(subscription.getCompactionCount()).toBe(0); - emit({ type: "auto_compaction_end", willRetry: false }); - expect(subscription.getCompactionCount()).toBe(1); + emit({ type: "auto_compaction_end", willRetry: false, aborted: true }); + expect(subscription.getCompactionCount()).toBe(0); }); it("emits compaction events on the agent event bus", async () => { diff --git a/src/plugins/wired-hooks-compaction.test.ts b/src/plugins/wired-hooks-compaction.test.ts index 7ba3c3ad090..5081922ec1d 100644 --- a/src/plugins/wired-hooks-compaction.test.ts +++ b/src/plugins/wired-hooks-compaction.test.ts @@ -100,6 +100,7 @@ describe("compaction hook wiring", () => { { type: "auto_compaction_end", willRetry: false, + result: { summary: "compacted" }, } as never, ); @@ -122,7 +123,7 @@ describe("compaction hook wiring", () => { }); }); - it("does not call runAfterCompaction when willRetry is true", () => { + it("does not call runAfterCompaction when willRetry is true but still increments counter", () => { hookMocks.runner.hasHooks.mockReturnValue(true); const ctx = { @@ -132,7 +133,8 @@ describe("compaction hook wiring", () => { noteCompactionRetry: vi.fn(), resetForCompactionRetry: vi.fn(), maybeResolveCompactionWait: vi.fn(), - getCompactionCount: () => 0, + incrementCompactionCount: vi.fn(), + getCompactionCount: () => 1, }; handleAutoCompactionEnd( @@ -140,10 +142,13 @@ describe("compaction hook wiring", () => { { type: "auto_compaction_end", willRetry: true, + result: { summary: "compacted" }, } as never, ); expect(hookMocks.runner.runAfterCompaction).not.toHaveBeenCalled(); + // Counter is incremented even with willRetry — compaction succeeded (#38905) + expect(ctx.incrementCompactionCount).toHaveBeenCalledTimes(1); expect(ctx.noteCompactionRetry).toHaveBeenCalledTimes(1); expect(ctx.resetForCompactionRetry).toHaveBeenCalledTimes(1); expect(ctx.maybeResolveCompactionWait).not.toHaveBeenCalled(); @@ -154,6 +159,75 @@ describe("compaction hook wiring", () => { }); }); + it("does not increment counter when compaction was aborted", () => { + const ctx = { + params: { runId: "r3b", session: { messages: [] } }, + state: { compactionInFlight: true }, + log: { debug: vi.fn(), warn: vi.fn() }, + maybeResolveCompactionWait: vi.fn(), + incrementCompactionCount: vi.fn(), + getCompactionCount: () => 0, + }; + + handleAutoCompactionEnd( + ctx as never, + { + type: "auto_compaction_end", + willRetry: false, + result: undefined, + aborted: true, + } as never, + ); + + expect(ctx.incrementCompactionCount).not.toHaveBeenCalled(); + }); + + it("does not increment counter when compaction has result but was aborted", () => { + const ctx = { + params: { runId: "r3b2", session: { messages: [] } }, + state: { compactionInFlight: true }, + log: { debug: vi.fn(), warn: vi.fn() }, + maybeResolveCompactionWait: vi.fn(), + incrementCompactionCount: vi.fn(), + getCompactionCount: () => 0, + }; + + handleAutoCompactionEnd( + ctx as never, + { + type: "auto_compaction_end", + willRetry: false, + result: { summary: "compacted" }, + aborted: true, + } as never, + ); + + expect(ctx.incrementCompactionCount).not.toHaveBeenCalled(); + }); + + it("does not increment counter when result is undefined", () => { + const ctx = { + params: { runId: "r3c", session: { messages: [] } }, + state: { compactionInFlight: true }, + log: { debug: vi.fn(), warn: vi.fn() }, + maybeResolveCompactionWait: vi.fn(), + incrementCompactionCount: vi.fn(), + getCompactionCount: () => 0, + }; + + handleAutoCompactionEnd( + ctx as never, + { + type: "auto_compaction_end", + willRetry: false, + result: undefined, + aborted: false, + } as never, + ); + + expect(ctx.incrementCompactionCount).not.toHaveBeenCalled(); + }); + it("resets stale assistant usage after final compaction", () => { const messages = [ { role: "user", content: "hello" }, @@ -183,6 +257,7 @@ describe("compaction hook wiring", () => { { type: "auto_compaction_end", willRetry: false, + result: { summary: "compacted" }, } as never, );