* 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>)
| 1980 | * Mutates the Map in place. |
| 1981 | */ |
| 1982 | function applySnipRemovals(messages: Map<UUID, TranscriptMessage>): void { |
| 1983 | // Structural check — snipMetadata only exists on the boundary subtype. |
| 1984 | // Avoids the subtype literal which is in excluded-strings.txt |
| 1985 | // (HISTORY_SNIP is ant-only; the literal must not leak into external builds). |
| 1986 | type WithSnipMeta = { snipMetadata?: { removedUuids?: UUID[] } } |
| 1987 | const toDelete = new Set<UUID>() |
| 1988 | for (const entry of messages.values()) { |
| 1989 | const removedUuids = (entry as WithSnipMeta).snipMetadata?.removedUuids |
| 1990 | if (!removedUuids) continue |
| 1991 | for (const uuid of removedUuids) toDelete.add(uuid) |
| 1992 | } |
| 1993 | if (toDelete.size === 0) return |
| 1994 | |
| 1995 | // Capture each to-delete entry's own parentUuid BEFORE deleting so we can |
| 1996 | // walk backward through contiguous removed ranges. Entries not in the Map |
| 1997 | // (already absent, e.g. from a prior compact_boundary prune) contribute no |
| 1998 | // link; the relink walk will stop at the gap and pick up null (chain-root |
| 1999 | // behavior — same as if compact truncated there, which it did). |
| 2000 | const deletedParent = new Map<UUID, UUID | null>() |
| 2001 | let removedCount = 0 |
| 2002 | for (const uuid of toDelete) { |
| 2003 | const entry = messages.get(uuid) |
| 2004 | if (!entry) continue |
| 2005 | deletedParent.set(uuid, entry.parentUuid) |
| 2006 | messages.delete(uuid) |
| 2007 | removedCount++ |
| 2008 | } |
| 2009 | |
| 2010 | // Relink survivors with dangling parentUuid. Walk backward through |
| 2011 | // deletedParent until we hit a UUID not in toDelete (or null). Path |
| 2012 | // compression: after resolving, seed the map with the resolved link so |
| 2013 | // subsequent survivors sharing the same chain segment don't re-walk. |
| 2014 | const resolve = (start: UUID): UUID | null => { |
| 2015 | const path: UUID[] = [] |
| 2016 | let cur: UUID | null | undefined = start |
| 2017 | while (cur && toDelete.has(cur)) { |
| 2018 | path.push(cur) |
| 2019 | cur = deletedParent.get(cur) |
| 2020 | if (cur === undefined) { |
| 2021 | cur = null |
| 2022 | break |
| 2023 | } |
| 2024 | } |
| 2025 | for (const p of path) deletedParent.set(p, cur) |
| 2026 | return cur |
| 2027 | } |
| 2028 | let relinkedCount = 0 |
| 2029 | for (const [uuid, msg] of messages) { |
| 2030 | if (!msg.parentUuid || !toDelete.has(msg.parentUuid)) continue |
| 2031 | messages.set(uuid, { ...msg, parentUuid: resolve(msg.parentUuid) }) |
| 2032 | relinkedCount++ |
| 2033 | } |
| 2034 | |
| 2035 | logEvent('tengu_snip_resume_filtered', { |
| 2036 | removed_count: removedCount, |
| 2037 | relinked_count: relinkedCount, |
| 2038 | }) |
| 2039 | } |