@@ -19,6 +19,12 @@ interface MatchResult {
1919 matchType : "exact" | "fuzzy"
2020}
2121
22+ interface AnchorQuery {
23+ type : "muid" | "uid"
24+ value : string
25+ id : string
26+ }
27+
2228function summarizeMatches (
2329 matches : MatchResult [ ] ,
2430 limit = 8 ,
@@ -94,6 +100,91 @@ function extractMessageContent(msg: WithParts): string {
94100 return content
95101}
96102
103+ function parseAnchorQuery ( searchString : string ) : AnchorQuery | undefined {
104+ const muid = searchString . match ( / ^ \[ ( m u i d ) _ ( \d + ) \] $ / )
105+ if ( muid ) {
106+ return {
107+ type : "muid" ,
108+ value : searchString ,
109+ id : muid [ 2 ] ,
110+ }
111+ }
112+
113+ const uid = searchString . match ( / ^ \[ ( u i d ) _ ( \d + ) \] $ / )
114+ if ( uid ) {
115+ return {
116+ type : "uid" ,
117+ value : searchString ,
118+ id : uid [ 2 ] ,
119+ }
120+ }
121+
122+ return undefined
123+ }
124+
125+ function findAnchorMatches ( messages : WithParts [ ] , anchor : AnchorQuery ) : MatchResult [ ] {
126+ if ( anchor . type === "muid" ) {
127+ const matches : MatchResult [ ] = [ ]
128+ for ( let i = 0 ; i < messages . length ; i ++ ) {
129+ const msg = messages [ i ]
130+ if ( msg . info . role !== "user" ) {
131+ continue
132+ }
133+ const parts = Array . isArray ( msg . parts ) ? msg . parts : [ ]
134+ const found = parts . some (
135+ ( part ) =>
136+ part . type === "text" &&
137+ typeof part . text === "string" &&
138+ part . text . startsWith ( anchor . value ) ,
139+ )
140+ if ( ! found ) {
141+ continue
142+ }
143+ matches . push ( {
144+ messageId : msg . info . id ,
145+ messageIndex : i ,
146+ score : 100 ,
147+ matchType : "exact" ,
148+ } )
149+ }
150+ return matches
151+ }
152+
153+ const matches : MatchResult [ ] = [ ]
154+ const uidPrefix = new RegExp ( `^\\[uid_${ anchor . id } (?:\\]|,)` )
155+ for ( let i = 0 ; i < messages . length ; i ++ ) {
156+ const msg = messages [ i ]
157+ const parts = Array . isArray ( msg . parts ) ? msg . parts : [ ]
158+ const found = parts . some ( ( part ) => {
159+ if ( part . type !== "tool" ) {
160+ return false
161+ }
162+
163+ const output = part . state ?. status === "completed" ? part . state . output : undefined
164+ if ( typeof output === "string" && uidPrefix . test ( output ) ) {
165+ return true
166+ }
167+
168+ const error = part . state ?. status === "error" ? part . state . error : undefined
169+ if ( typeof error === "string" && uidPrefix . test ( error ) ) {
170+ return true
171+ }
172+
173+ return false
174+ } )
175+ if ( ! found ) {
176+ continue
177+ }
178+ matches . push ( {
179+ messageId : msg . info . id ,
180+ messageIndex : i ,
181+ score : 100 ,
182+ matchType : "exact" ,
183+ } )
184+ }
185+ return matches
186+ }
187+
97188function findExactMatches ( messages : WithParts [ ] , searchString : string ) : MatchResult [ ] {
98189 const matches : MatchResult [ ] = [ ]
99190
@@ -152,6 +243,52 @@ export function findStringInMessages(
152243
153244 const searchableMessages = messages . length > 1 ? messages . slice ( 0 , - 1 ) : messages
154245 const lastMessage = messages . length > 0 ? messages [ messages . length - 1 ] : undefined
246+ const anchor = parseAnchorQuery ( searchString )
247+
248+ if ( anchor ) {
249+ const anchorMatches = findAnchorMatches ( searchableMessages , anchor )
250+ const anchorSummary = summarizeMatches ( anchorMatches )
251+ clog . info ( C . BOUNDARY , `${ stringType } : anchor match results` , {
252+ type : anchor . type ,
253+ count : anchorSummary . total ,
254+ sample : anchorSummary . sample ,
255+ omitted : anchorSummary . omitted ,
256+ } )
257+
258+ if ( anchorMatches . length === 1 ) {
259+ clog . info ( C . BOUNDARY , `${ stringType } : single anchor match found` , {
260+ messageId : anchorMatches [ 0 ] . messageId ,
261+ messageIndex : anchorMatches [ 0 ] . messageIndex ,
262+ } )
263+ return {
264+ messageId : anchorMatches [ 0 ] . messageId ,
265+ messageIndex : anchorMatches [ 0 ] . messageIndex ,
266+ }
267+ }
268+
269+ if ( anchorMatches . length > 1 ) {
270+ clog . error ( C . BOUNDARY , `${ stringType } : MULTIPLE anchor matches - ambiguous` , {
271+ type : anchor . type ,
272+ count : anchorMatches . length ,
273+ matches : anchorMatches . map ( ( m ) => ( { msgId : m . messageId , idx : m . messageIndex } ) ) ,
274+ searchString : searchString . substring ( 0 , 150 ) ,
275+ } )
276+ throw new Error (
277+ `Found multiple matches for ${ stringType } . ` +
278+ `Use a different [muid_x] or [uid_x] anchor that appears only once.` ,
279+ )
280+ }
281+
282+ clog . error ( C . BOUNDARY , `${ stringType } : anchor NOT FOUND` , {
283+ type : anchor . type ,
284+ searchString : searchString . substring ( 0 , 150 ) ,
285+ messageCount : searchableMessages . length ,
286+ } )
287+ throw new Error (
288+ `${ stringType } anchor not found in conversation. ` +
289+ `Use an existing [muid_x] or [uid_x] marker from the current context.` ,
290+ )
291+ }
155292
156293 clog . debug (
157294 C . BOUNDARY ,
0 commit comments