switchroom 0.5.0 → 0.7.8

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 (89) hide show
  1. package/README.md +142 -121
  2. package/bin/autoaccept.exp +29 -6
  3. package/dist/agent-scheduler/index.js +12261 -0
  4. package/dist/cli/autoaccept-poll.js +10 -0
  5. package/dist/cli/switchroom.js +27250 -25324
  6. package/dist/vault/approvals/kernel-server.js +12709 -0
  7. package/dist/vault/broker/server.js +15724 -0
  8. package/package.json +4 -3
  9. package/profiles/_base/start.sh.hbs +133 -0
  10. package/profiles/_shared/telegram-style.md.hbs +3 -3
  11. package/profiles/default/CLAUDE.md +3 -3
  12. package/profiles/default/CLAUDE.md.hbs +2 -2
  13. package/profiles/default/workspace/CLAUDE.md.hbs +9 -0
  14. package/skills/docx/VENDORED.md +1 -1
  15. package/skills/mcp-builder/VENDORED.md +1 -1
  16. package/skills/pdf/VENDORED.md +1 -1
  17. package/skills/pptx/VENDORED.md +1 -1
  18. package/skills/skill-creator/VENDORED.md +1 -1
  19. package/skills/switchroom-architecture/SKILL.md +8 -7
  20. package/skills/switchroom-cli/SKILL.md +23 -15
  21. package/skills/switchroom-health/SKILL.md +7 -7
  22. package/skills/switchroom-install/SKILL.md +36 -39
  23. package/skills/switchroom-manage/SKILL.md +4 -4
  24. package/skills/switchroom-status/SKILL.md +1 -1
  25. package/skills/webapp-testing/VENDORED.md +1 -1
  26. package/skills/xlsx/VENDORED.md +1 -1
  27. package/telegram-plugin/admin-commands/dispatch.test.ts +119 -1
  28. package/telegram-plugin/admin-commands/index.ts +71 -0
  29. package/telegram-plugin/ask-user.ts +1 -0
  30. package/telegram-plugin/card-event-log.ts +138 -0
  31. package/telegram-plugin/dist/bridge/bridge.js +178 -31
  32. package/telegram-plugin/dist/foreman/foreman.js +6875 -6526
  33. package/telegram-plugin/dist/gateway/gateway.js +13862 -11834
  34. package/telegram-plugin/dist/server.js +202 -40
  35. package/telegram-plugin/fleet-state.ts +25 -10
  36. package/telegram-plugin/foreman/foreman.ts +38 -3
  37. package/telegram-plugin/gateway/approval-callback.ts +126 -0
  38. package/telegram-plugin/gateway/approval-card.test.ts +90 -0
  39. package/telegram-plugin/gateway/approval-card.ts +127 -0
  40. package/telegram-plugin/gateway/approvals-commands.ts +126 -0
  41. package/telegram-plugin/gateway/boot-card.ts +31 -6
  42. package/telegram-plugin/gateway/boot-probes.ts +503 -72
  43. package/telegram-plugin/gateway/gateway.ts +822 -94
  44. package/telegram-plugin/gateway/ipc-protocol.ts +34 -1
  45. package/telegram-plugin/gateway/ipc-server.ts +35 -0
  46. package/telegram-plugin/gateway/startup-mutex.ts +110 -2
  47. package/telegram-plugin/hooks/hooks.json +19 -0
  48. package/telegram-plugin/hooks/tool-label-pretool.mjs +216 -0
  49. package/telegram-plugin/hooks/tool-label-stop.mjs +63 -0
  50. package/telegram-plugin/package.json +4 -1
  51. package/telegram-plugin/plugin-logger.ts +20 -1
  52. package/telegram-plugin/progress-card-driver.ts +202 -13
  53. package/telegram-plugin/progress-card.ts +2 -2
  54. package/telegram-plugin/quota-check.ts +1 -0
  55. package/telegram-plugin/registry/subagents-schema.ts +37 -0
  56. package/telegram-plugin/registry/subagents.test.ts +64 -0
  57. package/telegram-plugin/session-tail.ts +58 -5
  58. package/telegram-plugin/shared/bot-runtime.ts +48 -2
  59. package/telegram-plugin/subagent-watcher.ts +139 -7
  60. package/telegram-plugin/tests/_progress-card-harness.ts +4 -0
  61. package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +201 -0
  62. package/telegram-plugin/tests/boot-card-probe-target.test.ts +10 -34
  63. package/telegram-plugin/tests/boot-card-render.test.ts +6 -5
  64. package/telegram-plugin/tests/boot-probes.test.ts +558 -0
  65. package/telegram-plugin/tests/card-event-log.test.ts +145 -0
  66. package/telegram-plugin/tests/gateway-startup-mutex.test.ts +102 -0
  67. package/telegram-plugin/tests/ipc-server-validate-inject-inbound.test.ts +134 -0
  68. package/telegram-plugin/tests/progress-card-delay-842.test.ts +160 -0
  69. package/telegram-plugin/tests/quota-check.test.ts +37 -1
  70. package/telegram-plugin/tests/subagent-registry-bugs.test.ts +5 -0
  71. package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +104 -1
  72. package/telegram-plugin/tests/subagent-watcher.test.ts +5 -0
  73. package/telegram-plugin/tests/tool-label-sidecar.test.ts +114 -0
  74. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +5 -3
  75. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +10 -0
  76. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +58 -14
  77. package/telegram-plugin/tests/welcome-text.test.ts +57 -0
  78. package/telegram-plugin/tool-label-sidecar.ts +140 -0
  79. package/telegram-plugin/tool-labels.ts +55 -0
  80. package/telegram-plugin/two-zone-card.ts +27 -7
  81. package/telegram-plugin/uat/SETUP.md +160 -0
  82. package/telegram-plugin/uat/assertions.ts +140 -0
  83. package/telegram-plugin/uat/driver.ts +174 -0
  84. package/telegram-plugin/uat/harness.ts +161 -0
  85. package/telegram-plugin/uat/login.ts +134 -0
  86. package/telegram-plugin/uat/port-allocator.ts +71 -0
  87. package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +61 -0
  88. package/telegram-plugin/welcome-text.ts +44 -2
  89. package/bin/bridge-watchdog.sh +0 -967
@@ -16931,7 +16931,7 @@ var init_zod = __esm(() => {
16931
16931
  });
16932
16932
 
16933
16933
  // plugin-logger.ts
16934
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, renameSync as renameSync3, statSync as statSync2 } from "fs";
16934
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, renameSync as renameSync3, statSync as statSync2, existsSync as existsSync2 } from "fs";
16935
16935
  import { homedir as homedir2 } from "os";
16936
16936
  import { dirname as dirname2, join as join2 } from "path";
16937
16937
  function resolveLogPath2(env = process.env) {
@@ -16950,6 +16950,14 @@ function rotateIfNeeded2(path) {
16950
16950
  const st = statSync2(path);
16951
16951
  if (st.size < ROTATE_AT_BYTES2)
16952
16952
  return;
16953
+ for (let i = ROTATION_BACKUPS2 - 1;i >= 1; i--) {
16954
+ const src = `${path}.${i}`;
16955
+ const dst = `${path}.${i + 1}`;
16956
+ try {
16957
+ if (existsSync2(src))
16958
+ renameSync3(src, dst);
16959
+ } catch {}
16960
+ }
16953
16961
  const backup = `${path}.1`;
16954
16962
  renameSync3(path, backup);
16955
16963
  } catch {}
@@ -16981,11 +16989,14 @@ function installPluginLogger2(env = process.env) {
16981
16989
  };
16982
16990
  return activeHandle2;
16983
16991
  }
16984
- var DEFAULT_LOG_PATH2, ROTATE_AT_BYTES2, activeHandle2 = null;
16992
+ var DEFAULT_LOG_PATH2, ROTATE_AT_BYTES2, ROTATION_BACKUPS2 = 5, activeHandle2 = null;
16985
16993
  var init_plugin_logger = __esm(() => {
16986
16994
  DEFAULT_LOG_PATH2 = join2(homedir2(), ".switchroom", "logs", "telegram-plugin.log");
16987
- ROTATE_AT_BYTES2 = 5 * 1024 * 1024;
16995
+ ROTATE_AT_BYTES2 = 50 * 1024 * 1024;
16988
16996
  });
16997
+
16998
+ // tool-labels.ts
16999
+ var init_tool_labels = () => {};
16989
17000
  // tool-error-filter.ts
16990
17001
  var init_tool_error_filter = () => {};
16991
17002
 
@@ -16995,6 +17006,7 @@ var init_fleet_state = () => {};
16995
17006
  // two-zone-card.ts
16996
17007
  var init_two_zone_card = __esm(() => {
16997
17008
  init_fleet_state();
17009
+ init_tool_labels();
16998
17010
  });
16999
17011
 
17000
17012
  // progress-card.ts
@@ -17003,6 +17015,7 @@ function isMultiAgentEnabled(env = process.env) {
17003
17015
  }
17004
17016
  var STUCK_THRESHOLD_MS;
17005
17017
  var init_progress_card = __esm(() => {
17018
+ init_tool_labels();
17006
17019
  init_tool_error_filter();
17007
17020
  init_two_zone_card();
17008
17021
  STUCK_THRESHOLD_MS = 2 * 60000;
@@ -17076,26 +17089,117 @@ var init_operator_events = __esm(() => {
17076
17089
  cooldownMap = new Map;
17077
17090
  });
17078
17091
 
17092
+ // tool-label-sidecar.ts
17093
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
17094
+ import { join as join3 } from "node:path";
17095
+ function createToolLabelSidecar(opts) {
17096
+ const path = join3(opts.stateDir, `tool-labels-${opts.sessionId}.jsonl`);
17097
+ const labels = new Map;
17098
+ const subscribers = new Set;
17099
+ let offset = 0;
17100
+ let stopped = false;
17101
+ const sched = opts.scheduler ?? {
17102
+ setInterval: (cb, ms) => setInterval(cb, ms),
17103
+ clearInterval: (h) => clearInterval(h)
17104
+ };
17105
+ function ingestSuffix(text) {
17106
+ if (!text)
17107
+ return;
17108
+ const lines = text.split(`
17109
+ `);
17110
+ for (const raw of lines) {
17111
+ const line = raw.trim();
17112
+ if (!line)
17113
+ continue;
17114
+ let row = null;
17115
+ try {
17116
+ row = JSON.parse(line);
17117
+ } catch {
17118
+ continue;
17119
+ }
17120
+ if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string")
17121
+ continue;
17122
+ if (labels.has(row.tool_use_id))
17123
+ continue;
17124
+ labels.set(row.tool_use_id, row.label);
17125
+ for (const cb of subscribers) {
17126
+ try {
17127
+ cb(row.tool_use_id, row.label);
17128
+ } catch {}
17129
+ }
17130
+ }
17131
+ }
17132
+ function poll() {
17133
+ if (stopped)
17134
+ return;
17135
+ if (!existsSync3(path))
17136
+ return;
17137
+ let size = 0;
17138
+ try {
17139
+ size = statSync3(path).size;
17140
+ } catch {
17141
+ return;
17142
+ }
17143
+ if (size <= offset) {
17144
+ if (size < offset)
17145
+ offset = 0;
17146
+ else
17147
+ return;
17148
+ }
17149
+ let text = "";
17150
+ try {
17151
+ const buf = readFileSync2(path);
17152
+ text = buf.subarray(offset).toString("utf8");
17153
+ offset = buf.length;
17154
+ } catch {
17155
+ return;
17156
+ }
17157
+ ingestSuffix(text);
17158
+ }
17159
+ poll();
17160
+ const handle = sched.setInterval(poll, opts.pollMs ?? 250);
17161
+ return {
17162
+ getLabel(toolUseId) {
17163
+ return labels.get(toolUseId);
17164
+ },
17165
+ onLabel(cb) {
17166
+ subscribers.add(cb);
17167
+ return () => subscribers.delete(cb);
17168
+ },
17169
+ poll,
17170
+ stop() {
17171
+ if (stopped)
17172
+ return;
17173
+ stopped = true;
17174
+ try {
17175
+ sched.clearInterval(handle);
17176
+ } catch {}
17177
+ subscribers.clear();
17178
+ }
17179
+ };
17180
+ }
17181
+ var init_tool_label_sidecar = () => {};
17182
+
17079
17183
  // session-tail.ts
17080
17184
  import {
17081
17185
  closeSync,
17082
- existsSync as existsSync3,
17186
+ existsSync as existsSync4,
17083
17187
  openSync,
17084
17188
  readdirSync,
17085
17189
  readSync,
17086
- statSync as statSync3,
17190
+ statSync as statSync4,
17087
17191
  watch
17088
17192
  } from "fs";
17089
17193
  import { homedir as homedir3 } from "os";
17090
- import { basename, join as join3 } from "path";
17194
+ import { basename, join as join4 } from "path";
17091
17195
  function sanitizeCwdToProjectName(cwd) {
17092
17196
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
17093
17197
  }
17094
- function getProjectsDirForCwd(cwd = process.cwd(), claudeHome = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude")) {
17095
- return join3(claudeHome, "projects", sanitizeCwdToProjectName(cwd));
17198
+ function getProjectsDirForCwd(cwd = process.cwd(), claudeHome = process.env.CLAUDE_CONFIG_DIR ?? join4(homedir3(), ".claude")) {
17199
+ return join4(claudeHome, "projects", sanitizeCwdToProjectName(cwd));
17096
17200
  }
17097
17201
  function findActiveSessionFile(projectsDir) {
17098
- if (!existsSync3(projectsDir))
17202
+ if (!existsSync4(projectsDir))
17099
17203
  return null;
17100
17204
  let entries;
17101
17205
  try {
@@ -17108,9 +17212,9 @@ function findActiveSessionFile(projectsDir) {
17108
17212
  for (const e of entries) {
17109
17213
  if (!e.endsWith(".jsonl"))
17110
17214
  continue;
17111
- const p = join3(projectsDir, e);
17215
+ const p = join4(projectsDir, e);
17112
17216
  try {
17113
- const s = statSync3(p);
17217
+ const s = statSync4(p);
17114
17218
  if (s.mtimeMs > bestMtime) {
17115
17219
  bestMtime = s.mtimeMs;
17116
17220
  bestPath = p;
@@ -17335,13 +17439,53 @@ function extractDetailMessage(obj) {
17335
17439
  }
17336
17440
  function startSessionTail(config2) {
17337
17441
  const cwd = config2.cwd ?? process.cwd();
17338
- const claudeHome = config2.claudeHome ?? process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
17442
+ const claudeHome = config2.claudeHome ?? process.env.CLAUDE_CONFIG_DIR ?? join4(homedir3(), ".claude");
17339
17443
  const projectsDir = getProjectsDirForCwd(cwd, claudeHome);
17340
17444
  const rescanMs = config2.rescanIntervalMs ?? 500;
17341
17445
  const log = config2.log;
17342
- const onEvent = config2.onEvent;
17446
+ const rawOnEvent = config2.onEvent;
17343
17447
  const onOperatorEvent = config2.onOperatorEvent;
17344
17448
  log?.(`session-tail: projectsDir=${projectsDir}`);
17449
+ const sidecars = new Map;
17450
+ const stateDirForSidecar = process.env.TELEGRAM_STATE_DIR ?? null;
17451
+ function sessionIdForFile(file) {
17452
+ if (!file)
17453
+ return null;
17454
+ const b = file.endsWith(".jsonl") ? basename(file, ".jsonl") : null;
17455
+ return b && b.length > 0 ? b : null;
17456
+ }
17457
+ function ensureSidecar(sessionId) {
17458
+ if (!stateDirForSidecar)
17459
+ return null;
17460
+ const existing = sidecars.get(sessionId);
17461
+ if (existing)
17462
+ return existing;
17463
+ try {
17464
+ const s = createToolLabelSidecar({ stateDir: stateDirForSidecar, sessionId });
17465
+ sidecars.set(sessionId, s);
17466
+ return s;
17467
+ } catch (err) {
17468
+ log?.(`session-tail: sidecar create failed: ${err.message}`);
17469
+ return null;
17470
+ }
17471
+ }
17472
+ function decorate(ev, sessionId) {
17473
+ if (!sessionId)
17474
+ return ev;
17475
+ if (ev.kind !== "tool_use" && ev.kind !== "sub_agent_tool_use")
17476
+ return ev;
17477
+ if (!ev.toolUseId)
17478
+ return ev;
17479
+ const s = ensureSidecar(sessionId);
17480
+ if (!s)
17481
+ return ev;
17482
+ s.poll();
17483
+ const label = s.getLabel(ev.toolUseId);
17484
+ if (!label)
17485
+ return ev;
17486
+ return { ...ev, precomputedLabel: label };
17487
+ }
17488
+ const onEvent = (ev) => rawOnEvent(ev);
17345
17489
  let currentFile = null;
17346
17490
  let cursor = 0;
17347
17491
  let watcher = null;
@@ -17353,7 +17497,7 @@ function startSessionTail(config2) {
17353
17497
  if (stopped || !currentFile)
17354
17498
  return;
17355
17499
  try {
17356
- const stat = statSync3(currentFile);
17500
+ const stat = statSync4(currentFile);
17357
17501
  if (stat.size < cursor) {
17358
17502
  cursor = 0;
17359
17503
  pendingPartial = "";
@@ -17378,9 +17522,10 @@ function startSessionTail(config2) {
17378
17522
  if (!line)
17379
17523
  continue;
17380
17524
  const events = projectTranscriptLine(line);
17525
+ const sid = sessionIdForFile(currentFile);
17381
17526
  for (const ev of events) {
17382
17527
  try {
17383
- onEvent(ev);
17528
+ onEvent(decorate(ev, sid));
17384
17529
  } catch (err) {
17385
17530
  log?.(`session-tail: onEvent threw: ${err.message}`);
17386
17531
  }
@@ -17421,7 +17566,7 @@ function startSessionTail(config2) {
17421
17566
  } else {
17422
17567
  pendingPartial = "";
17423
17568
  try {
17424
- cursor = statSync3(file).size;
17569
+ cursor = statSync4(file).size;
17425
17570
  } catch {
17426
17571
  cursor = 0;
17427
17572
  }
@@ -17441,7 +17586,7 @@ function startSessionTail(config2) {
17441
17586
  if (stopped)
17442
17587
  return;
17443
17588
  try {
17444
- const stat = statSync3(t.file);
17589
+ const stat = statSync4(t.file);
17445
17590
  if (stat.size < t.cursor) {
17446
17591
  t.cursor = 0;
17447
17592
  t.pendingPartial = "";
@@ -17482,7 +17627,8 @@ function startSessionTail(config2) {
17482
17627
  t.hasSeenTerminal = true;
17483
17628
  }
17484
17629
  try {
17485
- onEvent(ev);
17630
+ const subSid = sessionIdForFile(t.file);
17631
+ onEvent(decorate(ev, subSid));
17486
17632
  } catch (err) {
17487
17633
  log?.(`session-tail: sub onEvent threw: ${err.message}`);
17488
17634
  }
@@ -17498,7 +17644,7 @@ function startSessionTail(config2) {
17498
17644
  return;
17499
17645
  let cursor2 = 0;
17500
17646
  try {
17501
- cursor2 = statSync3(file).size;
17647
+ cursor2 = statSync4(file).size;
17502
17648
  } catch {}
17503
17649
  const t = {
17504
17650
  agentId,
@@ -17553,8 +17699,8 @@ function startSessionTail(config2) {
17553
17699
  if (!currentFile)
17554
17700
  return;
17555
17701
  const sessionId = basename(currentFile, ".jsonl");
17556
- const subDir = join3(projectsDir, sessionId, "subagents");
17557
- if (!existsSync3(subDir))
17702
+ const subDir = join4(projectsDir, sessionId, "subagents");
17703
+ if (!existsSync4(subDir))
17558
17704
  return;
17559
17705
  let entries;
17560
17706
  try {
@@ -17566,7 +17712,7 @@ function startSessionTail(config2) {
17566
17712
  if (!e.startsWith("agent-") || !e.endsWith(".jsonl"))
17567
17713
  continue;
17568
17714
  const agentId = e.slice("agent-".length, -".jsonl".length);
17569
- const file = join3(subDir, e);
17715
+ const file = join4(subDir, e);
17570
17716
  if (!subTails.has(file)) {
17571
17717
  attachSub(file, agentId);
17572
17718
  } else {
@@ -17607,6 +17753,12 @@ function startSessionTail(config2) {
17607
17753
  }
17608
17754
  }
17609
17755
  subTails.clear();
17756
+ for (const s of sidecars.values()) {
17757
+ try {
17758
+ s.stop();
17759
+ } catch {}
17760
+ }
17761
+ sidecars.clear();
17610
17762
  if (pollTimer) {
17611
17763
  clearInterval(pollTimer);
17612
17764
  pollTimer = null;
@@ -17621,6 +17773,7 @@ var MAX_JSONL_LINE_BYTES, MAX_ERROR_TEXT_CHARS = 500;
17621
17773
  var init_session_tail = __esm(() => {
17622
17774
  init_progress_card();
17623
17775
  init_operator_events();
17776
+ init_tool_label_sidecar();
17624
17777
  MAX_JSONL_LINE_BYTES = 2 * 1024 * 1024;
17625
17778
  });
17626
17779
 
@@ -23180,7 +23333,7 @@ ${i3.join(`
23180
23333
  });
23181
23334
 
23182
23335
  // pty-tail.ts
23183
- import { existsSync as existsSync4, statSync as statSync4, watch as watch2, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
23336
+ import { existsSync as existsSync5, statSync as statSync5, watch as watch2, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
23184
23337
  function debugDumpBufferOnMiss(buf) {
23185
23338
  if (!PTY_DEBUG)
23186
23339
  return;
@@ -23450,11 +23603,11 @@ function startPtyTail(config2) {
23450
23603
  function readNew() {
23451
23604
  if (stopped)
23452
23605
  return;
23453
- if (!existsSync4(config2.logFile))
23606
+ if (!existsSync5(config2.logFile))
23454
23607
  return;
23455
23608
  let stat;
23456
23609
  try {
23457
- stat = statSync4(config2.logFile);
23610
+ stat = statSync5(config2.logFile);
23458
23611
  } catch {
23459
23612
  return;
23460
23613
  }
@@ -23483,13 +23636,13 @@ function startPtyTail(config2) {
23483
23636
  });
23484
23637
  }
23485
23638
  function attachWatcher() {
23486
- if (!existsSync4(config2.logFile))
23639
+ if (!existsSync5(config2.logFile))
23487
23640
  return;
23488
23641
  if (watcher)
23489
23642
  return;
23490
23643
  let size = 0;
23491
23644
  try {
23492
- size = statSync4(config2.logFile).size;
23645
+ size = statSync5(config2.logFile).size;
23493
23646
  } catch {
23494
23647
  return;
23495
23648
  }
@@ -23815,7 +23968,7 @@ var init_ipc_client = () => {};
23815
23968
 
23816
23969
  // bridge/bridge.ts
23817
23970
  var exports_bridge = {};
23818
- import { dirname as dirname3, join as join4 } from "path";
23971
+ import { dirname as dirname3, join as join5 } from "path";
23819
23972
  import { homedir as homedir4 } from "os";
23820
23973
  function onInbound(msg) {
23821
23974
  mcp.notification({
@@ -23882,7 +24035,7 @@ async function main() {
23882
24035
  onStatus,
23883
24036
  log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
23884
24037
  `),
23885
- livenessFilePath: join4(STATE_DIR, ".bridge-alive")
24038
+ livenessFilePath: join5(STATE_DIR, ".bridge-alive")
23886
24039
  });
23887
24040
  if (ipc.isConnected()) {
23888
24041
  process.stderr.write(`telegram bridge: connected to gateway at ${SOCKET_PATH}
@@ -23904,8 +24057,8 @@ var init_bridge = __esm(async () => {
23904
24057
  init_pty_tail();
23905
24058
  init_ipc_client();
23906
24059
  installPluginLogger2();
23907
- STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join4(homedir4(), ".claude", "channels", "telegram");
23908
- SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join4(STATE_DIR, "gateway.sock");
24060
+ STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join5(homedir4(), ".claude", "channels", "telegram");
24061
+ SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join5(STATE_DIR, "gateway.sock");
23909
24062
  TOPIC_ID = process.env.TELEGRAM_TOPIC_ID ? Number(process.env.TELEGRAM_TOPIC_ID) : undefined;
23910
24063
  AGENT_NAME = process.env.SWITCHROOM_AGENT_NAME;
23911
24064
  if (!AGENT_NAME) {
@@ -24334,7 +24487,7 @@ var init_bridge = __esm(async () => {
24334
24487
  if (ptyTailEnabled) {
24335
24488
  try {
24336
24489
  const agentDir = process.env.CLAUDE_CONFIG_DIR ? dirname3(process.env.CLAUDE_CONFIG_DIR) : process.cwd();
24337
- const serviceLogPath = process.env.SWITCHROOM_SERVICE_LOG_PATH ?? join4(agentDir, "service.log");
24490
+ const serviceLogPath = process.env.SWITCHROOM_SERVICE_LOG_PATH ?? join5(agentDir, "service.log");
24338
24491
  ptyTailHandle = startPtyTail({
24339
24492
  logFile: serviceLogPath,
24340
24493
  log: (msg) => process.stderr.write(`telegram bridge: ${msg}
@@ -24383,16 +24536,17 @@ var init_bridge = __esm(async () => {
24383
24536
  });
24384
24537
 
24385
24538
  // server.ts
24386
- import { existsSync as existsSync5 } from "fs";
24539
+ import { existsSync as existsSync6 } from "fs";
24387
24540
  import { homedir as homedir5 } from "os";
24388
- import { join as join5 } from "path";
24541
+ import { join as join6 } from "path";
24389
24542
 
24390
24543
  // plugin-logger.ts
24391
- import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
24544
+ import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
24392
24545
  import { homedir } from "os";
24393
24546
  import { dirname, join } from "path";
24394
24547
  var DEFAULT_LOG_PATH = join(homedir(), ".switchroom", "logs", "telegram-plugin.log");
24395
- var ROTATE_AT_BYTES = 5 * 1024 * 1024;
24548
+ var ROTATE_AT_BYTES = 50 * 1024 * 1024;
24549
+ var ROTATION_BACKUPS = 5;
24396
24550
  var activeHandle = null;
24397
24551
  function resolveLogPath(env = process.env) {
24398
24552
  const override = env.SWITCHROOM_TELEGRAM_LOG_PATH;
@@ -24410,6 +24564,14 @@ function rotateIfNeeded(path) {
24410
24564
  const st = statSync(path);
24411
24565
  if (st.size < ROTATE_AT_BYTES)
24412
24566
  return;
24567
+ for (let i = ROTATION_BACKUPS - 1;i >= 1; i--) {
24568
+ const src = `${path}.${i}`;
24569
+ const dst = `${path}.${i + 1}`;
24570
+ try {
24571
+ if (existsSync(src))
24572
+ renameSync(src, dst);
24573
+ } catch {}
24574
+ }
24413
24575
  const backup = `${path}.1`;
24414
24576
  renameSync(path, backup);
24415
24577
  } catch {}
@@ -24482,12 +24644,12 @@ function shouldFallBackToLegacy(input) {
24482
24644
  // server.ts
24483
24645
  installPluginLogger();
24484
24646
  {
24485
- const _stateDir = process.env.TELEGRAM_STATE_DIR ?? join5(homedir5(), ".claude", "channels", "telegram");
24486
- const _gatewaySocket = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join5(_stateDir, "gateway.sock");
24487
- const _gatewayPidPath = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join5(_stateDir, "gateway.pid.json");
24647
+ const _stateDir = process.env.TELEGRAM_STATE_DIR ?? join6(homedir5(), ".claude", "channels", "telegram");
24648
+ const _gatewaySocket = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join6(_stateDir, "gateway.sock");
24649
+ const _gatewayPidPath = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join6(_stateDir, "gateway.pid.json");
24488
24650
  let _gatewayLive = false;
24489
24651
  async function probeSocketOnce() {
24490
- if (!existsSync5(_gatewaySocket))
24652
+ if (!existsSync6(_gatewaySocket))
24491
24653
  return;
24492
24654
  try {
24493
24655
  await Bun.connect({
@@ -39,6 +39,13 @@ export interface FleetMember {
39
39
  errorSeen: boolean
40
40
  /** Snapshot of driver's currentTurnKey at sub_agent_started. Stable across turns. */
41
41
  originatingTurnKey: string
42
+ /**
43
+ * True if this member was dispatched with `run_in_background: true`.
44
+ * Sticky — does NOT clear when status later promotes from background →
45
+ * running. Used by `hasLiveBackground` to keep the parent turn's
46
+ * PerChatState alive even after the member starts doing tool work.
47
+ */
48
+ isBackgroundDispatch: boolean
42
49
  }
43
50
 
44
51
  export interface CreateFleetMemberArgs {
@@ -46,6 +53,7 @@ export interface CreateFleetMemberArgs {
46
53
  role: string
47
54
  startedAt: number
48
55
  originatingTurnKey: string
56
+ isBackgroundDispatch?: boolean
49
57
  }
50
58
 
51
59
  export function createFleetMember(args: CreateFleetMemberArgs): FleetMember {
@@ -60,6 +68,7 @@ export function createFleetMember(args: CreateFleetMemberArgs): FleetMember {
60
68
  terminalAt: null,
61
69
  errorSeen: false,
62
70
  originatingTurnKey: args.originatingTurnKey,
71
+ isBackgroundDispatch: args.isBackgroundDispatch ?? false,
63
72
  }
64
73
  }
65
74
 
@@ -69,10 +78,12 @@ export function applyToolUse(
69
78
  input: Record<string, unknown> | undefined,
70
79
  now: number,
71
80
  ): FleetMember {
72
- // P3 of #662 recovery from stuck. A live tool event proves the
73
- // sub-agent is alive again, so flip status back to running. Terminal
74
- // statuses (done/failed/killed) are sticky and never reset here.
75
- const status: FleetStatus = member.status === 'stuck' ? 'running' : member.status
81
+ // A live tool event proves the sub-agent is active flip stuck or
82
+ // background back to running. Terminal statuses (done/failed/killed)
83
+ // are sticky and never reset here. Fixes #757: background members
84
+ // previously stayed even while doing real work.
85
+ const status: FleetStatus =
86
+ member.status === 'stuck' || member.status === 'background' ? 'running' : member.status
76
87
  return {
77
88
  ...member,
78
89
  status,
@@ -126,15 +137,19 @@ export function markStuck(member: FleetMember, now: number, idleMs: number = 60_
126
137
  }
127
138
 
128
139
  /**
129
- * P2 of #662 / fixes #64 — true if any fleet member is in
130
- * `status: 'background'` AND has not yet reached terminal state. Used by
131
- * the driver's dispose path to keep a PerChatState alive past parent
132
- * turn_end while background sub-agents are still running, and by the v2
133
- * renderer's phase resolver to choose ⏸ Background vs ✅ Done.
140
+ * P2 of #662 / fixes #64 — true if any fleet member was dispatched as a
141
+ * background worker AND has not yet reached terminal state. Used by the
142
+ * driver's dispose path to keep a PerChatState alive past parent turn_end
143
+ * while background sub-agents are still running, and by the v2 renderer's
144
+ * phase resolver to choose ⏸ Background vs ✅ Done.
145
+ *
146
+ * Uses `isBackgroundDispatch` (sticky) rather than current `status`, because
147
+ * background members promote to `running` once tool activity is observed
148
+ * (fixes #757 — card goes silent for active background workers).
134
149
  */
135
150
  export function hasLiveBackground(fleet: ReadonlyMap<string, FleetMember>): boolean {
136
151
  for (const m of fleet.values()) {
137
- if (m.status === 'background' && m.terminalAt == null) return true
152
+ if (m.isBackgroundDispatch && m.terminalAt == null) return true
138
153
  }
139
154
  return false
140
155
  }
@@ -114,12 +114,47 @@ try {
114
114
  }
115
115
 
116
116
  // ─── Bot token ────────────────────────────────────────────────────────────
117
- const TOKEN = process.env.TELEGRAM_BOT_TOKEN
118
- if (!TOKEN) {
117
+ // Issue #758: when bot_token is a `vault:` ref and no .env was written,
118
+ // materialize it from the vault at startup (in-memory only).
119
+ //
120
+ // The outer try/catch is narrowed (post-#761 review) to ONLY catch
121
+ // ERR_MODULE_NOT_FOUND from the dynamic import. Other errors (e.g. throws
122
+ // from inside materializeBotToken that aren't BotTokenMaterializeError)
123
+ // must propagate so we don't mask real bugs behind the legacy "set in .env"
124
+ // hint.
125
+ type MaterializeMod = typeof import('../../src/telegram/materialize-bot-token.js')
126
+ let materializeMod: MaterializeMod | null = null
127
+ try {
128
+ materializeMod = await import('../../src/telegram/materialize-bot-token.js')
129
+ } catch (err) {
130
+ const code = (err as NodeJS.ErrnoException | undefined)?.code
131
+ if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
132
+ // Module missing — fall through with materializeMod=null.
133
+ } else {
134
+ throw err
135
+ }
136
+ }
137
+
138
+ let TOKEN: string
139
+ if (materializeMod !== null) {
140
+ const { materializeBotToken, BotTokenMaterializeError } = materializeMod
141
+ try {
142
+ TOKEN = await materializeBotToken()
143
+ } catch (err) {
144
+ if (err instanceof BotTokenMaterializeError) {
145
+ process.stderr.write(`foreman: ${err.message}\n`)
146
+ process.exit(1)
147
+ }
148
+ throw err
149
+ }
150
+ } else if (process.env.TELEGRAM_BOT_TOKEN) {
151
+ TOKEN = process.env.TELEGRAM_BOT_TOKEN
152
+ } else {
119
153
  process.stderr.write(
120
154
  `foreman: TELEGRAM_BOT_TOKEN required\n` +
121
155
  ` set in ${ENV_FILE}\n` +
122
- ` format: TELEGRAM_BOT_TOKEN=123456789:AAH...\n`,
156
+ ` format: TELEGRAM_BOT_TOKEN=123456789:AAH...\n` +
157
+ ` (token-materialization helper not found)\n`,
123
158
  )
124
159
  process.exit(1)
125
160
  }