switchroom 0.19.4 → 0.19.6

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.
Files changed (43) hide show
  1. package/dist/auth-broker/index.js +7 -3
  2. package/dist/cli/autoaccept-poll.js +8 -2
  3. package/dist/cli/switchroom.js +20 -5
  4. package/dist/host-control/main.js +1 -1
  5. package/package.json +1 -1
  6. package/profiles/_base/start.sh.hbs +67 -4
  7. package/telegram-plugin/dist/gateway/gateway.js +585 -302
  8. package/telegram-plugin/flushed-turn-supersede.ts +43 -7
  9. package/telegram-plugin/gateway/command-format.ts +253 -0
  10. package/telegram-plugin/gateway/gateway-heartbeat.ts +72 -0
  11. package/telegram-plugin/gateway/gateway.ts +128 -259
  12. package/telegram-plugin/gateway/hang-restart-decision.ts +189 -0
  13. package/telegram-plugin/gateway/liveness-wiring.ts +35 -1
  14. package/telegram-plugin/gateway/outbound-send-path.ts +51 -11
  15. package/telegram-plugin/gateway/pending-inbound-buffer.ts +27 -0
  16. package/telegram-plugin/gateway/session-model-file.ts +13 -0
  17. package/telegram-plugin/gateway/stream-render.ts +18 -1
  18. package/telegram-plugin/gateway/subagent-handback-marker.ts +42 -0
  19. package/telegram-plugin/gateway/turn-active-marker.ts +29 -17
  20. package/telegram-plugin/gateway/worker-feed-dispatch.ts +139 -0
  21. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +87 -36
  22. package/telegram-plugin/hooks/silent-end-scan.mjs +263 -3
  23. package/telegram-plugin/render/line-start-guard.ts +76 -4
  24. package/telegram-plugin/reply-owner-resolve.ts +43 -7
  25. package/telegram-plugin/rich-send.ts +8 -1
  26. package/telegram-plugin/tests/command-format.test.ts +212 -0
  27. package/telegram-plugin/tests/flushed-turn-supersede.test.ts +89 -0
  28. package/telegram-plugin/tests/gateway-heartbeat.test.ts +70 -0
  29. package/telegram-plugin/tests/hang-restart-decision.test.ts +146 -0
  30. package/telegram-plugin/tests/hang-restart-marker-integration.test.ts +98 -0
  31. package/telegram-plugin/tests/narrative-lane-golden.test.ts +2 -1
  32. package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +86 -0
  33. package/telegram-plugin/tests/render/heading-guard.test.ts +114 -0
  34. package/telegram-plugin/tests/render/rich-corpus-seam-regression.test.ts +76 -0
  35. package/telegram-plugin/tests/reply-owner-resolve.test.ts +74 -0
  36. package/telegram-plugin/tests/send-reply-golden.test.ts +221 -6
  37. package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +63 -0
  38. package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +60 -16
  39. package/telegram-plugin/tests/silent-end-single-writer-election.test.ts +193 -0
  40. package/telegram-plugin/tests/silent-end.test.ts +60 -5
  41. package/telegram-plugin/tests/stream-render-golden.test.ts +2 -1
  42. package/telegram-plugin/tests/subagent-handback-marker.test.ts +36 -0
  43. package/telegram-plugin/tests/worker-feed-origin-race-defer.test.ts +321 -0
@@ -8674,10 +8674,42 @@ function guardAccidentalBlockConstructs(text) {
8674
8674
  }
8675
8675
  return out;
8676
8676
  }
8677
- var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST;
8677
+ function escapeAccidentalHeadingLine(line) {
8678
+ return line.replace(ACCIDENTAL_HEADING, "$1\\$2");
8679
+ }
8680
+ function guardAccidentalHeading(text) {
8681
+ if (!text.includes("#"))
8682
+ return text;
8683
+ const segments = splitProtectedSegments(text);
8684
+ let out = "";
8685
+ let atLineStart = true;
8686
+ for (const seg of segments) {
8687
+ if (seg.code) {
8688
+ out += seg.text;
8689
+ atLineStart = seg.text.endsWith(`
8690
+ `);
8691
+ continue;
8692
+ }
8693
+ const lines = seg.text.split(`
8694
+ `);
8695
+ for (let k = 0;k < lines.length; k++) {
8696
+ const lineIsAtStart = k === 0 ? atLineStart : true;
8697
+ const processed = lineIsAtStart ? escapeAccidentalHeadingLine(lines[k]) : lines[k];
8698
+ out += processed;
8699
+ if (k < lines.length - 1)
8700
+ out += `
8701
+ `;
8702
+ }
8703
+ atLineStart = seg.text.endsWith(`
8704
+ `);
8705
+ }
8706
+ return out;
8707
+ }
8708
+ var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST, ACCIDENTAL_HEADING;
8678
8709
  var init_line_start_guard = __esm(() => {
8679
8710
  ACCIDENTAL_BLOCKQUOTE = /^>[0-9=]/;
8680
8711
  ACCIDENTAL_ORDERED_LIST = /^(\d{4,})([.)])(\s|$)/;
8712
+ ACCIDENTAL_HEADING = /^([ \t]{0,3})(#{1,6})(?=[^\s#])/;
8681
8713
  });
8682
8714
 
8683
8715
  // render/inline-pairs-guard.ts
@@ -8727,6 +8759,7 @@ var init_inline_pairs_guard = __esm(() => {
8727
8759
  function guardAccidentalFormatting(markdown) {
8728
8760
  let out = markdown;
8729
8761
  out = guardAccidentalEmphasis(out);
8762
+ out = guardAccidentalHeading(out);
8730
8763
  out = guardAccidentalBlockConstructs(out);
8731
8764
  out = guardAccidentalInlinePairs(out);
8732
8765
  out = guardDollarMath(out);
@@ -37338,7 +37371,7 @@ __export(exports_tmux2, {
37338
37371
  captureAgentPane: () => captureAgentPane2
37339
37372
  });
37340
37373
  import { execFileSync as execFileSync8 } from "node:child_process";
37341
- import { chmodSync as chmodSync13, mkdirSync as mkdirSync45, readdirSync as readdirSync12, statSync as statSync18, unlinkSync as unlinkSync25, writeFileSync as writeFileSync46 } from "node:fs";
37374
+ import { chmodSync as chmodSync13, mkdirSync as mkdirSync46, readdirSync as readdirSync12, statSync as statSync18, unlinkSync as unlinkSync25, writeFileSync as writeFileSync47 } from "node:fs";
37342
37375
  import { resolve as resolve12 } from "node:path";
37343
37376
  function captureAgentPane2(opts) {
37344
37377
  const { agentName: agentName3, agentDir, reason } = opts;
@@ -37350,7 +37383,7 @@ function captureAgentPane2(opts) {
37350
37383
  const reasonSlug = sanitizeReason2(reason);
37351
37384
  const outPath = resolve12(outDir, `${ts}-${reasonSlug}.txt`);
37352
37385
  try {
37353
- mkdirSync45(outDir, { recursive: true, mode: 448 });
37386
+ mkdirSync46(outDir, { recursive: true, mode: 448 });
37354
37387
  } catch (err) {
37355
37388
  const msg = `mkdir crash-reports failed: ${err.message}`;
37356
37389
  console.error(`[tmux-capture] ${agentName3}: ${msg}`);
@@ -37389,7 +37422,7 @@ function captureAgentPane2(opts) {
37389
37422
  ` + `
37390
37423
  `;
37391
37424
  try {
37392
- writeFileSync46(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
37425
+ writeFileSync47(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
37393
37426
  mode: 384
37394
37427
  });
37395
37428
  } catch (err) {
@@ -38210,7 +38243,7 @@ function buildGrantedKeyboard(scope) {
38210
38243
  const btn = scopeToOpenInDriveButton(scope);
38211
38244
  if (btn === null)
38212
38245
  return;
38213
- return new import_grammy13.InlineKeyboard().url(btn.text, btn.url);
38246
+ return new import_grammy14.InlineKeyboard().url(btn.text, btn.url);
38214
38247
  }
38215
38248
  async function handleApprovalCallback(ctx, data) {
38216
38249
  const parsed = parseApprovalCallback(data);
@@ -38256,11 +38289,11 @@ async function handleApprovalCallback(ctx, data) {
38256
38289
  } catch {}
38257
38290
  await ctx.answerCallbackQuery({ text: granted ? "Approved" : "Denied" });
38258
38291
  }
38259
- var import_grammy13;
38292
+ var import_grammy14;
38260
38293
  var init_approval_callback = __esm(() => {
38261
38294
  init_approval_card();
38262
38295
  init_client3();
38263
- import_grammy13 = __toESM(require_mod2(), 1);
38296
+ import_grammy14 = __toESM(require_mod2(), 1);
38264
38297
  });
38265
38298
 
38266
38299
  // ../src/telegram/materialize-bot-token.ts
@@ -38433,14 +38466,14 @@ var init_approvals_commands = __esm(() => {
38433
38466
  });
38434
38467
 
38435
38468
  // gateway/gateway.ts
38436
- var import_grammy14 = __toESM(require_mod(), 1);
38469
+ var import_grammy15 = __toESM(require_mod(), 1);
38437
38470
  var import_runner3 = __toESM(require_mod3(), 1);
38438
38471
  import { randomBytes as randomBytes10, createHash as createHash4 } from "crypto";
38439
38472
  import { execFileSync as execFileSync9, execSync as execSync2, spawn as spawn2 } from "child_process";
38440
38473
  import {
38441
38474
  readFileSync as readFileSync58,
38442
- writeFileSync as writeFileSync47,
38443
- mkdirSync as mkdirSync46,
38475
+ writeFileSync as writeFileSync48,
38476
+ mkdirSync as mkdirSync47,
38444
38477
  readdirSync as readdirSync13,
38445
38478
  rmSync as rmSync6,
38446
38479
  statSync as statSync19,
@@ -38454,7 +38487,7 @@ import {
38454
38487
  appendFileSync as appendFileSync7
38455
38488
  } from "fs";
38456
38489
  import { homedir as homedir19 } from "os";
38457
- import { join as join59, sep as sep4, basename as basename15 } from "path";
38490
+ import { join as join60, sep as sep4, basename as basename15 } from "path";
38458
38491
 
38459
38492
  // plugin-logger.ts
38460
38493
  import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
@@ -40393,7 +40426,7 @@ function decideSupersede(record, args) {
40393
40426
  if (!sameTurn) {
40394
40427
  return { supersede: false, deleteMessageIds: [], reason: "different-turn" };
40395
40428
  }
40396
- if (args.replyText != null && !flushedAnswerMatchesReply(record.text, args.replyText)) {
40429
+ if (!args.positiveAttribution && args.replyText != null && !flushedAnswerMatchesReply(record.text, args.replyText)) {
40397
40430
  return { supersede: false, deleteMessageIds: [], reason: "new-content", recordText: record.text };
40398
40431
  }
40399
40432
  return {
@@ -40436,6 +40469,7 @@ class FlushedTurnSupersedeRegistry {
40436
40469
  return decideSupersede(rec, {
40437
40470
  liveTurnId: args.liveTurnId,
40438
40471
  replyText: args.replyText,
40472
+ positiveAttribution: args.positiveAttribution,
40439
40473
  now: args.now,
40440
40474
  ttlMs: this.ttlMs
40441
40475
  });
@@ -50451,6 +50485,25 @@ function sweepPermissionTtl(deps) {
50451
50485
  return expired;
50452
50486
  }
50453
50487
 
50488
+ // gateway/hang-restart-decision.ts
50489
+ var HUMAN_WAIT_TOOLS = new Set(["ask_user"]);
50490
+ var BACKGROUND_TOOLS = new Set(["Task", "Agent"]);
50491
+ var HANG_PROTECTED_EXACT = new Set([
50492
+ "Task",
50493
+ "Agent",
50494
+ "Bash",
50495
+ "WebFetch",
50496
+ "WebSearch"
50497
+ ]);
50498
+ var DEFAULT_HANG_STALENESS_MS = 300000;
50499
+ function hangStalenessMs(env = process.env) {
50500
+ const raw = env.TURN_HANG_SECS;
50501
+ const n = Number(raw);
50502
+ if (!Number.isFinite(n) || n <= 0)
50503
+ return DEFAULT_HANG_STALENESS_MS;
50504
+ return Math.floor(n * 1000);
50505
+ }
50506
+
50454
50507
  // gateway/missed-approvals-store.ts
50455
50508
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7 } from "node:fs";
50456
50509
  import { join as join13 } from "node:path";
@@ -73377,15 +73430,142 @@ function backOffOpenInline2(text4, cut) {
73377
73430
  return earliest;
73378
73431
  }
73379
73432
 
73433
+ // gateway/command-format.ts
73434
+ init_format();
73435
+ var import_grammy8 = __toESM(require_mod2(), 1);
73436
+ function formatSwitchroomOutput(output, maxLen = RICH_MESSAGE_MAX_CHARS) {
73437
+ const trimmed = output.trim();
73438
+ if (trimmed.length <= maxLen)
73439
+ return trimmed;
73440
+ return trimmed.slice(0, maxLen - 20) + `
73441
+ ... (truncated)`;
73442
+ }
73443
+ function stripAnsi4(text4) {
73444
+ return text4.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
73445
+ }
73446
+ function escapeHtmlForTg2(text4) {
73447
+ return text4.replace(/([\\`*_~=\[\]|])/g, "\\$1");
73448
+ }
73449
+ function preBlock(text4) {
73450
+ return "```\n" + text4.replace(/```/g, "`\u200b``") + "\n```";
73451
+ }
73452
+ function getCommandArgs(ctx) {
73453
+ const fromMatch = typeof ctx.match === "string" ? ctx.match.trim() : "";
73454
+ if (fromMatch)
73455
+ return fromMatch;
73456
+ const text4 = ctx.msg?.text ?? ctx.message?.text ?? "";
73457
+ const m = text4.match(/^\/\S+\s+([\s\S]*)$/);
73458
+ return m ? m[1].trim() : "";
73459
+ }
73460
+ function hasDemoFlag(args) {
73461
+ return /(?:^|\s)demo$/i.test(args.trim());
73462
+ }
73463
+ function assertSafeAgentName(name) {
73464
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name) && name !== "all") {
73465
+ throw new Error(`invalid agent name: ${name}`);
73466
+ }
73467
+ }
73468
+ function formatAuthOutputForTelegram(output) {
73469
+ const trimmed = stripAnsi4(output).trim();
73470
+ const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
73471
+ const lines = trimmed.split(/\n+/).map((l) => l.trim()).filter(Boolean);
73472
+ if (!url)
73473
+ return { text: preBlock(formatSwitchroomOutput(trimmed)), url: null };
73474
+ const body = lines.filter((line) => {
73475
+ if (line === url)
73476
+ return false;
73477
+ if (line.startsWith("switchroom auth code"))
73478
+ return false;
73479
+ if (line.startsWith("switchroom auth cancel"))
73480
+ return false;
73481
+ if (line.startsWith("Use 'tmux attach"))
73482
+ return false;
73483
+ if (line.startsWith("After Claude shows you a browser code"))
73484
+ return false;
73485
+ if (line.startsWith("Then finish with:"))
73486
+ return false;
73487
+ if (line.startsWith("Cancel with:"))
73488
+ return false;
73489
+ return true;
73490
+ });
73491
+ const rendered = body.map((line) => {
73492
+ if (line.startsWith("Started Claude auth") || line.startsWith("Auth session already running"))
73493
+ return `**${escapeHtmlForTg2(line)}**`;
73494
+ if (line.startsWith("Open this URL"))
73495
+ return `_${escapeHtmlForTg2(line)}_`;
73496
+ return escapeHtmlForTg2(line);
73497
+ });
73498
+ rendered.push("", "\uD83D\uDC47 Tap **\uD83D\uDD10 Open Claude auth** below, then **reply with the browser code**.", "", `_Wrong Anthropic account getting authorized? Long-press the URL below and choose "Copy Link" or "Open in Browser" \u2014 lands in your main browser where the right account is signed in, bypassing Telegram's in-app browser cookies._`, "", url);
73499
+ return { text: rendered.join(`
73500
+ `), url };
73501
+ }
73502
+ function buildAuthUrlKeyboard(authorizeUrl) {
73503
+ return new import_grammy8.InlineKeyboard().url("\uD83D\uDD10 Open Claude auth", authorizeUrl);
73504
+ }
73505
+ function buildDeferredSecretKeyboard(deferKey) {
73506
+ const unlockData = `vd:unlock:${deferKey}`;
73507
+ const cancelData = `vd:cancel:${deferKey}`;
73508
+ if (unlockData.length > 64 || cancelData.length > 64) {
73509
+ process.stderr.write(`telegram gateway: callback_data overflow \u2014 deferKey=${deferKey} unlockLen=${unlockData.length} cancelLen=${cancelData.length}
73510
+ `);
73511
+ throw new Error(`callback_data overflow: deferKey too long (${deferKey.length} chars)`);
73512
+ }
73513
+ return new import_grammy8.InlineKeyboard().text("\uD83D\uDD13 Unlock vault & save", unlockData).text("\uD83D\uDDD1 Discard", cancelData);
73514
+ }
73515
+ function renderVaultOpFailure(verbLabel, cliOutput, key) {
73516
+ const parsed = parseVaultCliError(cliOutput);
73517
+ const verb = verbLabel === "delete" ? "remove" : verbLabel;
73518
+ const rendered = renderVaultCliError(parsed, { verb, key });
73519
+ if (rendered.suppressRaw)
73520
+ return rendered.html;
73521
+ return `**vault ${verbLabel} failed:**
73522
+ ${preBlock(cliOutput)}`;
73523
+ }
73524
+ function statusIcon(status) {
73525
+ if (status === "active" || status === "running")
73526
+ return "\uD83D\uDFE2";
73527
+ if (status === "inactive" || status === "stopped" || status === "dead")
73528
+ return "\uD83D\uDD34";
73529
+ if (status === "failed")
73530
+ return "\u26a0\ufe0f";
73531
+ return "\u26aa";
73532
+ }
73533
+ function renderAuthCodeOutcome(outcome) {
73534
+ if (!outcome || outcome.kind === "success")
73535
+ return null;
73536
+ const tail = outcome.paneTailText ? `
73537
+ _${escapeHtmlForTg2(outcome.paneTailText)}_` : "";
73538
+ switch (outcome.kind) {
73539
+ case "invalid-code":
73540
+ case "expired-code":
73541
+ return `Code rejected by Claude \u2014 tap **Restart flow** for a fresh URL.${tail}`;
73542
+ case "pane-not-ready":
73543
+ return `Auth pane not ready \u2014 tap **Retry**.`;
73544
+ case "timeout":
73545
+ return `Still waiting after 2 min \u2014 tap **Retry** or check \`switchroom auth list\`.${tail}`;
73546
+ }
73547
+ }
73548
+ function buildDoctorScopeKeyboard() {
73549
+ return new import_grammy8.InlineKeyboard().text("\uD83E\uDE7A Whole fleet", "dr:fleet").text("\uD83E\uDE7A This agent", "dr:self");
73550
+ }
73551
+ function formatDoctorReport(raw) {
73552
+ const trimmed = stripAnsi4(raw).trim();
73553
+ if (!trimmed)
73554
+ return "doctor: no output";
73555
+ const pretty = trimmed.replace(/^( *)\u2713 /gm, "$1\uD83D\uDFE2 ").replace(/^( *)\u2717 /gm, "$1\uD83D\uDD34 ").replace(/^( *)! /gm, "$1\uD83D\uDFE1 ");
73556
+ return preBlock(formatSwitchroomOutput(pretty));
73557
+ }
73558
+
73380
73559
  // rich-send.ts
73381
73560
  init_dollar_math_guard();
73382
73561
  init_emphasis_guard();
73383
73562
  init_line_start_guard();
73384
73563
  init_inline_pairs_guard();
73385
- var import_grammy8 = __toESM(require_mod2(), 1);
73564
+ var import_grammy9 = __toESM(require_mod2(), 1);
73386
73565
  function guardAccidentalFormatting2(markdown) {
73387
73566
  let out = markdown;
73388
73567
  out = guardAccidentalEmphasis(out);
73568
+ out = guardAccidentalHeading(out);
73389
73569
  out = guardAccidentalBlockConstructs(out);
73390
73570
  out = guardAccidentalInlinePairs(out);
73391
73571
  out = guardDollarMath(out);
@@ -73537,7 +73717,7 @@ function scrubVoice2(text4) {
73537
73717
  // gateway/outbound-send-path.ts
73538
73718
  init_format();
73539
73719
  init_text_voice_scrub();
73540
- var import_grammy9 = __toESM(require_mod2(), 1);
73720
+ var import_grammy10 = __toESM(require_mod2(), 1);
73541
73721
  import { statSync as statSync9 } from "fs";
73542
73722
  import { extname } from "path";
73543
73723
 
@@ -73775,7 +73955,7 @@ function decideSupersede2(record, args) {
73775
73955
  if (!sameTurn) {
73776
73956
  return { supersede: false, deleteMessageIds: [], reason: "different-turn" };
73777
73957
  }
73778
- if (args.replyText != null && !flushedAnswerMatchesReply2(record.text, args.replyText)) {
73958
+ if (!args.positiveAttribution && args.replyText != null && !flushedAnswerMatchesReply2(record.text, args.replyText)) {
73779
73959
  return { supersede: false, deleteMessageIds: [], reason: "new-content", recordText: record.text };
73780
73960
  }
73781
73961
  return {
@@ -73825,6 +74005,7 @@ class FlushedTurnSupersedeRegistry2 {
73825
74005
  return decideSupersede2(rec, {
73826
74006
  liveTurnId: args.liveTurnId,
73827
74007
  replyText: args.replyText,
74008
+ positiveAttribution: args.positiveAttribution,
73828
74009
  now: args.now,
73829
74010
  ttlMs: this.ttlMs
73830
74011
  });
@@ -74326,7 +74507,7 @@ async function sendReplyChunks(deps, state3) {
74326
74507
  return { threadId, previewMessageId };
74327
74508
  }
74328
74509
  function classifyReadBackError(err) {
74329
- if (err instanceof import_grammy9.GrammyError && err.error_code === 400) {
74510
+ if (err instanceof import_grammy10.GrammyError && err.error_code === 400) {
74330
74511
  const d = (err.description || "").toLowerCase();
74331
74512
  if (d.includes("message to edit not found"))
74332
74513
  return "absent";
@@ -74404,6 +74585,7 @@ async function sendReply(deps, req) {
74404
74585
  resolveAnswerThreadWithLog,
74405
74586
  resolveThreadId,
74406
74587
  getLatestInboundMessageId: getLatestInboundMessageId2,
74588
+ getLastSubagentHandbackAt,
74407
74589
  recordOutbound: recordOutbound2,
74408
74590
  emissionAuthorityFor,
74409
74591
  clearActivitySummary,
@@ -74477,9 +74659,14 @@ async function sendReply(deps, req) {
74477
74659
  let supersedeFlushIds = [];
74478
74660
  {
74479
74661
  const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
74480
- const ownerTurn = resolveReplyOwnerTurn(turn, chat_id, args);
74662
+ const { turn: ownerTurn, tier: ownerTier } = resolveReplyOwnerTurn(turn, chat_id, args);
74481
74663
  const resolvedTurnId = ownerTurn?.turnId ?? null;
74482
- const decision = flushedTurnSupersede.take(chat_id, replyThreadId, { liveTurnId: resolvedTurnId, replyText: text4, now: Date.now() });
74664
+ const ownerEndedAt = ownerTurn?.endedAt ?? null;
74665
+ const handbackAt = getLastSubagentHandbackAt(chat_id);
74666
+ const now = Date.now();
74667
+ const handbackCouldOwnReply = handbackAt != null && ownerEndedAt != null && handbackAt > ownerEndedAt && now - handbackAt <= DEFAULT_SUPERSEDE_TTL_MS2;
74668
+ const replyIsOwnAnswer = ownerTier === "live" || !handbackCouldOwnReply;
74669
+ const decision = flushedTurnSupersede.take(chat_id, replyThreadId, { liveTurnId: resolvedTurnId, replyText: text4, positiveAttribution: replyIsOwnAnswer, now });
74483
74670
  if (decision.supersede) {
74484
74671
  process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) ` + `chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
74485
74672
  `);
@@ -74901,7 +75088,7 @@ ${url}`;
74901
75088
  opts.message_thread_id = tid;
74902
75089
  else
74903
75090
  delete opts.message_thread_id;
74904
- return lockedBot.api.sendVoice(chat_id, new import_grammy9.InputFile(Buffer.from(oggBytes)), opts);
75091
+ return lockedBot.api.sendVoice(chat_id, new import_grammy10.InputFile(Buffer.from(oggBytes)), opts);
74905
75092
  }, { threadId, chat_id, verb: "sendVoice" });
74906
75093
  sentIds.push(sentVoice.message_id);
74907
75094
  logOutbound("reply", chat_id, sentVoice.message_id, voiceOutPlan?.ttsChunks[v]?.length ?? 0, `voice-note=${v + 1}/${voiceOggs.length}`);
@@ -74974,12 +75161,12 @@ ${url}`;
74974
75161
  ...replyParams,
74975
75162
  ...tid != null ? { message_thread_id: tid } : {}
74976
75163
  };
74977
- return lockedBot.api.sendDocument(chat_id, new import_grammy9.InputFile(f), baseOpts);
75164
+ return lockedBot.api.sendDocument(chat_id, new import_grammy10.InputFile(f), baseOpts);
74978
75165
  }, { threadId, chat_id, verb: "sendDocument" });
74979
75166
  if (allPhotos) {
74980
75167
  const media = files.map((f) => ({
74981
75168
  type: "photo",
74982
- media: new import_grammy9.InputFile(f)
75169
+ media: new import_grammy10.InputFile(f)
74983
75170
  }));
74984
75171
  let sent;
74985
75172
  try {
@@ -75012,7 +75199,7 @@ ${url}`;
75012
75199
  sentIds.push(m.message_id);
75013
75200
  } else {
75014
75201
  for (const f of files) {
75015
- const input = new import_grammy9.InputFile(f);
75202
+ const input = new import_grammy10.InputFile(f);
75016
75203
  const isPhoto = sendableAsPhoto(f);
75017
75204
  let sent;
75018
75205
  try {
@@ -76202,6 +76389,15 @@ function removeTurnActiveMarker(stateDir) {
76202
76389
  unlinkSync13(join31(stateDir, TURN_ACTIVE_MARKER_FILE));
76203
76390
  } catch {}
76204
76391
  }
76392
+ function readTurnActiveMarkerAgeMs(stateDir, now) {
76393
+ const path2 = join31(stateDir, TURN_ACTIVE_MARKER_FILE);
76394
+ try {
76395
+ const st = statSync10(path2);
76396
+ return (now ?? Date.now()) - st.mtimeMs;
76397
+ } catch {
76398
+ return null;
76399
+ }
76400
+ }
76205
76401
 
76206
76402
  // gateway/turn-end-gate-backstop.ts
76207
76403
  function withTurnEndGateBackstop(key, endingTurn, body, deps) {
@@ -77149,10 +77345,14 @@ function handleSessionEvent(deps, ev) {
77149
77345
  ended_via: outboundMetrics.outboundCount > 0 ? "reply" : "silent"
77150
77346
  });
77151
77347
  if (turnEndDecision === "reprompt") {
77348
+ const gatewayCapturedEmpty = turn.capturedText.join(`
77349
+
77350
+ `).trim().length === 0;
77351
+ const proseMinChars = !turn.replyCalled && gatewayCapturedEmpty ? 1 : CAPTURED_PROSE_MIN_CHARS;
77152
77352
  const proseDecision = CAPTURED_PROSE_DELIVERY_ENABLED ? decideCapturedProseDelivery({
77153
77353
  turnKey: tKey,
77154
77354
  turnId: turn.turnId,
77155
- minChars: CAPTURED_PROSE_MIN_CHARS
77355
+ minChars: proseMinChars
77156
77356
  }) : { deliver: false, reason: "no-state" };
77157
77357
  if (proseDecision.deliver && proseDecision.text != null) {
77158
77358
  process.stderr.write(`telegram gateway: captured-prose delivery engaged on first silent-end chat=${chatId} turnKey=${tKey} (#3227)
@@ -77950,8 +78150,41 @@ function latestEndedAccepted(candidates) {
77950
78150
  return true;
77951
78151
  return age <= ttl;
77952
78152
  }
78153
+ function resolveReplyOwnerTier(candidates) {
78154
+ if (candidates.liveTurnId != null)
78155
+ return "live";
78156
+ if (candidates.originTurnId != null)
78157
+ return "origin";
78158
+ if (candidates.quotedTurnId != null)
78159
+ return "quoted";
78160
+ if (latestEndedAccepted(candidates))
78161
+ return "latest-ended";
78162
+ return "none";
78163
+ }
77953
78164
  function resolveReplyOwnerTurnId(candidates) {
77954
- return candidates.liveTurnId ?? candidates.originTurnId ?? candidates.quotedTurnId ?? (latestEndedAccepted(candidates) ? candidates.latestEndedTurnId : null) ?? null;
78165
+ switch (resolveReplyOwnerTier(candidates)) {
78166
+ case "live":
78167
+ return candidates.liveTurnId;
78168
+ case "origin":
78169
+ return candidates.originTurnId;
78170
+ case "quoted":
78171
+ return candidates.quotedTurnId;
78172
+ case "latest-ended":
78173
+ return candidates.latestEndedTurnId;
78174
+ case "none":
78175
+ return null;
78176
+ }
78177
+ }
78178
+
78179
+ // gateway/subagent-handback-marker.ts
78180
+ class SubagentHandbackMarker {
78181
+ lastAtByChat = new Map;
78182
+ record(chatId, now) {
78183
+ this.lastAtByChat.set(chatId, now);
78184
+ }
78185
+ lastAt(chatId) {
78186
+ return this.lastAtByChat.get(chatId) ?? null;
78187
+ }
77955
78188
  }
77956
78189
 
77957
78190
  // answer-ready-flush.ts
@@ -78555,7 +78788,7 @@ async function loadFromAuthBroker(options = {}) {
78555
78788
  }
78556
78789
 
78557
78790
  // gateway/folder-picker-handler.ts
78558
- var import_grammy10 = __toESM(require_mod2(), 1);
78791
+ var import_grammy11 = __toESM(require_mod2(), 1);
78559
78792
 
78560
78793
  // ../src/drive/folder-picker.ts
78561
78794
  var DRIVE_ID_RE2 = /^[A-Za-z0-9_-]+$/;
@@ -78874,7 +79107,7 @@ async function recordGrantAndConfirm(ctx, deps, parsed) {
78874
79107
  const confirmText = `\u2705 Granted ${deps.agentName} access to folder ${parsed.folder_id}
78875
79108
  ` + `Scope: \`${scope}\`
78876
79109
  ` + `Revoke with: \`/approvals revoke ${decisionId}\``;
78877
- const kb = new import_grammy10.InlineKeyboard().url("\uD83D\uDCD6 Open in Drive", `https://drive.google.com/drive/folders/${parsed.folder_id}`);
79110
+ const kb = new import_grammy11.InlineKeyboard().url("\uD83D\uDCD6 Open in Drive", `https://drive.google.com/drive/folders/${parsed.folder_id}`);
78878
79111
  try {
78879
79112
  await ctx.editMessageText({ markdown: confirmText }, {
78880
79113
  reply_markup: kb
@@ -78883,7 +79116,7 @@ async function recordGrantAndConfirm(ctx, deps, parsed) {
78883
79116
  await ctx.answerCallbackQuery({ text: "Allowed" });
78884
79117
  }
78885
79118
  function toKeyboard(spec) {
78886
- const kb = new import_grammy10.InlineKeyboard;
79119
+ const kb = new import_grammy11.InlineKeyboard;
78887
79120
  for (let r = 0;r < spec.rows.length; r++) {
78888
79121
  const row = spec.rows[r];
78889
79122
  for (const btn of row) {
@@ -80346,6 +80579,9 @@ function writeSessionModelFile(agentDir, model, configuredDefaultAtWrite) {
80346
80579
  if (!isValidModelArg(model)) {
80347
80580
  throw new Error(`refusing to persist non-canonical session model token: ${JSON.stringify(model)}`);
80348
80581
  }
80582
+ try {
80583
+ rmSync4(join35(agentDir, SESSION_MODEL_BOOT_ATTEMPTS_FILE), { force: true });
80584
+ } catch {}
80349
80585
  atomicWrite(join35(agentDir, SESSION_MODEL_FILE), serializeSessionModel({ model, configuredDefaultAtWrite, ts: Date.now() }));
80350
80586
  }
80351
80587
  function readSessionModelFileRaw(agentDir) {
@@ -81151,20 +81387,20 @@ function registerOpsInfoCommands(bot, deps) {
81151
81387
  isAuthorizedSender,
81152
81388
  getMyAgentName,
81153
81389
  switchroomReply,
81154
- buildDoctorScopeKeyboard,
81390
+ buildDoctorScopeKeyboard: buildDoctorScopeKeyboard2,
81155
81391
  renderSelfDoctor,
81156
- preBlock,
81157
- formatSwitchroomOutput,
81158
- getCommandArgs,
81159
- assertSafeAgentName,
81392
+ preBlock: preBlock2,
81393
+ formatSwitchroomOutput: formatSwitchroomOutput2,
81394
+ getCommandArgs: getCommandArgs2,
81395
+ assertSafeAgentName: assertSafeAgentName2,
81160
81396
  runSwitchroomCommand,
81161
81397
  switchroomExecCombined,
81162
- stripAnsi: stripAnsi4,
81163
- hasDemoFlag,
81164
- escapeHtmlForTg: escapeHtmlForTg2
81398
+ stripAnsi: stripAnsi5,
81399
+ hasDemoFlag: hasDemoFlag2,
81400
+ escapeHtmlForTg: escapeHtmlForTg3
81165
81401
  } = deps;
81166
81402
  function formatWhoamiCard(v, demo = false) {
81167
- const esc = escapeHtmlForTg2;
81403
+ const esc = escapeHtmlForTg3;
81168
81404
  const yn = (b) => b ? "\u2713" : "\u2717";
81169
81405
  const lines = [];
81170
81406
  lines.push(`\uD83D\uDC64 **${esc(v.name ?? "?")}** \u00b7 ${esc(v.tier ?? "standard")}`);
@@ -81196,20 +81432,20 @@ function registerOpsInfoCommands(bot, deps) {
81196
81432
  if (AGENT_ADMIN && hostdWillBeUsed(getMyAgentName())) {
81197
81433
  await switchroomReply(ctx, "\uD83E\uDE7A **Doctor** \u2014 which scope?", {
81198
81434
  html: true,
81199
- reply_markup: buildDoctorScopeKeyboard()
81435
+ reply_markup: buildDoctorScopeKeyboard2()
81200
81436
  });
81201
81437
  return;
81202
81438
  }
81203
81439
  await renderSelfDoctor(ctx);
81204
81440
  } catch (err) {
81205
81441
  await switchroomReply(ctx, `**doctor failed:**
81206
- ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
81442
+ ${preBlock2(formatSwitchroomOutput2(err.message ?? "unknown error"))}`, { html: true });
81207
81443
  }
81208
81444
  });
81209
81445
  bot.command("grant", async (ctx) => {
81210
81446
  if (!isAuthorizedSender(ctx))
81211
81447
  return;
81212
- const parts = getCommandArgs(ctx).split(/\s+/).filter(Boolean);
81448
+ const parts = getCommandArgs2(ctx).split(/\s+/).filter(Boolean);
81213
81449
  if (parts.length === 0) {
81214
81450
  await switchroomReply(ctx, "Usage: /grant <tool> or /grant <agent> <tool>");
81215
81451
  return;
@@ -81224,7 +81460,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81224
81460
  tool = parts.slice(1).join(" ");
81225
81461
  }
81226
81462
  try {
81227
- assertSafeAgentName(agentName3);
81463
+ assertSafeAgentName2(agentName3);
81228
81464
  } catch {
81229
81465
  await switchroomReply(ctx, "Invalid agent name.");
81230
81466
  return;
@@ -81234,7 +81470,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81234
81470
  bot.command("dangerous", async (ctx) => {
81235
81471
  if (!isAuthorizedSender(ctx))
81236
81472
  return;
81237
- const parts = getCommandArgs(ctx).split(/\s+/).filter(Boolean);
81473
+ const parts = getCommandArgs2(ctx).split(/\s+/).filter(Boolean);
81238
81474
  let agentName3;
81239
81475
  let off = false;
81240
81476
  if (parts.length === 0) {
@@ -81248,7 +81484,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81248
81484
  off = true;
81249
81485
  }
81250
81486
  try {
81251
- assertSafeAgentName(agentName3);
81487
+ assertSafeAgentName2(agentName3);
81252
81488
  } catch {
81253
81489
  await switchroomReply(ctx, "Invalid agent name.");
81254
81490
  return;
@@ -81263,7 +81499,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81263
81499
  return;
81264
81500
  const agentName3 = (typeof ctx.match === "string" ? ctx.match : "").trim() || getMyAgentName();
81265
81501
  try {
81266
- assertSafeAgentName(agentName3);
81502
+ assertSafeAgentName2(agentName3);
81267
81503
  } catch {
81268
81504
  await switchroomReply(ctx, "Invalid agent name.");
81269
81505
  return;
@@ -81280,21 +81516,21 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81280
81516
  } catch (err) {
81281
81517
  output = err.stdout ?? err.message ?? "version failed";
81282
81518
  }
81283
- const trimmed = stripAnsi4(output).trim();
81519
+ const trimmed = stripAnsi5(output).trim();
81284
81520
  if (!trimmed) {
81285
81521
  await switchroomReply(ctx, "version: no output");
81286
81522
  return;
81287
81523
  }
81288
- await switchroomReply(ctx, preBlock(formatSwitchroomOutput(trimmed)), { html: true });
81524
+ await switchroomReply(ctx, preBlock2(formatSwitchroomOutput2(trimmed)), { html: true });
81289
81525
  } catch (err) {
81290
81526
  await switchroomReply(ctx, `**version failed:**
81291
- ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
81527
+ ${preBlock2(formatSwitchroomOutput2(err.message ?? "unknown error"))}`, { html: true });
81292
81528
  }
81293
81529
  });
81294
81530
  bot.command("whoami", async (ctx) => {
81295
81531
  if (!isAuthorizedSender(ctx))
81296
81532
  return;
81297
- const demo = hasDemoFlag(getCommandArgs(ctx));
81533
+ const demo = hasDemoFlag2(getCommandArgs2(ctx));
81298
81534
  try {
81299
81535
  let raw;
81300
81536
  try {
@@ -81302,18 +81538,18 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81302
81538
  } catch (err) {
81303
81539
  raw = err.stdout ?? err.message ?? "whoami failed";
81304
81540
  }
81305
- const trimmed = stripAnsi4(raw).trim();
81541
+ const trimmed = stripAnsi5(raw).trim();
81306
81542
  let card;
81307
81543
  try {
81308
81544
  card = formatWhoamiCard(JSON.parse(trimmed.split(`
81309
81545
  `).pop() ?? trimmed), demo);
81310
81546
  } catch {
81311
- card = preBlock(formatSwitchroomOutput(trimmed || "whoami: no output"));
81547
+ card = preBlock2(formatSwitchroomOutput2(trimmed || "whoami: no output"));
81312
81548
  }
81313
81549
  await switchroomReply(ctx, card, { html: true });
81314
81550
  } catch (err) {
81315
81551
  await switchroomReply(ctx, `**whoami failed:**
81316
- ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
81552
+ ${preBlock2(formatSwitchroomOutput2(err.message ?? "unknown error"))}`, { html: true });
81317
81553
  }
81318
81554
  });
81319
81555
  bot.command("commands", async (ctx) => {
@@ -84359,7 +84595,7 @@ async function handleRequestMs365Approval(client3, msg, deps) {
84359
84595
 
84360
84596
  // gateway/diff-preview-card.ts
84361
84597
  init_format();
84362
- var import_grammy11 = __toESM(require_mod2(), 1);
84598
+ var import_grammy12 = __toESM(require_mod2(), 1);
84363
84599
  var REQUEST_ID_RE = /^[0-9a-f]{32}$/;
84364
84600
  var PENDING_FILE_ID_SENTINEL = "pending-create";
84365
84601
  function buildDiffPreviewCard(input) {
@@ -84377,7 +84613,7 @@ function buildDiffPreviewCard(input) {
84377
84613
  }
84378
84614
  const text4 = bodyLines.join(`
84379
84615
  `);
84380
- const kb = new import_grammy11.InlineKeyboard;
84616
+ const kb = new import_grammy12.InlineKeyboard;
84381
84617
  const ROW_BREAK_AFTER = [
84382
84618
  "apply_suggestion"
84383
84619
  ];
@@ -84541,6 +84777,11 @@ function createPendingInboundBuffer(opts = {}) {
84541
84777
  }
84542
84778
  }
84543
84779
  q.push(msg);
84780
+ if (msg.meta?.source === "subagent_handback" && opts.onHandbackEnqueue != null) {
84781
+ try {
84782
+ opts.onHandbackEnqueue(msg.chatId, msg.ts);
84783
+ } catch {}
84784
+ }
84544
84785
  spool?.put(agent, msg);
84545
84786
  log(`pending-inbound-buffer: agent=${agent} buffered source=${msg.meta?.source ?? "-"} ` + `depth_after=${q.length} evicted=${evicted}
84546
84787
  `);
@@ -87221,6 +87462,49 @@ function purgeStaleTurnsForChat(chatId, keys, purger, isStale = () => true) {
87221
87462
  return { purged };
87222
87463
  }
87223
87464
 
87465
+ // gateway/hang-restart-decision.ts
87466
+ var HUMAN_WAIT_TOOLS2 = new Set(["ask_user"]);
87467
+ var BACKGROUND_TOOLS2 = new Set(["Task", "Agent"]);
87468
+ function classifyToolClass(toolName, opts = {}) {
87469
+ if (opts.awaitingApproval === true)
87470
+ return "human";
87471
+ if (HUMAN_WAIT_TOOLS2.has(toolName))
87472
+ return "human";
87473
+ if (BACKGROUND_TOOLS2.has(toolName))
87474
+ return "background";
87475
+ if (toolName === "Bash" && opts.backgroundBash === true)
87476
+ return "background";
87477
+ return "standard";
87478
+ }
87479
+ var HANG_PROTECTED_EXACT2 = new Set([
87480
+ "Task",
87481
+ "Agent",
87482
+ "Bash",
87483
+ "WebFetch",
87484
+ "WebSearch"
87485
+ ]);
87486
+ var HANG_PROTECTED_SUBSTRINGS = ["research", "perplexity", "crawl", "deep_research", "webkite"];
87487
+ function isHangRestartProtectedTool(toolName) {
87488
+ if (HANG_PROTECTED_EXACT2.has(toolName))
87489
+ return true;
87490
+ if (classifyToolClass(toolName) !== "standard")
87491
+ return true;
87492
+ const lower = toolName.toLowerCase();
87493
+ return HANG_PROTECTED_SUBSTRINGS.some((s) => lower.includes(s));
87494
+ }
87495
+ function decideHangRestart(input) {
87496
+ if (input.inFlightToolNames.length === 0)
87497
+ return { restart: false, reason: "not-mid-tool" };
87498
+ const protectedName = input.inFlightToolNames.find(isHangRestartProtectedTool);
87499
+ if (protectedName != null) {
87500
+ return { restart: false, reason: `protected-long-tool:${protectedName}` };
87501
+ }
87502
+ if (input.markerAgeMs != null && input.markerAgeMs < input.stalenessThresholdMs) {
87503
+ return { restart: false, reason: "marker-advancing" };
87504
+ }
87505
+ return { restart: true, reason: "mid-tool-marker-stale" };
87506
+ }
87507
+
87224
87508
  // gateway/update-status-line.ts
87225
87509
  function latestHostdPhase(tail) {
87226
87510
  if (!tail)
@@ -87276,7 +87560,8 @@ function buildSilencePokeOptions(deps) {
87276
87560
  getPendingInboundBuffer,
87277
87561
  trackRedeliveredInbound,
87278
87562
  closeActivityLane,
87279
- closeProgressLane
87563
+ closeProgressLane,
87564
+ hangRestart
87280
87565
  } = deps;
87281
87566
  return {
87282
87567
  thresholdsMs: { fallback: SILENCE_FALLBACK_MS, fallbackHardCeiling: SILENCE_FALLBACK_HARD_MS, floor: SILENCE_FLOOR_MS },
@@ -87324,6 +87609,21 @@ function buildSilencePokeOptions(deps) {
87324
87609
  endTurn2(ctx.key);
87325
87610
  return;
87326
87611
  }
87612
+ if (hangRestart != null && ctx.inFlightTools.length > 0) {
87613
+ const markerAgeMs = readTurnActiveMarkerAgeMs(STATE_DIR);
87614
+ const inFlightToolNames = ctx.inFlightTools.map((t) => t.name);
87615
+ const decision = decideHangRestart({
87616
+ inFlightToolNames,
87617
+ markerAgeMs,
87618
+ stalenessThresholdMs: hangRestart.stalenessThresholdMs
87619
+ });
87620
+ process.stderr.write(`telegram gateway: [hang-watchdog] fallback mid-tool chat=${ctx.chatId} ` + `thread=${ctx.threadId ?? "-"} silence_ms=${ctx.silenceMs} ` + `tools=${inFlightToolNames.join(",")} marker_age_ms=${markerAgeMs ?? "null"} ` + `restart=${decision.restart} reason=${decision.reason}
87621
+ `);
87622
+ if (decision.restart) {
87623
+ hangRestart.request(decision.reason, markerAgeMs ?? ctx.silenceMs);
87624
+ return;
87625
+ }
87626
+ }
87327
87627
  let text4 = null;
87328
87628
  const blockedOnApproval = activeStatusReactions.get(statusKey(ctx.chatId, ctx.threadId))?.isAwaiting() ?? false;
87329
87629
  const upd = getInFlightUpdate();
@@ -93121,7 +93421,7 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
93121
93421
  return false;
93122
93422
  }
93123
93423
  }
93124
- function readTurnActiveMarkerAgeMs(stateDir, now) {
93424
+ function readTurnActiveMarkerAgeMs2(stateDir, now) {
93125
93425
  const path2 = join56(stateDir, TURN_ACTIVE_MARKER_FILE2);
93126
93426
  try {
93127
93427
  const st = statSync16(path2);
@@ -93134,11 +93434,36 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
93134
93434
  return markerAgeMs ?? now - turnStartedAt;
93135
93435
  }
93136
93436
 
93437
+ // gateway/gateway-heartbeat.ts
93438
+ import { mkdirSync as mkdirSync44, utimesSync as utimesSync3, writeFileSync as writeFileSync45 } from "node:fs";
93439
+ import { join as join57 } from "node:path";
93440
+ var GATEWAY_HEARTBEAT_FILE = "gateway-heartbeat";
93441
+ var GATEWAY_HEARTBEAT_INTERVAL_MS = 15000;
93442
+ function touchGatewayHeartbeat(stateDir) {
93443
+ const path2 = join57(stateDir, GATEWAY_HEARTBEAT_FILE);
93444
+ const now = new Date;
93445
+ try {
93446
+ utimesSync3(path2, now, now);
93447
+ } catch {
93448
+ try {
93449
+ mkdirSync44(stateDir, { recursive: true });
93450
+ writeFileSync45(path2, `${Date.now()}
93451
+ `, { mode: 384 });
93452
+ } catch {}
93453
+ }
93454
+ }
93455
+ function startGatewayHeartbeat(stateDir, intervalMs = GATEWAY_HEARTBEAT_INTERVAL_MS) {
93456
+ touchGatewayHeartbeat(stateDir);
93457
+ const timer3 = setInterval(() => touchGatewayHeartbeat(stateDir), intervalMs);
93458
+ timer3.unref?.();
93459
+ return timer3;
93460
+ }
93461
+
93137
93462
  // ../src/build-info.ts
93138
- var VERSION = "0.19.4";
93139
- var COMMIT_SHA = "7cdf5428";
93140
- var COMMIT_DATE = "2026-07-20T11:09:10+10:00";
93141
- var LATEST_PR = 3457;
93463
+ var VERSION = "0.19.6";
93464
+ var COMMIT_SHA = "ae57dd83";
93465
+ var COMMIT_DATE = "2026-07-20T16:11:50Z";
93466
+ var LATEST_PR = 3476;
93142
93467
  var COMMITS_AHEAD_OF_TAG = 0;
93143
93468
 
93144
93469
  // gateway/boot-version.ts
@@ -93183,10 +93508,10 @@ function composeBootVersionString(inputs) {
93183
93508
  }
93184
93509
 
93185
93510
  // gateway/unhandled-rejection-policy.ts
93186
- var import_grammy12 = __toESM(require_mod2(), 1);
93511
+ var import_grammy13 = __toESM(require_mod2(), 1);
93187
93512
  function classifyRejection(err, opts = {}) {
93188
- const isGrammy = opts.isGrammyError != null ? opts.isGrammyError(err) : err instanceof import_grammy12.GrammyError;
93189
- const isHttp = opts.isHttpError != null ? opts.isHttpError(err) : err instanceof import_grammy12.HttpError;
93513
+ const isGrammy = opts.isGrammyError != null ? opts.isGrammyError(err) : err instanceof import_grammy13.GrammyError;
93514
+ const isHttp = opts.isHttpError != null ? opts.isHttpError(err) : err instanceof import_grammy13.HttpError;
93190
93515
  if (isHttp)
93191
93516
  return "log_only";
93192
93517
  if (err instanceof Error && err.message === FLOOD_WAIT_ACTIVE)
@@ -93216,11 +93541,11 @@ init_peercred();
93216
93541
  import * as net5 from "node:net";
93217
93542
  import * as fs2 from "node:fs";
93218
93543
  import { homedir as homedir18 } from "node:os";
93219
- import { join as join57 } from "node:path";
93544
+ import { join as join58 } from "node:path";
93220
93545
  var DEFAULT_TIMEOUT_MS4 = 2000;
93221
93546
  var UNLOCK_TIMEOUT_MS = 30000;
93222
- var LEGACY_SOCKET_PATH2 = join57(homedir18(), ".switchroom", "vault-broker.sock");
93223
- var OPERATOR_SOCKET_PATH2 = join57(homedir18(), ".switchroom", "broker-operator", "sock");
93547
+ var LEGACY_SOCKET_PATH2 = join58(homedir18(), ".switchroom", "vault-broker.sock");
93548
+ var OPERATOR_SOCKET_PATH2 = join58(homedir18(), ".switchroom", "broker-operator", "sock");
93224
93549
  function defaultBrokerSocketPath2() {
93225
93550
  if (fs2.existsSync(OPERATOR_SOCKET_PATH2))
93226
93551
  return OPERATOR_SOCKET_PATH2;
@@ -94102,8 +94427,8 @@ function resolveVaultApprovalPosture(broker) {
94102
94427
  }
94103
94428
 
94104
94429
  // registry/turns-schema.ts
94105
- import { chmodSync as chmodSync12, mkdirSync as mkdirSync44 } from "fs";
94106
- import { join as join58 } from "path";
94430
+ import { chmodSync as chmodSync12, mkdirSync as mkdirSync45 } from "fs";
94431
+ import { join as join59 } from "path";
94107
94432
  var DatabaseClass3 = null;
94108
94433
  function loadDatabaseClass3() {
94109
94434
  if (DatabaseClass3 != null)
@@ -94175,9 +94500,9 @@ function applySchema(db3) {
94175
94500
  }
94176
94501
  function openTurnsDb(agentDir) {
94177
94502
  const Database = loadDatabaseClass3();
94178
- const dir = join58(agentDir, "telegram");
94179
- mkdirSync44(dir, { recursive: true, mode: 448 });
94180
- const path2 = join58(dir, "registry.db");
94503
+ const dir = join59(agentDir, "telegram");
94504
+ mkdirSync45(dir, { recursive: true, mode: 448 });
94505
+ const path2 = join59(dir, "registry.db");
94181
94506
  const db3 = new Database(path2, { create: true });
94182
94507
  applySchema(db3);
94183
94508
  try {
@@ -94521,7 +94846,7 @@ function selectResumeBuilder(endedVia, opts) {
94521
94846
  }
94522
94847
 
94523
94848
  // gateway/bridge-dead-watchdog.ts
94524
- import { readFileSync as readFileSync56, writeFileSync as writeFileSync45, renameSync as renameSync20, unlinkSync as unlinkSync24 } from "node:fs";
94849
+ import { readFileSync as readFileSync56, writeFileSync as writeFileSync46, renameSync as renameSync20, unlinkSync as unlinkSync24 } from "node:fs";
94525
94850
 
94526
94851
  // gateway/cron-session.ts
94527
94852
  var CRON_IDENTITY_SUFFIX2 = "-cron";
@@ -94575,7 +94900,7 @@ function readFreshCrashLogTail(path2, opts = {}) {
94575
94900
  }
94576
94901
  function writeBridgeDeadEscalationMarker(path2, marker) {
94577
94902
  const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
94578
- writeFileSync45(tmp, JSON.stringify(marker), "utf8");
94903
+ writeFileSync46(tmp, JSON.stringify(marker), "utf8");
94579
94904
  renameSync20(tmp, path2);
94580
94905
  }
94581
94906
  function consumeBridgeDeadEscalationMarker(path2, nowMs3 = Date.now(), maxAgeMs = ESCALATION_MARKER_MAX_AGE_MS) {
@@ -94900,6 +95225,46 @@ function handleWorkerResume(feed, agentId, log) {
94900
95225
  }
94901
95226
  log(`telegram gateway: worker ${agentId} card RE-SURFACED \u2014 resumed via SendMessage after a genuine terminal (issue #3373)`);
94902
95227
  }
95228
+ function decideWorkerFeedOriginDefer(input) {
95229
+ const { originResolved, cardExists, priorDeferrals, maxDeferrals } = input;
95230
+ if (originResolved || cardExists)
95231
+ return { defer: false, deferrals: 0 };
95232
+ const deferrals = priorDeferrals + 1;
95233
+ if (deferrals >= maxDeferrals)
95234
+ return { defer: false, deferrals };
95235
+ return { defer: true, deferrals };
95236
+ }
95237
+ function decideWorkerFeedDestination(input) {
95238
+ const { origin, cardExists, priorDeferrals, maxDeferrals, fleetChatId, stampChatId, stampThreadId, ownerDm } = input;
95239
+ const originResolved = origin != null;
95240
+ const { defer, deferrals } = decideWorkerFeedOriginDefer({
95241
+ originResolved,
95242
+ cardExists,
95243
+ priorDeferrals,
95244
+ maxDeferrals
95245
+ });
95246
+ if (defer)
95247
+ return { action: "defer", deferrals };
95248
+ const exhausted = !originResolved && !cardExists;
95249
+ const usingStampFallback = fleetChatId.length === 0;
95250
+ const workerFleetChatId = usingStampFallback ? stampChatId ?? fleetChatId : fleetChatId;
95251
+ const fallbackThreadId = usingStampFallback ? stampThreadId : undefined;
95252
+ if (origin != null && origin.chatId.length > 0) {
95253
+ return { action: "paint", chatId: origin.chatId, threadId: origin.threadId, deferrals, exhausted, ownerDmFallback: false };
95254
+ }
95255
+ if (workerFleetChatId.length > 0) {
95256
+ return { action: "paint", chatId: workerFleetChatId, threadId: fallbackThreadId, deferrals, exhausted, ownerDmFallback: false };
95257
+ }
95258
+ const ownerDmFallback = origin == null && workerFleetChatId.length === 0 && ownerDm.length > 0;
95259
+ return {
95260
+ action: "paint",
95261
+ chatId: ownerDm,
95262
+ threadId: origin?.threadId ?? fallbackThreadId,
95263
+ deferrals,
95264
+ exhausted,
95265
+ ownerDmFallback
95266
+ };
95267
+ }
94903
95268
  function resolveWorkerFeedDispatch(sub, watcherDescription, entryBackground) {
94904
95269
  return {
94905
95270
  isBackground: sub?.background ?? entryBackground ?? false,
@@ -94952,7 +95317,7 @@ if (isGatewayMain) {
94952
95317
  shutdownAnalytics();
94953
95318
  });
94954
95319
  }
94955
- var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join59(homedir19(), ".claude", "channels", "telegram");
95320
+ var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join60(homedir19(), ".claude", "channels", "telegram");
94956
95321
  var permCardStore = createPermissionCardStore(STATE_DIR);
94957
95322
  var BLOCKED_APPROVALS_DIR = process.env.SWITCHROOM_BLOCKED_APPROVALS_DIR ?? "/state/blocked-approvals";
94958
95323
  var AGENT_NAME = process.env.SWITCHROOM_AGENT_NAME ?? "agent";
@@ -95052,11 +95417,11 @@ function scheduleAlwaysAllowPersistDrain() {
95052
95417
  }, ALWAYS_ALLOW_DRAIN_INTERVAL_MS);
95053
95418
  timer3.unref?.();
95054
95419
  }
95055
- var ACCESS_FILE = join59(STATE_DIR, "access.json");
95056
- var APPROVED_DIR = join59(STATE_DIR, "approved");
95057
- var ENV_FILE = join59(STATE_DIR, ".env");
95058
- var INBOX_DIR = join59(STATE_DIR, "inbox");
95059
- var PEOPLE_FILE = join59(STATE_DIR, "people.json");
95420
+ var ACCESS_FILE = join60(STATE_DIR, "access.json");
95421
+ var APPROVED_DIR = join60(STATE_DIR, "approved");
95422
+ var ENV_FILE = join60(STATE_DIR, ".env");
95423
+ var INBOX_DIR = join60(STATE_DIR, "inbox");
95424
+ var PEOPLE_FILE = join60(STATE_DIR, "people.json");
95060
95425
  function triggerSelfRestart(targetAgent, reason, delayMs = 300) {
95061
95426
  const isDocker = process.env.SWITCHROOM_RUNTIME === "docker";
95062
95427
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
@@ -95205,7 +95570,7 @@ function assertSendable(f) {
95205
95570
  } catch {
95206
95571
  throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
95207
95572
  }
95208
- const inbox = join59(stateReal, "inbox");
95573
+ const inbox = join60(stateReal, "inbox");
95209
95574
  if (real.startsWith(stateReal + sep4) && !real.startsWith(inbox + sep4)) {
95210
95575
  throw new Error(`refusing to send channel state: ${f}`);
95211
95576
  }
@@ -95308,9 +95673,9 @@ function assertAllowedChat(chat_id) {
95308
95673
  function saveAccess(a) {
95309
95674
  if (STATIC)
95310
95675
  return;
95311
- mkdirSync46(STATE_DIR, { recursive: true, mode: 448 });
95676
+ mkdirSync47(STATE_DIR, { recursive: true, mode: 448 });
95312
95677
  const tmp = ACCESS_FILE + ".tmp";
95313
- writeFileSync47(tmp, JSON.stringify(a, null, 2) + `
95678
+ writeFileSync48(tmp, JSON.stringify(a, null, 2) + `
95314
95679
  `, { mode: 384 });
95315
95680
  renameSync21(tmp, ACCESS_FILE);
95316
95681
  }
@@ -95330,7 +95695,7 @@ var HISTORY_ENABLED = HISTORY_ACCESS.historyEnabled !== false;
95330
95695
  if (isGatewayMain && HISTORY_ENABLED) {
95331
95696
  try {
95332
95697
  initHistory(STATE_DIR, HISTORY_ACCESS.historyRetentionDays ?? 30);
95333
- process.stderr.write(`telegram gateway: history capture enabled at ${join59(STATE_DIR, "history.db")}
95698
+ process.stderr.write(`telegram gateway: history capture enabled at ${join60(STATE_DIR, "history.db")}
95334
95699
  `);
95335
95700
  } catch (err) {
95336
95701
  process.stderr.write(`telegram gateway: history init failed (${err.message}) \u2014 capture disabled
@@ -95349,7 +95714,7 @@ if (isGatewayMain)
95349
95714
  let markerTurnKey = null;
95350
95715
  let markerAgeMs = null;
95351
95716
  try {
95352
- const markerPath = join59(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
95717
+ const markerPath = join60(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
95353
95718
  if (existsSync54(markerPath)) {
95354
95719
  const st = statSync19(markerPath);
95355
95720
  markerAgeMs = Date.now() - st.mtimeMs;
@@ -95374,10 +95739,10 @@ if (isGatewayMain)
95374
95739
  process.stderr.write(`telegram gateway: turn-registry boot-reaper stamped ${reaped} orphaned turn(s)` + `${timeoutTurnKey ? ` (turnKey=${timeoutTurnKey} as 'timeout', markerAgeMs=${markerAgeMs})` : " as 'restart'"}
95375
95740
  `);
95376
95741
  } else {
95377
- process.stderr.write(`telegram gateway: turn-registry initialized at ${join59(agentDir, "telegram", "registry.db")}
95742
+ process.stderr.write(`telegram gateway: turn-registry initialized at ${join60(agentDir, "telegram", "registry.db")}
95378
95743
  `);
95379
95744
  }
95380
- const bridgeDeadMarker = consumeBridgeDeadEscalationMarker(join59(STATE_DIR, "bridge-dead-escalation.json"));
95745
+ const bridgeDeadMarker = consumeBridgeDeadEscalationMarker(join60(STATE_DIR, "bridge-dead-escalation.json"));
95381
95746
  if (bridgeDeadMarker != null) {
95382
95747
  bridgeDeadPriorStreak = bridgeDeadMarker.count ?? 1;
95383
95748
  process.stderr.write(`telegram gateway: boot: prior restart was a bridge-dead escalation (reason=${bridgeDeadMarker.reason}` + `, consecutive=${bridgeDeadPriorStreak}` + `${bridgeDeadMarker.crashTail ? `, crashTail=${bridgeDeadMarker.crashTail}` : ""})
@@ -95390,7 +95755,7 @@ if (isGatewayMain)
95390
95755
  const pending2 = findLatestTurnIfInterrupted(turnsDb);
95391
95756
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
95392
95757
  if (pending2 != null && selfAgent) {
95393
- const bootResumeMarkerPath = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join59(STATE_DIR, "clean-shutdown.json");
95758
+ const bootResumeMarkerPath = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join60(STATE_DIR, "clean-shutdown.json");
95394
95759
  const bootResumeCleanMarker = readCleanShutdownMarker(bootResumeMarkerPath);
95395
95760
  const bootResumeForceAlways = process.env.SWITCHROOM_BOOT_RESUME_ALWAYS === "1";
95396
95761
  const bootResumeMode = parseBootResumeMode(process.env.SWITCHROOM_BOOT_RESUME);
@@ -95503,7 +95868,7 @@ if (isGatewayMain)
95503
95868
  `);
95504
95869
  }
95505
95870
  }
95506
- const pendingEnvPath = join59(agentDir, ".pending-turn.env");
95871
+ const pendingEnvPath = join60(agentDir, ".pending-turn.env");
95507
95872
  try {
95508
95873
  if (pending2 != null) {
95509
95874
  const lines = [
@@ -95517,7 +95882,7 @@ if (isGatewayMain)
95517
95882
  pending2.interrupt_reason != null ? `SWITCHROOM_PENDING_INTERRUPT_REASON=${pending2.interrupt_reason}` : `SWITCHROOM_PENDING_INTERRUPT_REASON=`
95518
95883
  ];
95519
95884
  const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
95520
- writeFileSync47(pendingEnvTmp, lines.join(`
95885
+ writeFileSync48(pendingEnvTmp, lines.join(`
95521
95886
  `) + `
95522
95887
  `, { mode: 384 });
95523
95888
  renameSync21(pendingEnvTmp, pendingEnvPath);
@@ -95564,26 +95929,31 @@ var WORKER_FEED_GROUP_MESSAGE_LIFETIME_CAP_MS = (() => {
95564
95929
  return Number.isFinite(v) && v > 0 ? v : 60 * 60000;
95565
95930
  })();
95566
95931
  var workerFeedOwnerDmFallbackLogged = new Set;
95567
- function resolveWorkerFeedChat(agentId, fleetChatId) {
95932
+ function noteWorkerFeedOwnerDmFallback(agentId) {
95933
+ if (workerFeedOwnerDmFallbackLogged.has(agentId))
95934
+ return;
95935
+ workerFeedOwnerDmFallbackLogged.add(agentId);
95936
+ if (workerFeedOwnerDmFallbackLogged.size > WORKER_FEED_FALLBACK_LOG_CAP) {
95937
+ const oldest = workerFeedOwnerDmFallbackLogged.values().next().value;
95938
+ if (oldest != null)
95939
+ workerFeedOwnerDmFallbackLogged.delete(oldest);
95940
+ }
95941
+ process.stderr.write(`telegram gateway: worker-feed origin unresolved agent=${agentId} \u2014 routing card to owner DM
95942
+ `);
95943
+ }
95944
+ var workerFeedOriginDeferrals = new Map;
95945
+ var WORKER_FEED_ORIGIN_DEFER_MAX = 10;
95946
+ function resolveWorkerFeedChat(agentId, fleetChatId, fallbackThreadId) {
95568
95947
  const origin = resolveSubagentOriginChat(agentId);
95569
95948
  if (origin != null && origin.chatId.length > 0)
95570
95949
  return origin;
95571
95950
  if (fleetChatId.length > 0)
95572
- return { chatId: fleetChatId };
95951
+ return { chatId: fleetChatId, threadId: fallbackThreadId };
95573
95952
  const ownerDm = loadAccess().allowFrom[0] ?? "";
95574
95953
  if (origin == null && fleetChatId.length === 0 && ownerDm.length > 0) {
95575
- if (!workerFeedOwnerDmFallbackLogged.has(agentId)) {
95576
- workerFeedOwnerDmFallbackLogged.add(agentId);
95577
- if (workerFeedOwnerDmFallbackLogged.size > WORKER_FEED_FALLBACK_LOG_CAP) {
95578
- const oldest = workerFeedOwnerDmFallbackLogged.values().next().value;
95579
- if (oldest != null)
95580
- workerFeedOwnerDmFallbackLogged.delete(oldest);
95581
- }
95582
- process.stderr.write(`telegram gateway: worker-feed origin unresolved agent=${agentId} \u2014 routing card to owner DM
95583
- `);
95584
- }
95954
+ noteWorkerFeedOwnerDmFallback(agentId);
95585
95955
  }
95586
- return { chatId: ownerDm, threadId: origin?.threadId };
95956
+ return { chatId: ownerDm, threadId: origin?.threadId ?? fallbackThreadId };
95587
95957
  }
95588
95958
  var REGISTRY_REAPER_INTERVAL_MS = 6 * 60 * 60 * 1000;
95589
95959
  function runHistoryReaperNow(reason) {
@@ -95626,7 +95996,7 @@ function checkApprovals() {
95626
95996
  return;
95627
95997
  }
95628
95998
  for (const senderId of files) {
95629
- const file = join59(APPROVED_DIR, senderId);
95999
+ const file = join60(APPROVED_DIR, senderId);
95630
96000
  bot.api.sendMessage(senderId, "Paired! Say hi to Claude.").then(() => rmSync6(file, { force: true }), (err) => {
95631
96001
  process.stderr.write(`telegram gateway: failed to send approval confirm: ${err}
95632
96002
  `);
@@ -95636,6 +96006,8 @@ function checkApprovals() {
95636
96006
  }
95637
96007
  if (isGatewayMain && !STATIC)
95638
96008
  setInterval(checkApprovals, 5000).unref();
96009
+ if (isGatewayMain && !STATIC)
96010
+ startGatewayHeartbeat(STATE_DIR);
95639
96011
  var IDLE_CLEAR_CHECK_MS = Number(process.env.SWITCHROOM_IDLE_CLEAR_CHECK_MS ?? 60000);
95640
96012
  if (isGatewayMain && !STATIC && IDLE_CLEAR_CHECK_MS > 0)
95641
96013
  setInterval(maybeIdleClear, IDLE_CLEAR_CHECK_MS).unref();
@@ -95797,10 +96169,10 @@ function noteAgentOutputAt(key, ts) {
95797
96169
  lastAgentOutputAt.delete(oldest);
95798
96170
  }
95799
96171
  }
95800
- var OBLIGATION_STORE_PATH = join59(STATE_DIR, "obligations.json");
96172
+ var OBLIGATION_STORE_PATH = join60(STATE_DIR, "obligations.json");
95801
96173
  var obligationStoreFs = {
95802
96174
  readFileSync: (p) => readFileSync58(p, "utf8"),
95803
- writeFileSync: (p, d) => writeFileSync47(p, d),
96175
+ writeFileSync: (p, d) => writeFileSync48(p, d),
95804
96176
  renameSync: (a, b) => renameSync21(a, b),
95805
96177
  existsSync: (p) => existsSync54(p)
95806
96178
  };
@@ -95854,7 +96226,7 @@ function turnInFlightMachineOnly() {
95854
96226
  function liveTurnAgeMs(now) {
95855
96227
  if (currentTurn === null)
95856
96228
  return null;
95857
- return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs(STATE_DIR, now), currentTurn.startedAt, now);
96229
+ return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs2(STATE_DIR, now), currentTurn.startedAt, now);
95858
96230
  }
95859
96231
  function oldestPendingApprovalAgeMs(now) {
95860
96232
  let oldest = null;
@@ -96009,6 +96381,8 @@ function cardDrainGate(turn, ea, run4) {
96009
96381
  var RECENT_TURNS_MAX = 32;
96010
96382
  var recentTurnsById = new Map;
96011
96383
  var recentTurnIdBySourceMessageId = new Map;
96384
+ var subagentHandbackMarker = new SubagentHandbackMarker;
96385
+ var getLastSubagentHandbackAt = (chatId) => subagentHandbackMarker.lastAt(chatId);
96012
96386
  function rememberRecentTurn(turn) {
96013
96387
  recentTurnsById.set(turn.turnId, turn);
96014
96388
  if (turn.sourceMessageId != null) {
@@ -96075,15 +96449,17 @@ function resolveReplyOwnerTurn(liveTurn, chatId, args) {
96075
96449
  byId.set(t.turnId, t);
96076
96450
  }
96077
96451
  const latestEndedAgeMs = latestEnded?.endedAt != null ? Date.now() - latestEnded.endedAt : null;
96078
- const winnerId = resolveReplyOwnerTurnId({
96452
+ const candidates = {
96079
96453
  liveTurnId: liveTurn?.turnId ?? null,
96080
96454
  originTurnId: origin?.turnId ?? null,
96081
96455
  quotedTurnId: quoted?.turnId ?? null,
96082
96456
  latestEndedTurnId: latestEnded?.turnId ?? null,
96083
96457
  latestEndedAgeMs,
96084
96458
  latestEndedTtlMs: DEFAULT_SUPERSEDE_TTL_MS
96085
- });
96086
- return winnerId != null ? byId.get(winnerId) ?? null : null;
96459
+ };
96460
+ const tier = resolveReplyOwnerTier(candidates);
96461
+ const winnerId = resolveReplyOwnerTurnId(candidates);
96462
+ return { turn: winnerId != null ? byId.get(winnerId) ?? null : null, tier };
96087
96463
  }
96088
96464
  function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, originVia, liveTurn, surface) {
96089
96465
  const recovered = LATE_REPLY_TOPIC_RECOVERY_ENABLED && explicitThreadId == null && originTurn == null && liveTurn == null ? findLatestEndedTurnForChat(chatId) : null;
@@ -97061,7 +97437,7 @@ function wrapBootCardApi(threadId) {
97061
97437
  await lockedBot.api.editMessageText(cid, mid, richMessage2(text5), editOpts);
97062
97438
  return "edited";
97063
97439
  } catch (err) {
97064
- const desc = err instanceof import_grammy14.GrammyError ? err.description : err instanceof Error ? err.message : String(err);
97440
+ const desc = err instanceof import_grammy15.GrammyError ? err.description : err instanceof Error ? err.message : String(err);
97065
97441
  if (typeof desc === "string" && desc.toLowerCase().includes("not modified"))
97066
97442
  return "edited";
97067
97443
  return "gone";
@@ -98141,26 +98517,26 @@ var statusPinState = new Map;
98141
98517
  var statusPinChatIds = new Map;
98142
98518
  var statusPinPinnedAt = new Map;
98143
98519
  var statusPinRightsCache = new PinRightsCache2;
98144
- var STATUS_PIN_STORE_PATH = join59(STATE_DIR, "status-pins.json");
98520
+ var STATUS_PIN_STORE_PATH = join60(STATE_DIR, "status-pins.json");
98145
98521
  var statusPinStoreFs = {
98146
98522
  readFileSync: (p) => readFileSync58(p, "utf8"),
98147
- writeFileSync: (p, d) => writeFileSync47(p, d),
98523
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98148
98524
  renameSync: (a, b) => renameSync21(a, b),
98149
98525
  existsSync: (p) => existsSync54(p)
98150
98526
  };
98151
98527
  var statusPinPersistEnabled = !STATIC && PIN_STATUS_WHILE_WORKING;
98152
- var ACTIVITY_CARD_STORE_PATH = join59(STATE_DIR, "activity-cards-pending.json");
98528
+ var ACTIVITY_CARD_STORE_PATH = join60(STATE_DIR, "activity-cards-pending.json");
98153
98529
  var activityCardStoreFs = {
98154
98530
  readFileSync: (p) => readFileSync58(p, "utf8"),
98155
- writeFileSync: (p, d) => writeFileSync47(p, d),
98531
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98156
98532
  renameSync: (a, b) => renameSync21(a, b),
98157
98533
  existsSync: (p) => existsSync54(p)
98158
98534
  };
98159
98535
  var activityCardPersistEnabled = !STATIC;
98160
- var QUEUED_CARD_STORE_PATH = join59(STATE_DIR, "queued-cards-pending.json");
98536
+ var QUEUED_CARD_STORE_PATH = join60(STATE_DIR, "queued-cards-pending.json");
98161
98537
  var queuedCardStoreFs = {
98162
98538
  readFileSync: (p) => readFileSync58(p, "utf8"),
98163
- writeFileSync: (p, d) => writeFileSync47(p, d),
98539
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98164
98540
  renameSync: (a, b) => renameSync21(a, b),
98165
98541
  existsSync: (p) => existsSync54(p)
98166
98542
  };
@@ -98535,12 +98911,12 @@ var getPinnedProgressCardMessageId = null;
98535
98911
  var completeProgressCardTurn = null;
98536
98912
  var subagentWatcher = null;
98537
98913
  var workerActivityFeed = null;
98538
- var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join59(STATE_DIR, "gateway.sock");
98914
+ var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join60(STATE_DIR, "gateway.sock");
98539
98915
  if (isGatewayMain)
98540
- mkdirSync46(STATE_DIR, { recursive: true, mode: 448 });
98541
- var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join59(STATE_DIR, "gateway.pid.json");
98542
- var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join59(STATE_DIR, "gateway-session.json");
98543
- var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join59(STATE_DIR, "clean-shutdown.json");
98916
+ mkdirSync47(STATE_DIR, { recursive: true, mode: 448 });
98917
+ var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join60(STATE_DIR, "gateway.pid.json");
98918
+ var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join60(STATE_DIR, "gateway-session.json");
98919
+ var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join60(STATE_DIR, "clean-shutdown.json");
98544
98920
  var GATEWAY_STARTED_AT_MS = Date.now();
98545
98921
  var BOOT_CARD_ENABLED = process.env.SWITCHROOM_BOOT_CARD !== "false";
98546
98922
  var activeBootCard = null;
@@ -98569,7 +98945,7 @@ function ensureIssuesCard(chatId, threadId) {
98569
98945
  bot: botApi,
98570
98946
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
98571
98947
  `),
98572
- persistPath: join59(stateDir, "issues-card.json")
98948
+ persistPath: join60(stateDir, "issues-card.json")
98573
98949
  });
98574
98950
  activeIssuesWatcher = startIssuesWatcher({
98575
98951
  stateDir,
@@ -98688,7 +99064,13 @@ function gatewayLivenessWiringDeps() {
98688
99064
  getPendingInboundBuffer: () => pendingInboundBuffer,
98689
99065
  trackRedeliveredInbound,
98690
99066
  closeActivityLane,
98691
- closeProgressLane
99067
+ closeProgressLane,
99068
+ hangRestart: {
99069
+ stalenessThresholdMs: hangStalenessMs(),
99070
+ request: (reason, idleMs) => {
99071
+ triggerSelfRestart(getMyAgentName(), `hang-watchdog:${reason} idle=${Math.round(idleMs)}ms`);
99072
+ }
99073
+ }
98692
99074
  };
98693
99075
  }
98694
99076
  if (isGatewayMain)
@@ -98760,11 +99142,11 @@ if (isGatewayMain)
98760
99142
  var inboundSpool;
98761
99143
  if (isGatewayMain)
98762
99144
  inboundSpool = STATIC ? undefined : createInboundSpool({
98763
- path: join59(STATE_DIR, "inbound-spool.jsonl"),
99145
+ path: join60(STATE_DIR, "inbound-spool.jsonl"),
98764
99146
  fs: {
98765
99147
  appendFileSync: (p, d) => appendFileSync7(p, d),
98766
99148
  readFileSync: (p) => readFileSync58(p, "utf8"),
98767
- writeFileSync: (p, d) => writeFileSync47(p, d),
99149
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98768
99150
  renameSync: (a, b) => renameSync21(a, b),
98769
99151
  existsSync: (p) => existsSync54(p),
98770
99152
  statSizeSync: (p) => statSync19(p).size
@@ -98789,12 +99171,13 @@ var pendingInboundBuffer = createPendingInboundBuffer({
98789
99171
  evictNoticeByChat.set(key, nowMs3);
98790
99172
  const threadOpts = evThread != null ? { message_thread_id: evThread } : {};
98791
99173
  swallowingApiCall(() => bot.api.sendMessage(chat, "\u23F3 Messages are arriving faster than I can process them. Your messages are saved and will be handled once I finish the current turn \u2014 if any can't be picked up, I'll ask you to resend it.", { ...threadOpts }), { chat_id: chat, verb: "inbound-buffer-eviction" });
98792
- }
99174
+ },
99175
+ onHandbackEnqueue: (chatId, ts) => subagentHandbackMarker.record(chatId, ts)
98793
99176
  });
98794
99177
  function agentHasInFlightBackgroundWork(now) {
98795
99178
  if (countRunningWorkers() > 0)
98796
99179
  return true;
98797
- const ageMs = readTurnActiveMarkerAgeMs(STATE_DIR, now);
99180
+ const ageMs = readTurnActiveMarkerAgeMs2(STATE_DIR, now);
98798
99181
  return ageMs != null && ageMs < TURN_ACTIVE_MARKER_FRESH_MS;
98799
99182
  }
98800
99183
  async function deliverCapturedProse2(args) {
@@ -98827,7 +99210,7 @@ async function maybeRedeliverUndeliveredAnswer() {
98827
99210
  let transcriptText;
98828
99211
  try {
98829
99212
  const projectsDir = getProjectsDirForCwd();
98830
- const path2 = join59(projectsDir, `${sessionId}.jsonl`);
99213
+ const path2 = join60(projectsDir, `${sessionId}.jsonl`);
98831
99214
  if (!existsSync54(path2)) {
98832
99215
  process.stderr.write(`telegram gateway: crash-redelivery \u2014 transcript not found for turnKey=${turn.turn_key} session=${sessionId} (${path2}); skipping
98833
99216
  `);
@@ -98930,7 +99313,7 @@ if (isGatewayMain && inboundSpool != null) {
98930
99313
  }
98931
99314
  var pendingPermissionBuffer = createPendingPermissionBuffer();
98932
99315
  function buildPermissionActionRow(requestId, showAlways) {
98933
- const kb = new import_grammy14.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow", `perm:allow:${requestId}`);
99316
+ const kb = new import_grammy15.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow", `perm:allow:${requestId}`);
98934
99317
  if (showAlways)
98935
99318
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
98936
99319
  return kb;
@@ -98992,8 +99375,8 @@ var bridgeDeadWatchdog = createBridgeDeadWatchdog({
98992
99375
  isSessionAlive: () => findAgentProcessInContainer2()?.comm === "claude",
98993
99376
  isShuttingDown: () => shuttingDown,
98994
99377
  escalate: (reason) => triggerSelfRestart(process.env.SWITCHROOM_AGENT_NAME ?? "", reason, 1500),
98995
- crashLogPath: join59(STATE_DIR, "bridge-crash.log"),
98996
- markerPath: join59(STATE_DIR, "bridge-dead-escalation.json"),
99378
+ crashLogPath: join60(STATE_DIR, "bridge-crash.log"),
99379
+ markerPath: join60(STATE_DIR, "bridge-dead-escalation.json"),
98997
99380
  log: (line) => process.stderr.write(`${line}
98998
99381
  `),
98999
99382
  priorStreak: bridgeDeadPriorStreak,
@@ -99099,8 +99482,8 @@ if (isGatewayMain)
99099
99482
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
99100
99483
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
99101
99484
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
99102
- configSnapshotPath: join59(resolvedAgentDirForCard, ".config-snapshot.json"),
99103
- bootCardStatePath: join59(resolvedAgentDirForCard, ".boot-card-msgid.json"),
99485
+ configSnapshotPath: join60(resolvedAgentDirForCard, ".config-snapshot.json"),
99486
+ bootCardStatePath: join60(resolvedAgentDirForCard, ".boot-card-msgid.json"),
99104
99487
  floodStatePath: FLOOD_STATE_PATH,
99105
99488
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
99106
99489
  }, ackMsgId).then((handle) => {
@@ -99453,7 +99836,7 @@ if (isGatewayMain)
99453
99836
  },
99454
99837
  async onRequestConfigApproval(client3, msg) {
99455
99838
  const { handleRequestConfigApproval: handleRequestConfigApproval2 } = await Promise.resolve().then(() => (init_config_approval_handler(), exports_config_approval_handler));
99456
- const { InlineKeyboard: InlineKeyboard7 } = await Promise.resolve().then(() => __toESM(require_mod(), 1));
99839
+ const { InlineKeyboard: InlineKeyboard8 } = await Promise.resolve().then(() => __toESM(require_mod(), 1));
99457
99840
  await handleRequestConfigApproval2(client3, msg, {
99458
99841
  agentName: getMyAgentName(),
99459
99842
  loadTargetChat: () => {
@@ -99475,7 +99858,7 @@ if (isGatewayMain)
99475
99858
  ...cfgTopic != null ? { threadId: cfgTopic } : {}
99476
99859
  };
99477
99860
  },
99478
- buildKeyboard: (requestId, epoch) => new InlineKeyboard7().text("\u2705 Approve", `cfg:${requestId}:${epoch}:approve`).text("\uD83D\uDEAB Deny", `cfg:${requestId}:${epoch}:deny`),
99861
+ buildKeyboard: (requestId, epoch) => new InlineKeyboard8().text("\u2705 Approve", `cfg:${requestId}:${epoch}:approve`).text("\uD83D\uDEAB Deny", `cfg:${requestId}:${epoch}:deny`),
99479
99862
  postCard: async (args) => {
99480
99863
  try {
99481
99864
  const sent = await robustApiCall(() => bot.api.sendRichMessage(args.chatId, richMessage2(args.text), {
@@ -99504,7 +99887,7 @@ if (isGatewayMain)
99504
99887
  }
99505
99888
  },
99506
99889
  postAttachment: async (args) => {
99507
- const input = new import_grammy14.InputFile(Buffer.from(args.content, "utf8"), args.filename);
99890
+ const input = new import_grammy15.InputFile(Buffer.from(args.content, "utf8"), args.filename);
99508
99891
  await robustApiCall(() => bot.api.sendDocument(args.chatId, input, {
99509
99892
  ...args.threadId !== undefined ? { message_thread_id: args.threadId } : {}
99510
99893
  }), {
@@ -99782,7 +100165,7 @@ if (isGatewayMain)
99782
100165
  const receiverUid = receiverUidRaw ? Number(receiverUidRaw) : NaN;
99783
100166
  if (Number.isInteger(receiverUid))
99784
100167
  allowedUids.push(receiverUid);
99785
- const socketPath = join59(STATE_DIR, "webhook.sock");
100168
+ const socketPath = join60(STATE_DIR, "webhook.sock");
99786
100169
  const webhookInject = (agentName3, inbound) => {
99787
100170
  const msg = inbound;
99788
100171
  const delivered = ipcServer.sendToAgent(agentName3, msg);
@@ -100010,9 +100393,9 @@ function redactOutboundText(text5, site) {
100010
100393
  var VOICE_OUT_DEFAULT_CHUNK_CHARS = 600;
100011
100394
  var VOICE_OUT_HARD_CHUNK_CAP = 4096;
100012
100395
  var voiceOnDemandCache = new VoiceOnDemandCache({
100013
- persistPath: join59(STATE_DIR, "voice-ondemand.json")
100396
+ persistPath: join60(STATE_DIR, "voice-ondemand.json")
100014
100397
  });
100015
- var VOICE_CACHE_DIR = join59(STATE_DIR, "voice-cache");
100398
+ var VOICE_CACHE_DIR = join60(STATE_DIR, "voice-cache");
100016
100399
  var voicePreSynthQueue = new PreSynthQueue({
100017
100400
  runJob: async (job) => {
100018
100401
  const sidecarToken = await materializeSidecarToken2();
@@ -100167,6 +100550,7 @@ function gatewaySendReplyDeps() {
100167
100550
  resolveAnswerThreadWithLog,
100168
100551
  resolveThreadId,
100169
100552
  getLatestInboundMessageId,
100553
+ getLastSubagentHandbackAt,
100170
100554
  recordOutbound,
100171
100555
  emissionAuthorityFor,
100172
100556
  clearActivitySummary,
@@ -100278,7 +100662,7 @@ async function executeAskUser(rawArgs) {
100278
100662
  }
100279
100663
  }
100280
100664
  const askId = generateAskId();
100281
- const keyboard = new import_grammy14.InlineKeyboard;
100665
+ const keyboard = new import_grammy15.InlineKeyboard;
100282
100666
  for (let i = 0;i < args.options.length; i++) {
100283
100667
  keyboard.text(args.options[i], encodeAskCallback(askId, i));
100284
100668
  if (i < args.options.length - 1)
@@ -100413,7 +100797,7 @@ async function executeSendGif(rawArgs) {
100413
100797
  };
100414
100798
  }
100415
100799
  async function publishToTelegraph(text5, shortName, authorName) {
100416
- const accountPath = join59(STATE_DIR, "telegraph-account.json");
100800
+ const accountPath = join60(STATE_DIR, "telegraph-account.json");
100417
100801
  let account = null;
100418
100802
  try {
100419
100803
  if (existsSync54(accountPath)) {
@@ -100436,8 +100820,8 @@ async function publishToTelegraph(text5, shortName, authorName) {
100436
100820
  }
100437
100821
  account = created.value;
100438
100822
  try {
100439
- mkdirSync46(STATE_DIR, { recursive: true, mode: 448 });
100440
- writeFileSync47(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
100823
+ mkdirSync47(STATE_DIR, { recursive: true, mode: 448 });
100824
+ writeFileSync48(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
100441
100825
  } catch (err) {
100442
100826
  process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
100443
100827
  `);
@@ -100592,9 +100976,9 @@ async function executeDownloadAttachment(args) {
100592
100976
  fileUniqueId: file.file_unique_id,
100593
100977
  now: Date.now()
100594
100978
  });
100595
- mkdirSync46(INBOX_DIR, { recursive: true, mode: 448 });
100979
+ mkdirSync47(INBOX_DIR, { recursive: true, mode: 448 });
100596
100980
  assertInsideInbox2(INBOX_DIR, dlPath);
100597
- writeFileSync47(dlPath, buf, { mode: 384 });
100981
+ writeFileSync48(dlPath, buf, { mode: 384 });
100598
100982
  return { content: [{ type: "text", text: dlPath }] };
100599
100983
  }
100600
100984
  async function executeEditMessage(args) {
@@ -101909,24 +102293,8 @@ function switchroomExecCombined(args, timeoutMs = 15000) {
101909
102293
  shell: "/bin/bash"
101910
102294
  });
101911
102295
  }
101912
- function formatSwitchroomOutput(output, maxLen = RICH_MESSAGE_MAX_CHARS2) {
101913
- const trimmed = output.trim();
101914
- if (trimmed.length <= maxLen)
101915
- return trimmed;
101916
- return trimmed.slice(0, maxLen - 20) + `
101917
- ... (truncated)`;
101918
- }
101919
- function stripAnsi4(text5) {
101920
- return text5.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
101921
- }
101922
- function escapeHtmlForTg2(text5) {
101923
- return text5.replace(/([\\`*_~=\[\]|])/g, "\\$1");
101924
- }
101925
102296
  var buttonConfirmParseModeWarned = new Set;
101926
102297
  var buttonConfirmSingleUseWarned = new Set;
101927
- function preBlock(text5) {
101928
- return "```\n" + text5.replace(/```/g, "`\u200B``") + "\n```";
101929
- }
101930
102298
  async function switchroomReply(ctx, text5, options = {}) {
101931
102299
  const chatId = String(ctx.chat.id);
101932
102300
  const baseThreadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
@@ -101956,22 +102324,6 @@ Please delete message \`${msgId}\` manually so the value is not retained in chat
101956
102324
  _Reason: ${escapeHtmlForTg2(msg)}_`), {}), { chat_id: chatId, verb: "deleteSensitiveMessage.warning" });
101957
102325
  }
101958
102326
  }
101959
- function getCommandArgs(ctx) {
101960
- const fromMatch = typeof ctx.match === "string" ? ctx.match.trim() : "";
101961
- if (fromMatch)
101962
- return fromMatch;
101963
- const text5 = ctx.msg?.text ?? ctx.message?.text ?? "";
101964
- const m = text5.match(/^\/\S+\s+([\s\S]*)$/);
101965
- return m ? m[1].trim() : "";
101966
- }
101967
- function hasDemoFlag(args) {
101968
- return /(?:^|\s)demo$/i.test(args.trim());
101969
- }
101970
- function assertSafeAgentName(name) {
101971
- if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name) && name !== "all") {
101972
- throw new Error(`invalid agent name: ${name}`);
101973
- }
101974
- }
101975
102327
  function getMyAgentName() {
101976
102328
  const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
101977
102329
  if (fromEnv && fromEnv.trim().length > 0)
@@ -101989,14 +102341,14 @@ function restartMarkerPath() {
101989
102341
  const agentDir = resolveAgentDirFromEnv();
101990
102342
  if (!agentDir)
101991
102343
  return null;
101992
- return join59(agentDir, "restart-pending.json");
102344
+ return join60(agentDir, "restart-pending.json");
101993
102345
  }
101994
102346
  function writeRestartMarker(marker) {
101995
102347
  const p = restartMarkerPath();
101996
102348
  if (!p)
101997
102349
  return;
101998
102350
  try {
101999
- writeFileSync47(p, JSON.stringify(marker));
102351
+ writeFileSync48(p, JSON.stringify(marker));
102000
102352
  lastPlannedRestartAt = Date.now();
102001
102353
  process.stderr.write(`telegram gateway: restart-marker: write chat_id=${marker.chat_id} thread_id=${marker.thread_id ?? "-"} ack=${marker.ack_message_id ?? "-"} path=${p}
102002
102354
  `);
@@ -102181,12 +102533,12 @@ function _resetDockerReachableCache() {
102181
102533
  }
102182
102534
  function spawnSwitchroomDetached(args, onFailure) {
102183
102535
  const fullArgs = SWITCHROOM_CONFIG ? ["--config", SWITCHROOM_CONFIG, ...args] : args;
102184
- const logPath = join59(STATE_DIR, "detached-spawn.log");
102536
+ const logPath = join60(STATE_DIR, "detached-spawn.log");
102185
102537
  let outFd = null;
102186
102538
  try {
102187
- mkdirSync46(STATE_DIR, { recursive: true });
102539
+ mkdirSync47(STATE_DIR, { recursive: true });
102188
102540
  outFd = openSync12(logPath, "a");
102189
- writeFileSync47(logPath, `
102541
+ writeFileSync48(logPath, `
102190
102542
  [${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
102191
102543
  `, { flag: "a" });
102192
102544
  } catch {}
@@ -102286,53 +102638,6 @@ function scheduleGrantRestart(agentName3, chatId, threadId, reason) {
102286
102638
  }
102287
102639
  return decision;
102288
102640
  }
102289
- function formatAuthOutputForTelegram(output) {
102290
- const trimmed = stripAnsi4(output).trim();
102291
- const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
102292
- const lines = trimmed.split(/\n+/).map((l) => l.trim()).filter(Boolean);
102293
- if (!url)
102294
- return { text: preBlock(formatSwitchroomOutput(trimmed)), url: null };
102295
- const body = lines.filter((line) => {
102296
- if (line === url)
102297
- return false;
102298
- if (line.startsWith("switchroom auth code"))
102299
- return false;
102300
- if (line.startsWith("switchroom auth cancel"))
102301
- return false;
102302
- if (line.startsWith("Use 'tmux attach"))
102303
- return false;
102304
- if (line.startsWith("After Claude shows you a browser code"))
102305
- return false;
102306
- if (line.startsWith("Then finish with:"))
102307
- return false;
102308
- if (line.startsWith("Cancel with:"))
102309
- return false;
102310
- return true;
102311
- });
102312
- const rendered = body.map((line) => {
102313
- if (line.startsWith("Started Claude auth") || line.startsWith("Auth session already running"))
102314
- return `**${escapeHtmlForTg2(line)}**`;
102315
- if (line.startsWith("Open this URL"))
102316
- return `_${escapeHtmlForTg2(line)}_`;
102317
- return escapeHtmlForTg2(line);
102318
- });
102319
- rendered.push("", "\uD83D\uDC47 Tap **\uD83D\uDD10 Open Claude auth** below, then **reply with the browser code**.", "", `_Wrong Anthropic account getting authorized? Long-press the URL below and choose "Copy Link" or "Open in Browser" \u2014 lands in your main browser where the right account is signed in, bypassing Telegram's in-app browser cookies._`, "", url);
102320
- return { text: rendered.join(`
102321
- `), url };
102322
- }
102323
- function buildAuthUrlKeyboard(authorizeUrl) {
102324
- return new import_grammy14.InlineKeyboard().url("\uD83D\uDD10 Open Claude auth", authorizeUrl);
102325
- }
102326
- function buildDeferredSecretKeyboard(deferKey) {
102327
- const unlockData = `vd:unlock:${deferKey}`;
102328
- const cancelData = `vd:cancel:${deferKey}`;
102329
- if (unlockData.length > 64 || cancelData.length > 64) {
102330
- process.stderr.write(`telegram gateway: callback_data overflow \u2014 deferKey=${deferKey} unlockLen=${unlockData.length} cancelLen=${cancelData.length}
102331
- `);
102332
- throw new Error(`callback_data overflow: deferKey too long (${deferKey.length} chars)`);
102333
- }
102334
- return new import_grammy14.InlineKeyboard().text("\uD83D\uDD13 Unlock vault & save", unlockData).text("\uD83D\uDDD1 Discard", cancelData);
102335
- }
102336
102641
  async function runSwitchroomAuthCommand(ctx, args, label) {
102337
102642
  try {
102338
102643
  const output = switchroomExecCombined(args, 30000);
@@ -102383,15 +102688,6 @@ function runVaultCli(args, passphrase, stdinValue) {
102383
102688
  `).trim() };
102384
102689
  }
102385
102690
  }
102386
- function renderVaultOpFailure(verbLabel, cliOutput, key) {
102387
- const parsed = parseVaultCliError2(cliOutput);
102388
- const verb = verbLabel === "delete" ? "remove" : verbLabel;
102389
- const rendered = renderVaultCliError2(parsed, { verb, key });
102390
- if (rendered.suppressRaw)
102391
- return rendered.html;
102392
- return `**vault ${verbLabel} failed:**
102393
- ${preBlock(cliOutput)}`;
102394
- }
102395
102691
  async function executeVaultOp(ctx, chatId, op, key, passphrase, setValue) {
102396
102692
  if (op === "list") {
102397
102693
  const r = runVaultCli(["list"], passphrase);
@@ -102495,30 +102791,6 @@ function switchroomExecJson(args) {
102495
102791
  return null;
102496
102792
  }
102497
102793
  }
102498
- function statusIcon(status) {
102499
- if (status === "active" || status === "running")
102500
- return "\uD83D\uDFE2";
102501
- if (status === "inactive" || status === "stopped" || status === "dead")
102502
- return "\uD83D\uDD34";
102503
- if (status === "failed")
102504
- return "\u26A0\uFE0F";
102505
- return "\u26AA";
102506
- }
102507
- function renderAuthCodeOutcome(outcome) {
102508
- if (!outcome || outcome.kind === "success")
102509
- return null;
102510
- const tail = outcome.paneTailText ? `
102511
- _${escapeHtmlForTg2(outcome.paneTailText)}_` : "";
102512
- switch (outcome.kind) {
102513
- case "invalid-code":
102514
- case "expired-code":
102515
- return `Code rejected by Claude \u2014 tap **Restart flow** for a fresh URL.${tail}`;
102516
- case "pane-not-ready":
102517
- return `Auth pane not ready \u2014 tap **Retry**.`;
102518
- case "timeout":
102519
- return `Still waiting after 2 min \u2014 tap **Retry** or check \`switchroom auth list\`.${tail}`;
102520
- }
102521
- }
102522
102794
  function execAuthCode(agent, code2) {
102523
102795
  try {
102524
102796
  const output = switchroomExec(["auth", "code", agent, code2, "--json"], 150000);
@@ -102554,7 +102826,7 @@ ${preBlock(formatSwitchroomOutput(detail))}`, { html: true });
102554
102826
  }
102555
102827
  function readRecentDenialsForAgent(agentName3, windowMs, limit) {
102556
102828
  try {
102557
- const auditPath = join59(homedir19(), ".switchroom", "vault-audit.log");
102829
+ const auditPath = join60(homedir19(), ".switchroom", "vault-audit.log");
102558
102830
  if (!existsSync54(auditPath))
102559
102831
  return [];
102560
102832
  const raw = readFileSync58(auditPath, "utf8");
@@ -102608,7 +102880,7 @@ async function buildAgentMetadata(agentName3) {
102608
102880
  try {
102609
102881
  const agentDir = resolveAgentDirFromEnv();
102610
102882
  if (agentDir) {
102611
- const raw = readFileSync58(join59(agentDir, ".claude", ".claude.json"), "utf8");
102883
+ const raw = readFileSync58(join60(agentDir, ".claude", ".claude.json"), "utf8");
102612
102884
  claudeJson = JSON.parse(raw);
102613
102885
  }
102614
102886
  } catch {}
@@ -102737,7 +103009,7 @@ function buildModelDeps(restartCtx) {
102737
103009
  try {
102738
103010
  const agentDir = resolveAgentDirFromEnv();
102739
103011
  if (agentDir) {
102740
- const local = await fetchQuota2({ claudeConfigDir: join59(agentDir, ".claude") });
103012
+ const local = await fetchQuota2({ claudeConfigDir: join60(agentDir, ".claude") });
102741
103013
  if (local.ok)
102742
103014
  return formatQuotaLine2(local.data);
102743
103015
  }
@@ -102832,7 +103104,7 @@ function buildModelDeps(restartCtx) {
102832
103104
  function modelMenuReplyMarkup(reply) {
102833
103105
  if (!reply.keyboard)
102834
103106
  return;
102835
- const kb = new import_grammy14.InlineKeyboard;
103107
+ const kb = new import_grammy15.InlineKeyboard;
102836
103108
  for (const row of reply.keyboard) {
102837
103109
  for (const btn of row)
102838
103110
  kb.text(btn.text, btn.callback_data);
@@ -102972,7 +103244,7 @@ function buildEffortDeps() {
102972
103244
  function effortMenuReplyMarkup(reply) {
102973
103245
  if (!reply.keyboard)
102974
103246
  return;
102975
- const kb = new import_grammy14.InlineKeyboard;
103247
+ const kb = new import_grammy15.InlineKeyboard;
102976
103248
  for (const row of reply.keyboard) {
102977
103249
  for (const btn of row)
102978
103250
  kb.text(btn.text, btn.callback_data);
@@ -102983,7 +103255,7 @@ function effortMenuReplyMarkup(reply) {
102983
103255
  function flushAgentHandoff(agentDir) {
102984
103256
  let removed = 0;
102985
103257
  for (const fname of [".handoff.md", ".handoff-topic"]) {
102986
- const p = join59(agentDir, fname);
103258
+ const p = join60(agentDir, fname);
102987
103259
  try {
102988
103260
  if (existsSync54(p)) {
102989
103261
  unlinkSync26(p);
@@ -103041,7 +103313,7 @@ async function handleNewCommand(ctx) {
103041
103313
  writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
103042
103314
  if (agentDir != null) {
103043
103315
  try {
103044
- writeFileSync47(join59(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
103316
+ writeFileSync48(join60(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
103045
103317
  `, "utf8");
103046
103318
  } catch (err) {
103047
103319
  process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
@@ -103155,10 +103427,10 @@ function buildFolderPickerDeps() {
103155
103427
  }
103156
103428
  var lockoutOps = {
103157
103429
  readFileSync: (p, enc) => readFileSync58(p, enc),
103158
- writeFileSync: (p, data, opts) => writeFileSync47(p, data, opts),
103430
+ writeFileSync: (p, data, opts) => writeFileSync48(p, data, opts),
103159
103431
  existsSync: (p) => existsSync54(p),
103160
- mkdirSync: (p, opts) => mkdirSync46(p, opts),
103161
- joinPath: (...parts) => join59(...parts)
103432
+ mkdirSync: (p, opts) => mkdirSync47(p, opts),
103433
+ joinPath: (...parts) => join60(...parts)
103162
103434
  };
103163
103435
  var FLEET_FALLBACK_DEDUP_MS = 30000;
103164
103436
  function isAuthBrokerSocketReachable() {
@@ -103418,7 +103690,7 @@ async function runCreditWatch() {
103418
103690
  if (!agentDir)
103419
103691
  return;
103420
103692
  const agentName3 = getMyAgentName();
103421
- const claudeConfigDir = join59(agentDir, ".claude");
103693
+ const claudeConfigDir = join60(agentDir, ".claude");
103422
103694
  const stateDir = STATE_DIR;
103423
103695
  const reason = readClaudeJsonOverage(claudeConfigDir);
103424
103696
  const prev = loadCreditState(stateDir);
@@ -103738,16 +104010,6 @@ async function probeQuotaForBootCard(agent, timeoutMs) {
103738
104010
  }
103739
104011
  var callbackQueryHandlers;
103740
104012
  var cardToolHandlers;
103741
- function buildDoctorScopeKeyboard() {
103742
- return new import_grammy14.InlineKeyboard().text("\uD83E\uDE7A Whole fleet", "dr:fleet").text("\uD83E\uDE7A This agent", "dr:self");
103743
- }
103744
- function formatDoctorReport(raw) {
103745
- const trimmed = stripAnsi4(raw).trim();
103746
- if (!trimmed)
103747
- return "doctor: no output";
103748
- const pretty = trimmed.replace(/^( *)\u2713 /gm, "$1\uD83D\uDFE2 ").replace(/^( *)\u2717 /gm, "$1\uD83D\uDD34 ").replace(/^( *)! /gm, "$1\uD83D\uDFE1 ");
103749
- return preBlock(formatSwitchroomOutput(pretty));
103750
- }
103751
104013
  async function renderSelfDoctor(ctx) {
103752
104014
  let output;
103753
104015
  try {
@@ -104635,7 +104897,7 @@ _Google accounts are connected from the host CLI._`, { html: true });
104635
104897
  return;
104636
104898
  }
104637
104899
  const appNote = started.source === "default" ? "_Using switchroom's shipped Microsoft app._" : "_Using your configured Microsoft app._";
104638
- const keyboard = new import_grammy14.InlineKeyboard().url("\uD83D\uDD10 Sign in to Microsoft", started.device.verification_uri).row().text("\u2716 Cancel", `cn:cancel:${key}`);
104900
+ const keyboard = new import_grammy15.InlineKeyboard().url("\uD83D\uDD10 Sign in to Microsoft", started.device.verification_uri).row().text("\u2716 Cancel", `cn:cancel:${key}`);
104639
104901
  const sent = await ctx.replyWithRichMessage(richMessage2(`\uD83D\uDD17 **Connect a Microsoft account**
104640
104902
 
104641
104903
  ` + `1. Tap **Sign in to Microsoft** below.
@@ -104771,7 +105033,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
104771
105033
  tz: process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ
104772
105034
  });
104773
105035
  if (reply.keyboard && reply.keyboard.length > 0) {
104774
- const kb = new import_grammy14.InlineKeyboard;
105036
+ const kb = new import_grammy15.InlineKeyboard;
104775
105037
  for (let i = 0;i < reply.keyboard.length; i++) {
104776
105038
  const row = reply.keyboard[i];
104777
105039
  for (const b of row) {
@@ -104913,7 +105175,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
104913
105175
  }
104914
105176
  lines.push("");
104915
105177
  const denials = readRecentDenialsForAgent(targetAgent, 604800000, 5);
104916
- const denialKeyboard = new import_grammy14.InlineKeyboard;
105178
+ const denialKeyboard = new import_grammy15.InlineKeyboard;
104917
105179
  if (denials.length > 0) {
104918
105180
  lines.push("**Recent denials (last 7d):**");
104919
105181
  for (const d of denials) {
@@ -104961,7 +105223,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
104961
105223
  byAgent.set(g.agent_slug, list3);
104962
105224
  }
104963
105225
  const lines = ["**\uD83D\uDCDC Active grants**", ""];
104964
- const keyboard = new import_grammy14.InlineKeyboard;
105226
+ const keyboard = new import_grammy15.InlineKeyboard;
104965
105227
  for (const [agentName3, agentGrants] of byAgent) {
104966
105228
  lines.push(`**${escapeHtmlForTg2(agentName3)}:**`);
104967
105229
  for (const g of agentGrants) {
@@ -105158,7 +105420,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
105158
105420
  if (ctx.chat?.type !== "private") {
105159
105421
  kbRows = kbRows.filter((row) => !row.some((b) => b.callbackData?.startsWith("auth:use:")));
105160
105422
  }
105161
- const keyboard = new import_grammy14.InlineKeyboard;
105423
+ const keyboard = new import_grammy15.InlineKeyboard;
105162
105424
  kbRows.forEach((row, ri) => {
105163
105425
  if (ri > 0)
105164
105426
  keyboard.row();
@@ -105184,7 +105446,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
105184
105446
  await switchroomReply(ctx, "**/usage:** cannot resolve agent dir.", { html: true });
105185
105447
  return;
105186
105448
  }
105187
- const result = await fetchQuota2({ claudeConfigDir: join59(agentDir, ".claude") });
105449
+ const result = await fetchQuota2({ claudeConfigDir: join60(agentDir, ".claude") });
105188
105450
  if (!result.ok) {
105189
105451
  await switchroomReply(ctx, `**/usage:** ${escapeHtmlForTg2(result.reason)}`, { html: true });
105190
105452
  return;
@@ -105510,7 +105772,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
105510
105772
  await ctx.answerCallbackQuery({ text: "No always-allow rule for this tool." }).catch(() => {});
105511
105773
  return;
105512
105774
  }
105513
- keyboard = new import_grammy14.InlineKeyboard().text("\u2190 Back", `perm:back:${request_id}`);
105775
+ keyboard = new import_grammy15.InlineKeyboard().text("\u2190 Back", `perm:back:${request_id}`);
105514
105776
  if (choices.specific)
105515
105777
  keyboard.text(choices.specific.buttonLabel, `perm:asn:${request_id}`);
105516
105778
  keyboard.text(`${choices.broad.buttonLabel} \u26A0\uFE0F`, `perm:asb:${request_id}`);
@@ -105812,7 +106074,7 @@ ${labelWithResume}` : labelWithResume,
105812
106074
  verb: "voice-ondemand.sendVoice",
105813
106075
  ...threadId != null ? { threadId } : {}
105814
106076
  }),
105815
- sendVoiceByUpload: (chatId, audio, sendOpts, threadId) => robustApiCall(() => bot2.api.sendVoice(chatId, new import_grammy14.InputFile(Buffer.from(audio)), sendOpts), {
106077
+ sendVoiceByUpload: (chatId, audio, sendOpts, threadId) => robustApiCall(() => bot2.api.sendVoice(chatId, new import_grammy15.InputFile(Buffer.from(audio)), sendOpts), {
105816
106078
  chat_id: chatId,
105817
106079
  verb: "voice-ondemand.sendVoice",
105818
106080
  ...threadId != null ? { threadId } : {}
@@ -106071,7 +106333,7 @@ async function initGatewayBot() {
106071
106333
  `);
106072
106334
  process.exit(1);
106073
106335
  }
106074
- bot = new import_grammy14.Bot(TOKEN);
106336
+ bot = new import_grammy15.Bot(TOKEN);
106075
106337
  installTgPostLogger(bot);
106076
106338
  installUpdateTap(bot, (line) => process.stderr.write(line));
106077
106339
  bot.api.config.use(async (prev, method, payload, signal) => {
@@ -106425,7 +106687,7 @@ async function startGateway() {
106425
106687
  return;
106426
106688
  }
106427
106689
  })();
106428
- const resolvedAgentDirForBootCard = agentDir ?? join59(homedir19(), ".switchroom", "agents", agentSlug);
106690
+ const resolvedAgentDirForBootCard = agentDir ?? join60(homedir19(), ".switchroom", "agents", agentSlug);
106429
106691
  const handle = await startBootCard(chatId, threadId, botApiForCard, {
106430
106692
  agentName: agentDisplayName,
106431
106693
  agentSlug,
@@ -106439,8 +106701,8 @@ async function startGateway() {
106439
106701
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
106440
106702
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
106441
106703
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
106442
- configSnapshotPath: join59(resolvedAgentDirForBootCard, ".config-snapshot.json"),
106443
- bootCardStatePath: join59(resolvedAgentDirForBootCard, ".boot-card-msgid.json"),
106704
+ configSnapshotPath: join60(resolvedAgentDirForBootCard, ".config-snapshot.json"),
106705
+ bootCardStatePath: join60(resolvedAgentDirForBootCard, ".boot-card-msgid.json"),
106444
106706
  floodStatePath: FLOOD_STATE_PATH,
106445
106707
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
106446
106708
  }, ackMsgId);
@@ -106471,7 +106733,7 @@ async function startGateway() {
106471
106733
  try {
106472
106734
  const smAgentDir = resolveAgentDirFromEnv();
106473
106735
  if (smAgentDir) {
106474
- const activePath = join59(smAgentDir, ".active-session-model");
106736
+ const activePath = join60(smAgentDir, ".active-session-model");
106475
106737
  if (existsSync54(activePath)) {
106476
106738
  try {
106477
106739
  const launched = readFileSync58(activePath, "utf8").trim();
@@ -106507,12 +106769,12 @@ async function startGateway() {
106507
106769
  deliverModelSwitchBootNotice({
106508
106770
  ...modelBootCardDeps,
106509
106771
  confirmation,
106510
- hasSessionModelAlert: existsSync54(join59(smAgentDir, ".session-model-alert"))
106772
+ hasSessionModelAlert: existsSync54(join60(smAgentDir, ".session-model-alert"))
106511
106773
  });
106512
106774
  }
106513
106775
  } catch {}
106514
106776
  }
106515
- const activeEffortPath = join59(smAgentDir, ".active-session-effort");
106777
+ const activeEffortPath = join60(smAgentDir, ".active-session-effort");
106516
106778
  if (existsSync54(activeEffortPath)) {
106517
106779
  try {
106518
106780
  const launchedEffort = readFileSync58(activeEffortPath, "utf8").trim();
@@ -106520,7 +106782,7 @@ async function startGateway() {
106520
106782
  sessionEffortOverride = launchedEffort.length > 0 && launchedEffort !== configuredEffort ? launchedEffort : null;
106521
106783
  } catch {}
106522
106784
  }
106523
- const alertPath = join59(smAgentDir, ".session-model-alert");
106785
+ const alertPath = join60(smAgentDir, ".session-model-alert");
106524
106786
  if (existsSync54(alertPath)) {
106525
106787
  let alertText = null;
106526
106788
  try {
@@ -106700,6 +106962,7 @@ async function startGateway() {
106700
106962
  `);
106701
106963
  },
106702
106964
  onTerminalCleanup: (agentId) => {
106965
+ workerFeedOriginDeferrals.delete(agentId);
106703
106966
  try {
106704
106967
  workerActivityFeed?.terminate(agentId);
106705
106968
  } catch (err) {
@@ -106708,6 +106971,7 @@ async function startGateway() {
106708
106971
  }
106709
106972
  },
106710
106973
  onFinish: ({ agentId, outcome, description, resultText, toolCount, totalTokens, durationMs, background: entryBackground }) => {
106974
+ workerFeedOriginDeferrals.delete(agentId);
106711
106975
  deferredDoneReactions.promote();
106712
106976
  let fleetChatId = "";
106713
106977
  try {
@@ -106928,9 +107192,28 @@ async function startGateway() {
106928
107192
  stampTurn.subagentActivityAt = Date.now();
106929
107193
  }
106930
107194
  if (workerFeedEnabled) {
106931
- const wk = resolveWorkerFeedChat(agentId, fleetChatId);
106932
- const wkChat = wk.chatId;
106933
- workerActivityFeed?.update(agentId, wkChat, {
107195
+ const dest = decideWorkerFeedDestination({
107196
+ origin: resolveSubagentOriginChat(agentId),
107197
+ cardExists: workerActivityFeed?.has(agentId) === true,
107198
+ priorDeferrals: workerFeedOriginDeferrals.get(agentId) ?? 0,
107199
+ maxDeferrals: WORKER_FEED_ORIGIN_DEFER_MAX,
107200
+ fleetChatId,
107201
+ stampChatId: stampTurn?.sessionChatId,
107202
+ stampThreadId: stampTurn?.sessionThreadId,
107203
+ ownerDm: loadAccess().allowFrom[0] ?? ""
107204
+ });
107205
+ if (dest.action === "defer") {
107206
+ workerFeedOriginDeferrals.set(agentId, dest.deferrals);
107207
+ return;
107208
+ }
107209
+ workerFeedOriginDeferrals.delete(agentId);
107210
+ if (dest.exhausted) {
107211
+ process.stderr.write(`telegram gateway: worker-feed origin backfill never linked agent=${agentId} after ${dest.deferrals} deferrals \u2014 painting card
107212
+ `);
107213
+ }
107214
+ if (dest.ownerDmFallback)
107215
+ noteWorkerFeedOwnerDmFallback(agentId);
107216
+ workerActivityFeed?.update(agentId, dest.chatId, {
106934
107217
  description: dispatch.feedDescription,
106935
107218
  lastTool,
106936
107219
  toolCount,
@@ -106939,7 +107222,7 @@ async function startGateway() {
106939
107222
  state: "running",
106940
107223
  model: feedModel,
106941
107224
  totalTokens
106942
- }, wk.threadId);
107225
+ }, dest.threadId);
106943
107226
  return;
106944
107227
  }
106945
107228
  const progressOrigin = resolveSubagentOriginChat(agentId);
@@ -107004,7 +107287,7 @@ async function startGateway() {
107004
107287
  await runnerHandle.task();
107005
107288
  return;
107006
107289
  } catch (err) {
107007
- if (err instanceof import_grammy14.GrammyError && err.error_code === 409) {
107290
+ if (err instanceof import_grammy15.GrammyError && err.error_code === 409) {
107008
107291
  const delay = Math.min(1000 * attempt, 15000);
107009
107292
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME ?? "-";
107010
107293
  process.stderr.write(`telegram gateway: poll.409.detected attempt=${attempt} retry_in_ms=${delay} agent=${agentName3}