From 32e9d67fa9a6e408c3b3175a647ea89aaa19d305 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:57:59 +0800 Subject: [PATCH] feat: report valid compressible gap ranges on overlap error When a compress call overlaps existing blocks, the error message now includes the exact mNNN..mNNN ranges that are available for compression (before, after, and between existing blocks). This lets the model immediately retry with valid ranges instead of guessing. --- compress-tool.ts | 59 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/compress-tool.ts b/compress-tool.ts index afc98d4..3078af0 100644 --- a/compress-tool.ts +++ b/compress-tool.ts @@ -150,6 +150,7 @@ export function registerCompressTool( } // ── Overlap check against existing active blocks ───────────────── + const overlappingBlocks: CompressionBlock[] = [] for (const existing of state.compressionBlocks) { if (!existing.active) continue // Skip blocks with corrupted timestamps @@ -160,16 +161,60 @@ export function registerCompressTool( startTimestamp <= existing.endTimestamp && existing.startTimestamp <= endTimestamp if (overlaps) { - throw new Error( - `Overlapping compression ranges are not supported. ` + - `New range (${startId}..${endId}) overlaps existing block ` + - `b${existing.id} "${existing.topic}" ` + - `(b${existing.id} covers ${existing.startTimestamp}..${existing.endTimestamp}, ` + - `new range covers ${startTimestamp}..${endTimestamp})`, - ) + overlappingBlocks.push(existing) } } + if (overlappingBlocks.length > 0) { + // Compute valid compressible gaps: raw message ranges not covered by any active block. + const activeBlocks = state.compressionBlocks + .filter(b => b.active && Number.isFinite(b.startTimestamp) && Number.isFinite(b.endTimestamp)) + .sort((a, b) => a.startTimestamp - b.startTimestamp) + + const sortedEntries = [...state.messageIdSnapshot.entries()] + .filter(([id]) => id.startsWith('m')) // only mNNN ids + .sort((a, b) => a[1] - b[1]) + + // Walk sorted messages, grouping consecutive uncovered ones into gap ranges + const gaps: string[] = [] + let gapStart: string | null = null + let gapEnd: string | null = null + + for (const [id, ts] of sortedEntries) { + const covered = activeBlocks.some( + b => ts >= b.startTimestamp && ts <= b.endTimestamp + ) + if (!covered) { + if (gapStart === null) gapStart = id + gapEnd = id + } else { + if (gapStart !== null) { + gaps.push(`${gapStart}..${gapEnd}`) + gapStart = null + gapEnd = null + } + } + } + // Close trailing gap + if (gapStart !== null && gapEnd !== null) { + gaps.push(`${gapStart}..${gapEnd}`) + } + + const overlapInfo = overlappingBlocks + .map(b => `b${b.id} "${b.topic}"`) + .join(', ') + + const gapInfo = gaps.length > 0 + ? gaps.join('; ') + : 'None — all visible messages are covered by existing blocks' + + throw new Error( + `Overlapping compression ranges are not supported. ` + + `New range (${startId}..${endId}) overlaps existing block(s): ${overlapInfo}. ` + + `Valid compressible ranges: ${gapInfo}`, + ) + } + // ── Anchor: first raw message after the range ──────────────────── const anchorTimestamp = resolveAnchorTimestamp(endTimestamp, state)