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/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/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") + }) +}) 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") + }) })