* Delete messages that Snip executions removed from the in-memory array, * and relink parentUuid across the gaps. * * Unlike compact_boundary which truncates a prefix, snip removes * middle ranges. The JSONL is append-only, so removed messages stay on disk * and the surviving messages' parentUu
(messages: Map<UUID, TranscriptMessage>)
| 2138 | * Mutates the Map in place. |
| 2139 | */ |
| 2140 | function applySnipRemovals(messages: Map<UUID, TranscriptMessage>): void { |
| 2141 | // Structural check — snipMetadata only exists on the boundary subtype. |
| 2142 | // Avoids the subtype literal which is in excluded-strings.txt |
| 2143 | // (HISTORY_SNIP is ant-only; the literal must not leak into external builds). |
| 2144 | type WithSnipMeta = { snipMetadata?: { removedUuids?: UUID[] } } |
| 2145 | const toDelete = new Set<UUID>() |
| 2146 | for (const entry of messages.values()) { |
| 2147 | const removedUuids = (entry as WithSnipMeta).snipMetadata?.removedUuids |
| 2148 | if (!removedUuids) continue |
| 2149 | for (const uuid of removedUuids) toDelete.add(uuid) |
| 2150 | } |
| 2151 | if (toDelete.size === 0) return |
| 2152 | |
| 2153 | // Capture each to-delete entry's own parentUuid BEFORE deleting so we can |
| 2154 | // walk backward through contiguous removed ranges. Entries not in the Map |
| 2155 | // (already absent, e.g. from a prior compact_boundary prune) contribute no |
| 2156 | // link; the relink walk will stop at the gap and pick up null (chain-root |
| 2157 | // behavior — same as if compact truncated there, which it did). |
| 2158 | const deletedParent = new Map<UUID, UUID | null>() |
| 2159 | let removedCount = 0 |
| 2160 | for (const uuid of toDelete) { |
| 2161 | const entry = messages.get(uuid) |
| 2162 | if (!entry) continue |
| 2163 | deletedParent.set(uuid, entry.parentUuid) |
| 2164 | messages.delete(uuid) |
| 2165 | removedCount++ |
| 2166 | } |
| 2167 | |
| 2168 | // Relink survivors with dangling parentUuid. Walk backward through |
| 2169 | // deletedParent until we hit a UUID not in toDelete (or null). Path |
| 2170 | // compression: after resolving, seed the map with the resolved link so |
| 2171 | // subsequent survivors sharing the same chain segment don't re-walk. |
| 2172 | const resolve = (start: UUID): UUID | null => { |
| 2173 | const path: UUID[] = [] |
| 2174 | let cur: UUID | null | undefined = start |
| 2175 | while (cur && toDelete.has(cur)) { |
| 2176 | path.push(cur) |
| 2177 | cur = deletedParent.get(cur) |
| 2178 | if (cur === undefined) { |
| 2179 | cur = null |
| 2180 | break |
| 2181 | } |
| 2182 | } |
| 2183 | for (const p of path) deletedParent.set(p, cur) |
| 2184 | return cur |
| 2185 | } |
| 2186 | let relinkedCount = 0 |
| 2187 | for (const [uuid, msg] of messages) { |
| 2188 | if (!msg.parentUuid || !toDelete.has(msg.parentUuid)) continue |
| 2189 | messages.set(uuid, { ...msg, parentUuid: resolve(msg.parentUuid) }) |
| 2190 | relinkedCount++ |
| 2191 | } |
| 2192 | |
| 2193 | logEvent('ncode_snip_resume_filtered', { |
| 2194 | removed_count: removedCount, |
| 2195 | relinked_count: relinkedCount, |
| 2196 | }) |
| 2197 | } |