switchroom 0.13.51 → 0.13.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,11 @@ export interface DisconnectFlushDeps<Ctrl extends { finalize: (reason?: 'done' |
37
37
  activeReactionMsgIds: Map<string, { chatId: string; messageId: number }>
38
38
  /** Mirror map: same keys → turn-start timestamps. */
39
39
  activeTurnStartedAt: Map<string, number>
40
+ /** PR3b: keys claude has actually been handed (delivered, not just
41
+ * received). Cleared on disconnect for the same reason as
42
+ * activeTurnStartedAt — the bridge just died, every turn it
43
+ * was handed is dead by definition. */
44
+ claudeBusyKeys: Set<string>
40
45
 
41
46
  /** Open draft-stream handles keyed by chat:thread:replyId. */
42
47
  activeDraftStreams: Map<string, Stream>
@@ -78,6 +83,7 @@ export function flushOnAgentDisconnect<
78
83
  activeStatusReactions,
79
84
  activeReactionMsgIds,
80
85
  activeTurnStartedAt,
86
+ claudeBusyKeys,
81
87
  activeDraftStreams,
82
88
  activeDraftParseModes,
83
89
  clearActiveReactions,
@@ -105,6 +111,7 @@ export function flushOnAgentDisconnect<
105
111
  activeStatusReactions.delete(key)
106
112
  activeReactionMsgIds.delete(key)
107
113
  activeTurnStartedAt.delete(key)
114
+ claudeBusyKeys.delete(key)
108
115
  }
109
116
  clearActiveReactions()
110
117
 
@@ -124,6 +131,7 @@ export function flushOnAgentDisconnect<
124
131
  for (const k of danglingKeys) {
125
132
  activeTurnStartedAt.delete(k)
126
133
  activeReactionMsgIds.delete(k)
134
+ claudeBusyKeys.delete(k)
127
135
  }
128
136
  log(
129
137
  `telegram gateway: disconnect-flush swept ${danglingKeys.length} dangling turn key(s) ` +
@@ -132,6 +140,30 @@ export function flushOnAgentDisconnect<
132
140
  onDanglingTurnsSwept?.(danglingKeys)
133
141
  }
134
142
 
143
+ // PR3b orphan-sweep (#1880 follow-up): claudeBusyKeys can hold keys
144
+ // that activeTurnStartedAt does NOT — specifically when a synthetic
145
+ // inbound (cron via onInjectInbound, reaction dispatch, vault
146
+ // grant-approved / -denied / save-discarded / -failed / -completed,
147
+ // button-callback) was delivered. Those paths bypass handleInbound's
148
+ // fresh-turn branch (which is what would set activeTurnStartedAt),
149
+ // so the sweep loop above wouldn't notice them. Pre-PR3b this was
150
+ // invisible because the fleet gate read activeTurnStartedAt.size —
151
+ // synthetic-only turns never registered. PR3b's claudeBusyKeys.add
152
+ // is the more-accurate "claude is busy on this" gate, which means
153
+ // a synthetic-delivered turn that dies WITHOUT turn_end leaves an
154
+ // orphan that the activeTurnStartedAt-keyed sweep can't see.
155
+ // Cure: clear any leftover busy keys here. Bridge died → every
156
+ // busy key is dead by definition. Same justification as the
157
+ // dangling-sweep above for activeTurnStartedAt.
158
+ if (claudeBusyKeys.size > 0) {
159
+ const orphanCount = claudeBusyKeys.size
160
+ claudeBusyKeys.clear()
161
+ log(
162
+ `telegram gateway: disconnect-flush cleared ${orphanCount} orphan claudeBusyKeys ` +
163
+ `entr${orphanCount === 1 ? 'y' : 'ies'} (synthetic-inbound deliveries that never turn_ended)`,
164
+ )
165
+ }
166
+
135
167
  // Stop coalesce timers that could emit into a finalized draft stream, but
136
168
  // preserve chats with pendingCompletion=true — those have background
137
169
  // sub-agents that legitimately outlive the parent bridge disconnect. The