Skip to content

Commit 65b016c

Browse files
committed
fix(compress): reject ambiguous summary matches
1 parent 6ab7f24 commit 65b016c

File tree

2 files changed

+49
-20
lines changed

2 files changed

+49
-20
lines changed

lib/tools/compress.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { saveSessionState } from "../state/persistence"
66
import { COMPRESS_TOOL_SPEC } from "../prompts"
77
import { getCurrentParams, countAllMessageTokens, countTokens } from "../strategies/utils"
88
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
9-
import { findStringInMessages, collectToolIdsInRange, collectMessageIdsInRange } from "./utils"
9+
import {
10+
findStringInMessages,
11+
collectToolIdsInRange,
12+
collectMessageIdsInRange,
13+
findSummaryAnchorForBoundary,
14+
} from "./utils"
1015
import { sendCompressNotification } from "../ui/notification"
1116
import { buildCompressionGraphData, cacheSystemPromptTokens } from "../ui/utils"
1217
import { prune as applyPruneTransforms } from "../messages/prune"
@@ -183,17 +188,19 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
183188
summaries: state.compressSummaries.length,
184189
},
185190
)
186-
// TODO: This takes the first summary text match and does not error on
187-
// multiple matching summaries (ambiguous fallback).
188-
const s = state.compressSummaries.find((s) => s.summary.includes(startString))
191+
const s = findSummaryAnchorForBoundary(
192+
state.compressSummaries,
193+
startString,
194+
"startString",
195+
)
189196
if (s) {
190197
rawStartIndex = messages.findIndex((m) => m.info.id === s.anchorMessageId)
191198
clog.info(C.COMPRESS, `✓ Start resolved via summary anchor`, {
192199
anchorMessageId: s.anchorMessageId,
193200
rawStartIndex,
194201
})
195202
} else {
196-
clog.error(
203+
clog.warn(
197204
C.COMPRESS,
198205
`✗ Start not found in any summary either\nCannot resolve boundary`,
199206
)
@@ -208,17 +215,19 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
208215
summaries: state.compressSummaries.length,
209216
},
210217
)
211-
// TODO: This takes the first summary text match and does not error on
212-
// multiple matching summaries (ambiguous fallback).
213-
const s = state.compressSummaries.find((s) => s.summary.includes(endString))
218+
const s = findSummaryAnchorForBoundary(
219+
state.compressSummaries,
220+
endString,
221+
"endString",
222+
)
214223
if (s) {
215224
rawEndIndex = messages.findIndex((m) => m.info.id === s.anchorMessageId)
216225
clog.info(C.COMPRESS, `✓ End resolved via summary anchor`, {
217226
anchorMessageId: s.anchorMessageId,
218227
rawEndIndex,
219228
})
220229
} else {
221-
clog.error(
230+
clog.warn(
222231
C.COMPRESS,
223232
`✗ End not found in any summary either\nCannot resolve boundary`,
224233
)
@@ -398,16 +407,6 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
398407

399408
state.stats.pruneTokenCounter += estimatedCompressedTokens
400409

401-
const rawStartResult = {
402-
messageId: anchorMessageId,
403-
messageIndex: rawStartIndex,
404-
}
405-
const rawEndMessageId = messages[rawEndIndex]?.info.id || endResult.messageId
406-
const rawEndResult = {
407-
messageId: rawEndMessageId,
408-
messageIndex: rawEndIndex,
409-
}
410-
411410
const currentParams = getCurrentParams(state, messages, logger)
412411
const summaryTokens = countTokens(args.content.summary)
413412

lib/tools/utils.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { partial_ratio } from "fuzzball"
2-
import type { WithParts } from "../state"
2+
import type { CompressSummary, WithParts } from "../state"
33
import type { Logger } from "../logger"
44
import { isIgnoredUserMessage } from "../messages/utils"
55
import { clog, C } from "../compress-logger"
@@ -21,6 +21,36 @@ interface MatchResult {
2121
matchType: "exact" | "fuzzy"
2222
}
2323

24+
export function findSummaryAnchorForBoundary(
25+
summaries: CompressSummary[],
26+
searchString: string,
27+
stringType: "startString" | "endString",
28+
): CompressSummary | undefined {
29+
const matches = summaries.filter((s) => s.summary.includes(searchString))
30+
31+
if (matches.length > 1) {
32+
const sample = matches.slice(0, 8).map((s) => ({
33+
anchorMessageId: s.anchorMessageId,
34+
preview: s.summary.substring(0, 120),
35+
}))
36+
37+
clog.error(C.BOUNDARY, `✗ Multiple Summary Matches (ambiguous)`, {
38+
type: stringType,
39+
count: matches.length,
40+
matches: sample,
41+
omitted: Math.max(0, matches.length - sample.length),
42+
searchPreview: searchString.substring(0, 150),
43+
})
44+
45+
throw new Error(
46+
`Found multiple matches for ${stringType}. ` +
47+
`Provide more surrounding context to uniquely identify the intended match.`,
48+
)
49+
}
50+
51+
return matches[0]
52+
}
53+
2454
function summarizeMatches(
2555
matches: MatchResult[],
2656
limit = 8,

0 commit comments

Comments
 (0)