11import { PluginConfig } from "../config"
22import { Logger } from "../logger"
33import type { SessionState , WithParts } from "../state"
4+ import { buildToolIdList } from "../messages/utils"
5+ import { calculateTokensSaved } from "./utils"
46
57/**
68 * Supersede Writes strategy - prunes write tool inputs for files that have
@@ -19,4 +21,80 @@ export const supersedeWrites = (
1921 if ( ! config . strategies . supersedeWrites . enabled ) {
2022 return
2123 }
24+
25+ // Build list of all tool call IDs from messages (chronological order)
26+ const allToolIds = buildToolIdList ( state , messages , logger )
27+ if ( allToolIds . length === 0 ) {
28+ return
29+ }
30+
31+ // Filter out IDs already pruned
32+ const alreadyPruned = new Set ( state . prune . toolIds )
33+
34+ const unprunedIds = allToolIds . filter ( id => ! alreadyPruned . has ( id ) )
35+ if ( unprunedIds . length === 0 ) {
36+ return
37+ }
38+
39+ // Track write tools by file path: filePath -> [{ id, index }]
40+ // We track index to determine chronological order
41+ const writesByFile = new Map < string , { id : string , index : number } [ ] > ( )
42+
43+ // Track read file paths with their index
44+ const readsByFile = new Map < string , number [ ] > ( )
45+
46+ for ( let i = 0 ; i < allToolIds . length ; i ++ ) {
47+ const id = allToolIds [ i ]
48+ const metadata = state . toolParameters . get ( id )
49+ if ( ! metadata ) {
50+ continue
51+ }
52+
53+ const filePath = metadata . parameters ?. filePath
54+ if ( ! filePath ) {
55+ continue
56+ }
57+
58+ if ( metadata . tool === 'write' ) {
59+ if ( ! writesByFile . has ( filePath ) ) {
60+ writesByFile . set ( filePath , [ ] )
61+ }
62+ writesByFile . get ( filePath ) ! . push ( { id, index : i } )
63+ } else if ( metadata . tool === 'read' ) {
64+ if ( ! readsByFile . has ( filePath ) ) {
65+ readsByFile . set ( filePath , [ ] )
66+ }
67+ readsByFile . get ( filePath ) ! . push ( i )
68+ }
69+ }
70+
71+ // Find writes that are superseded by subsequent reads
72+ const newPruneIds : string [ ] = [ ]
73+
74+ for ( const [ filePath , writes ] of writesByFile . entries ( ) ) {
75+ const reads = readsByFile . get ( filePath )
76+ if ( ! reads || reads . length === 0 ) {
77+ continue
78+ }
79+
80+ // For each write, check if there's a read that comes after it
81+ for ( const write of writes ) {
82+ // Skip if already pruned
83+ if ( alreadyPruned . has ( write . id ) ) {
84+ continue
85+ }
86+
87+ // Check if any read comes after this write
88+ const hasSubsequentRead = reads . some ( readIndex => readIndex > write . index )
89+ if ( hasSubsequentRead ) {
90+ newPruneIds . push ( write . id )
91+ }
92+ }
93+ }
94+
95+ if ( newPruneIds . length > 0 ) {
96+ state . stats . totalPruneTokens += calculateTokensSaved ( state , messages , newPruneIds )
97+ state . prune . toolIds . push ( ...newPruneIds )
98+ logger . debug ( `Marked ${ newPruneIds . length } superseded write tool calls for pruning` )
99+ }
22100}
0 commit comments