From 529d0ceae75eb90e1bd37568a8cda583d0c6a134 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Thu, 30 Jul 2026 09:30:36 +0800 Subject: [PATCH 1/2] fix: acp_status hides consumed compress calls from PROTECTED list hideConsumedCompressCalls was only running in the transform pipeline (hooks.ts), not in the acp_status tool handler. The tool fetched raw DB messages, so consumed (inactive) compress calls showed as PROTECTED even though they're invisible in the model's actual context. This misled the model into thinking there was much more protected content than actually existed. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../2026-07-30_acp-status-consumed-fix/REQ.md | 23 +++ .../WORKLOG.md | 14 ++ lib/compress/status.ts | 3 + tests/acp-status-consumed-fix.test.ts | 135 ++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 devlog/2026-07-30_acp-status-consumed-fix/REQ.md create mode 100644 devlog/2026-07-30_acp-status-consumed-fix/WORKLOG.md create mode 100644 tests/acp-status-consumed-fix.test.ts diff --git a/devlog/2026-07-30_acp-status-consumed-fix/REQ.md b/devlog/2026-07-30_acp-status-consumed-fix/REQ.md new file mode 100644 index 0000000..f42861b --- /dev/null +++ b/devlog/2026-07-30_acp-status-consumed-fix/REQ.md @@ -0,0 +1,23 @@ +# REQ: acp_status shows consumed compress calls as PROTECTED + +## Problem + +`acp_status` fetches messages directly from the DB via `fetchSessionMessages`, +bypassing the transform pipeline. `hideConsumedCompressCalls` only modifies +`output.messages` in the transform hook — the DB retains original compress parts. + +Result: consumed (inactive) compress calls show as `[PROTECTED: compress — not compressible]` +in `acp_status` output, even though they're invisible in the model's actual context. +This confuses the model into thinking there's much more protected content than +actually exists, causing misjudgment about what's available for compression. + +## Fix + +Call `hideConsumedCompressCalls(state, rawMessages)` in `createAcpStatusTool`'s +execute handler, after `fetchSessionMessages` but before `buildStatusReport`. +This ensures `acp_status` reflects the same message view the model sees. + +## Files + +- `lib/compress/status.ts` — import + call `hideConsumedCompressCalls` +- `tests/acp-status-consumed-fix.test.ts` — 2 tests (consumed hidden, active visible) diff --git a/devlog/2026-07-30_acp-status-consumed-fix/WORKLOG.md b/devlog/2026-07-30_acp-status-consumed-fix/WORKLOG.md new file mode 100644 index 0000000..97ca780 --- /dev/null +++ b/devlog/2026-07-30_acp-status-consumed-fix/WORKLOG.md @@ -0,0 +1,14 @@ +# WORKLOG: acp_status consumed-compress fix + +## Implementation + +1. Added `hideConsumedCompressCalls` import in `lib/compress/status.ts` +2. Added `hideConsumedCompressCalls(ctx.state, rawMessages)` call after `fetchSessionMessages`, before `buildStatusReport` +3. Wrote 2 tests: + - "consumed compress calls not shown as PROTECTED after hideConsumedCompressCalls" — verifies the before/after fix behavior + - "active compress calls still shown as PROTECTED" — verifies active compress calls survive filtering + +## Verification + +- typecheck: 0 errors +- full test suite: 936/936 pass diff --git a/lib/compress/status.ts b/lib/compress/status.ts index 621b013..f4ca415 100644 --- a/lib/compress/status.ts +++ b/lib/compress/status.ts @@ -12,6 +12,7 @@ import { formatCompressibleRanges, } from "../messages/inject/utils" import { fetchSessionMessages } from "./search" +import { hideConsumedCompressCalls } from "./hide-consumed" import { estimateSystemPromptTokens } from "../token-utils" const ACP_STATUS_TOOL_DESCRIPTION = `Show context status — overview includes compressible ranges by default. @@ -607,6 +608,8 @@ export function createAcpStatusTool(factoryCtx: ToolFactoryContext): ReturnType< rawMessages = [] } + hideConsumedCompressCalls(ctx.state, rawMessages) + return buildStatusReport( { state: ctx.state, config: ctx.config }, rawMessages, diff --git a/tests/acp-status-consumed-fix.test.ts b/tests/acp-status-consumed-fix.test.ts new file mode 100644 index 0000000..a1bdd49 --- /dev/null +++ b/tests/acp-status-consumed-fix.test.ts @@ -0,0 +1,135 @@ +import { describe, it } from "node:test" +import assert from "node:assert/strict" +import { createSessionState } from "../lib/state" +import type { WithParts, SessionState } from "../lib/state" +import { assignMessageRefs } from "../lib/message-ids" +import { buildStatusReport } from "../lib/compress/status" +import { hideConsumedCompressCalls } from "../lib/compress/hide-consumed" + +const SID = "ses-consumed-status" + +function makeMsg( + id: string, + role: "user" | "assistant", + text: string, + toolParts: unknown[] = [], +): WithParts { + const parts: unknown[] = [] + if (text) parts.push({ type: "text", text }) + for (const tp of toolParts) parts.push(tp) + return { + info: { id, role, sessionID: SID, agent: "a", time: { created: 1 } } as never, + parts, + } as WithParts +} + +function compressToolPart(callID: string, summary: string, input?: unknown): unknown { + return { + type: "tool", + callID, + tool: "compress", + state: { status: "completed", input: input ?? { content: [{ summary }] } }, + } +} + +function setupRefs(state: SessionState, messages: WithParts[]): void { + assignMessageRefs(state, messages) +} + +describe("acp_status consumed-compress fix", () => { + it("consumed compress calls not shown as PROTECTED after hideConsumedCompressCalls", () => { + const state = createSessionState() + const messages = [ + makeMsg("msg-1", "user", "hello"), + makeMsg("msg-2", "assistant", "response", [compressToolPart("c-old", "old summary text")]), + makeMsg("msg-3", "assistant", "normal text"), + makeMsg("msg-4", "assistant", "active compress", [compressToolPart("c-active", "active summary")]), + ] + setupRefs(state, messages) + + const blocksById = new Map() + blocksById.set(1, { + blockId: 1, + runId: 1, + active: false, + deactivatedByUser: false, + deactivatedByUserDeep: false, + deactivatedByBlockId: 2, + compressMessageId: "msg-2", + compressCallId: "c-old", + tier: 1, + }) + blocksById.set(2, { + blockId: 2, + runId: 2, + active: true, + deactivatedByUser: false, + deactivatedByUserDeep: false, + compressMessageId: "msg-4", + compressCallId: "c-active", + tier: 2, + }) + const activeBlockIds = new Set([2]) + state.prune = { + messages: { blocksById, activeBlockIds, byMessageId: new Map(), activeByAnchorMessageId: new Map() }, + } as never + + const beforeFix = buildStatusReport( + { state, config: { compress: { protectedTools: ["skill", "compress"] } } } as never, + messages, + { scope: "uncompressed" }, + ) + assert.ok( + beforeFix.includes("PROTECTED"), + "before fix: consumed compress should appear as PROTECTED", + ) + + hideConsumedCompressCalls(state, messages) + const afterFix = buildStatusReport( + { state, config: { compress: { protectedTools: ["skill", "compress"] } } } as never, + messages, + { scope: "uncompressed" }, + ) + assert.ok( + afterFix.includes("PROTECTED"), + "active compress should still be PROTECTED", + ) + assert.ok( + !afterFix.includes("old summary text"), + "after fix: consumed compress summary should not appear in status output", + ) + }) + + it("active compress calls still shown as PROTECTED", () => { + const state = createSessionState() + const messages = [ + makeMsg("msg-1", "user", "hello"), + makeMsg("msg-2", "assistant", "work"), + makeMsg("msg-3", "assistant", "active compress", [compressToolPart("c-active", "my active summary")]), + ] + setupRefs(state, messages) + + const blocksById = new Map() + blocksById.set(1, { + blockId: 1, + runId: 1, + active: true, + deactivatedByUser: false, + deactivatedByUserDeep: false, + compressMessageId: "msg-3", + compressCallId: "c-active", + tier: 1, + }) + state.prune = { + messages: { blocksById, activeBlockIds: new Set([1]), byMessageId: new Map(), activeByAnchorMessageId: new Map() }, + } as never + + hideConsumedCompressCalls(state, messages) + const report = buildStatusReport( + { state, config: { compress: { protectedTools: ["skill", "compress"] } } } as never, + messages, + { scope: "uncompressed" }, + ) + assert.ok(report.includes("PROTECTED"), "active compress should still be PROTECTED") + }) +}) From 736d714a7a1551cfc2f6585338c5dc76ce99b404 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Thu, 30 Jul 2026 09:59:17 +0800 Subject: [PATCH 2/2] fix: whitelist hideConsumedCompressCalls + keep last 2 orphaned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switched from blacklist (hide consumed blocks) to whitelist (keep active blocks) + keep-last-2-orphaned approach. This handles failed compress calls that never created a block (status=error) — they were previously untouched and accumulated in context. Logic: - Collect activeCallIds from active blocks (whitelist) - Collect allBlockCallIds from ALL blocks (to identify orphaned) - Find last 2 orphaned compress calls (callID not in any block) - keepCallIds = activeCallIds ∪ last2OrphanedCallIds - Hide compress parts not in keepCallIds Removed early return when blocksById.size === 0 — orphaned compress calls should still be processed even with no blocks in state. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- lib/compress/hide-consumed.ts | 54 ++++----- tests/hide-consumed.test.ts | 219 ++++++++++++++++++++++------------ 2 files changed, 167 insertions(+), 106 deletions(-) diff --git a/lib/compress/hide-consumed.ts b/lib/compress/hide-consumed.ts index 83dcf53..b653dba 100644 --- a/lib/compress/hide-consumed.ts +++ b/lib/compress/hide-consumed.ts @@ -1,52 +1,42 @@ import type { SessionState, WithParts } from "../state" import { hasMeaningfulContent } from "./parts" -/** - * Hide compress tool-call parts whose blocks have been consumed by another - * compression (tier escalation or range overlap). - * - * This is the compress-as-anchor equivalent of filterCompressedRanges: instead - * of hiding whole messages covered by active blocks, it hides individual - * compress tool-call PARTS whose blocks are no longer active. The message shell - * survives (it may carry other parts); only the consumed compress call disappears. - * - * If after removal the message contains only structural parts (step-start, - * step-finish, reasoning), it is spliced entirely — those carry no useful - * content once the compress call they wrapped is gone. - * - * Block data (full summary, original messages) is preserved in session state and - * remains recoverable via `decompress`. - * - * Returns the number of compress-call parts hidden. - */ +const KEEP_LAST_ORPHANED = 2 + export function hideConsumedCompressCalls(state: SessionState, messages: WithParts[]): number { - if (state.prune.messages.blocksById.size === 0) { - return 0 - } - const consumedMessageIds = new Set() + const activeCallIds = new Set() + const allBlockCallIds = new Set() for (const block of state.prune.messages.blocksById.values()) { - if (block.active) continue - if (block.deactivatedByUser) continue - if (block.deactivatedByUserDeep) continue - if (block.deactivatedByBlockId === undefined) continue - if (!block.compressMessageId) continue - consumedMessageIds.add(block.compressMessageId) + if (block.compressCallId) { + allBlockCallIds.add(block.compressCallId) + if (block.active && !block.deactivatedByUser && !block.deactivatedByUserDeep) { + activeCallIds.add(block.compressCallId) + } + } } - if (consumedMessageIds.size === 0) { - return 0 + const lastOrphanedCallIds: string[] = [] + for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) { + const parts = Array.isArray(messages[i]?.parts) ? messages[i]!.parts : [] + for (let j = parts.length - 1; j >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; j--) { + const p = parts[j]! + if (p.type === "tool" && p.tool === "compress" && p.callID && !allBlockCallIds.has(p.callID)) { + lastOrphanedCallIds.push(p.callID) + } + } } + const keepCallIds = new Set([...activeCallIds, ...lastOrphanedCallIds]) + let hidden = 0 for (let i = 0; i < messages.length; i++) { const msg = messages[i]! - if (!consumedMessageIds.has(msg.info.id)) continue - const parts = Array.isArray(msg.parts) ? msg.parts : [] let changed = false const remaining = parts.filter((p) => { if (p.type === "tool" && p.tool === "compress") { + if (p.callID && keepCallIds.has(p.callID)) return true hidden++ changed = true return false diff --git a/tests/hide-consumed.test.ts b/tests/hide-consumed.test.ts index 9ee5f7f..5301a63 100644 --- a/tests/hide-consumed.test.ts +++ b/tests/hide-consumed.test.ts @@ -1,82 +1,64 @@ import { describe, it } from "node:test" import assert from "node:assert/strict" -import type { SessionState, WithParts } from "../lib/state" +import type { WithParts, SessionState } from "../lib/state" import { hideConsumedCompressCalls } from "../lib/compress/hide-consumed" +import type { CompressionBlock } from "../lib/state/types" -function makeBlock(overrides: Record): Record { +function makeBlock(overrides: Partial & { blockId: number }): CompressionBlock { return { - blockId: 0, - runId: 0, - active: true, - deactivatedByUser: false, - deactivatedByUserDeep: false, - deactivatedAt: undefined, - deactivatedByBlockId: undefined, - compressMessageId: undefined, - compressCallId: undefined, - anchorMessageId: "anchor-1", - summary: "test summary", - summaryTokens: 10, - survivedCount: 0, - generation: "young", - mode: "range", - tier: 1, - topic: "", - batchTopic: undefined, - startId: "m00001", - endId: "m00010", - includedBlockIds: [], - consumedBlockIds: [], - parentBlockIds: [], - directMessageIds: [], - directToolIds: [], - effectiveMessageIds: [], - effectiveToolIds: [], - createdAt: Date.now(), - durationMs: 0, - compressedTokens: 0, + blockId: overrides.blockId, + runId: overrides.runId ?? 1, + displayId: overrides.displayId ?? `b${overrides.blockId}`, + active: overrides.active ?? true, + tier: overrides.tier ?? 1, + topic: overrides.topic ?? "test", + summary: overrides.summary ?? "test summary", + compressMessageId: overrides.compressMessageId ?? "", + compressCallId: overrides.compressCallId ?? "", + directMessageIds: overrides.directMessageIds ?? [], + effectiveMessageIds: overrides.effectiveMessageIds ?? [], + generation: overrides.generation ?? "young", + survivedCount: overrides.survivedCount ?? 0, + createdAt: overrides.createdAt ?? Date.now(), + compressedTokens: overrides.compressedTokens ?? 100, + summaryLength: overrides.summaryLength ?? 20, + deactivatedByBlockId: overrides.deactivatedByBlockId, + deactivatedByUser: overrides.deactivatedByUser, + deactivatedByUserDeep: overrides.deactivatedByUserDeep, + consumedBlockIds: overrides.consumedBlockIds, ...overrides, - } + } as CompressionBlock } -function makeState(blocks: Record[]): SessionState { - const blocksById = new Map() - const activeBlockIds = new Set() - for (const b of blocks) { - blocksById.set(b.blockId as number, b) - if (b.active) activeBlockIds.add(b.blockId as number) - } +function makeState(blocks: CompressionBlock[]): Pick { + const blocksById = new Map() + for (const b of blocks) blocksById.set(b.blockId, b) return { - sessionId: "test-session", prune: { messages: { blocksById, - activeBlockIds, byMessageId: new Map(), - activeByAnchorMessageId: new Map(), + activeBlockIds: blocks.filter((b) => b.active).map((b) => b.blockId), }, }, - nudges: {}, - stats: { pruneTokenCounter: 0, totalPruneTokens: 0 }, - messageIds: { byRef: new Map(), byId: new Map(), nextRefId: 1 }, - compressionTiming: { startsByCallId: new Map(), pendingByCallId: new Map() }, - toolParameters: [], - } as unknown as SessionState + } as any } describe("hideConsumedCompressCalls", () => { - it("hides T1 compress call when T2 consumes it (previous turn)", () => { + it("hides consumed T1 compress call when T2 consumes it (previous turn)", () => { const b1 = makeBlock({ blockId: 1, active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -87,17 +69,17 @@ describe("hideConsumedCompressCalls", () => { info: { id: "msg-t1-compress", role: "assistant" } as any, parts: [ { type: "text", text: "Compressing" }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, ], }, { info: { id: "msg-t2-compress", role: "assistant" } as any, - parts: [{ type: "tool", tool: "compress", state: { status: "completed" } }], + parts: [{ type: "tool", tool: "compress", callID: "call-t4", state: { status: "completed" } }], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) const t1Msg = messages.find((m) => m.info.id === "msg-t1-compress")! @@ -107,18 +89,20 @@ describe("hideConsumedCompressCalls", () => { ) }) - it("hides T1 compress call even when it is AFTER lastUserIdx (same-turn T1+T2)", () => { + it("hides consumed T1 compress call even when it is AFTER lastUserIdx (same-turn T1+T2)", () => { const b1 = makeBlock({ blockId: 1, active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -127,17 +111,17 @@ describe("hideConsumedCompressCalls", () => { { info: { id: "msg-user-1", role: "user" } as any, parts: [{ type: "text", text: "Hi" }] }, { info: { id: "msg-t1-compress", role: "assistant" } as any, - parts: [{ type: "tool", tool: "compress", state: { status: "completed" } }], + parts: [{ type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }], }, { info: { id: "msg-t2-compress", role: "assistant" } as any, - parts: [{ type: "tool", tool: "compress", state: { status: "completed" } }], + parts: [{ type: "tool", tool: "compress", callID: "call-t4", state: { status: "completed" } }], }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) - assert.equal(hidden, 1, "T1 compress call after lastUserIdx should be hidden") + assert.equal(hidden, 1, "consumed T1 compress call should be hidden") assert.equal( messages.find((m) => m.info.id === "msg-t1-compress"), undefined, @@ -149,11 +133,12 @@ describe("hideConsumedCompressCalls", () => { ) }) - it("does NOT hide T1 compress call when T1 is still active", () => { + it("does NOT hide active T1 compress call", () => { const b1 = makeBlock({ blockId: 1, active: true, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) @@ -162,29 +147,31 @@ describe("hideConsumedCompressCalls", () => { { info: { id: "msg-user-1", role: "user" } as any, parts: [{ type: "text", text: "Hi" }] }, { info: { id: "msg-t1-compress", role: "assistant" } as any, - parts: [{ type: "tool", tool: "compress", state: { status: "completed" } }], + parts: [{ type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 0) assert.ok(messages.find((m) => m.info.id === "msg-t1-compress")) }) - it("preserves non-compress parts when hiding compress call", () => { + it("preserves non-compress parts when hiding consumed compress call", () => { const b1 = makeBlock({ blockId: 1, active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -195,14 +182,14 @@ describe("hideConsumedCompressCalls", () => { info: { id: "msg-t1-compress", role: "assistant" } as any, parts: [ { type: "text", text: "Let me compress" }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, { type: "tool", tool: "bash", state: { status: "completed" } }, ], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) const t1Msg = messages.find((m) => m.info.id === "msg-t1-compress")! @@ -219,12 +206,14 @@ describe("hideConsumedCompressCalls", () => { active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -235,14 +224,14 @@ describe("hideConsumedCompressCalls", () => { info: { id: "msg-t1-compress", role: "assistant" } as any, parts: [ { type: "reasoning", text: "I need to compress the early messages..." }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, { type: "step-finish", reason: "stop" }, ], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) assert.equal( @@ -258,12 +247,14 @@ describe("hideConsumedCompressCalls", () => { active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -274,13 +265,13 @@ describe("hideConsumedCompressCalls", () => { info: { id: "msg-t1-compress", role: "assistant" } as any, parts: [ { type: "reasoning", text: "Analyzing context usage..." }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, ], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) assert.equal( @@ -296,12 +287,14 @@ describe("hideConsumedCompressCalls", () => { active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -313,14 +306,14 @@ describe("hideConsumedCompressCalls", () => { parts: [ { type: "reasoning", text: "I need to compress..." }, { type: "text", text: "Compressing early messages" }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, { type: "step-finish", reason: "stop" }, ], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) const t1Msg = messages.find((m) => m.info.id === "msg-t1-compress")! @@ -338,12 +331,14 @@ describe("hideConsumedCompressCalls", () => { active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -354,7 +349,7 @@ describe("hideConsumedCompressCalls", () => { info: { id: "msg-t1-compress", role: "assistant" } as any, parts: [ { type: "reasoning", text: "I need to compress..." }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, { type: "tool", tool: "bash", state: { status: "completed" } }, { type: "step-finish", reason: "stop" }, ], @@ -362,7 +357,7 @@ describe("hideConsumedCompressCalls", () => { { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) const t1Msg = messages.find((m) => m.info.id === "msg-t1-compress")! @@ -379,12 +374,14 @@ describe("hideConsumedCompressCalls", () => { active: false, deactivatedByBlockId: 4, compressMessageId: "msg-t1-compress", + compressCallId: "call-t1", tier: 1, }) const b4 = makeBlock({ blockId: 4, active: true, compressMessageId: "msg-t2-compress", + compressCallId: "call-t4", tier: 2, }) @@ -395,14 +392,14 @@ describe("hideConsumedCompressCalls", () => { info: { id: "msg-t1-compress", role: "assistant" } as any, parts: [ { type: "step-start" }, - { type: "tool", tool: "compress", state: { status: "completed" } }, + { type: "tool", tool: "compress", callID: "call-t1", state: { status: "completed" } }, { type: "step-finish", reason: "stop" }, ], }, { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, ] - const hidden = hideConsumedCompressCalls(state, messages) + const hidden = hideConsumedCompressCalls(state as SessionState, messages) assert.equal(hidden, 1) assert.equal( @@ -411,4 +408,78 @@ describe("hideConsumedCompressCalls", () => { "step-start + step-finish orphan should be spliced", ) }) + + it("keeps last 2 orphaned (failed) compress calls, hides older ones", () => { + const b1 = makeBlock({ + blockId: 1, + active: true, + compressMessageId: "msg-good-compress", + compressCallId: "call-good", + tier: 1, + }) + + const state = makeState([b1]) + const messages: WithParts[] = [ + { info: { id: "msg-user-1", role: "user" } as any, parts: [{ type: "text", text: "Hi" }] }, + { + info: { id: "msg-fail-1", role: "assistant" } as any, + parts: [ + { type: "text", text: "Trying compress..." }, + { type: "tool", tool: "compress", callID: "call-fail-1", state: { status: "error" } }, + ], + }, + { + info: { id: "msg-fail-2", role: "assistant" } as any, + parts: [ + { type: "text", text: "Retry..." }, + { type: "tool", tool: "compress", callID: "call-fail-2", state: { status: "error" } }, + ], + }, + { + info: { id: "msg-fail-3", role: "assistant" } as any, + parts: [ + { type: "text", text: "Retry..." }, + { type: "tool", tool: "compress", callID: "call-fail-3", state: { status: "error" } }, + ], + }, + { + info: { id: "msg-good-compress", role: "assistant" } as any, + parts: [{ type: "tool", tool: "compress", callID: "call-good", state: { status: "completed" } }], + }, + { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, + ] + + const hidden = hideConsumedCompressCalls(state as SessionState, messages) + + assert.equal(hidden, 1) + const fail1Msg = messages.find((m) => m.info.id === "msg-fail-1")! + assert.ok(fail1Msg, "message with text survives") + assert.equal( + fail1Msg.parts.filter((p: any) => p.type === "tool" && p.tool === "compress").length, + 0, + "oldest orphaned compress part removed", + ) + assert.ok(messages.find((m) => m.info.id === "msg-fail-2"), "2nd-last orphaned kept") + assert.ok(messages.find((m) => m.info.id === "msg-fail-3"), "last orphaned kept") + assert.ok(messages.find((m) => m.info.id === "msg-good-compress"), "active block kept") + }) + + it("hides all orphaned compress calls beyond the last 2", () => { + const state = makeState([]) + const messages: WithParts[] = [ + { info: { id: "msg-user-1", role: "user" } as any, parts: [{ type: "text", text: "Hi" }] }, + ...Array.from({ length: 5 }, (_, i) => ({ + info: { id: `msg-fail-${i}`, role: "assistant" } as any, + parts: [ + { type: "tool", tool: "compress", callID: `call-fail-${i}`, state: { status: "error" } }, + ], + })), + { info: { id: "msg-user-2", role: "user" } as any, parts: [{ type: "text", text: "Next" }] }, + ] + + const hidden = hideConsumedCompressCalls(state as SessionState, messages) + + assert.equal(hidden, 3, "5 orphaned - 2 kept = 3 hidden") + assert.equal(messages.length, 4, "user + 2 kept + user = 4 messages") + }) })