(messages: Message[], opts: ReadCacheOptions = {})
| 109 | * mutated); array length and message order are preserved. |
| 110 | */ |
| 111 | export function compactFileReads(messages: Message[], opts: ReadCacheOptions = {}): Message[] { |
| 112 | if (!Array.isArray(messages) || messages.length === 0) return messages; |
| 113 | const recentWindow = opts.recentWindow ?? 24; |
| 114 | |
| 115 | // 1) tool_call_id -> ReadTarget, from assistant tool_calls that name a read tool. |
| 116 | const targetById = new Map<string, ReadTarget>(); |
| 117 | for (const m of messages) { |
| 118 | if (m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue; |
| 119 | for (const tc of m.tool_calls) { |
| 120 | if (!tc?.function || !READ_TOOLS.has(tc.function.name)) continue; |
| 121 | const t = readTargetFor(parseArgs(tc.function.arguments)); |
| 122 | if (t) targetById.set(tc.id, t); |
| 123 | } |
| 124 | } |
| 125 | if (targetById.size === 0) return messages; |
| 126 | |
| 127 | // 2) read tool-result messages, in order. |
| 128 | const reads: Array<{ idx: number; t: ReadTarget }> = []; |
| 129 | messages.forEach((m, idx) => { |
| 130 | if (m.role === 'tool' && m.tool_call_id && targetById.has(m.tool_call_id)) { |
| 131 | reads.push({ idx, t: targetById.get(m.tool_call_id)! }); |
| 132 | } |
| 133 | }); |
| 134 | if (reads.length === 0) return messages; |
| 135 | |
| 136 | // 3) superseded: an earlier read covered by a LATER read of the same path |
| 137 | // (later full read, or later identical slice). |
| 138 | const superseded = new Set<number>(); |
| 139 | for (let i = 0; i < reads.length; i++) { |
| 140 | for (let j = i + 1; j < reads.length; j++) { |
| 141 | if (reads[j].t.path !== reads[i].t.path) continue; |
| 142 | if (reads[j].t.full || reads[j].t.key === reads[i].t.key) { |
| 143 | superseded.add(reads[i].idx); |
| 144 | break; |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | // 4) rewrite. Superseded → lossless stub (any age). Else, if aging is on and the |
| 150 | // read is older than the recent window → outline stub. |
| 151 | const boundary = messages.length - recentWindow; |
| 152 | const idxToTarget = new Map(reads.map(r => [r.idx, r.t])); |
| 153 | let changed = false; |
| 154 | const out = messages.map((m, idx) => { |
| 155 | const t = idxToTarget.get(idx); |
| 156 | if (!t) return m; |
| 157 | if (isStub(m.content)) return m; // already compacted on a prior pass |
| 158 | if (superseded.has(idx)) { |
| 159 | changed = true; |
| 160 | return { ...m, content: supersededStub(t.path) }; |
| 161 | } |
| 162 | if (opts.agingOutline && idx < boundary) { |
| 163 | changed = true; |
| 164 | return { ...m, content: agedStub(t.path, m.content) }; |
| 165 | } |
| 166 | return m; |
| 167 | }); |
| 168 | return changed ? out : messages; |
no test coverage detected