switchroom 0.13.1 → 0.13.2
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.
package/dist/cli/switchroom.js
CHANGED
|
@@ -47248,8 +47248,8 @@ var {
|
|
|
47248
47248
|
} = import__.default;
|
|
47249
47249
|
|
|
47250
47250
|
// src/build-info.ts
|
|
47251
|
-
var VERSION = "0.13.
|
|
47252
|
-
var COMMIT_SHA = "
|
|
47251
|
+
var VERSION = "0.13.2";
|
|
47252
|
+
var COMMIT_SHA = "afa0fbea";
|
|
47253
47253
|
|
|
47254
47254
|
// src/cli/agent.ts
|
|
47255
47255
|
init_source();
|
package/package.json
CHANGED
|
@@ -47679,10 +47679,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
47679
47679
|
}
|
|
47680
47680
|
|
|
47681
47681
|
// ../src/build-info.ts
|
|
47682
|
-
var VERSION = "0.13.
|
|
47683
|
-
var COMMIT_SHA = "
|
|
47684
|
-
var COMMIT_DATE = "2026-05-21T00:
|
|
47685
|
-
var LATEST_PR =
|
|
47682
|
+
var VERSION = "0.13.2";
|
|
47683
|
+
var COMMIT_SHA = "afa0fbea";
|
|
47684
|
+
var COMMIT_DATE = "2026-05-21T00:58:22Z";
|
|
47685
|
+
var LATEST_PR = 1610;
|
|
47686
47686
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
47687
47687
|
|
|
47688
47688
|
// gateway/boot-version.ts
|
|
@@ -48632,21 +48632,18 @@ function statusKey(chatId, threadId) {
|
|
|
48632
48632
|
function streamKey3(chatId, threadId) {
|
|
48633
48633
|
return chatKey(chatId, threadId);
|
|
48634
48634
|
}
|
|
48635
|
-
function
|
|
48635
|
+
function purgeReactionTracking(key, endingTurn) {
|
|
48636
|
+
const outboundEmitted = endingTurn != null ? endingTurn.replyCalled === true : currentTurn?.replyCalled === true;
|
|
48637
|
+
shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted });
|
|
48636
48638
|
const msgInfo = activeReactionMsgIds.get(key);
|
|
48637
48639
|
activeStatusReactions.delete(key);
|
|
48638
48640
|
activeReactionMsgIds.delete(key);
|
|
48641
|
+
activeTurnStartedAt.delete(key);
|
|
48639
48642
|
if (msgInfo) {
|
|
48640
48643
|
const agentDir = resolveAgentDirFromEnv();
|
|
48641
48644
|
if (agentDir != null)
|
|
48642
48645
|
removeActiveReaction(agentDir, msgInfo.chatId, msgInfo.messageId);
|
|
48643
48646
|
}
|
|
48644
|
-
}
|
|
48645
|
-
function purgeReactionTracking(key, endingTurn, outboundEmittedOverride) {
|
|
48646
|
-
const outboundEmitted = outboundEmittedOverride !== undefined ? outboundEmittedOverride : endingTurn != null ? endingTurn.replyCalled === true : currentTurn?.replyCalled === true;
|
|
48647
|
-
shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted });
|
|
48648
|
-
clearReactionState(key);
|
|
48649
|
-
activeTurnStartedAt.delete(key);
|
|
48650
48647
|
if (activeTurnStartedAt.size === 0) {
|
|
48651
48648
|
const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
48652
48649
|
if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
|
|
@@ -48796,7 +48793,7 @@ function endStatusReaction(chatId, threadId, outcome) {
|
|
|
48796
48793
|
ctrl.setDone();
|
|
48797
48794
|
else
|
|
48798
48795
|
ctrl.setError();
|
|
48799
|
-
|
|
48796
|
+
purgeReactionTracking(key);
|
|
48800
48797
|
}
|
|
48801
48798
|
function resolveThreadId(chat_id, explicit) {
|
|
48802
48799
|
if (explicit != null)
|
|
@@ -49519,8 +49516,8 @@ startTimer({
|
|
|
49519
49516
|
lastPtyPreviewByChat.delete(fbKey);
|
|
49520
49517
|
preambleSuppressor.dropNow();
|
|
49521
49518
|
endTurn(fbKey);
|
|
49522
|
-
purgeReactionTracking(fbKey
|
|
49523
|
-
const fbExtraPurge = purgeStaleTurnsForChat(fbChatId, activeTurnStartedAt.keys(),
|
|
49519
|
+
purgeReactionTracking(fbKey);
|
|
49520
|
+
const fbExtraPurge = purgeStaleTurnsForChat(fbChatId, activeTurnStartedAt.keys(), purgeReactionTracking);
|
|
49524
49521
|
if (turnMatchesFallback && currentTurn === wedgedTurn)
|
|
49525
49522
|
currentTurn = null;
|
|
49526
49523
|
try {
|
|
@@ -51342,6 +51339,7 @@ function handleSessionEvent(ev) {
|
|
|
51342
51339
|
const ctrl = activeStatusReactions.get(ceKey);
|
|
51343
51340
|
if (ctrl)
|
|
51344
51341
|
ctrl.setError();
|
|
51342
|
+
purgeReactionTracking(ceKey);
|
|
51345
51343
|
endTurn(ceKey);
|
|
51346
51344
|
if (turn.answerStream != null) {
|
|
51347
51345
|
turn.answerStream.stop();
|
|
@@ -51424,6 +51422,7 @@ function handleSessionEvent(ev) {
|
|
|
51424
51422
|
unpinProgressCardForChat?.(chatId, threadId);
|
|
51425
51423
|
if (ctrl)
|
|
51426
51424
|
ctrl.setDone();
|
|
51425
|
+
purgeReactionTracking(statusKey(chatId, threadId));
|
|
51427
51426
|
{
|
|
51428
51427
|
const sKey = streamKey3(chatId, threadId);
|
|
51429
51428
|
const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
|
|
@@ -51494,7 +51493,7 @@ function handleSessionEvent(ev) {
|
|
|
51494
51493
|
if (recentCount > 0) {
|
|
51495
51494
|
process.stderr.write(`telegram gateway: turn-flush suppressed \u2014 reply tool sent ${recentCount} message(s) within 2s
|
|
51496
51495
|
`);
|
|
51497
|
-
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId)
|
|
51496
|
+
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId));
|
|
51498
51497
|
return;
|
|
51499
51498
|
}
|
|
51500
51499
|
} catch {}
|
|
@@ -51579,13 +51578,14 @@ function handleSessionEvent(ev) {
|
|
|
51579
51578
|
if (backstopCtrl)
|
|
51580
51579
|
backstopCtrl.setError();
|
|
51581
51580
|
} finally {
|
|
51582
|
-
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId)
|
|
51581
|
+
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId));
|
|
51583
51582
|
}
|
|
51584
51583
|
})();
|
|
51585
51584
|
return;
|
|
51586
51585
|
}
|
|
51587
51586
|
if (ctrl)
|
|
51588
51587
|
ctrl.setDone();
|
|
51588
|
+
purgeReactionTracking(statusKey(chatId, threadId));
|
|
51589
51589
|
{
|
|
51590
51590
|
const sKey = streamKey3(chatId, threadId);
|
|
51591
51591
|
const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
|
|
@@ -1278,62 +1278,32 @@ function streamKey(chatId: string, threadId?: number | null): string {
|
|
|
1278
1278
|
return chatKey(chatId, threadId)
|
|
1279
1279
|
}
|
|
1280
1280
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1281
|
+
function purgeReactionTracking(key: string, endingTurn?: CurrentTurn): void {
|
|
1282
|
+
// Phase 2b: turn end. The key was registered via setTurnStarted when
|
|
1283
|
+
// the inbound arrived; purge is the canonical turn-end signal.
|
|
1284
|
+
//
|
|
1285
|
+
// outboundEmitted: read from the explicit `endingTurn` parameter when
|
|
1286
|
+
// provided (canonical path via endCurrentTurnAtomic — module-scope
|
|
1287
|
+
// currentTurn is already null by the time we get here), falling back
|
|
1288
|
+
// to `currentTurn?.replyCalled` for the legacy callsites that haven't
|
|
1289
|
+
// been threaded yet (sibling-key purges, restart-init cleanup).
|
|
1290
|
+
// Without this explicit-turn handoff the shadow trace would report
|
|
1291
|
+
// outboundEmitted=false on every replied turn (the dominant happy
|
|
1292
|
+
// path), producing strictly worse data than the blind `true` it
|
|
1293
|
+
// replaced. Invariant #5's `lastOutboundAt` correctness depends on
|
|
1294
|
+
// this signal being accurate.
|
|
1295
|
+
const outboundEmitted = endingTurn != null
|
|
1296
|
+
? endingTurn.replyCalled === true
|
|
1297
|
+
: currentTurn?.replyCalled === true
|
|
1298
|
+
shadowEmit({ kind: 'turnEnd', key: key as _ChatKey, at: Date.now(), outboundEmitted })
|
|
1297
1299
|
const msgInfo = activeReactionMsgIds.get(key)
|
|
1298
1300
|
activeStatusReactions.delete(key)
|
|
1299
1301
|
activeReactionMsgIds.delete(key)
|
|
1302
|
+
activeTurnStartedAt.delete(key)
|
|
1300
1303
|
if (msgInfo) {
|
|
1301
1304
|
const agentDir = resolveAgentDirFromEnv()
|
|
1302
1305
|
if (agentDir != null) removeActiveReaction(agentDir, msgInfo.chatId, msgInfo.messageId)
|
|
1303
1306
|
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function purgeReactionTracking(
|
|
1307
|
-
key: string,
|
|
1308
|
-
endingTurn?: CurrentTurn,
|
|
1309
|
-
outboundEmittedOverride?: boolean,
|
|
1310
|
-
): void {
|
|
1311
|
-
// Phase 2b: turn end. The key was registered via setTurnStarted when
|
|
1312
|
-
// the inbound arrived; purge is the canonical turn-end signal.
|
|
1313
|
-
//
|
|
1314
|
-
// outboundEmitted derivation, in precedence order:
|
|
1315
|
-
// 1. Explicit `outboundEmittedOverride` (e.g. silence-poke
|
|
1316
|
-
// framework fallback FORCES false because the 5-min fallback
|
|
1317
|
-
// firing proves visible delivery never happened — regardless of
|
|
1318
|
-
// whatever `replyCalled` the wedged turn object carries).
|
|
1319
|
-
// 2. `endingTurn.replyCalled` when the canonical caller threads
|
|
1320
|
-
// the authoritative turn (endCurrentTurnAtomic path; module-scope
|
|
1321
|
-
// currentTurn is already null by the time we get here).
|
|
1322
|
-
// 3. `currentTurn?.replyCalled` fallback for the (now-vanishing)
|
|
1323
|
-
// legacy callsites. Without the explicit-turn handoff the shadow
|
|
1324
|
-
// trace would report outboundEmitted=false on every replied
|
|
1325
|
-
// turn (the dominant happy path), producing strictly worse data
|
|
1326
|
-
// than the blind `true` it replaced. Invariant #5's
|
|
1327
|
-
// `lastOutboundAt` correctness depends on this signal being
|
|
1328
|
-
// accurate.
|
|
1329
|
-
const outboundEmitted = outboundEmittedOverride !== undefined
|
|
1330
|
-
? outboundEmittedOverride
|
|
1331
|
-
: endingTurn != null
|
|
1332
|
-
? endingTurn.replyCalled === true
|
|
1333
|
-
: currentTurn?.replyCalled === true
|
|
1334
|
-
shadowEmit({ kind: 'turnEnd', key: key as _ChatKey, at: Date.now(), outboundEmitted })
|
|
1335
|
-
clearReactionState(key)
|
|
1336
|
-
activeTurnStartedAt.delete(key)
|
|
1337
1307
|
|
|
1338
1308
|
// If no more active turns and a restart is pending, perform it now.
|
|
1339
1309
|
//
|
|
@@ -1623,24 +1593,12 @@ async function resolveCompactCard(
|
|
|
1623
1593
|
}
|
|
1624
1594
|
|
|
1625
1595
|
function endStatusReaction(chatId: string, threadId: number | undefined, outcome: 'done' | 'error'): void {
|
|
1626
|
-
// Mid-turn signal: the reply tool fired, or stream_reply finalized,
|
|
1627
|
-
// and the status-reaction needs to transition to its terminal emoji
|
|
1628
|
-
// (👍 / ⚠️). The turn itself is still active — the canonical turn-end
|
|
1629
|
-
// signal is `endCurrentTurnAtomic(turn)`, which runs later via the
|
|
1630
|
-
// turn_end handler / context-exhaust path / silent-marker path.
|
|
1631
|
-
//
|
|
1632
|
-
// Pre-#1603 audit step 2 (this commit), this called
|
|
1633
|
-
// `purgeReactionTracking(key)` directly, which would fire shadow
|
|
1634
|
-
// `turnEnd` and clear the turn-active marker mid-turn — the latter
|
|
1635
|
-
// triggering the model-idle restart probe + pendingInbound flush as
|
|
1636
|
-
// if claude had gone idle. Use `clearReactionState` to only do the
|
|
1637
|
-
// reaction-cleanup work.
|
|
1638
1596
|
const key = statusKey(chatId, threadId)
|
|
1639
1597
|
const ctrl = activeStatusReactions.get(key)
|
|
1640
1598
|
if (!ctrl) return
|
|
1641
1599
|
if (outcome === 'done') ctrl.setDone()
|
|
1642
1600
|
else ctrl.setError()
|
|
1643
|
-
|
|
1601
|
+
purgeReactionTracking(key)
|
|
1644
1602
|
}
|
|
1645
1603
|
|
|
1646
1604
|
function resolveThreadId(chat_id: string, explicit?: string | number | null): number | undefined {
|
|
@@ -3135,15 +3093,7 @@ silencePoke.startTimer({
|
|
|
3135
3093
|
// Drop silence-poke state and clear turn-active so the next inbound
|
|
3136
3094
|
// for this chat starts a fresh turn instead of queueing forever.
|
|
3137
3095
|
silencePoke.endTurn(fbKey)
|
|
3138
|
-
|
|
3139
|
-
// framework fallback fires precisely because visible delivery
|
|
3140
|
-
// didn't happen in 5 min — `wedgedTurn.replyCalled` may have been
|
|
3141
|
-
// set during the turn (e.g. reply tool invoked but Telegram side
|
|
3142
|
-
// never confirmed delivery), but from the user's perspective no
|
|
3143
|
-
// outbound landed. The state machine's `noteOutbound` effect
|
|
3144
|
-
// must NOT fire for this path. Pass `undefined` for endingTurn
|
|
3145
|
-
// and `false` as the explicit override.
|
|
3146
|
-
purgeReactionTracking(fbKey, undefined, false)
|
|
3096
|
+
purgeReactionTracking(fbKey)
|
|
3147
3097
|
// Defense-in-depth: the fallback's purgeReactionTracking above
|
|
3148
3098
|
// clears the canonical statusKey(chatId, threadId) for fbKey
|
|
3149
3099
|
// only. activeTurnStartedAt can hold sibling entries for the
|
|
@@ -3156,14 +3106,10 @@ silencePoke.startTimer({
|
|
|
3156
3106
|
// purger. Multi-chat-safe — only touches keys for fbChatId, so
|
|
3157
3107
|
// #1546's intentional cross-chat safety guard is preserved.
|
|
3158
3108
|
// See turn-state-purge.ts.
|
|
3159
|
-
//
|
|
3160
|
-
// Same `outboundEmitted=false` rationale as the bare call above —
|
|
3161
|
-
// wrap the purger so every sibling-key purge emits a fallback
|
|
3162
|
-
// shadow turnEnd with the truthful "no visible delivery" signal.
|
|
3163
3109
|
const fbExtraPurge = purgeStaleTurnsForChat(
|
|
3164
3110
|
fbChatId,
|
|
3165
3111
|
activeTurnStartedAt.keys(),
|
|
3166
|
-
|
|
3112
|
+
purgeReactionTracking,
|
|
3167
3113
|
)
|
|
3168
3114
|
// Null `currentTurn` if it's still pointing at the wedged turn —
|
|
3169
3115
|
// when claude eventually fires a late `turn_end` for this session
|
|
@@ -5882,10 +5828,7 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
5882
5828
|
const ceKey = statusKey(chatId, threadId)
|
|
5883
5829
|
const ctrl = activeStatusReactions.get(ceKey)
|
|
5884
5830
|
if (ctrl) ctrl.setError()
|
|
5885
|
-
|
|
5886
|
-
// endCurrentTurnAtomic(turn) call at line ~5851 below already
|
|
5887
|
-
// invokes purgeReactionTracking on the same ceKey. The bare
|
|
5888
|
-
// call here was firing a second shadow `turnEnd` per traversal.
|
|
5831
|
+
purgeReactionTracking(ceKey)
|
|
5889
5832
|
// Surfaced during CC-5 investigation (`docs/status-ask-cause-classes.md`):
|
|
5890
5833
|
// the context-exhaust bail path teardown was missing
|
|
5891
5834
|
// `silencePoke.endTurn(key)`. Without it, the silence-poke state for
|
|
@@ -6043,10 +5986,7 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
6043
5986
|
// Fall through to normal state cleanup (ctrl.setDone, purge, etc.)
|
|
6044
5987
|
// but skip the regular closeProgressLane so we don't re-finalize.
|
|
6045
5988
|
if (ctrl) ctrl.setDone()
|
|
6046
|
-
|
|
6047
|
-
// at line ~6049 below invokes purgeReactionTracking on the same key
|
|
6048
|
-
// (statusKey(chatId, threadId)). The bare call here was firing a
|
|
6049
|
-
// second shadow `turnEnd` per silent-marker traversal.
|
|
5989
|
+
purgeReactionTracking(statusKey(chatId, threadId))
|
|
6050
5990
|
// Match the normal turn_end path's telemetry so silent-marker turns
|
|
6051
5991
|
// still appear in turn-duration graphs.
|
|
6052
5992
|
{
|
|
@@ -6187,15 +6127,7 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
6187
6127
|
// mirroring this contract — so reply-only turns transition
|
|
6188
6128
|
// to terminal 👍 in their own success path rather than
|
|
6189
6129
|
// relying on this dedup heuristic.
|
|
6190
|
-
|
|
6191
|
-
// PR 3b step 3 (#1603 audit): thread the captured `turn`
|
|
6192
|
-
// explicitly. `endCurrentTurnAtomic(turn)` ran at line ~6120
|
|
6193
|
-
// before this IIFE started, so `currentTurn === null` by
|
|
6194
|
-
// now — without an explicit endingTurn argument, the shadow
|
|
6195
|
-
// trace would read `outboundEmitted=false` for this dedup
|
|
6196
|
-
// path even though `recentCount > 0` proves the reply tool
|
|
6197
|
-
// did fire (turn.replyCalled === true).
|
|
6198
|
-
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId), turn)
|
|
6130
|
+
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId))
|
|
6199
6131
|
return
|
|
6200
6132
|
}
|
|
6201
6133
|
} catch {}
|
|
@@ -6323,35 +6255,14 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
6323
6255
|
process.stderr.write(`telegram gateway: turn-flush send failed: ${(err as Error).message}\n`)
|
|
6324
6256
|
if (backstopCtrl) backstopCtrl.setError()
|
|
6325
6257
|
} finally {
|
|
6326
|
-
|
|
6327
|
-
// explicitly. The turn-flush backstop runs inside this IIFE
|
|
6328
|
-
// after `endCurrentTurnAtomic(turn)` already nulled
|
|
6329
|
-
// `currentTurn` at line ~6120. Without threading, the shadow
|
|
6330
|
-
// trace would read `outboundEmitted=currentTurn?.replyCalled
|
|
6331
|
-
// === undefined` → false. For the turn-flush path
|
|
6332
|
-
// `turn.replyCalled` is `false` regardless (the model didn't
|
|
6333
|
-
// call the reply tool — the gateway backstop did the work),
|
|
6334
|
-
// so the threaded value matches the existing fallback here.
|
|
6335
|
-
// But pinning the source via the captured turn matches the
|
|
6336
|
-
// canonical pattern and survives any future change to how
|
|
6337
|
-
// `currentTurn` is sequenced.
|
|
6338
|
-
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId), turn)
|
|
6258
|
+
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId))
|
|
6339
6259
|
}
|
|
6340
6260
|
})()
|
|
6341
6261
|
return
|
|
6342
6262
|
}
|
|
6343
6263
|
|
|
6344
6264
|
if (ctrl) ctrl.setDone()
|
|
6345
|
-
|
|
6346
|
-
// original "route through endCurrentTurnAtomic" recommendation
|
|
6347
|
-
// missed that this same code path already calls
|
|
6348
|
-
// `endCurrentTurnAtomic(turn)` ~90 lines below at line ~6412
|
|
6349
|
-
// on the same key — `chatId === turn.sessionChatId` and
|
|
6350
|
-
// `threadId === turn.sessionThreadId` per the bindings at
|
|
6351
|
-
// ~5946-5947. Removing this bare call closes the last duplicate
|
|
6352
|
-
// shadow-`turnEnd` emit on the dominant happy-path turn-end
|
|
6353
|
-
// tail; the canonical primitive below still fires the single
|
|
6354
|
-
// authoritative turnEnd with the threaded turn).
|
|
6265
|
+
purgeReactionTracking(statusKey(chatId, threadId))
|
|
6355
6266
|
{
|
|
6356
6267
|
const sKey = streamKey(chatId, threadId)
|
|
6357
6268
|
const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0
|