fix: skip retention suppressor for completed-orphan keys during tab close

A completed-orphan key's tab is already gone, so buildRetainedAgentsSyncSnapshot
never snapshots it and no live->gone transition ever fires to consume a planted
retention suppressor. Planting one leaked a permanent retentionSuppressedPaneKeys
entry per swept orphan. Plant suppressors only for prefix-matched keys.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-11 03:17:07 -07:00
co-authored by Orca
parent 84aa3b5f8f
commit b2bc6b9020
2 changed files with 10 additions and 2 deletions
@@ -186,7 +186,9 @@ describe('dropAgentStatus + retention suppressor', () => {
expect(s.tabsByWorktree['wt-1']?.some((tab) => tab.id === 'tab-closed')).toBe(false)
expect(s.agentStatusByPaneKey['tab-closed:0']).toBeUndefined()
expect(s.agentStatusByPaneKey['tab-orphan:0']).toBeUndefined()
expect(s.retentionSuppressedPaneKeys['tab-orphan:0']).toBe(true)
// No suppressor for the orphan: its tab is already gone, so retention sync
// never re-surfaces it and a suppressor would leak permanently.
expect(s.retentionSuppressedPaneKeys['tab-orphan:0']).toBeUndefined()
expect(s.agentStatusByPaneKey['tab-active-child:0']).toBeDefined()
expect(s.agentStatusByPaneKey['tab-live:0']).toBeDefined()
expect(s.agentStatusByPaneKey['tab-other-orphan:0']).toBeDefined()
@@ -1895,7 +1895,13 @@ export const createAgentStatusSlice: StateCreator<AppState, [], [], AgentStatusS
// the user just tore it down. Planting suppressors is the cheap guard
// for the common ordering; the rare inverse ordering has the same
// bounded suppressor-leak tradeoff described in dropAgentStatus.
const suppressorAdds = liveKeys.filter((k) => !(k in s.retentionSuppressedPaneKeys))
//
// Skip completed-orphan keys: their tab is already gone, so retention
// sync never snapshots them and no live→gone transition ever fires to
// consume the suppressor — planting one would leak permanently.
const suppressorAdds = liveKeys.filter(
(k) => !completedOrphanKeySet.has(k) && !(k in s.retentionSuppressedPaneKeys)
)
let nextRetentionSuppressedPaneKeys = s.retentionSuppressedPaneKeys
if (suppressorAdds.length > 0) {
nextRetentionSuppressedPaneKeys = { ...s.retentionSuppressedPaneKeys }