switchroom 0.19.37 → 0.19.39

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.
@@ -21311,7 +21311,7 @@ function allocateAgentUid(name) {
21311
21311
  }
21312
21312
 
21313
21313
  // src/build-info.ts
21314
- var VERSION = "0.19.37";
21314
+ var VERSION = "0.19.39";
21315
21315
 
21316
21316
  // src/setup/hindsight-recall-tunables.ts
21317
21317
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -21440,8 +21440,8 @@ var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 16 * 1024;
21440
21440
  var HINDSIGHT_PG_APP_ANON_MIB = 2560;
21441
21441
  var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
21442
21442
  var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
21443
- var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 3072;
21444
- var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 7168;
21443
+ var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 6144;
21444
+ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 12288;
21445
21445
  function pgMib(mib) {
21446
21446
  return `${mib}MB`;
21447
21447
  }
@@ -21485,7 +21485,7 @@ var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
21485
21485
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
21486
21486
  var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
21487
21487
  var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
21488
- var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
21488
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 250;
21489
21489
  var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
21490
21490
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
21491
21491
  var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
@@ -19337,8 +19337,8 @@ var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 16 * 1024;
19337
19337
  var HINDSIGHT_PG_APP_ANON_MIB = 2560;
19338
19338
  var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
19339
19339
  var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
19340
- var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 3072;
19341
- var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 7168;
19340
+ var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 6144;
19341
+ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 12288;
19342
19342
  function pgMib(mib) {
19343
19343
  return `${mib}MB`;
19344
19344
  }
@@ -19382,7 +19382,7 @@ var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
19382
19382
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
19383
19383
  var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
19384
19384
  var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
19385
- var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
19385
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 250;
19386
19386
  var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
19387
19387
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
19388
19388
  var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
@@ -19750,8 +19750,8 @@ var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 16 * 1024;
19750
19750
  var HINDSIGHT_PG_APP_ANON_MIB = 2560;
19751
19751
  var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
19752
19752
  var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
19753
- var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 3072;
19754
- var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 7168;
19753
+ var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 6144;
19754
+ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 12288;
19755
19755
  function pgMib(mib) {
19756
19756
  return `${mib}MB`;
19757
19757
  }
@@ -19795,7 +19795,7 @@ var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
19795
19795
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
19796
19796
  var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
19797
19797
  var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
19798
- var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
19798
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 250;
19799
19799
  var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
19800
19800
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
19801
19801
  var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.37",
4
+ "version": "0.19.39",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -47609,6 +47609,23 @@ function computeLabel(toolName, input) {
47609
47609
  }
47610
47610
  return "Working\u2026";
47611
47611
  }
47612
+ function isSubagentToolCall(event) {
47613
+ if (!event || typeof event !== "object")
47614
+ return false;
47615
+ const at = event.agent_type;
47616
+ if (typeof at === "string" && at.length > 0)
47617
+ return at !== "main";
47618
+ const aid = event.agent_id;
47619
+ const sid = event.session_id;
47620
+ return typeof aid === "string" && aid.length > 0 && typeof sid === "string" && sid.length > 0 && aid !== sid;
47621
+ }
47622
+ function hasAttributionSignal(event) {
47623
+ if (!event || typeof event !== "object")
47624
+ return false;
47625
+ const at = event.agent_type;
47626
+ const aid = event.agent_id;
47627
+ return typeof at === "string" && at.length > 0 || typeof aid === "string" && aid.length > 0;
47628
+ }
47612
47629
  function main() {
47613
47630
  const raw = readStdin().trim();
47614
47631
  if (!raw)
@@ -47627,6 +47644,14 @@ function main() {
47627
47644
  const toolName = event.tool_name;
47628
47645
  if (!sessionId || !toolUseId || !toolName)
47629
47646
  process.exit(0);
47647
+ if (!hasAttributionSignal(event)) {
47648
+ try {
47649
+ process.stderr.write(`[tool-label-pretool] WARNING: PreToolUse payload carries neither ` + `agent_type nor agent_id \u2014 the sidechain-attribution invariant may be ` + `broken by a Claude Code schema change; sub-agent labels may be ` + `mis-attributed to the parent session (tool_use_id=${toolUseId}).
47650
+ `);
47651
+ } catch {}
47652
+ }
47653
+ if (isSubagentToolCall(event))
47654
+ process.exit(0);
47630
47655
  let label;
47631
47656
  try {
47632
47657
  label = computeLabel(toolName, event.tool_input);
@@ -53405,6 +53430,12 @@ function createHandbackPreturnSignal(deps) {
53405
53430
  entry.reapTimer = null;
53406
53431
  if (entry.consumed)
53407
53432
  return;
53433
+ if (deps.isClaudeBusy?.() === true) {
53434
+ log(`handback-preturn-signal: reap deferred key=${entry.statusKey} ` + `(claude busy \u2014 handback enqueued behind an in-flight turn, not orphaned)
53435
+ `);
53436
+ entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs);
53437
+ return;
53438
+ }
53408
53439
  entry.consumed = true;
53409
53440
  stopTypingUnlessTurnLive(entry, "orphan reap");
53410
53441
  if (entry.activityMessageId != null) {
@@ -53501,6 +53532,13 @@ function createHandbackPreturnSignal(deps) {
53501
53532
  dropEntry(entry);
53502
53533
  return adoption;
53503
53534
  },
53535
+ hasPendingForTurnId(turnId) {
53536
+ for (const e of byKey.values()) {
53537
+ if (e.adoptTurnId === turnId && !e.consumed)
53538
+ return true;
53539
+ }
53540
+ return false;
53541
+ },
53504
53542
  isPreTurnRecord(turnKey2) {
53505
53543
  return turnKey2.startsWith(PRETURN_TURNKEY_PREFIX);
53506
53544
  },
@@ -80098,23 +80136,35 @@ function decideFallbackTeardownNotice2(input) {
80098
80136
  // gateway/stream-render.ts
80099
80137
  var PARKED_TURN_START_MAX = 16;
80100
80138
  var PARKED_TURN_START_TTL_MS = 30 * 60000;
80139
+ var QUEUED_CARD_ENABLED = process.env.SWITCHROOM_QUEUED_CARD !== "0";
80140
+ var QUEUED_CARD_HTML = "\u23f3 Queued \u2014 waiting for the current task to finish\u2026";
80141
+ var QUEUED_CARD_FOLDED_HTML = "\u2705 Folded into the current task.";
80142
+ var QUEUED_CARD_EXPIRED_HTML = "\u26a0\ufe0f This queued message timed out before it could start.";
80101
80143
  var parkedTurnStarts = [];
80102
- function pruneParkedTurnStarts(now) {
80144
+ function pruneParkedTurnStarts(now, onDrop) {
80103
80145
  for (let i = parkedTurnStarts.length - 1;i >= 0; i--) {
80104
80146
  if (now - parkedTurnStarts[i].parkedAt > PARKED_TURN_START_TTL_MS) {
80105
80147
  const [dropped] = parkedTurnStarts.splice(i, 1);
80106
80148
  process.stderr.write(`telegram gateway: parked-turn-start expired chat=${dropped.chatId ?? "-"} ` + `msg=${dropped.messageId ?? "-"} age_ms=${now - dropped.parkedAt}
80107
80149
  `);
80150
+ try {
80151
+ onDrop?.(dropped);
80152
+ } catch {}
80108
80153
  }
80109
80154
  }
80110
80155
  }
80111
- function parkTurnStart(env, now) {
80112
- parkedTurnStarts.push({ ...env, parkedAt: now });
80156
+ function parkTurnStart(env, now, onEvict) {
80157
+ const entry = { ...env, parkedAt: now };
80158
+ parkedTurnStarts.push(entry);
80113
80159
  while (parkedTurnStarts.length > PARKED_TURN_START_MAX) {
80114
80160
  const [dropped] = parkedTurnStarts.splice(0, 1);
80115
80161
  process.stderr.write(`telegram gateway: parked-turn-start evicted (cap ${PARKED_TURN_START_MAX}) ` + `chat=${dropped.chatId ?? "-"} msg=${dropped.messageId ?? "-"}
80116
80162
  `);
80163
+ try {
80164
+ onEvict?.(dropped);
80165
+ } catch {}
80117
80166
  }
80167
+ return entry;
80118
80168
  }
80119
80169
  function takeParkedTurnStart() {
80120
80170
  return parkedTurnStarts.pop() ?? null;
@@ -80122,11 +80172,56 @@ function takeParkedTurnStart() {
80122
80172
  function discardParkedTurnStart(rawContent) {
80123
80173
  for (let i = parkedTurnStarts.length - 1;i >= 0; i--) {
80124
80174
  if (parkedTurnStarts[i].rawContent === rawContent) {
80125
- parkedTurnStarts.splice(i, 1);
80126
- return true;
80175
+ const [dropped] = parkedTurnStarts.splice(i, 1);
80176
+ return dropped;
80127
80177
  }
80128
80178
  }
80129
- return false;
80179
+ return null;
80180
+ }
80181
+ async function openQueuedCard(deps, chatId, threadId, replyToMessageId) {
80182
+ try {
80183
+ const sent = await deps.robustApiCall(() => deps.bot.api.sendRichMessage(chatId, richMessage(QUEUED_CARD_HTML), {
80184
+ ...threadId != null ? { message_thread_id: threadId } : {},
80185
+ ...replyToMessageId != null ? { reply_parameters: { message_id: replyToMessageId, allow_sending_without_reply: true } } : {},
80186
+ disable_notification: true
80187
+ }), {
80188
+ chat_id: chatId,
80189
+ ...threadId != null ? { threadId } : {},
80190
+ verb: "queued-card.send"
80191
+ });
80192
+ return sent?.message_id ?? null;
80193
+ } catch (err) {
80194
+ process.stderr.write(`telegram gateway: queued card send failed: ${err.message}
80195
+ `);
80196
+ return null;
80197
+ }
80198
+ }
80199
+ function finalizeQueuedCard(deps, chatId, threadId, messageId, html) {
80200
+ deps.robustApiCall(() => deps.bot.api.editMessageText(chatId, messageId, richMessage(html), {}), {
80201
+ chat_id: chatId,
80202
+ ...threadId != null ? { threadId } : {},
80203
+ verb: "queued-card.finalize"
80204
+ }).catch(() => {
80205
+ return;
80206
+ });
80207
+ }
80208
+ function deleteQueuedCard(deps, chatId, messageId) {
80209
+ deps.robustApiCall(() => deps.bot.api.deleteMessage(chatId, messageId), { chat_id: chatId, verb: "queued-card.delete-orphan" }).catch(() => {
80210
+ return;
80211
+ });
80212
+ }
80213
+ function envThreadIdNum(threadId) {
80214
+ if (threadId == null)
80215
+ return null;
80216
+ const n = Number(threadId);
80217
+ return Number.isFinite(n) ? n : null;
80218
+ }
80219
+ function finalizeParkedEntryCard(deps, entry, html) {
80220
+ if (!QUEUED_CARD_ENABLED)
80221
+ return;
80222
+ if (entry.queuedCardMessageId == null || entry.chatId == null)
80223
+ return;
80224
+ finalizeQueuedCard(deps, entry.chatId, envThreadIdNum(entry.threadId), entry.queuedCardMessageId, html);
80130
80225
  }
80131
80226
  function beginTurn(deps, ev) {
80132
80227
  const {
@@ -80240,6 +80335,16 @@ function beginTurn(deps, ev) {
80240
80335
  `);
80241
80336
  }
80242
80337
  }
80338
+ if (QUEUED_CARD_ENABLED && ev.queuedCardMessageId != null && next.activityMessageId == null) {
80339
+ next.activityMessageId = ev.queuedCardMessageId;
80340
+ next.activityEverOpened = true;
80341
+ process.stderr.write(`telegram gateway: queued card adopted turnId=${turnId} card=${ev.queuedCardMessageId}
80342
+ `);
80343
+ } else if (QUEUED_CARD_ENABLED && ev.queuedCardMessageId != null && ev.chatId != null && next.activityMessageId != null && next.activityMessageId !== ev.queuedCardMessageId) {
80344
+ deleteQueuedCard(deps, ev.chatId, ev.queuedCardMessageId);
80345
+ process.stderr.write(`telegram gateway: queued card superseded by handback adoption turnId=${turnId} ` + `queued=${ev.queuedCardMessageId} adopted=${next.activityMessageId}
80346
+ `);
80347
+ }
80243
80348
  startTurnTypingLoop(ev.chatId, enqThreadIdNum ?? null);
80244
80349
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
80245
80350
  scheduleEarlyLivenessOpen(next);
@@ -80388,7 +80493,7 @@ function handleSessionEvent(deps, ev) {
80388
80493
  ackDelivery(deliveryQueue, chatKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null), ev.messageId, ev.rawContent);
80389
80494
  }
80390
80495
  }
80391
- pruneParkedTurnStarts(now);
80496
+ pruneParkedTurnStarts(now, (e) => finalizeParkedEntryCard(deps, e, QUEUED_CARD_EXPIRED_HTML));
80392
80497
  if (!ev.chatId) {
80393
80498
  beginTurn(deps, ev);
80394
80499
  return;
@@ -80399,18 +80504,38 @@ function handleSessionEvent(deps, ev) {
80399
80504
  beginTurn(deps, ev);
80400
80505
  return;
80401
80506
  }
80402
- parkTurnStart({
80507
+ const parkedEntry = parkTurnStart({
80403
80508
  chatId: ev.chatId,
80404
80509
  messageId: ev.messageId,
80405
80510
  threadId: ev.threadId,
80406
80511
  rawContent: ev.rawContent
80407
- }, now);
80512
+ }, now, (evicted) => finalizeParkedEntryCard(deps, evicted, QUEUED_CARD_EXPIRED_HTML));
80408
80513
  process.stderr.write(`telegram gateway: turn-start parked (session busy) chat=${ev.chatId} ` + `thread=${enqThreadIdNum ?? "-"} msg=${ev.messageId ?? "-"} ` + `parked=${parkedTurnStarts.length}
80409
80514
  `);
80515
+ const parkTurnId = deriveTurnId(ev.chatId, enqThreadIdNum ?? null, ev.messageId);
80516
+ const handbackOwnsSurface = HANDBACK_PRETURN_ENABLED && parkTurnId != null && handbackPreturnSignal.hasPendingForTurnId(parkTurnId);
80517
+ if (handbackOwnsSurface) {
80518
+ process.stderr.write(`telegram gateway: queued card suppressed (handback owns surface) ` + `turnId=${parkTurnId} msg=${ev.messageId ?? "-"}
80519
+ `);
80520
+ }
80521
+ if (QUEUED_CARD_ENABLED && !handbackOwnsSurface) {
80522
+ const cardChatId = ev.chatId;
80523
+ const replyToRaw = ev.messageId != null ? Number(ev.messageId) : null;
80524
+ const replyTo = replyToRaw != null && Number.isFinite(replyToRaw) ? replyToRaw : null;
80525
+ openQueuedCard(deps, cardChatId, enqThreadIdNum ?? null, replyTo).then((cardId) => {
80526
+ if (cardId == null)
80527
+ return;
80528
+ if (parkedTurnStarts.includes(parkedEntry)) {
80529
+ parkedEntry.queuedCardMessageId = cardId;
80530
+ } else {
80531
+ deleteQueuedCard(deps, cardChatId, cardId);
80532
+ }
80533
+ });
80534
+ }
80410
80535
  return;
80411
80536
  }
80412
80537
  case "dequeue": {
80413
- pruneParkedTurnStarts(Date.now());
80538
+ pruneParkedTurnStarts(Date.now(), (e) => finalizeParkedEntryCard(deps, e, QUEUED_CARD_EXPIRED_HTML));
80414
80539
  const parked = takeParkedTurnStart();
80415
80540
  if (parked == null)
80416
80541
  return;
@@ -80418,8 +80543,10 @@ function handleSessionEvent(deps, ev) {
80418
80543
  return;
80419
80544
  }
80420
80545
  case "queue_remove": {
80421
- pruneParkedTurnStarts(Date.now());
80422
- discardParkedTurnStart(ev.rawContent);
80546
+ pruneParkedTurnStarts(Date.now(), (e) => finalizeParkedEntryCard(deps, e, QUEUED_CARD_EXPIRED_HTML));
80547
+ const removed = discardParkedTurnStart(ev.rawContent);
80548
+ if (removed != null)
80549
+ finalizeParkedEntryCard(deps, removed, QUEUED_CARD_FOLDED_HTML);
80423
80550
  return;
80424
80551
  }
80425
80552
  case "model": {
@@ -84938,8 +85065,8 @@ var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 16 * 1024;
84938
85065
  var HINDSIGHT_PG_APP_ANON_MIB = 2560;
84939
85066
  var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
84940
85067
  var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
84941
- var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 3072;
84942
- var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 7168;
85068
+ var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 6144;
85069
+ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 12288;
84943
85070
  function pgMib(mib) {
84944
85071
  return `${mib}MB`;
84945
85072
  }
@@ -84983,7 +85110,7 @@ var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
84983
85110
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
84984
85111
  var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
84985
85112
  var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
84986
- var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
85113
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 250;
84987
85114
  var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
84988
85115
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
84989
85116
  var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
@@ -99672,10 +99799,10 @@ function startOutboxSweep(deps) {
99672
99799
  }
99673
99800
 
99674
99801
  // ../src/build-info.ts
99675
- var VERSION2 = "0.19.37";
99676
- var COMMIT_SHA = "5d2f9a17";
99677
- var COMMIT_DATE = "2026-07-30T08:57:38Z";
99678
- var LATEST_PR = 4023;
99802
+ var VERSION2 = "0.19.39";
99803
+ var COMMIT_SHA = "4aff2bdd";
99804
+ var COMMIT_DATE = "2026-07-31T00:54:36Z";
99805
+ var LATEST_PR = 4041;
99679
99806
  var COMMITS_AHEAD_OF_TAG = 0;
99680
99807
 
99681
99808
  // gateway/boot-version.ts
@@ -104885,7 +105012,7 @@ async function runMidSessionCardReaper() {
104885
105012
  const { finalized, vanished, total } = await runActivityCardMidSessionReaper({
104886
105013
  path: ACTIVITY_CARD_STORE_PATH,
104887
105014
  fs: activityCardStoreFs,
104888
- isLive: (record2) => topicKeys.has(record2.turnKey),
105015
+ isLive: (record2) => topicKeys.has(record2.turnKey) || handbackPreturnSignal.isPreTurnRecord(record2.turnKey) && isMachineInTurn(),
104889
105016
  ttlMs: MID_SESSION_CARD_REAPER_TTL_MS,
104890
105017
  now,
104891
105018
  finalizeCard: (record2) => {
@@ -107525,7 +107652,8 @@ var handbackPreturnSignal = createHandbackPreturnSignal({
107525
107652
  if (live == null || live.endedAt != null)
107526
107653
  return false;
107527
107654
  return statusKey(live.sessionChatId, live.sessionThreadId) === key;
107528
- }
107655
+ },
107656
+ isClaudeBusy: () => isMachineInTurn()
107529
107657
  });
107530
107658
  var memoryLegibilityStager = new MemoryLegibilityStager;
107531
107659
  function sendMemoryLegibilityLine(event, chatId, threadId) {
@@ -8909,10 +8909,14 @@ async function runMidSessionCardReaper(): Promise<void> {
8909
8909
  const { finalized, vanished, total } = await runActivityCardMidSessionReaper({
8910
8910
  path: ACTIVITY_CARD_STORE_PATH,
8911
8911
  fs: activityCardStoreFs,
8912
- // A synthetic pre-turn record's key is never a live topic key, so it is
8913
- // correctly seen as ownerless here (the seam's own age-based self-reap is
8914
- // the fast path; this is the crash / long-lived backstop).
8915
- isLive: (record) => topicKeys.has(record.turnKey),
8912
+ // A synthetic pre-turn key reads as ownerless here (self-reap is the fast
8913
+ // path; this is the crash backstop). EXCEPTION: a pre-turn handback still
8914
+ // enqueued behind an in-flight turn is NOT an orphan keep it live while
8915
+ // claude is busy, else this 15-min backstop re-posts the false nudge the
8916
+ // self-reap gate removed. (Boot reaper stays ungated: boot is never mid-turn.)
8917
+ isLive: (record) =>
8918
+ topicKeys.has(record.turnKey) ||
8919
+ (handbackPreturnSignal.isPreTurnRecord(record.turnKey) && isMachineInTurn()),
8916
8920
  ttlMs: MID_SESSION_CARD_REAPER_TTL_MS,
8917
8921
  now,
8918
8922
  finalizeCard: (record) => {
@@ -13827,6 +13831,10 @@ const handbackPreturnSignal = createHandbackPreturnSignal({
13827
13831
  // dark-indicator bug). Flag-ON is already per-key; the check is a no-op there.
13828
13832
  return statusKey(live.sessionChatId, live.sessionThreadId) === key
13829
13833
  },
13834
+ // Queue-state gate for the orphan reap (see the signal's `isClaudeBusy` doc):
13835
+ // while a turn is in flight the handback is enqueued behind it, not orphaned.
13836
+ // False for both `bridge_alive_idle` and `bridge_dead` → genuine orphan reaps.
13837
+ isClaudeBusy: () => isMachineInTurn(),
13830
13838
  })
13831
13839
 
13832
13840
  /**
@@ -54,9 +54,23 @@
54
54
  * On adoption the durable record is re-keyed from its synthetic pre-turn key
55
55
  * to the real `statusKey` so that teardown matches.
56
56
  *
57
- * • NEVER-ADOPTED ORPHAN REAP. A degenerate case (bridge death after release,
58
- * no enqueue ever arrives) would leave a frozen card + a forever `typing…`
59
- * loop. The pre-turn card is persisted as a COMPLETE `ActivityCardRecord`
57
+ * • NEVER-ADOPTED ORPHAN REAP GATED ON A GLOBAL-BUSY QUEUE CHECK. A
58
+ * degenerate case (bridge death after release, no enqueue ever arrives)
59
+ * would leave a frozen card + a forever `typing…` loop. But the reap must
60
+ * fire ONLY for that genuine orphan, never for a handback that is correctly
61
+ * enqueued and simply waiting behind a long in-flight turn: claude has ONE
62
+ * bridge / ONE input queue, so a released handback mints its adopting turn
63
+ * only when claude next goes idle. A flat age-based reap cannot tell the two
64
+ * apart and (before this gate) posted a false "never started — needs a
65
+ * nudge" card every time a parent turn out-ran the timeout (the 3-cards-in-
66
+ * 4-min incident). The reap therefore consults `isClaudeBusy()` — the
67
+ * machine-level "a turn is in flight right now" signal — and DEFERS
68
+ * (re-arms for another `adoptTimeoutMs`) while claude is busy, finalizing
69
+ * only once claude is idle AND the handback still has not been adopted. That
70
+ * idle-with-no-adoption state is the genuine bridge-death / dropped-inbound
71
+ * degenerate case. Lengthening the flat timeout would merely move the false
72
+ * positive; the queue-state gate removes it. The pre-turn card is persisted
73
+ * as a COMPLETE `ActivityCardRecord`
60
74
  * under a SYNTHETIC `turnKey` (`preturn:<statusKey>:<startedAt>`) — synthetic
61
75
  * so a sibling REAL turn on the same topic key can neither upsert-clobber it
62
76
  * nor keep it "live" (the mid-session reaper's `isLive` keys on live topic
@@ -171,6 +185,22 @@ export interface HandbackPreturnSignalDeps {
171
185
  * canonical turn-end owns the stop. Optional; defaults to "no live turn"
172
186
  * (the pre-#3544 behaviour). */
173
187
  hasLiveTurn?: (statusKey: string) => boolean
188
+ /** True IFF claude is CURRENTLY producing a turn (ANY topic — the machine has
189
+ * ONE bridge / ONE input queue). Consulted by the orphan reap: a released
190
+ * handback is delivered into that single queue and only mints its adopting
191
+ * turn when claude next goes idle, so while claude is busy the handback is
192
+ * NOT orphaned — it is correctly enqueued BEHIND the in-flight turn (a long
193
+ * parent turn can run many minutes). Finalizing it as an orphan in that
194
+ * window posts a false "never started — needs a nudge" card. The reap
195
+ * therefore DEFERS (re-arms) while this is true and finalizes only once
196
+ * claude is idle and the handback still has not been adopted — the genuine
197
+ * bridge-death / dropped-inbound degenerate case the reap exists for.
198
+ * Distinct from `hasLiveTurn`, which is PER-KEY and only governs the
199
+ * typing-loop stop; this is the GLOBAL busy signal because the handback
200
+ * queues behind whatever turn is running, regardless of topic. Optional;
201
+ * defaults to "not busy" (immediate reap — the pre-fix behaviour), so the
202
+ * genuine-orphan path is unchanged. */
203
+ isClaudeBusy?: () => boolean
174
204
  now?: () => number
175
205
  /** Debounce before painting the pre-turn card (kills sub-second flicker). */
176
206
  debounceMs?: number
@@ -216,6 +246,15 @@ export interface HandbackPreturnSignal {
216
246
  * re-keyed to the real `statusKey` so the turn's end-of-turn
217
247
  * `clearActivitySummary` finalizes it. Cancels the debounce/self-reap. */
218
248
  tryAdopt: (turnId: string) => HandbackAdoption | null
249
+ /** True IFF a live (un-consumed) pre-turn entry is armed whose adopting turn
250
+ * is `turnId`. Consulted by the queued-card surface (stream-render Part B) at
251
+ * PARK time: when a handback pre-turn signal already owns the surface for the
252
+ * turn this parked message will mint, the queued card must NOT be posted —
253
+ * otherwise the same message ends up with two cards (the handback card the
254
+ * turn adopts, plus a queued card that then freezes). Turn-scoped, NOT
255
+ * key-scoped, so an unrelated user message parked on the same topic (a
256
+ * DIFFERENT adoptTurnId) is not wrongly suppressed. */
257
+ hasPendingForTurnId: (turnId: string) => boolean
219
258
  /** True IFF `turnKey` is a synthetic pre-turn record key. */
220
259
  isPreTurnRecord: (turnKey: string) => boolean
221
260
  /** Reap hook for the gateway's mid-session / boot card reapers: stop the
@@ -349,6 +388,25 @@ export function createHandbackPreturnSignal(
349
388
  function reap(entry: PreTurnEntry): void {
350
389
  entry.reapTimer = null
351
390
  if (entry.consumed) return
391
+ // PRIMARY GATE (queue state, not a flat timeout): a released handback is
392
+ // delivered into claude's SINGLE input queue and mints its adopting turn
393
+ // only when claude next goes idle. If claude is STILL in a turn, this
394
+ // handback is not orphaned — it is correctly enqueued behind the in-flight
395
+ // turn (a long parent turn can run many minutes). Finalizing now would post
396
+ // a false "never started — needs a nudge" card (the 3-cards-in-4-min
397
+ // incident). Defer: re-arm and re-check. We finalize only once claude is
398
+ // idle AND the handback still has not been adopted (the genuine bridge-
399
+ // death / dropped-inbound case). This cannot loop forever on a healthy
400
+ // agent: claude processes its queue FIFO, so the handback is either adopted
401
+ // (cancelling the reap) or claude eventually goes idle (letting it fire).
402
+ if (deps.isClaudeBusy?.() === true) {
403
+ log(
404
+ `handback-preturn-signal: reap deferred key=${entry.statusKey} ` +
405
+ `(claude busy — handback enqueued behind an in-flight turn, not orphaned)\n`,
406
+ )
407
+ entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs)
408
+ return
409
+ }
352
410
  entry.consumed = true
353
411
  // Stop the forever-running typing loop and finalize the frozen card.
354
412
  stopTypingUnlessTurnLive(entry, 'orphan reap')
@@ -467,6 +525,13 @@ export function createHandbackPreturnSignal(
467
525
  return adoption
468
526
  },
469
527
 
528
+ hasPendingForTurnId(turnId) {
529
+ for (const e of byKey.values()) {
530
+ if (e.adoptTurnId === turnId && !e.consumed) return true
531
+ }
532
+ return false
533
+ },
534
+
470
535
  isPreTurnRecord(turnKey) {
471
536
  return turnKey.startsWith(PRETURN_TURNKEY_PREFIX)
472
537
  },