switchroom 0.16.47 → 0.17.0

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 (106) hide show
  1. package/dist/agent-scheduler/index.js +3 -1
  2. package/dist/auth-broker/index.js +24 -8
  3. package/dist/cli/drive-write-pretool.mjs +0 -5
  4. package/dist/cli/notion-write-pretool.mjs +3 -1
  5. package/dist/cli/switchroom.js +1358 -1030
  6. package/dist/cli/ui/index.html +84 -12
  7. package/dist/host-control/main.js +53 -17
  8. package/dist/vault/approvals/kernel-server.js +4 -1
  9. package/dist/vault/broker/server.js +201 -56
  10. package/package.json +3 -3
  11. package/profiles/_base/cron-session.sh.hbs +1 -1
  12. package/profiles/_base/start.sh.hbs +54 -3
  13. package/skills/switchroom-architecture/telegram.md +8 -15
  14. package/skills/switchroom-cli/SKILL.md +4 -5
  15. package/skills/telegram-test-harness/SKILL.md +1 -1
  16. package/telegram-plugin/README.md +18 -29
  17. package/telegram-plugin/bridge/bridge.ts +1 -41
  18. package/telegram-plugin/bridge/tool-filter.ts +3 -4
  19. package/telegram-plugin/dist/bridge/bridge.js +8 -43
  20. package/telegram-plugin/dist/gateway/gateway.js +682 -773
  21. package/telegram-plugin/dist/server.js +8 -43
  22. package/telegram-plugin/gateway/busy-key-reaper.ts +113 -0
  23. package/telegram-plugin/gateway/disconnect-flush.ts +11 -0
  24. package/telegram-plugin/gateway/escalation-bridge-gate.ts +46 -0
  25. package/telegram-plugin/gateway/gate-parity-probe.ts +102 -0
  26. package/telegram-plugin/gateway/gateway.ts +518 -624
  27. package/telegram-plugin/gateway/inbound-delivery-confirm.ts +89 -7
  28. package/telegram-plugin/gateway/inbound-spool.ts +108 -10
  29. package/telegram-plugin/gateway/model-command.ts +51 -3
  30. package/telegram-plugin/gateway/pending-inbound-buffer.ts +26 -0
  31. package/telegram-plugin/gateway/represent-guard.ts +28 -11
  32. package/telegram-plugin/gateway/status-pin-store.ts +124 -45
  33. package/telegram-plugin/gateway/worker-feed-dispatch.ts +19 -0
  34. package/telegram-plugin/history.ts +5 -0
  35. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +1 -2
  36. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +9 -1
  37. package/telegram-plugin/registry/subagents-schema.ts +126 -1
  38. package/telegram-plugin/registry/turns-schema.ts +65 -1
  39. package/telegram-plugin/session-tail.ts +26 -4
  40. package/telegram-plugin/slot-banner-driver.ts +42 -2
  41. package/telegram-plugin/status-query-telemetry.ts +100 -0
  42. package/telegram-plugin/stream-reply-handler.ts +15 -16
  43. package/telegram-plugin/subagent-watcher.ts +182 -30
  44. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +4 -10
  45. package/telegram-plugin/tests/busy-key-reaper.test.ts +191 -0
  46. package/telegram-plugin/tests/emission-authority-facade.test.ts +11 -17
  47. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +5 -26
  48. package/telegram-plugin/tests/escalation-bridge-gate.test.ts +38 -0
  49. package/telegram-plugin/tests/gate-parity-probe.test.ts +171 -0
  50. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +13 -0
  51. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +14 -11
  52. package/telegram-plugin/tests/inbound-delivery-confirm.test.ts +146 -0
  53. package/telegram-plugin/tests/inbound-spool.test.ts +143 -0
  54. package/telegram-plugin/tests/model-command.test.ts +54 -1
  55. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +5 -11
  56. package/telegram-plugin/tests/nested-worker-visibility-harness.test.ts +329 -0
  57. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +53 -0
  58. package/telegram-plugin/tests/progress-update-redact.test.ts +99 -0
  59. package/telegram-plugin/tests/registry-turns.test.ts +67 -0
  60. package/telegram-plugin/tests/represent-guard.test.ts +42 -6
  61. package/telegram-plugin/tests/resume-inbound-builder.test.ts +1 -0
  62. package/telegram-plugin/tests/session-tail.test.ts +10 -1
  63. package/telegram-plugin/tests/slot-banner-boot-recovery.test.ts +246 -0
  64. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +0 -14
  65. package/telegram-plugin/tests/status-pin-store.test.ts +220 -5
  66. package/telegram-plugin/tests/status-query-telemetry.test.ts +115 -0
  67. package/telegram-plugin/tests/subagent-nested-dispatch.test.ts +209 -0
  68. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +37 -0
  69. package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +167 -0
  70. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +46 -3
  71. package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +70 -0
  72. package/telegram-plugin/tests/tool-activity-summary.test.ts +16 -0
  73. package/telegram-plugin/tests/tool-filter.test.ts +1 -3
  74. package/telegram-plugin/tests/tool-label-pretool.test.ts +1 -4
  75. package/telegram-plugin/tests/turn-flush-safety.test.ts +222 -1
  76. package/telegram-plugin/tests/worker-activity-feed.test.ts +202 -9
  77. package/telegram-plugin/tests/worker-feed-dispatch.test.ts +25 -0
  78. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +295 -0
  79. package/telegram-plugin/tool-activity-summary.ts +19 -0
  80. package/telegram-plugin/turn-flush-safety.ts +16 -1
  81. package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +8 -9
  82. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +1 -1
  83. package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +1 -1
  84. package/telegram-plugin/worker-activity-feed.ts +75 -15
  85. package/vendor/hindsight-memory/CHANGELOG.md +24 -0
  86. package/vendor/hindsight-memory/README.md +5 -0
  87. package/vendor/hindsight-memory/scripts/lib/client.py +31 -1
  88. package/vendor/hindsight-memory/scripts/lib/config.py +41 -2
  89. package/vendor/hindsight-memory/scripts/lib/content.py +4 -1
  90. package/vendor/hindsight-memory/scripts/lib/daemon.py +11 -2
  91. package/vendor/hindsight-memory/scripts/recall.py +74 -1
  92. package/vendor/hindsight-memory/scripts/retain.py +8 -1
  93. package/vendor/hindsight-memory/scripts/tests/test_config_client_casts.py +111 -0
  94. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +85 -1
  95. package/vendor/hindsight-memory/scripts/tests/test_recall_tag_filters.py +107 -0
  96. package/vendor/hindsight-memory/settings.json +4 -0
  97. package/vendor/hindsight-memory/tests/test_client.py +130 -0
  98. package/vendor/hindsight-memory/tests/test_config.py +47 -0
  99. package/vendor/hindsight-memory/tests/test_content.py +18 -0
  100. package/vendor/hindsight-memory/tests/test_hooks.py +62 -0
  101. package/telegram-plugin/gateway/error-envelope-card.ts +0 -64
  102. package/telegram-plugin/gateway/resolve-calling-subagent.ts +0 -78
  103. package/telegram-plugin/silent-reply.ts +0 -58
  104. package/telegram-plugin/tests/error-envelope-unlock-card.test.ts +0 -79
  105. package/telegram-plugin/tests/resolve-calling-subagent.test.ts +0 -269
  106. package/telegram-plugin/tests/silent-reply-guard.test.ts +0 -122
@@ -16826,13 +16826,11 @@ var exports_client = {};
16826
16826
  __export(exports_client, {
16827
16827
  withAuthBrokerClient: () => withAuthBrokerClient,
16828
16828
  resolveAuthBrokerSocketPath: () => resolveAuthBrokerSocketPath,
16829
- authBrokerSocketExists: () => authBrokerSocketExists,
16830
16829
  AuthBrokerUnreachableError: () => AuthBrokerUnreachableError,
16831
16830
  AuthBrokerError: () => AuthBrokerError,
16832
16831
  AuthBrokerClient: () => AuthBrokerClient
16833
16832
  });
16834
16833
  import * as net from "node:net";
16835
- import { existsSync as existsSync7 } from "node:fs";
16836
16834
  import { homedir as homedir3 } from "node:os";
16837
16835
  import { randomUUID as randomUUID3 } from "node:crypto";
16838
16836
  import { join as join11 } from "node:path";
@@ -17133,9 +17131,6 @@ async function withAuthBrokerClient(fn, opts) {
17133
17131
  await client3.close();
17134
17132
  }
17135
17133
  }
17136
- function authBrokerSocketExists(opts) {
17137
- return existsSync7(resolveAuthBrokerSocketPath(opts));
17138
- }
17139
17134
  var DEFAULT_TIMEOUT_MS = 5000, AuthBrokerError, AuthBrokerUnreachableError;
17140
17135
  var init_client = __esm(() => {
17141
17136
  init_protocol();
@@ -17166,11 +17161,14 @@ var init_flock = () => {};
17166
17161
  import { randomBytes as randomBytes3, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
17167
17162
  import {
17168
17163
  readFileSync as readFileSync7,
17169
- writeFileSync as writeFileSync8,
17170
- existsSync as existsSync8,
17164
+ writeSync,
17165
+ existsSync as existsSync7,
17171
17166
  renameSync as renameSync4,
17172
17167
  mkdirSync as mkdirSync10,
17173
17168
  unlinkSync as unlinkSync4,
17169
+ fsyncSync,
17170
+ openSync,
17171
+ closeSync,
17174
17172
  lstatSync,
17175
17173
  realpathSync
17176
17174
  } from "node:fs";
@@ -17205,7 +17203,7 @@ function normalizeSecrets(raw) {
17205
17203
  return out;
17206
17204
  }
17207
17205
  function openVault(passphrase, vaultPath) {
17208
- if (!existsSync8(vaultPath)) {
17206
+ if (!existsSync7(vaultPath)) {
17209
17207
  throw new VaultError(`Vault file not found: ${vaultPath}`);
17210
17208
  }
17211
17209
  let vaultFile;
@@ -24385,7 +24383,9 @@ var init_schema = __esm(() => {
24385
24383
  enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
24386
24384
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
24387
24385
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
24388
- resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
24386
+ resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
24387
+ session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
24388
+ session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
24389
24389
  }).optional();
24390
24390
  webhookDispatchRule = exports_external.object({
24391
24391
  description: exports_external.string().optional(),
@@ -24912,7 +24912,7 @@ var init_schema = __esm(() => {
24912
24912
  });
24913
24913
 
24914
24914
  // ../src/config/paths.ts
24915
- import { existsSync as existsSync9 } from "node:fs";
24915
+ import { existsSync as existsSync8 } from "node:fs";
24916
24916
  import { resolve as resolve2 } from "node:path";
24917
24917
  function home() {
24918
24918
  return process.env.HOME ?? "/root";
@@ -24928,7 +24928,7 @@ function resolveStatePath2(fragment) {
24928
24928
  const h = home();
24929
24929
  const primary = resolve2(h, DEFAULT_STATE_DIR, fragment);
24930
24930
  const legacy = resolve2(h, LEGACY_STATE_DIR, fragment);
24931
- if (!existsSync9(primary) && existsSync9(legacy)) {
24931
+ if (!existsSync8(primary) && existsSync8(legacy)) {
24932
24932
  warnLegacyStateOnce(legacy);
24933
24933
  return legacy;
24934
24934
  }
@@ -24941,9 +24941,9 @@ function resolveDualPath(pathStr) {
24941
24941
  const absolute = resolve2(h, rest);
24942
24942
  if (rest.startsWith(`${DEFAULT_STATE_DIR}/`)) {
24943
24943
  const frag = rest.slice(DEFAULT_STATE_DIR.length + 1);
24944
- if (!existsSync9(absolute)) {
24944
+ if (!existsSync8(absolute)) {
24945
24945
  const legacy = resolve2(h, LEGACY_STATE_DIR, frag);
24946
- if (existsSync9(legacy)) {
24946
+ if (existsSync8(legacy)) {
24947
24947
  warnLegacyStateOnce(legacy);
24948
24948
  return legacy;
24949
24949
  }
@@ -24968,7 +24968,7 @@ var init_overlay_schema = __esm(() => {
24968
24968
  });
24969
24969
 
24970
24970
  // ../src/config/overlay-loader.ts
24971
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
24971
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
24972
24972
  import { basename as basename4, resolve as resolve3 } from "node:path";
24973
24973
  function deriveOverlayTitle(raw, fileName) {
24974
24974
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
@@ -24984,7 +24984,7 @@ function overlayDirFor(agentName3, subdir) {
24984
24984
  return resolve3(base);
24985
24985
  }
24986
24986
  function listYamlFiles(dir) {
24987
- if (!existsSync10(dir))
24987
+ if (!existsSync9(dir))
24988
24988
  return [];
24989
24989
  let entries;
24990
24990
  try {
@@ -25509,7 +25509,7 @@ function validateNotionWorkspaceConfig(config) {
25509
25509
  }
25510
25510
 
25511
25511
  // ../src/config/loader.ts
25512
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "node:fs";
25512
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "node:fs";
25513
25513
  import { homedir as homedir4 } from "node:os";
25514
25514
  import { resolve as resolve4 } from "node:path";
25515
25515
  function formatZodErrors(error) {
@@ -25576,7 +25576,7 @@ function findConfigFile(startDir) {
25576
25576
  resolve4(userDir, "clerk.yml")
25577
25577
  ].filter(Boolean);
25578
25578
  for (const path of searchPaths) {
25579
- if (existsSync11(path)) {
25579
+ if (existsSync10(path)) {
25580
25580
  return path;
25581
25581
  }
25582
25582
  }
@@ -25584,7 +25584,7 @@ function findConfigFile(startDir) {
25584
25584
  }
25585
25585
  function loadConfig(configPath) {
25586
25586
  const filePath = configPath ?? findConfigFile();
25587
- if (!existsSync11(filePath)) {
25587
+ if (!existsSync10(filePath)) {
25588
25588
  throw new ConfigError(`Config file not found: ${filePath}`);
25589
25589
  }
25590
25590
  let raw;
@@ -25725,7 +25725,6 @@ function getPeerCred(fd) {
25725
25725
  return null;
25726
25726
  }
25727
25727
  }
25728
-
25729
25728
  // ../src/vault/broker/peercred.ts
25730
25729
  function isReservedAgentName(name) {
25731
25730
  return RESERVED_AGENT_NAMES.has(name);
@@ -26258,7 +26257,7 @@ import {
26258
26257
  openSync as openSync2,
26259
26258
  rmSync,
26260
26259
  statSync as statSync5,
26261
- writeSync
26260
+ writeSync as writeSync2
26262
26261
  } from "node:fs";
26263
26262
  import { join as join13 } from "node:path";
26264
26263
  import { tmpdir } from "node:os";
@@ -26325,7 +26324,7 @@ function writeFileExclusive(filePath, content) {
26325
26324
  const buf = typeof content === "string" ? Buffer.from(content, "utf8") : content;
26326
26325
  const fd = openSync2(filePath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
26327
26326
  try {
26328
- writeSync(fd, buf);
26327
+ writeSync2(fd, buf);
26329
26328
  } finally {
26330
26329
  closeSync2(fd);
26331
26330
  }
@@ -28861,7 +28860,7 @@ __export(exports_history, {
28861
28860
  checkpointWal: () => checkpointWal,
28862
28861
  _resetForTests: () => _resetForTests
28863
28862
  });
28864
- import { chmodSync as chmodSync3, existsSync as existsSync18, mkdirSync as mkdirSync15 } from "fs";
28863
+ import { chmodSync as chmodSync3, existsSync as existsSync17, mkdirSync as mkdirSync15 } from "fs";
28865
28864
  import { join as join16 } from "path";
28866
28865
  function loadDatabaseClass() {
28867
28866
  if (DatabaseClass != null)
@@ -28890,6 +28889,7 @@ function initHistory(stateDir, retentionDays = 30) {
28890
28889
  db = new Database(path, { create: true });
28891
28890
  db.exec("PRAGMA journal_mode = WAL");
28892
28891
  db.exec("PRAGMA synchronous = NORMAL");
28892
+ db.exec("PRAGMA busy_timeout = 5000");
28893
28893
  db.exec(`
28894
28894
  CREATE TABLE IF NOT EXISTS messages (
28895
28895
  chat_id TEXT NOT NULL,
@@ -28922,7 +28922,7 @@ function initHistory(stateDir, retentionDays = 30) {
28922
28922
  }
28923
28923
  for (const suffix of ["", "-shm", "-wal"]) {
28924
28924
  const f = path + suffix;
28925
- if (existsSync18(f)) {
28925
+ if (existsSync17(f)) {
28926
28926
  try {
28927
28927
  chmodSync3(f, 420);
28928
28928
  } catch {}
@@ -28947,7 +28947,7 @@ function checkpointWal() {
28947
28947
  if (dbPath) {
28948
28948
  for (const suffix of ["-shm", "-wal"]) {
28949
28949
  const f = dbPath + suffix;
28950
- if (existsSync18(f)) {
28950
+ if (existsSync17(f)) {
28951
28951
  try {
28952
28952
  chmodSync3(f, 420);
28953
28953
  } catch {}
@@ -29114,11 +29114,11 @@ var init_history = __esm(() => {
29114
29114
  });
29115
29115
 
29116
29116
  // quota-check.ts
29117
- import { readFileSync as readFileSync14, existsSync as existsSync19 } from "fs";
29117
+ import { readFileSync as readFileSync14, existsSync as existsSync18 } from "fs";
29118
29118
  import { join as join17 } from "path";
29119
29119
  function readOauthToken(claudeConfigDir) {
29120
29120
  const tokenFile = join17(claudeConfigDir, ".oauth-token");
29121
- if (!existsSync19(tokenFile))
29121
+ if (!existsSync18(tokenFile))
29122
29122
  return null;
29123
29123
  try {
29124
29124
  const raw = readFileSync14(tokenFile, "utf-8").trim();
@@ -29455,7 +29455,7 @@ function renderAuthLine(state4, agentName3, now = Date.now()) {
29455
29455
  }
29456
29456
 
29457
29457
  // gateway/quota-cache.ts
29458
- import { existsSync as existsSync32, readFileSync as readFileSync29, writeFileSync as writeFileSync21, mkdirSync as mkdirSync22 } from "fs";
29458
+ import { existsSync as existsSync31, readFileSync as readFileSync29, writeFileSync as writeFileSync20, mkdirSync as mkdirSync22 } from "fs";
29459
29459
  import { join as join28, dirname as dirname9 } from "path";
29460
29460
  function defaultCachePath() {
29461
29461
  return process.env.SWITCHROOM_QUOTA_CACHE_PATH ?? join28(process.env.HOME ?? "/tmp", ".switchroom", "quota-cache.json");
@@ -29463,7 +29463,7 @@ function defaultCachePath() {
29463
29463
  function readQuotaCache(opts = {}) {
29464
29464
  const path = opts.path ?? defaultCachePath();
29465
29465
  const now = opts.now ?? Date.now();
29466
- if (!existsSync32(path))
29466
+ if (!existsSync31(path))
29467
29467
  return null;
29468
29468
  let entry;
29469
29469
  try {
@@ -29494,7 +29494,7 @@ function writeQuotaCache(result, opts = {}) {
29494
29494
  };
29495
29495
  try {
29496
29496
  mkdirSync22(dirname9(path), { recursive: true });
29497
- writeFileSync21(path, JSON.stringify(entry, null, 2), { mode: 384 });
29497
+ writeFileSync20(path, JSON.stringify(entry, null, 2), { mode: 384 });
29498
29498
  } catch {}
29499
29499
  }
29500
29500
  var DEFAULT_TTL_MS4, RATE_LIMIT_TTL_MS;
@@ -29504,10 +29504,10 @@ var init_quota_cache = __esm(() => {
29504
29504
  });
29505
29505
 
29506
29506
  // gateway/boot-probes.ts
29507
- import { readFileSync as readFileSync30, readdirSync as readdirSync6, existsSync as existsSync33 } from "fs";
29507
+ import { readFileSync as readFileSync30, readdirSync as readdirSync6, existsSync as existsSync32 } from "fs";
29508
29508
  import { join as join29 } from "path";
29509
29509
  import { execFile as execFileCb } from "child_process";
29510
- import { promisify as promisify3 } from "util";
29510
+ import { promisify } from "util";
29511
29511
  async function withTimeout(label, p, timeoutMs = PROBE_TIMEOUT_MS) {
29512
29512
  let timer3;
29513
29513
  const timeout = new Promise((resolve6) => {
@@ -29572,7 +29572,7 @@ async function probeAccount(agentDir) {
29572
29572
  join29(claudeDir, ".oauth-token.meta.json"),
29573
29573
  join29(claudeDir, "accounts", "default", ".oauth-token.meta.json")
29574
29574
  ]) {
29575
- if (existsSync33(candidate)) {
29575
+ if (existsSync32(candidate)) {
29576
29576
  try {
29577
29577
  const meta = JSON.parse(readFileSync30(candidate, "utf8"));
29578
29578
  if (meta.expiresAt) {
@@ -29749,7 +29749,7 @@ async function resolveTmuxSupervisorPid(agentName3, execFileImpl) {
29749
29749
  if (!cgroup)
29750
29750
  return null;
29751
29751
  const procsPath = `/sys/fs/cgroup${cgroup}/cgroup.procs`;
29752
- if (!existsSync33(procsPath))
29752
+ if (!existsSync32(procsPath))
29753
29753
  return null;
29754
29754
  const pidsRaw = readFileSync30(procsPath, "utf-8");
29755
29755
  const pids = pidsRaw.split(`
@@ -29826,7 +29826,7 @@ async function probeAgentProcess(agentName3, opts = {}) {
29826
29826
  const retryIntervalMs = opts.retryIntervalMs ?? AGENT_RETRY_INTERVAL_MS;
29827
29827
  const retryMaxMs = opts.retryMaxMs ?? AGENT_RETRY_MAX_MS;
29828
29828
  const sleep2 = opts.sleepImpl ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
29829
- const execFileFn = opts.execFileImpl ?? execFile3;
29829
+ const execFileFn = opts.execFileImpl ?? execFile;
29830
29830
  return withTimeout("Agent", (async () => {
29831
29831
  const startMs = Date.now();
29832
29832
  while (true) {
@@ -29868,7 +29868,7 @@ async function* watchAgentProcess(agentName3, opts = {}) {
29868
29868
  const pollIntervalMs = opts.pollIntervalMs ?? AGENT_LIVE_POLL_INTERVAL_MS;
29869
29869
  const followupRepollMs = opts.followupRepollMs ?? AGENT_LIVE_FOLLOWUP_REPOLL_MS;
29870
29870
  const sleep2 = opts.sleepImpl ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
29871
- const execFileFn = opts.execFileImpl ?? execFile3;
29871
+ const execFileFn = opts.execFileImpl ?? execFile;
29872
29872
  const now = opts.nowImpl ?? (() => Date.now());
29873
29873
  const startMs = now();
29874
29874
  let lastYieldedDetail = null;
@@ -29958,7 +29958,7 @@ async function probeQuota(claudeConfigDir, _agentDir, fetchImpl = fetch, opts =
29958
29958
  claudeConfigDir,
29959
29959
  join29(claudeConfigDir, "accounts", "default")
29960
29960
  ]) {
29961
- if (existsSync33(join29(candidate, ".oauth-token"))) {
29961
+ if (existsSync32(join29(candidate, ".oauth-token"))) {
29962
29962
  claudeDirForProbe = candidate;
29963
29963
  break;
29964
29964
  }
@@ -30129,7 +30129,7 @@ async function probeUds(label, socketPath, opts = {}) {
30129
30129
  }
30130
30130
  return withTimeout(label, (async () => {
30131
30131
  if (!opts.connectImpl) {
30132
- if (!existsSync33(socketPath)) {
30132
+ if (!existsSync32(socketPath)) {
30133
30133
  return {
30134
30134
  status: "fail",
30135
30135
  label,
@@ -30285,11 +30285,11 @@ async function probeConnections(agentDir, opts = {}) {
30285
30285
  };
30286
30286
  })());
30287
30287
  }
30288
- var execFile3, PROBE_TIMEOUT_MS = 2000, QUOTA_BROKER_TIMEOUT_MS = 7000, QUOTA_DIRECT_FALLBACK_TIMEOUT_MS = 5000, QUOTA_PROBE_OUTER_TIMEOUT_MS = 9000, TOKEN_EXPIRING_SOON_DAYS = 7, AGENT_RETRY_INTERVAL_MS = 1500, AGENT_RETRY_MAX_MS = 12000, AGENT_LIVE_WINDOW_MS = 45000, AGENT_LIVE_POLL_INTERVAL_MS = 2000, AGENT_LIVE_FOLLOWUP_REPOLL_MS = 30000, realProcFs, SCHEDULER_LOCK_PATH_DEFAULT = "/state/agent/scheduler.lock", SCHEDULER_JSONL_PATH_DEFAULT = "/state/agent/scheduler.jsonl", SCHEDULER_FRESH_BOOT_MS = 30000, realSchedulerFs, realSkillsFs;
30288
+ var execFile, PROBE_TIMEOUT_MS = 2000, QUOTA_BROKER_TIMEOUT_MS = 7000, QUOTA_DIRECT_FALLBACK_TIMEOUT_MS = 5000, QUOTA_PROBE_OUTER_TIMEOUT_MS = 9000, TOKEN_EXPIRING_SOON_DAYS = 7, AGENT_RETRY_INTERVAL_MS = 1500, AGENT_RETRY_MAX_MS = 12000, AGENT_LIVE_WINDOW_MS = 45000, AGENT_LIVE_POLL_INTERVAL_MS = 2000, AGENT_LIVE_FOLLOWUP_REPOLL_MS = 30000, realProcFs, SCHEDULER_LOCK_PATH_DEFAULT = "/state/agent/scheduler.lock", SCHEDULER_JSONL_PATH_DEFAULT = "/state/agent/scheduler.jsonl", SCHEDULER_FRESH_BOOT_MS = 30000, realSchedulerFs, realSkillsFs;
30289
30289
  var init_boot_probes = __esm(() => {
30290
30290
  init_quota_cache();
30291
30291
  init_quota_check();
30292
- execFile3 = promisify3(execFileCb);
30292
+ execFile = promisify(execFileCb);
30293
30293
  realProcFs = {
30294
30294
  readdir: (p) => readdirSync6(p),
30295
30295
  readFile: (p) => readFileSync30(p, "utf-8")
@@ -30300,16 +30300,16 @@ var init_boot_probes = __esm(() => {
30300
30300
  const { statSync: statSync10 } = __require("fs");
30301
30301
  return statSync10(p).mtimeMs;
30302
30302
  },
30303
- exists: (p) => existsSync33(p)
30303
+ exists: (p) => existsSync32(p)
30304
30304
  };
30305
30305
  realSkillsFs = {
30306
30306
  readdir: (p) => readdirSync6(p),
30307
- exists: (p) => existsSync33(p)
30307
+ exists: (p) => existsSync32(p)
30308
30308
  };
30309
30309
  });
30310
30310
 
30311
30311
  // gateway/boot-issue-cache.ts
30312
- import { existsSync as existsSync34, readFileSync as readFileSync31, writeFileSync as writeFileSync22, mkdirSync as mkdirSync23, renameSync as renameSync11 } from "fs";
30312
+ import { existsSync as existsSync33, readFileSync as readFileSync31, writeFileSync as writeFileSync21, mkdirSync as mkdirSync23, renameSync as renameSync11 } from "fs";
30313
30313
  import { dirname as dirname10 } from "path";
30314
30314
  function fingerprintProbe(key, r) {
30315
30315
  if (r.status === "ok")
@@ -30389,7 +30389,7 @@ function diffProbes(probes, cache, opts = {}) {
30389
30389
  return out;
30390
30390
  }
30391
30391
  function loadCache(path, now = Date.now) {
30392
- if (!existsSync34(path))
30392
+ if (!existsSync33(path))
30393
30393
  return { ...EMPTY_CACHE, probes: {} };
30394
30394
  let raw;
30395
30395
  try {
@@ -30438,7 +30438,7 @@ function applyAndSave(path, cache, diff) {
30438
30438
  try {
30439
30439
  mkdirSync23(dirname10(path), { recursive: true });
30440
30440
  const tmp = `${path}.tmp`;
30441
- writeFileSync22(tmp, JSON.stringify(next), { mode: 384 });
30441
+ writeFileSync21(tmp, JSON.stringify(next), { mode: 384 });
30442
30442
  renameSync11(tmp, path);
30443
30443
  } catch {}
30444
30444
  return next;
@@ -30452,7 +30452,7 @@ var init_boot_issue_cache = __esm(() => {
30452
30452
 
30453
30453
  // gateway/config-snapshot.ts
30454
30454
  import { createHash as createHash2 } from "crypto";
30455
- import { existsSync as existsSync35, readFileSync as readFileSync32, writeFileSync as writeFileSync23, mkdirSync as mkdirSync24, renameSync as renameSync12 } from "fs";
30455
+ import { existsSync as existsSync34, readFileSync as readFileSync32, writeFileSync as writeFileSync22, mkdirSync as mkdirSync24, renameSync as renameSync12 } from "fs";
30456
30456
  import { dirname as dirname11 } from "path";
30457
30457
  function hashStringArray(items) {
30458
30458
  if (!items || items.length === 0)
@@ -30517,7 +30517,7 @@ function renderConfigChangeDim(dim) {
30517
30517
  }
30518
30518
  }
30519
30519
  function loadSnapshot(path, now = Date.now) {
30520
- if (!existsSync35(path))
30520
+ if (!existsSync34(path))
30521
30521
  return null;
30522
30522
  let raw;
30523
30523
  try {
@@ -30553,7 +30553,7 @@ function persistSnapshot(path, snapshot) {
30553
30553
  try {
30554
30554
  mkdirSync24(dirname11(path), { recursive: true });
30555
30555
  const tmp = `${path}.tmp`;
30556
- writeFileSync23(tmp, JSON.stringify(snapshot), { mode: 384 });
30556
+ writeFileSync22(tmp, JSON.stringify(snapshot), { mode: 384 });
30557
30557
  renameSync12(tmp, path);
30558
30558
  } catch {}
30559
30559
  }
@@ -30562,7 +30562,7 @@ var init_config_snapshot = __esm(() => {
30562
30562
  });
30563
30563
 
30564
30564
  // gateway/boot-card-msgid.ts
30565
- import { readFileSync as readFileSync33, writeFileSync as writeFileSync24 } from "node:fs";
30565
+ import { readFileSync as readFileSync33, writeFileSync as writeFileSync23 } from "node:fs";
30566
30566
  function bootCardChatKey(chatId, threadId) {
30567
30567
  return `${chatId}:${threadId ?? ""}`;
30568
30568
  }
@@ -30587,7 +30587,7 @@ function saveBootCardMsgId(path, chatKey3, messageId) {
30587
30587
  if (store2[chatKey3] === messageId)
30588
30588
  return;
30589
30589
  store2[chatKey3] = messageId;
30590
- writeFileSync24(path, JSON.stringify(store2), "utf8");
30590
+ writeFileSync23(path, JSON.stringify(store2), "utf8");
30591
30591
  } catch {}
30592
30592
  }
30593
30593
  var init_boot_card_msgid = () => {};
@@ -31476,7 +31476,7 @@ __export(exports_materialize_bot_token, {
31476
31476
  materializeBotToken: () => materializeBotToken,
31477
31477
  BotTokenMaterializeError: () => BotTokenMaterializeError
31478
31478
  });
31479
- import { existsSync as existsSync43 } from "node:fs";
31479
+ import { existsSync as existsSync42 } from "node:fs";
31480
31480
  function pickConfiguredToken(config, agentName3) {
31481
31481
  if (agentName3) {
31482
31482
  const agent = config.agents?.[agentName3];
@@ -31490,7 +31490,7 @@ function tryDirectVaultRead3(ref, config, passphrase) {
31490
31490
  if (!passphrase)
31491
31491
  return null;
31492
31492
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
31493
- if (!existsSync43(vaultPath))
31493
+ if (!existsSync42(vaultPath))
31494
31494
  return null;
31495
31495
  try {
31496
31496
  const secrets = openVault(passphrase, vaultPath);
@@ -31582,7 +31582,7 @@ __export(exports_tmux, {
31582
31582
  captureAgentPane: () => captureAgentPane
31583
31583
  });
31584
31584
  import { execFileSync as execFileSync5 } from "node:child_process";
31585
- import { mkdirSync as mkdirSync31, readdirSync as readdirSync8, statSync as statSync14, unlinkSync as unlinkSync15, writeFileSync as writeFileSync30 } from "node:fs";
31585
+ import { mkdirSync as mkdirSync31, readdirSync as readdirSync8, statSync as statSync14, unlinkSync as unlinkSync15, writeFileSync as writeFileSync29 } from "node:fs";
31586
31586
  import { resolve as resolve7 } from "node:path";
31587
31587
  function captureAgentPane(opts) {
31588
31588
  const { agentName: agentName3, agentDir, reason } = opts;
@@ -31628,7 +31628,7 @@ function captureAgentPane(opts) {
31628
31628
  ` + `
31629
31629
  `;
31630
31630
  try {
31631
- writeFileSync30(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
31631
+ writeFileSync29(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
31632
31632
  mode: 420
31633
31633
  });
31634
31634
  } catch (err) {
@@ -32191,7 +32191,7 @@ import { randomBytes as randomBytes8 } from "crypto";
32191
32191
  import { execFileSync as execFileSync6, execSync as execSync2, spawn } from "child_process";
32192
32192
  import {
32193
32193
  readFileSync as readFileSync41,
32194
- writeFileSync as writeFileSync31,
32194
+ writeFileSync as writeFileSync30,
32195
32195
  mkdirSync as mkdirSync32,
32196
32196
  readdirSync as readdirSync9,
32197
32197
  rmSync as rmSync4,
@@ -32201,7 +32201,7 @@ import {
32201
32201
  chmodSync as chmodSync7,
32202
32202
  openSync as openSync9,
32203
32203
  closeSync as closeSync9,
32204
- existsSync as existsSync44,
32204
+ existsSync as existsSync43,
32205
32205
  unlinkSync as unlinkSync16,
32206
32206
  appendFileSync as appendFileSync6
32207
32207
  } from "fs";
@@ -34776,6 +34776,12 @@ function formatFeedElapsed(ms) {
34776
34776
  const m = Math.floor(s / 60);
34777
34777
  return `${m}m${(s % 60).toString().padStart(2, "0")}s`;
34778
34778
  }
34779
+ var STEP_TIMER_MIN_MS = 1e4;
34780
+ function formatStepSuffix(stepElapsedMs) {
34781
+ if (stepElapsedMs < STEP_TIMER_MIN_MS)
34782
+ return "";
34783
+ return ` \u00b7 ${formatFeedElapsed(stepElapsedMs)}`;
34784
+ }
34779
34785
  function escapeStepLine(raw) {
34780
34786
  const cleaned = stripMarkdown(raw).replace(/\s+/g, " ").trim();
34781
34787
  return escapeMarkdown(truncate(cleaned, STATUS_LINE_MAX));
@@ -34976,9 +34982,10 @@ function createWorkerActivityFeed(opts) {
34976
34982
  const line = view.latestSummary.trim();
34977
34983
  if (line.length === 0)
34978
34984
  return;
34979
- if (h.narrative[h.narrative.length - 1] === line)
34985
+ if (h.narrative.includes(line))
34980
34986
  return;
34981
34987
  h.narrative.push(line);
34988
+ h.stepStartedAtMs = nowFn();
34982
34989
  if (h.narrative.length > STATUS_ROLLING_LINES) {
34983
34990
  h.narrative.splice(0, h.narrative.length - STATUS_ROLLING_LINES);
34984
34991
  }
@@ -35045,7 +35052,7 @@ function createWorkerActivityFeed(opts) {
35045
35052
  function heartbeatTick() {
35046
35053
  const now = nowFn();
35047
35054
  for (const h of handles.values()) {
35048
- if (h.messageId == null)
35055
+ if (!handles.has(h.agentId))
35049
35056
  continue;
35050
35057
  if (h.lastView == null)
35051
35058
  continue;
@@ -35053,13 +35060,23 @@ function createWorkerActivityFeed(opts) {
35053
35060
  continue;
35054
35061
  if (now < h.cooldownUntil)
35055
35062
  continue;
35063
+ const liveElapsed = h.dispatchAtMs != null ? now - h.dispatchAtMs : h.lastView.elapsedMs;
35064
+ if (h.messageId == null) {
35065
+ if (liveElapsed < firstPaintMin)
35066
+ continue;
35067
+ const view2 = { ...h.lastView, elapsedMs: Math.max(h.lastView.elapsedMs, liveElapsed) };
35068
+ h.chain = h.chain.then(() => doUpdate(h, view2)).catch((err) => {
35069
+ log(`worker-feed: heartbeat first-paint chain error ${h.agentId}: ${err.message}`);
35070
+ });
35071
+ continue;
35072
+ }
35056
35073
  if (now - h.lastEditAt < minEditInterval)
35057
35074
  continue;
35058
35075
  const stale = now - h.lastEditAt >= heartbeatTickMs;
35059
35076
  if (!stale)
35060
35077
  continue;
35061
- const liveElapsed = h.dispatchAtMs != null ? now - h.dispatchAtMs : h.lastView.elapsedMs;
35062
- const liveSuffix = " \u00b7 " + formatFeedElapsed(liveElapsed);
35078
+ const stepElapsed = h.stepStartedAtMs != null ? now - h.stepStartedAtMs : liveElapsed;
35079
+ const liveSuffix = formatStepSuffix(stepElapsed);
35063
35080
  const view = { ...h.lastView, elapsedMs: Math.max(h.lastView.elapsedMs, liveElapsed) };
35064
35081
  h.chain = h.chain.then(() => doUpdate(h, view, liveSuffix)).catch((err) => {
35065
35082
  log(`worker-feed: heartbeat chain error ${h.agentId}: ${err.message}`);
@@ -35093,7 +35110,8 @@ function createWorkerActivityFeed(opts) {
35093
35110
  narrative: [],
35094
35111
  chain: Promise.resolve(),
35095
35112
  lastView: null,
35096
- dispatchAtMs: null
35113
+ dispatchAtMs: null,
35114
+ stepStartedAtMs: null
35097
35115
  };
35098
35116
  handles.set(agentId, h);
35099
35117
  }
@@ -35389,6 +35407,12 @@ function formatFeedElapsed2(ms) {
35389
35407
  const m = Math.floor(s / 60);
35390
35408
  return `${m}m${(s % 60).toString().padStart(2, "0")}s`;
35391
35409
  }
35410
+ var STEP_TIMER_MIN_MS2 = 1e4;
35411
+ function formatStepSuffix2(stepElapsedMs) {
35412
+ if (stepElapsedMs < STEP_TIMER_MIN_MS2)
35413
+ return "";
35414
+ return ` \u00b7 ${formatFeedElapsed2(stepElapsedMs)}`;
35415
+ }
35392
35416
  function escapeStepLine2(raw) {
35393
35417
  const cleaned = stripMarkdown(raw).replace(/\s+/g, " ").trim();
35394
35418
  return escapeMarkdown(truncate(cleaned, STATUS_LINE_MAX));
@@ -35767,9 +35791,6 @@ function chatKey(chatId, threadId) {
35767
35791
  const t = threadId == null || threadId === 0 ? "_" : String(threadId);
35768
35792
  return `${chatId}:${t}`;
35769
35793
  }
35770
- function chatKeyWithSuffix(chatId, threadId, suffix) {
35771
- return `${chatKey(chatId, threadId)}:${suffix}`;
35772
- }
35773
35794
  function chatIdOfChatKey(key) {
35774
35795
  const idx = key.indexOf(":");
35775
35796
  return idx === -1 ? key : key.slice(0, idx);
@@ -36218,185 +36239,6 @@ function handlePtyPartialPure(text, state, deps) {
36218
36239
  stream.update(text).catch(() => {});
36219
36240
  return created ? "update-new" : "update-existing";
36220
36241
  }
36221
- // stream-reply-handler.ts
36222
- function buildAccentHeader(accent) {
36223
- switch (accent) {
36224
- case "in-progress":
36225
- return `\uD83D\uDD35 _In progress\u2026_
36226
-
36227
- `;
36228
- case "done":
36229
- return `\u2705 **Done**
36230
-
36231
- `;
36232
- case "issue":
36233
- return `\u26a0\ufe0f **Issue**
36234
-
36235
- `;
36236
- default:
36237
- return "";
36238
- }
36239
- }
36240
- function streamKey2(chatId, threadId, lane, turnKey) {
36241
- const base = lane != null && lane.length > 0 ? chatKeyWithSuffix(chatId, threadId ?? null, lane) : chatKey(chatId, threadId ?? null);
36242
- return turnKey != null && turnKey.length > 0 ? `${base}:${turnKey}` : base;
36243
- }
36244
- async function handleStreamReply(args, state, deps) {
36245
- const chat_id = args.chat_id;
36246
- let rawText = deps.normalizeParagraphBreaks ? deps.normalizeParagraphBreaks(deps.repairEscapedWhitespace(args.text)) : deps.repairEscapedWhitespace(args.text);
36247
- if (deps.normalizePunctuation)
36248
- rawText = deps.normalizePunctuation(rawText);
36249
- if (deps.stripExcessBold)
36250
- rawText = deps.stripExcessBold(rawText);
36251
- const done = Boolean(args.done);
36252
- const format = args.format ?? deps.defaultFormat;
36253
- if (done) {
36254
- process.stderr.write(`telegram channel: stream_reply: invoked done=true chatId=${chat_id} lane=${args.lane ?? "default"} charCount=${rawText.length}
36255
- `);
36256
- }
36257
- deps.assertAllowedChat(chat_id);
36258
- const threadId = deps.resolveThreadId(chat_id, args.message_thread_id);
36259
- const literalText = format === "text";
36260
- let effectiveText = !literalText && deps.addParagraphSpacers ? deps.addParagraphSpacers(rawText) : rawText;
36261
- if (args.accent != null) {
36262
- const accentHeader = buildAccentHeader(args.accent);
36263
- if (accentHeader.length > 0) {
36264
- effectiveText = accentHeader + effectiveText;
36265
- }
36266
- }
36267
- if (effectiveText.length > RICH_MESSAGE_MAX_CHARS) {
36268
- throw new Error(`stream_reply rejected: text exceeds Telegram's ${RICH_MESSAGE_MAX_CHARS}-char rich-message limit ` + `(length=${effectiveText.length}, format=${format}). stream_reply does not ` + `auto-chunk \u2014 split the text or use \`reply\`, which chunks.`);
36269
- }
36270
- const sKey = streamKey2(chat_id, threadId, args.lane, args.turnKey);
36271
- state.suppressPtyPreview?.add(streamKey2(chat_id, threadId));
36272
- let stream = state.activeDraftStreams.get(sKey);
36273
- const streamExisted = stream != null;
36274
- deps.logStreamingEvent({
36275
- kind: "stream_reply_called",
36276
- chatId: chat_id,
36277
- charCount: effectiveText.length,
36278
- done,
36279
- streamExisted
36280
- });
36281
- if (!stream) {
36282
- let replyToMessageId;
36283
- if (args.reply_to != null) {
36284
- replyToMessageId = Number(args.reply_to);
36285
- } else if (args.quote !== false && deps.getLatestInboundMessageId != null) {
36286
- try {
36287
- const latest = deps.getLatestInboundMessageId(chat_id, threadId ?? null);
36288
- if (latest != null)
36289
- replyToMessageId = latest;
36290
- } catch (err) {
36291
- deps.writeError(`telegram channel: stream_reply quote-lookup failed: ${err}
36292
- `);
36293
- }
36294
- }
36295
- let initialMessageId;
36296
- if (deps.lookupExistingMessageId != null) {
36297
- try {
36298
- const looked = deps.lookupExistingMessageId({
36299
- chatId: chat_id,
36300
- threadId,
36301
- lane: args.lane,
36302
- turnKey: args.turnKey
36303
- });
36304
- if (typeof looked === "number" && Number.isFinite(looked)) {
36305
- initialMessageId = looked;
36306
- }
36307
- } catch (err) {
36308
- deps.writeError(`telegram channel: stream_reply lookupExistingMessageId failed: ${err}
36309
- `);
36310
- }
36311
- }
36312
- stream = createStreamController({
36313
- bot: deps.bot,
36314
- chatId: chat_id,
36315
- threadId,
36316
- literalText,
36317
- disableLinkPreview: deps.disableLinkPreview,
36318
- ...deps.throttleMs != null ? { throttleMs: deps.throttleMs } : {},
36319
- retry: deps.retry,
36320
- ...replyToMessageId != null ? { replyToMessageId } : {},
36321
- ...args.quote_text != null && replyToMessageId != null ? { quoteText: args.quote_text } : {},
36322
- ...args.protect_content === true ? { protectContent: true } : {},
36323
- ...args.disable_notification === true ? { disableNotification: true } : {},
36324
- ...args.reply_markup != null ? { replyMarkup: args.reply_markup } : {},
36325
- isPrivateChat: deps.isPrivateChat === true,
36326
- ...initialMessageId != null ? { initialMessageId } : {},
36327
- onSend: (messageId, charCount) => deps.logStreamingEvent({ kind: "draft_send", chatId: chat_id, messageId, charCount }),
36328
- onEdit: (messageId, charCount) => deps.logStreamingEvent({
36329
- kind: "draft_edit",
36330
- chatId: chat_id,
36331
- messageId,
36332
- charCount,
36333
- sameAsLast: false
36334
- }),
36335
- log: (msg) => {
36336
- if (msg.startsWith("stream \u2192 sent") || msg.startsWith("stream \u2192 edited") || msg.startsWith("stream \u2192 not modified") || msg.startsWith("stream finalized") || msg.startsWith("stream \u2192 materialized"))
36337
- return;
36338
- deps.writeError(`telegram channel: stream_reply ${msg}
36339
- `);
36340
- },
36341
- warn: (msg) => {
36342
- deps.writeError(`telegram channel: stream_reply ${msg}
36343
- `);
36344
- }
36345
- });
36346
- state.activeDraftStreams.set(sKey, stream);
36347
- }
36348
- await stream.update(effectiveText);
36349
- if (done) {
36350
- await stream.finalize();
36351
- state.activeDraftStreams.delete(sKey);
36352
- if (stream.getMessageId() == null) {
36353
- throw new Error(`stream_reply finalized without sending any message (length=${rawText.length}, ` + `max=${RICH_MESSAGE_MAX_CHARS}). Telegram's rich-message limit is ${RICH_MESSAGE_MAX_CHARS} chars and ` + `stream_reply does not auto-chunk. Split the text or use \`reply\` (which chunks).`);
36354
- }
36355
- if (deps.historyEnabled) {
36356
- const finalId = stream.getMessageId();
36357
- if (finalId != null) {
36358
- try {
36359
- deps.recordOutbound({
36360
- chat_id,
36361
- thread_id: threadId ?? null,
36362
- message_ids: [finalId],
36363
- texts: [rawText]
36364
- });
36365
- } catch (err) {
36366
- deps.writeError(`telegram channel: history recordOutbound (stream_reply) failed: ${err}
36367
- `);
36368
- }
36369
- }
36370
- }
36371
- }
36372
- const finalMessageId = stream.getMessageId();
36373
- if (done) {
36374
- process.stderr.write(`telegram channel: stream_reply: finalized done=true chatId=${chat_id} lane=${args.lane ?? "default"} messageId=${finalMessageId ?? "null"}
36375
- `);
36376
- const isDefaultLaneForCompletion = args.lane == null || args.lane.length === 0;
36377
- if (deps.recordOutboundDelivered != null && finalMessageId != null && isDefaultLaneForCompletion) {
36378
- try {
36379
- deps.recordOutboundDelivered(chat_id, threadId);
36380
- } catch (err) {
36381
- deps.writeError(`telegram channel: stream_reply: recordOutboundDelivered hook threw: ${err}
36382
- `);
36383
- }
36384
- }
36385
- if (deps.forceCompleteTurn != null && isDefaultLaneForCompletion) {
36386
- try {
36387
- deps.forceCompleteTurn(chat_id, threadId);
36388
- } catch (err) {
36389
- deps.writeError(`telegram channel: stream_reply: forceCompleteTurn hook threw: ${err}
36390
- `);
36391
- }
36392
- }
36393
- }
36394
- return {
36395
- messageId: finalMessageId,
36396
- status: done ? "finalized" : "updated"
36397
- };
36398
- }
36399
-
36400
36242
  // chat-lock.ts
36401
36243
  function createChatLock() {
36402
36244
  const chains = new Map;
@@ -43677,8 +43519,8 @@ function defaultAddAccount(label, credentials, opts) {
43677
43519
  }
43678
43520
 
43679
43521
  // ../src/auth/broker/client.ts
43680
- import * as net3 from "node:net";
43681
43522
  init_protocol();
43523
+ import * as net3 from "node:net";
43682
43524
  import { homedir as homedir6 } from "node:os";
43683
43525
  import { randomUUID as randomUUID4 } from "node:crypto";
43684
43526
  import { join as join14 } from "node:path";
@@ -43997,13 +43839,13 @@ class AuthBrokerClient2 {
43997
43839
  init_loader();
43998
43840
  init_resolver();
43999
43841
  init_vault();
44000
- import { existsSync as existsSync13 } from "node:fs";
43842
+ import { existsSync as existsSync12 } from "node:fs";
44001
43843
  var DEFAULT_VOICE_API_KEY_REF = "vault:openai/api-key";
44002
43844
  function tryDirectVaultRead(ref, config, passphrase) {
44003
43845
  if (!passphrase)
44004
43846
  return null;
44005
43847
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
44006
- if (!existsSync13(vaultPath))
43848
+ if (!existsSync12(vaultPath))
44007
43849
  return null;
44008
43850
  try {
44009
43851
  const secrets = openVault(passphrase, vaultPath);
@@ -44059,14 +43901,14 @@ async function materializeVoiceKey(opts = {}, logger2 = (line) => process.stderr
44059
43901
  init_loader();
44060
43902
  init_resolver();
44061
43903
  init_vault();
44062
- import { existsSync as existsSync14 } from "node:fs";
43904
+ import { existsSync as existsSync13 } from "node:fs";
44063
43905
  var VOICE_SIDECAR_TOKEN_KEY = "voice/sidecar-token";
44064
43906
  var DEFAULT_VOICE_SIDECAR_TOKEN_REF = `vault:${VOICE_SIDECAR_TOKEN_KEY}`;
44065
43907
  function tryDirectVaultRead2(ref, config, passphrase) {
44066
43908
  if (!passphrase)
44067
43909
  return null;
44068
43910
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
44069
- if (!existsSync14(vaultPath))
43911
+ if (!existsSync13(vaultPath))
44070
43912
  return null;
44071
43913
  try {
44072
43914
  const secrets = openVault(passphrase, vaultPath);
@@ -44119,14 +43961,14 @@ async function materializeSidecarToken(opts = {}, logger2 = (line) => process.st
44119
43961
  }
44120
43962
 
44121
43963
  // ../src/setup/host-capabilities.ts
44122
- import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "node:fs";
43964
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
44123
43965
  init_paths();
44124
43966
  function hostCapabilitiesPath() {
44125
43967
  return resolveStatePath2("host-capabilities.json");
44126
43968
  }
44127
43969
  function loadHostCapabilities() {
44128
43970
  const path = hostCapabilitiesPath();
44129
- if (!existsSync15(path))
43971
+ if (!existsSync14(path))
44130
43972
  return null;
44131
43973
  try {
44132
43974
  const parsed = JSON.parse(readFileSync11(path, "utf-8"));
@@ -44227,7 +44069,7 @@ function resolveExhaustUntil(resetAtMs, now = Date.now()) {
44227
44069
 
44228
44070
  // gateway/auth-add-flow.ts
44229
44071
  import { execFileSync } from "node:child_process";
44230
- import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync3, statSync as statSync7, writeFileSync as writeFileSync11 } from "node:fs";
44072
+ import { existsSync as existsSync16, mkdirSync as mkdirSync14, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync3, statSync as statSync7, writeFileSync as writeFileSync10 } from "node:fs";
44231
44073
  import { homedir as homedir7 } from "node:os";
44232
44074
  import { join as join15 } from "node:path";
44233
44075
  import { randomBytes as randomBytes4 } from "node:crypto";
@@ -44236,8 +44078,8 @@ import { randomBytes as randomBytes4 } from "node:crypto";
44236
44078
  import {
44237
44079
  readFileSync as readFileSync12,
44238
44080
  readdirSync as readdirSync3,
44239
- existsSync as existsSync16,
44240
- writeFileSync as writeFileSync10,
44081
+ existsSync as existsSync15,
44082
+ writeFileSync as writeFileSync9,
44241
44083
  mkdirSync as mkdirSync13,
44242
44084
  mkdtempSync as mkdtempSync2,
44243
44085
  rmSync as rmSync2,
@@ -44263,7 +44105,7 @@ function parseSetupTokenUrl(output) {
44263
44105
  }
44264
44106
  function readTokenFromCredentialsFile(credentialsFilePath) {
44265
44107
  try {
44266
- if (!existsSync16(credentialsFilePath))
44108
+ if (!existsSync15(credentialsFilePath))
44267
44109
  return null;
44268
44110
  const raw = readFileSync12(credentialsFilePath, "utf-8");
44269
44111
  const parsed = JSON.parse(raw);
@@ -44335,7 +44177,7 @@ function cleanScratchDir(scratchDir) {
44335
44177
  var AUTH_TMUX_SESSION_FILE = ".auth-tmux-session";
44336
44178
  function sweepOrphanSessions(home2, tmux, nowMs2 = Date.now()) {
44337
44179
  const inProgressDir = join15(home2, ".switchroom", "accounts", ".in-progress");
44338
- if (!existsSync17(inProgressDir))
44180
+ if (!existsSync16(inProgressDir))
44339
44181
  return;
44340
44182
  let entries;
44341
44183
  try {
@@ -44347,7 +44189,7 @@ function sweepOrphanSessions(home2, tmux, nowMs2 = Date.now()) {
44347
44189
  for (const entry of entries) {
44348
44190
  const dir = join15(inProgressDir, entry);
44349
44191
  const sessionFile = join15(dir, AUTH_TMUX_SESSION_FILE);
44350
- if (!existsSync17(sessionFile))
44192
+ if (!existsSync16(sessionFile))
44351
44193
  continue;
44352
44194
  let fileContents;
44353
44195
  let fileMtime;
@@ -44386,7 +44228,7 @@ async function startAccountAuthSession(label, opts = {}) {
44386
44228
  const tmuxSocket = `switchroom-${agentName3}`;
44387
44229
  const tmuxSession = `auth-add-${label}-${hexSuffix}`.slice(0, 64);
44388
44230
  try {
44389
- writeFileSync11(join15(scratchDir, AUTH_TMUX_SESSION_FILE), `${tmuxSocket}
44231
+ writeFileSync10(join15(scratchDir, AUTH_TMUX_SESSION_FILE), `${tmuxSocket}
44390
44232
  ${tmuxSession}`, "utf8");
44391
44233
  } catch {}
44392
44234
  const sessionEnv = {
@@ -44445,7 +44287,7 @@ async function submitAccountAuthCode(flow, code, opts = {}) {
44445
44287
  const deadline = Date.now() + pollTimeoutMs;
44446
44288
  while (Date.now() < deadline) {
44447
44289
  await new Promise((r) => setTimeout(r, pollIntervalMs));
44448
- if (existsSync17(credentialsPath)) {
44290
+ if (existsSync16(credentialsPath)) {
44449
44291
  const token = readTokenFromCredentialsFile(credentialsPath);
44450
44292
  if (token) {
44451
44293
  try {
@@ -44459,7 +44301,7 @@ async function submitAccountAuthCode(flow, code, opts = {}) {
44459
44301
  }
44460
44302
  }
44461
44303
  if (!tmux.hasSession(flow.tmuxSocket, flow.tmuxSession)) {
44462
- if (!existsSync17(credentialsPath)) {
44304
+ if (!existsSync16(credentialsPath)) {
44463
44305
  cleanScratchDir(flow.scratchDir);
44464
44306
  throw new Error("claude setup-token exited without writing credentials \u2014 the code may be invalid or expired");
44465
44307
  }
@@ -46228,6 +46070,7 @@ function decideTurnFlush(input) {
46228
46070
  if (input.chatId == null)
46229
46071
  return { kind: "skip", reason: "no-inbound-chat" };
46230
46072
  const joined = input.capturedText.join(`
46073
+
46231
46074
  `).trim();
46232
46075
  if (joined.length === 0)
46233
46076
  return { kind: "skip", reason: "empty-text" };
@@ -46259,7 +46102,7 @@ function resolveAgentDirFromEnv() {
46259
46102
  }
46260
46103
 
46261
46104
  // active-reactions.ts
46262
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, renameSync as renameSync5, existsSync as existsSync20, unlinkSync as unlinkSync5 } from "node:fs";
46105
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, renameSync as renameSync5, existsSync as existsSync19, unlinkSync as unlinkSync5 } from "node:fs";
46263
46106
  import { join as join18 } from "node:path";
46264
46107
  var ACTIVE_REACTIONS_FILENAME = ".active-reactions.json";
46265
46108
  function reactionsPath(agentDir) {
@@ -46267,7 +46110,7 @@ function reactionsPath(agentDir) {
46267
46110
  }
46268
46111
  function readActiveReactions(agentDir) {
46269
46112
  const p = reactionsPath(agentDir);
46270
- if (!existsSync20(p))
46113
+ if (!existsSync19(p))
46271
46114
  return [];
46272
46115
  let raw;
46273
46116
  try {
@@ -46303,7 +46146,7 @@ function writeActiveReactions(agentDir, reactions) {
46303
46146
  }
46304
46147
  const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
46305
46148
  try {
46306
- writeFileSync12(tmp, JSON.stringify(reactions) + `
46149
+ writeFileSync11(tmp, JSON.stringify(reactions) + `
46307
46150
  `, "utf-8");
46308
46151
  renameSync5(tmp, p);
46309
46152
  } catch {}
@@ -46327,7 +46170,7 @@ function clearActiveReactions(agentDir) {
46327
46170
  }
46328
46171
 
46329
46172
  // active-reactions.ts
46330
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, renameSync as renameSync6, existsSync as existsSync21, unlinkSync as unlinkSync6 } from "node:fs";
46173
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, renameSync as renameSync6, existsSync as existsSync20, unlinkSync as unlinkSync6 } from "node:fs";
46331
46174
  import { join as join19 } from "node:path";
46332
46175
  var ACTIVE_REACTIONS_FILENAME2 = ".active-reactions.json";
46333
46176
  function reactionsPath2(agentDir) {
@@ -46335,7 +46178,7 @@ function reactionsPath2(agentDir) {
46335
46178
  }
46336
46179
  function readActiveReactions2(agentDir) {
46337
46180
  const p = reactionsPath2(agentDir);
46338
- if (!existsSync21(p))
46181
+ if (!existsSync20(p))
46339
46182
  return [];
46340
46183
  let raw;
46341
46184
  try {
@@ -46399,6 +46242,7 @@ function flushOnAgentDisconnect(deps) {
46399
46242
  activeReactionMsgIds,
46400
46243
  activeTurnStartedAt,
46401
46244
  claudeBusyKeys,
46245
+ claudeBusyKeySince,
46402
46246
  activeDraftStreams,
46403
46247
  clearActiveReactions: clearActiveReactions3,
46404
46248
  disposeProgressDriver,
@@ -46415,6 +46259,7 @@ function flushOnAgentDisconnect(deps) {
46415
46259
  activeReactionMsgIds.delete(key);
46416
46260
  activeTurnStartedAt.delete(key);
46417
46261
  claudeBusyKeys.delete(key);
46262
+ claudeBusyKeySince.delete(key);
46418
46263
  }
46419
46264
  clearActiveReactions3();
46420
46265
  const danglingKeys = [...activeTurnStartedAt.keys()];
@@ -46423,6 +46268,7 @@ function flushOnAgentDisconnect(deps) {
46423
46268
  activeTurnStartedAt.delete(k);
46424
46269
  activeReactionMsgIds.delete(k);
46425
46270
  claudeBusyKeys.delete(k);
46271
+ claudeBusyKeySince.delete(k);
46426
46272
  }
46427
46273
  log(`telegram gateway: disconnect-flush swept ${danglingKeys.length} dangling turn key(s) ` + `post-bridge-death (controller loop missed \u2014 finalize raced disconnect)`);
46428
46274
  onDanglingTurnsSwept?.(danglingKeys);
@@ -46431,6 +46277,8 @@ function flushOnAgentDisconnect(deps) {
46431
46277
  const orphanCount = claudeBusyKeys.size;
46432
46278
  const orphanKeys = [...claudeBusyKeys];
46433
46279
  claudeBusyKeys.clear();
46280
+ for (const k of orphanKeys)
46281
+ claudeBusyKeySince.delete(k);
46434
46282
  log(`telegram gateway: disconnect-flush cleared ${orphanCount} orphan claudeBusyKeys ` + `entr${orphanCount === 1 ? "y" : "ies"} (synthetic-inbound deliveries that never turn_ended)` + ` keys=${orphanKeys.join(",")}`);
46435
46283
  }
46436
46284
  disposeProgressDriver();
@@ -46442,6 +46290,33 @@ function flushOnAgentDisconnect(deps) {
46442
46290
  return true;
46443
46291
  }
46444
46292
 
46293
+ // gateway/busy-key-reaper.ts
46294
+ function markBusyKeyLockstep(keys, since, key, now) {
46295
+ const wasBusy = keys.has(key);
46296
+ keys.add(key);
46297
+ if (!wasBusy)
46298
+ since.set(key, now);
46299
+ }
46300
+ function reapOrphanBusyKeys(keys, since, now, opts) {
46301
+ const reaped = [];
46302
+ for (const key of [...since.keys()]) {
46303
+ if (!keys.has(key)) {
46304
+ since.delete(key);
46305
+ continue;
46306
+ }
46307
+ if (opts.hasPendingDelivery(key))
46308
+ continue;
46309
+ const stampedAt = since.get(key) ?? now;
46310
+ if (now - stampedAt >= opts.ttlMs) {
46311
+ keys.delete(key);
46312
+ since.delete(key);
46313
+ reaped.push(key);
46314
+ opts.log?.(`telegram gateway: reaped orphan busy key=${key} (no turn in flight, ` + `no pending delivery, >${opts.ttlMs}ms) \u2014 unwedging idle-drain`);
46315
+ }
46316
+ }
46317
+ return reaped;
46318
+ }
46319
+
46445
46320
  // gateway/preamble-suppressor.ts
46446
46321
  var DEFAULT_BUFFER_MS = 150;
46447
46322
 
@@ -47214,14 +47089,14 @@ async function approvalRecord(args, opts) {
47214
47089
  }
47215
47090
 
47216
47091
  // quota-check.ts
47217
- import { readFileSync as readFileSync17, existsSync as existsSync22 } from "fs";
47092
+ import { readFileSync as readFileSync17, existsSync as existsSync21 } from "fs";
47218
47093
  import { join as join20 } from "path";
47219
47094
  var OAUTH_BETA2 = "oauth-2025-04-20";
47220
47095
  var DEFAULT_USER_AGENT2 = "claude-cli/1.0.0 (external, cli)";
47221
47096
  var DEFAULT_PROBE_MODEL2 = "claude-haiku-4-5-20251001";
47222
47097
  function readOauthToken2(claudeConfigDir) {
47223
47098
  const tokenFile = join20(claudeConfigDir, ".oauth-token");
47224
- if (!existsSync22(tokenFile))
47099
+ if (!existsSync21(tokenFile))
47225
47100
  return null;
47226
47101
  try {
47227
47102
  const raw = readFileSync17(tokenFile, "utf-8").trim();
@@ -47401,8 +47276,7 @@ init_loader();
47401
47276
  var TOKEN_VALIDITY_MS2 = 365 * 24 * 60 * 60000;
47402
47277
 
47403
47278
  // ../src/agents/inject.ts
47404
- import { execFile, execFileSync as execFileSync2 } from "node:child_process";
47405
- import { promisify } from "node:util";
47279
+ import { execFileSync as execFileSync2 } from "node:child_process";
47406
47280
 
47407
47281
  // ../src/agents/pane-lock.ts
47408
47282
  var tails = new Map;
@@ -47419,7 +47293,6 @@ function withPaneLock(key, fn) {
47419
47293
  }
47420
47294
 
47421
47295
  // ../src/agents/inject.ts
47422
- var execFileAsync = promisify(execFile);
47423
47296
  var INJECT_COMMANDS = new Map([
47424
47297
  ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
47425
47298
  ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
@@ -47715,9 +47588,7 @@ async function injectSlashCommandWith(runner, args) {
47715
47588
  }
47716
47589
 
47717
47590
  // ../src/agents/inject.ts
47718
- import { execFile as execFile2, execFileSync as execFileSync3 } from "node:child_process";
47719
- import { promisify as promisify2 } from "node:util";
47720
- var execFileAsync2 = promisify2(execFile2);
47591
+ import { execFileSync as execFileSync3 } from "node:child_process";
47721
47592
  var INJECT_COMMANDS2 = new Map([
47722
47593
  ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
47723
47594
  ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
@@ -47785,7 +47656,7 @@ function makeTmuxRunner2(tmuxBin) {
47785
47656
  };
47786
47657
  }
47787
47658
  // stream-reply-handler.ts
47788
- function buildAccentHeader2(accent) {
47659
+ function buildAccentHeader(accent) {
47789
47660
  switch (accent) {
47790
47661
  case "in-progress":
47791
47662
  return `\uD83D\uDD35 _In progress\u2026_
@@ -47909,7 +47780,7 @@ Allowed: \`${deps.escapeHtml(allow)}\``, { html: true });
47909
47780
  }
47910
47781
  }
47911
47782
  const { body, accent } = shapeReply(result, slashCommand, deps);
47912
- const finalText = buildAccentHeader2(accent) + body;
47783
+ const finalText = buildAccentHeader(accent) + body;
47913
47784
  await deps.reply(ctx, finalText, { html: true, accent });
47914
47785
  }
47915
47786
 
@@ -48023,11 +47894,22 @@ async function handleModelCommand(parsed, deps) {
48023
47894
  };
48024
47895
  }
48025
47896
  if (result.outcome === "ok") {
47897
+ const confirmation = modelSwitchConfirmationLine(result.output);
47898
+ if (confirmation) {
47899
+ return {
47900
+ text: [
47901
+ `${verbHtml}`,
47902
+ deps.preBlock(confirmation),
47903
+ ...result.truncated ? ["_truncated_"] : [],
47904
+ PERSIST_NOTE
47905
+ ].join(`
47906
+ `),
47907
+ html: true
47908
+ };
47909
+ }
48026
47910
  return {
48027
47911
  text: [
48028
- `${verbHtml}`,
48029
- deps.preBlock(result.output),
48030
- ...result.truncated ? ["_truncated_"] : [],
47912
+ `${verbHtml} \u2014 switched (session).`,
48031
47913
  PERSIST_NOTE
48032
47914
  ].join(`
48033
47915
  `),
@@ -48321,6 +48203,12 @@ async function handleModelMenuCallback(data, deps) {
48321
48203
  function isSrToClaudeTransition(prevModel, nextModel) {
48322
48204
  return !!prevModel?.startsWith("sr-") && !nextModel.startsWith("sr-");
48323
48205
  }
48206
+ function modelSwitchConfirmationLine(output) {
48207
+ const line = output.split(`
48208
+ `).map((l) => l.trim()).find((l) => MODEL_SWITCH_CONFIRMATION_PREFIX.test(l));
48209
+ return line && line.length > 0 ? line : null;
48210
+ }
48211
+ var MODEL_SWITCH_CONFIRMATION_PREFIX = /^\s*[\u23fa\u25cf\u2022>-]?\s*(?:Set model to|Switched to|Kept model as)\b/i;
48324
48212
  function sessionModelFromConfirmation(confirmation) {
48325
48213
  const m = /(?:Set model to|Switched to)\s+(.+?)(?:\s+for (?:this|the) session|\s*\(|\s*$)/i.exec(confirmation.trim());
48326
48214
  const name = m?.[1]?.trim();
@@ -48781,7 +48669,7 @@ init_schema();
48781
48669
  init_paths();
48782
48670
  init_overlay_loader();
48783
48671
  init_merge();
48784
- import { readFileSync as readFileSync18, existsSync as existsSync23 } from "node:fs";
48672
+ import { readFileSync as readFileSync18, existsSync as existsSync22 } from "node:fs";
48785
48673
  import { homedir as homedir8 } from "node:os";
48786
48674
  import { resolve as resolve5 } from "node:path";
48787
48675
 
@@ -48857,7 +48745,7 @@ function findConfigFile2(startDir) {
48857
48745
  resolve5(userDir, "clerk.yml")
48858
48746
  ].filter(Boolean);
48859
48747
  for (const path of searchPaths) {
48860
- if (existsSync23(path)) {
48748
+ if (existsSync22(path)) {
48861
48749
  return path;
48862
48750
  }
48863
48751
  }
@@ -48865,7 +48753,7 @@ function findConfigFile2(startDir) {
48865
48753
  }
48866
48754
  function loadConfig2(configPath) {
48867
48755
  const filePath = configPath ?? findConfigFile2();
48868
- if (!existsSync23(filePath)) {
48756
+ if (!existsSync22(filePath)) {
48869
48757
  throw new ConfigError2(`Config file not found: ${filePath}`);
48870
48758
  }
48871
48759
  let raw;
@@ -49363,9 +49251,9 @@ function topicForRecipient(args) {
49363
49251
  }
49364
49252
 
49365
49253
  // ../src/agents/perf.ts
49366
- import { existsSync as existsSync24, readFileSync as readFileSync19 } from "node:fs";
49254
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
49367
49255
  function readTurnUsages(jsonlPath, lastN) {
49368
- if (!existsSync24(jsonlPath))
49256
+ if (!existsSync23(jsonlPath))
49369
49257
  return [];
49370
49258
  if (lastN <= 0)
49371
49259
  return [];
@@ -49462,7 +49350,7 @@ function numField(obj, key) {
49462
49350
  }
49463
49351
 
49464
49352
  // gateway/context-occupancy.ts
49465
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "node:fs";
49353
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync13 } from "node:fs";
49466
49354
  import { join as join21 } from "node:path";
49467
49355
  var CONTEXT_OCCUPANCY_FILENAME = "context-occupancy.json";
49468
49356
  var TIGHT_FRACTION = 0.8;
@@ -49488,7 +49376,7 @@ function writeContextOccupancySnapshot(stateDir, snapshot, deps) {
49488
49376
  try {
49489
49377
  const path = join21(stateDir, CONTEXT_OCCUPANCY_FILENAME);
49490
49378
  (deps?.mkdir ?? ((p, o) => mkdirSync16(p, o)))(stateDir, { recursive: true });
49491
- (deps?.writeFile ?? ((p, d) => writeFileSync14(p, d)))(path, JSON.stringify(snapshot, null, 2) + `
49379
+ (deps?.writeFile ?? ((p, d) => writeFileSync13(p, d)))(path, JSON.stringify(snapshot, null, 2) + `
49492
49380
  `);
49493
49381
  } catch {}
49494
49382
  }
@@ -49571,7 +49459,7 @@ function nextCompactNotify(state3, ev) {
49571
49459
  }
49572
49460
 
49573
49461
  // gateway/hostd-dispatch.ts
49574
- import { existsSync as existsSync25 } from "node:fs";
49462
+ import { existsSync as existsSync24 } from "node:fs";
49575
49463
  import { randomBytes as randomBytes5 } from "node:crypto";
49576
49464
 
49577
49465
  // ../src/host-control/client.ts
@@ -49896,13 +49784,13 @@ function hostdSocketPath(agentName3) {
49896
49784
  function hostdWillBeUsed(agentName3) {
49897
49785
  if (!isHostdEnabled())
49898
49786
  return false;
49899
- return existsSync25(hostdSocketPath(agentName3));
49787
+ return existsSync24(hostdSocketPath(agentName3));
49900
49788
  }
49901
49789
  async function tryHostdDispatch(agentName3, req, timeoutMs = 5000) {
49902
49790
  if (!isHostdEnabled())
49903
49791
  return "not-configured";
49904
49792
  const sockPath = hostdSocketPath(agentName3);
49905
- if (!existsSync25(sockPath))
49793
+ if (!existsSync24(sockPath))
49906
49794
  return "not-configured";
49907
49795
  try {
49908
49796
  return await hostdRequest({ socketPath: sockPath, timeoutMs }, req);
@@ -49926,7 +49814,7 @@ async function hostdGetStatusOnce(agentName3, targetRequestId) {
49926
49814
  if (!isHostdEnabled())
49927
49815
  return "not-configured";
49928
49816
  const sockPath = hostdSocketPath(agentName3);
49929
- if (!existsSync25(sockPath))
49817
+ if (!existsSync24(sockPath))
49930
49818
  return "not-configured";
49931
49819
  try {
49932
49820
  const resp = await hostdRequest({ socketPath: sockPath, timeoutMs: 3000 }, {
@@ -49947,7 +49835,7 @@ async function pollHostdStatus(agentName3, targetRequestId, opts) {
49947
49835
  if (!isHostdEnabled())
49948
49836
  return "not-configured";
49949
49837
  const sockPath = hostdSocketPath(agentName3);
49950
- if (!existsSync25(sockPath))
49838
+ if (!existsSync24(sockPath))
49951
49839
  return "not-configured";
49952
49840
  const now = opts.now ?? Date.now;
49953
49841
  const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -50026,7 +49914,7 @@ function shouldSweepChatAtBoot(chatId) {
50026
49914
 
50027
49915
  // gateway/webhook-ingest-server.ts
50028
49916
  import net4 from "node:net";
50029
- import { chmodSync as chmodSync4, existsSync as existsSync26, unlinkSync as unlinkSync7 } from "node:fs";
49917
+ import { chmodSync as chmodSync4, existsSync as existsSync25, unlinkSync as unlinkSync7 } from "node:fs";
50030
49918
  var MAX_REQUEST_BYTES = 1024 * 1024;
50031
49919
  function fdOf(conn) {
50032
49920
  const handle = conn._handle;
@@ -50038,7 +49926,7 @@ function startWebhookIngestServer(opts) {
50038
49926
  const log = opts.log ?? ((s) => process.stderr.write(s));
50039
49927
  const allowed = new Set(opts.allowedUids);
50040
49928
  try {
50041
- if (existsSync26(opts.socketPath))
49929
+ if (existsSync25(opts.socketPath))
50042
49930
  unlinkSync7(opts.socketPath);
50043
49931
  } catch (err) {
50044
49932
  log(`webhook-ingest-server: could not unlink stale socket: ${err.message}
@@ -50131,7 +50019,7 @@ function startWebhookIngestServer(opts) {
50131
50019
  server.close();
50132
50020
  } catch {}
50133
50021
  try {
50134
- if (existsSync26(opts.socketPath))
50022
+ if (existsSync25(opts.socketPath))
50135
50023
  unlinkSync7(opts.socketPath);
50136
50024
  } catch {}
50137
50025
  }
@@ -50144,13 +50032,13 @@ import { join as join24 } from "path";
50144
50032
  import { homedir as homedir10 } from "os";
50145
50033
 
50146
50034
  // ../src/web/webhook-handler.ts
50147
- import { appendFileSync as appendFileSync4, existsSync as existsSync27, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
50035
+ import { appendFileSync as appendFileSync4, existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
50148
50036
  import { join as join22 } from "path";
50149
50037
  var DEDUP_MAX = 1000;
50150
50038
  var DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
50151
50039
  function loadDedupFile(path) {
50152
50040
  try {
50153
- if (!existsSync27(path))
50041
+ if (!existsSync26(path))
50154
50042
  return {};
50155
50043
  const raw = JSON.parse(readFileSync20(path, "utf-8"));
50156
50044
  return typeof raw.deliveries === "object" && raw.deliveries !== null ? raw.deliveries : {};
@@ -50166,7 +50054,7 @@ function saveDedupFile(path, deliveries, now) {
50166
50054
  }
50167
50055
  const sorted = Object.entries(pruned).sort((a, b) => b[1] - a[1]).slice(0, DEDUP_MAX);
50168
50056
  const final = Object.fromEntries(sorted);
50169
- writeFileSync15(path, JSON.stringify({ deliveries: final }), {
50057
+ writeFileSync14(path, JSON.stringify({ deliveries: final }), {
50170
50058
  mode: 384
50171
50059
  });
50172
50060
  }
@@ -50196,7 +50084,7 @@ var tokenBuckets = new Map;
50196
50084
  var throttleIssueWindow = new Map;
50197
50085
 
50198
50086
  // ../src/web/webhook-dispatch.ts
50199
- import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "fs";
50087
+ import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
50200
50088
  import { join as join23 } from "path";
50201
50089
  import { homedir as homedir9 } from "os";
50202
50090
 
@@ -50496,7 +50384,7 @@ function cooldownKey(source, eventType, repo, number, ruleIndex) {
50496
50384
  }
50497
50385
  function loadCooldownFile(path) {
50498
50386
  try {
50499
- if (!existsSync28(path))
50387
+ if (!existsSync27(path))
50500
50388
  return {};
50501
50389
  const raw = JSON.parse(readFileSync21(path, "utf-8"));
50502
50390
  return typeof raw.dispatches === "object" && raw.dispatches !== null ? raw.dispatches : {};
@@ -50506,7 +50394,7 @@ function loadCooldownFile(path) {
50506
50394
  }
50507
50395
  function saveCooldownFile(path, dispatches) {
50508
50396
  try {
50509
- writeFileSync16(path, JSON.stringify({ dispatches }), {
50397
+ writeFileSync15(path, JSON.stringify({ dispatches }), {
50510
50398
  mode: 384
50511
50399
  });
50512
50400
  } catch {}
@@ -51971,6 +51859,11 @@ function createPendingInboundBuffer(opts = {}) {
51971
51859
  evicted = true;
51972
51860
  log(`pending-inbound-buffer: agent=${agent} cap=${cap} reached \u2014 ` + `dropped oldest entry source=${dropped?.meta?.source ?? "-"} ts=${dropped?.ts ?? "-"}
51973
51861
  `);
51862
+ if (dropped != null && opts.onEvict != null) {
51863
+ try {
51864
+ opts.onEvict(agent, dropped);
51865
+ } catch {}
51866
+ }
51974
51867
  }
51975
51868
  q.push(msg);
51976
51869
  spool?.put(agent, msg);
@@ -52265,28 +52158,41 @@ async function runStatusPinBootCleanup(args) {
52265
52158
  persistStatusPins(args.path, args.fs, [], log);
52266
52159
  return { cleared, total: persisted.length };
52267
52160
  }
52268
- async function reconcileAndPersistStatusPin(args) {
52161
+ var storeLockTails = new Map;
52162
+ function withStoreLock(path, fn) {
52163
+ const prev = storeLockTails.get(path) ?? Promise.resolve();
52164
+ const run3 = prev.then(fn, fn);
52165
+ storeLockTails.set(path, run3.then(() => {
52166
+ return;
52167
+ }, () => {
52168
+ return;
52169
+ }));
52170
+ return run3;
52171
+ }
52172
+ function applyStatusPinRow(path, fs2, pinKey, row, log) {
52173
+ const current = loadStatusPins(path, fs2);
52174
+ const others = current.filter((p) => p.pinKey !== pinKey);
52175
+ const next = row == null ? others : [...others, row];
52176
+ persistStatusPins(path, fs2, next, log);
52177
+ }
52178
+ function reconcileAndPersistStatusPin(args) {
52269
52179
  const { path, fs: fs2, pinKey, chatId, op } = args;
52270
52180
  const log = args.log ?? ((l) => process.stderr.write(l));
52271
- if (op.kind === "pin") {
52272
- persistStatusPins(path, fs2, [
52273
- ...args.snapshotOthers(),
52274
- { pinKey, chatId, messageId: op.messageId, pending: true }
52275
- ], log);
52276
- const next2 = await args.applyPin();
52277
- if (next2 == null) {
52278
- persistStatusPins(path, fs2, args.snapshotOthers(), log);
52279
- return null;
52181
+ return withStoreLock(path, async () => {
52182
+ if (op.kind === "pin") {
52183
+ applyStatusPinRow(path, fs2, pinKey, { pinKey, chatId, messageId: op.messageId, pending: true }, log);
52184
+ const next2 = await args.applyPin();
52185
+ if (next2 == null) {
52186
+ applyStatusPinRow(path, fs2, pinKey, null, log);
52187
+ return null;
52188
+ }
52189
+ applyStatusPinRow(path, fs2, pinKey, { pinKey, chatId, messageId: next2.messageId }, log);
52190
+ return next2;
52280
52191
  }
52281
- persistStatusPins(path, fs2, [
52282
- ...args.snapshotOthers(),
52283
- { pinKey, chatId, messageId: next2.messageId }
52284
- ], log);
52285
- return next2;
52286
- }
52287
- const next = await args.applyPin();
52288
- persistStatusPins(path, fs2, args.snapshotOthers(), log);
52289
- return next;
52192
+ const next = await args.applyPin();
52193
+ applyStatusPinRow(path, fs2, pinKey, null, log);
52194
+ return next;
52195
+ });
52290
52196
  }
52291
52197
 
52292
52198
  // gateway/with-deadline.ts
@@ -52336,11 +52242,19 @@ function driveEscalation(args) {
52336
52242
  function shouldSuppressRepresent(o, deps) {
52337
52243
  if (!deps.historyEnabled)
52338
52244
  return false;
52339
- if (o.lastRepresentedAt == null)
52340
- return false;
52245
+ if (o.lastRepresentedAt == null) {
52246
+ if (o.openedAt == null)
52247
+ return false;
52248
+ return deps.hasOutboundDeliveredSince(o.chatId, o.openedAt, o.threadId);
52249
+ }
52341
52250
  return deps.hasOutboundDeliveredSince(o.chatId, o.lastRepresentedAt, o.threadId);
52342
52251
  }
52343
52252
 
52253
+ // gateway/escalation-bridge-gate.ts
52254
+ function shouldDeferEscalationForBridge(deps) {
52255
+ return !deps.bridgeAlive;
52256
+ }
52257
+
52344
52258
  // gateway/inbound-spool.ts
52345
52259
  function spoolId(msg) {
52346
52260
  if (msg.meta?.source === "subagent_handback" && typeof msg.meta?.subagent_jsonl_id === "string" && msg.meta.subagent_jsonl_id.length > 0) {
@@ -52372,6 +52286,8 @@ function createInboundSpool(opts) {
52372
52286
  const escalateNoticeCooldownMs = opts.escalateNoticeCooldownMs ?? 30 * 60 * 1000;
52373
52287
  const live = new Map;
52374
52288
  const escAttemptByChat = new Map;
52289
+ let consecutiveAppendFailures = 0;
52290
+ let degraded = false;
52375
52291
  function parseLine(line) {
52376
52292
  const s = line.trim();
52377
52293
  if (!s)
@@ -52439,9 +52355,33 @@ function createInboundSpool(opts) {
52439
52355
  try {
52440
52356
  fs2.appendFileSync(path, JSON.stringify(rec) + `
52441
52357
  `);
52358
+ if (consecutiveAppendFailures > 0) {
52359
+ log(`inbound-spool: append recovered path=${path} after ` + `${consecutiveAppendFailures} failure(s) \u2014 durability restored
52360
+ `);
52361
+ consecutiveAppendFailures = 0;
52362
+ if (degraded) {
52363
+ degraded = false;
52364
+ try {
52365
+ opts.onDegraded?.({ degraded: false, consecutiveFailures: 0, path });
52366
+ } catch {}
52367
+ }
52368
+ }
52442
52369
  } catch (err) {
52443
- log(`inbound-spool: append FAILED path=${path} id=${rec.id} t=${rec.t}: ` + `${err.message} \u2014 durability degraded to in-memory
52370
+ consecutiveAppendFailures++;
52371
+ const message = err.message;
52372
+ log(`inbound-spool: append FAILED path=${path} id=${rec.id} t=${rec.t}: ` + `${message} \u2014 durability degraded to in-memory ` + `(consecutive failures=${consecutiveAppendFailures})
52444
52373
  `);
52374
+ if (!degraded) {
52375
+ degraded = true;
52376
+ try {
52377
+ opts.onDegraded?.({
52378
+ degraded: true,
52379
+ consecutiveFailures: consecutiveAppendFailures,
52380
+ path,
52381
+ error: message
52382
+ });
52383
+ } catch {}
52384
+ }
52445
52385
  }
52446
52386
  }
52447
52387
  function maybeCompact() {
@@ -52531,11 +52471,18 @@ function createInboundSpool(opts) {
52531
52471
  sweepEscalations(onEscalate) {
52532
52472
  const tNow = now();
52533
52473
  const cutoff = tNow - escalateAfterMs;
52534
- let dropped = 0;
52535
- let posted = 0;
52536
- for (const [id, e] of [...live.entries()]) {
52474
+ const toDrop = [];
52475
+ const perChatCount = new Map;
52476
+ for (const [id, e] of live.entries()) {
52537
52477
  if (e.firstAt > cutoff)
52538
52478
  continue;
52479
+ toDrop.push({ id, e });
52480
+ const key = escChatKey(e.msg);
52481
+ perChatCount.set(key, (perChatCount.get(key) ?? 0) + 1);
52482
+ }
52483
+ let dropped = 0;
52484
+ let posted = 0;
52485
+ for (const { id, e } of toDrop) {
52539
52486
  live.delete(id);
52540
52487
  appendRecord({ t: "ack", id });
52541
52488
  const key = escChatKey(e.msg);
@@ -52546,7 +52493,7 @@ function createInboundSpool(opts) {
52546
52493
  const thread = typeof threadRaw === "string" && threadRaw.length > 0 ? threadRaw : undefined;
52547
52494
  appendRecord({ t: "esc", chat: e.msg.chatId, thread, at: tNow });
52548
52495
  try {
52549
- onEscalate({ agent: e.agent, msg: e.msg }, { postNotice });
52496
+ onEscalate({ agent: e.agent, msg: e.msg }, { postNotice, droppedCount: perChatCount.get(key) ?? 1 });
52550
52497
  } catch (err) {
52551
52498
  log(`inbound-spool: onEscalate threw id=${id}: ${err.message}
52552
52499
  `);
@@ -52565,6 +52512,12 @@ function createInboundSpool(opts) {
52565
52512
  },
52566
52513
  liveCount() {
52567
52514
  return live.size;
52515
+ },
52516
+ isDegraded() {
52517
+ return degraded;
52518
+ },
52519
+ appendFailureCount() {
52520
+ return consecutiveAppendFailures;
52568
52521
  }
52569
52522
  };
52570
52523
  }
@@ -52931,12 +52884,23 @@ function createDeliveryQueue() {
52931
52884
  function trackDelivery(q, key, inbound, now, messageId = null) {
52932
52885
  q.pending.set(key, { key, inbound, messageId, lastAttemptAt: now });
52933
52886
  }
52934
- function ackDelivery(q, key, enqueueMessageId = null) {
52887
+ function extractEnqueueMessageIds(content) {
52888
+ const ids = [];
52889
+ const re = /(?:^|[\s"'])message_id="([^"]+)"/g;
52890
+ let m;
52891
+ while ((m = re.exec(content)) != null)
52892
+ ids.push(m[1]);
52893
+ return ids;
52894
+ }
52895
+ function ackDelivery(q, key, enqueueMessageId = null, enqueueContent = null) {
52935
52896
  const entry = q.pending.get(key);
52936
52897
  if (!entry)
52937
52898
  return false;
52938
- if (entry.messageId != null && entry.messageId !== enqueueMessageId)
52939
- return false;
52899
+ if (entry.messageId != null) {
52900
+ const matches = entry.messageId === enqueueMessageId || enqueueContent != null && extractEnqueueMessageIds(enqueueContent).includes(entry.messageId);
52901
+ if (!matches)
52902
+ return false;
52903
+ }
52940
52904
  q.pending.delete(key);
52941
52905
  return true;
52942
52906
  }
@@ -52950,6 +52914,13 @@ function sweep(q, now, timeoutMs) {
52950
52914
  }
52951
52915
  return redeliver;
52952
52916
  }
52917
+ function isRedeliverySuspended(key, suspended) {
52918
+ if (suspended.keys.has(key))
52919
+ return true;
52920
+ const idx = key.indexOf(":");
52921
+ const chatId = idx < 0 ? key : key.slice(0, idx);
52922
+ return suspended.chats.has(chatId);
52923
+ }
52953
52924
  function forgetDelivery(q, key) {
52954
52925
  q.pending.delete(key);
52955
52926
  }
@@ -53426,6 +53397,27 @@ function dispatchOne(effect, ctx) {
53426
53397
  }
53427
53398
  }
53428
53399
 
53400
+ // gateway/gate-parity-probe.ts
53401
+ function gateParityDivergence(machineInTurn, busyKeysSize) {
53402
+ const busy = busyKeysSize > 0;
53403
+ if (machineInTurn === busy)
53404
+ return "none";
53405
+ if (!machineInTurn && busy)
53406
+ return "busykeys_dangle";
53407
+ return "machine_over_holds";
53408
+ }
53409
+ function isDangerousGateDivergence(d) {
53410
+ return d === "machine_over_holds";
53411
+ }
53412
+ function probeGateParity(machineInTurn, busyKeysSize, log = (line) => process.stderr.write(line)) {
53413
+ const d = gateParityDivergence(machineInTurn, busyKeysSize);
53414
+ if (isDangerousGateDivergence(d)) {
53415
+ log(`gw-trace gate-drift kind=${d} machineInTurn=${machineInTurn} ` + `busyKeys=${busyKeysSize} note=machine-authoritative-gate-holds-while-imperative-idle
53416
+ `);
53417
+ }
53418
+ return machineInTurn;
53419
+ }
53420
+
53429
53421
  // gateway/prefix-warmup.ts
53430
53422
  var WARMUP_COOLDOWN_MS = 5 * 60000;
53431
53423
  var lastWarmupAtPerAgent = new Map;
@@ -53686,11 +53678,11 @@ function buildSkillProposalApplyInbound(opts) {
53686
53678
  // ../src/self-improve/skill-proposals.ts
53687
53679
  import {
53688
53680
  closeSync as closeSync3,
53689
- existsSync as existsSync29,
53681
+ existsSync as existsSync28,
53690
53682
  mkdirSync as mkdirSync20,
53691
53683
  openSync as openSync3,
53692
53684
  readFileSync as readFileSync22,
53693
- writeSync as writeSync2
53685
+ writeSync as writeSync3
53694
53686
  } from "node:fs";
53695
53687
  import { join as join25 } from "node:path";
53696
53688
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -53705,21 +53697,21 @@ function rejectedPath(stateDir) {
53705
53697
  return join25(stateDir, REJECTED_FILE);
53706
53698
  }
53707
53699
  function ensureDir2(stateDir) {
53708
- if (!existsSync29(stateDir)) {
53700
+ if (!existsSync28(stateDir)) {
53709
53701
  mkdirSync20(stateDir, { recursive: true, mode: 493 });
53710
53702
  }
53711
53703
  }
53712
53704
  function appendLine(path, obj) {
53713
53705
  const fd = openSync3(path, "a");
53714
53706
  try {
53715
- writeSync2(fd, JSON.stringify(obj) + `
53707
+ writeSync3(fd, JSON.stringify(obj) + `
53716
53708
  `);
53717
53709
  } finally {
53718
53710
  closeSync3(fd);
53719
53711
  }
53720
53712
  }
53721
53713
  function readLines(path, isValid2) {
53722
- if (!existsSync29(path))
53714
+ if (!existsSync28(path))
53723
53715
  return [];
53724
53716
  let raw;
53725
53717
  try {
@@ -54352,10 +54344,10 @@ function escapeBody2(s) {
54352
54344
  }
54353
54345
 
54354
54346
  // gateway/pid-file.ts
54355
- import { writeFileSync as writeFileSync17, readFileSync as readFileSync23, unlinkSync as unlinkSync9, renameSync as renameSync8 } from "node:fs";
54347
+ import { writeFileSync as writeFileSync16, readFileSync as readFileSync23, unlinkSync as unlinkSync9, renameSync as renameSync8 } from "node:fs";
54356
54348
  function writePidFile(path, record) {
54357
54349
  const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
54358
- writeFileSync17(tmp, JSON.stringify(record), "utf-8");
54350
+ writeFileSync16(tmp, JSON.stringify(record), "utf-8");
54359
54351
  renameSync8(tmp, path);
54360
54352
  }
54361
54353
  function clearPidFile(path) {
@@ -54577,10 +54569,10 @@ function safeCount(fn) {
54577
54569
  }
54578
54570
 
54579
54571
  // gateway/session-marker.ts
54580
- import { writeFileSync as writeFileSync18, readFileSync as readFileSync25, renameSync as renameSync9, unlinkSync as unlinkSync10 } from "node:fs";
54572
+ import { writeFileSync as writeFileSync17, readFileSync as readFileSync25, renameSync as renameSync9, unlinkSync as unlinkSync10 } from "node:fs";
54581
54573
  function writeSessionMarker(path, marker) {
54582
54574
  const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
54583
- writeFileSync18(tmp, JSON.stringify(marker), "utf-8");
54575
+ writeFileSync17(tmp, JSON.stringify(marker), "utf-8");
54584
54576
  renameSync9(tmp, path);
54585
54577
  }
54586
54578
  function readSessionMarker(path) {
@@ -54607,11 +54599,11 @@ function shouldFireRestartBanner(input) {
54607
54599
  }
54608
54600
 
54609
54601
  // gateway/clean-shutdown-marker.ts
54610
- import { writeFileSync as writeFileSync19, readFileSync as readFileSync26, renameSync as renameSync10, unlinkSync as unlinkSync11 } from "node:fs";
54602
+ import { writeFileSync as writeFileSync18, readFileSync as readFileSync26, renameSync as renameSync10, unlinkSync as unlinkSync11 } from "node:fs";
54611
54603
  var DEFAULT_MAX_AGE_MS = 60000;
54612
54604
  function writeCleanShutdownMarker(path, marker) {
54613
54605
  const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
54614
- writeFileSync19(tmp, JSON.stringify(marker), "utf-8");
54606
+ writeFileSync18(tmp, JSON.stringify(marker), "utf-8");
54615
54607
  renameSync10(tmp, path);
54616
54608
  }
54617
54609
  function readCleanShutdownMarker(path) {
@@ -55207,7 +55199,7 @@ function classifyAdminGate(text, myAgentName) {
55207
55199
 
55208
55200
  // subagent-watcher.ts
55209
55201
  import {
55210
- existsSync as existsSync31,
55202
+ existsSync as existsSync30,
55211
55203
  openSync as openSync5,
55212
55204
  readSync,
55213
55205
  statSync as statSync9,
@@ -55408,7 +55400,12 @@ function projectSubagentLine(line, agentId, state4) {
55408
55400
  if (ct === "tool_use") {
55409
55401
  const name = c.name ?? "";
55410
55402
  if (name === "Agent" || name === "Task") {
55411
- events.push({ kind: "sub_agent_nested_spawn", agentId });
55403
+ events.push({
55404
+ kind: "sub_agent_nested_spawn",
55405
+ agentId,
55406
+ toolUseId: c.id ?? null,
55407
+ input: c.input ?? undefined
55408
+ });
55412
55409
  } else {
55413
55410
  events.push({
55414
55411
  kind: "sub_agent_tool_use",
@@ -55657,24 +55654,43 @@ function bumpSubagentActivity(db2, args) {
55657
55654
  WHERE id = ?
55658
55655
  `).run(args.ts, args.id);
55659
55656
  }
55657
+ function recordNestedSubagentDispatch(db2, args) {
55658
+ db2.prepare(`
55659
+ INSERT OR IGNORE INTO subagents
55660
+ (id, parent_session_id, parent_turn_key, agent_type, description,
55661
+ background, started_at, last_activity_at, status, jsonl_agent_id,
55662
+ parent_agent_id)
55663
+ VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, 'running', NULL, ?)
55664
+ `).run(args.toolUseId, args.agentType ?? null, args.description ?? null, args.background ? 1 : 0, args.now, args.now, args.parentJsonlAgentId);
55665
+ db2.prepare(`
55666
+ UPDATE subagents
55667
+ SET parent_agent_id = COALESCE(parent_agent_id, ?),
55668
+ parent_turn_key = COALESCE(
55669
+ parent_turn_key,
55670
+ (SELECT p.parent_turn_key FROM subagents p
55671
+ WHERE p.jsonl_agent_id = ? LIMIT 1)
55672
+ )
55673
+ WHERE id = ?
55674
+ `).run(args.parentJsonlAgentId, args.parentJsonlAgentId, args.toolUseId);
55675
+ }
55660
55676
 
55661
55677
  // gateway/turn-active-marker.ts
55662
55678
  import {
55663
55679
  closeSync as closeSync4,
55664
- existsSync as existsSync30,
55680
+ existsSync as existsSync29,
55665
55681
  mkdirSync as mkdirSync21,
55666
55682
  openSync as openSync4,
55667
55683
  readFileSync as readFileSync27,
55668
55684
  statSync as statSync8,
55669
55685
  unlinkSync as unlinkSync12,
55670
55686
  utimesSync,
55671
- writeFileSync as writeFileSync20
55687
+ writeFileSync as writeFileSync19
55672
55688
  } from "node:fs";
55673
55689
  import { join as join26 } from "node:path";
55674
55690
  var TURN_ACTIVE_MARKER_FILE = "turn-active.json";
55675
55691
  function touchTurnActiveMarker(stateDir) {
55676
55692
  const path = join26(stateDir, TURN_ACTIVE_MARKER_FILE);
55677
- if (!existsSync30(path))
55693
+ if (!existsSync29(path))
55678
55694
  return;
55679
55695
  const now = new Date;
55680
55696
  try {
@@ -55692,6 +55708,10 @@ var DEFAULT_RESCAN_MS = 1000;
55692
55708
  var DEFAULT_STALL_THRESHOLD_MS = 60000;
55693
55709
  var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
55694
55710
  var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
55711
+ var LONG_RUNNING_TOOLS = new Set(["Bash"]);
55712
+ function isLongRunningTool(name) {
55713
+ return name != null && LONG_RUNNING_TOOLS.has(name);
55714
+ }
55695
55715
  var DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS = 15 * 60000;
55696
55716
  var SUBAGENT_RESULT_TEXT_MAX = 3000;
55697
55717
  function parseEnvMs(varName) {
@@ -55706,6 +55726,7 @@ function parseEnvMs(varName) {
55706
55726
  var DEFAULT_REAPER_TTL_MS = 60 * 60000;
55707
55727
  var DEFAULT_REAPER_INTERVAL_MS = 15 * 60000;
55708
55728
  var TERMINAL_CLEANUP_GRACE_MS = 30000;
55729
+ var BACKFILL_RETRY_INTERVAL_MS = 3000;
55709
55730
  function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
55710
55731
  const metaPath = jsonlPath.replace(/\.jsonl$/, ".meta.json");
55711
55732
  let meta;
@@ -55754,14 +55775,38 @@ function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
55754
55775
  db2.prepare("UPDATE subagents SET jsonl_agent_id = ? WHERE id = ?").run(agentId, candidateId);
55755
55776
  log?.(`subagent-watcher: backfill linked ${agentId} \u2192 ${candidateId}`);
55756
55777
  try {
55757
- const linkedRow = db2.prepare("SELECT started_at, parent_turn_key FROM subagents WHERE id = ?").get(candidateId);
55778
+ const linkedRow = db2.prepare("SELECT started_at, parent_turn_key, parent_agent_id FROM subagents WHERE id = ?").get(candidateId);
55758
55779
  if (linkedRow != null && linkedRow.parent_turn_key == null) {
55759
- const turn = db2.prepare(`SELECT turn_key FROM turns
55760
- WHERE started_at <= ? AND (ended_at IS NULL OR ended_at >= ?)
55761
- ORDER BY started_at DESC LIMIT 1`).get(linkedRow.started_at, linkedRow.started_at);
55762
- if (turn?.turn_key != null) {
55763
- db2.prepare("UPDATE subagents SET parent_turn_key = ? WHERE id = ?").run(turn.turn_key, candidateId);
55764
- log?.(`subagent-watcher: backfill parent_turn_key ${candidateId} \u2192 ${turn.turn_key}`);
55780
+ let resolvedKey = null;
55781
+ if (linkedRow.parent_agent_id != null && linkedRow.parent_agent_id.length > 0) {
55782
+ const seen = new Set([agentId]);
55783
+ let cursor = linkedRow.parent_agent_id;
55784
+ for (let hop = 0;hop < 5 && cursor != null && !seen.has(cursor); hop++) {
55785
+ seen.add(cursor);
55786
+ const parentRow = db2.prepare("SELECT parent_turn_key, parent_agent_id FROM subagents WHERE jsonl_agent_id = ? LIMIT 1").get(cursor);
55787
+ if (parentRow == null)
55788
+ break;
55789
+ if (parentRow.parent_turn_key != null && parentRow.parent_turn_key.length > 0) {
55790
+ resolvedKey = parentRow.parent_turn_key;
55791
+ break;
55792
+ }
55793
+ cursor = parentRow.parent_agent_id ?? null;
55794
+ }
55795
+ if (resolvedKey != null) {
55796
+ log?.(`subagent-watcher: backfill parent_turn_key ${candidateId} \u2192 ${resolvedKey} (inherited via nested-parent chain)`);
55797
+ }
55798
+ }
55799
+ if (resolvedKey == null) {
55800
+ const turn = db2.prepare(`SELECT turn_key FROM turns
55801
+ WHERE started_at <= ? AND (ended_at IS NULL OR ended_at >= ?)
55802
+ ORDER BY started_at DESC LIMIT 1`).get(linkedRow.started_at, linkedRow.started_at);
55803
+ if (turn?.turn_key != null) {
55804
+ resolvedKey = turn.turn_key;
55805
+ log?.(`subagent-watcher: backfill parent_turn_key ${candidateId} \u2192 ${resolvedKey}`);
55806
+ }
55807
+ }
55808
+ if (resolvedKey != null) {
55809
+ db2.prepare("UPDATE subagents SET parent_turn_key = ? WHERE id = ?").run(resolvedKey, candidateId);
55765
55810
  }
55766
55811
  }
55767
55812
  } catch (err) {
@@ -55791,6 +55836,14 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
55791
55836
  const existing = db2.prepare("SELECT id, background FROM subagents WHERE jsonl_agent_id = ?").get(entry.agentId);
55792
55837
  if (existing == null) {
55793
55838
  log?.(`subagent-watcher: liveness skip ${entry.agentId} \u2014 row not in DB yet (Phase 2 Pre hook pending)`);
55839
+ if (entry.lastBackfillAttemptAt == null || now - entry.lastBackfillAttemptAt >= BACKFILL_RETRY_INTERVAL_MS) {
55840
+ entry.lastBackfillAttemptAt = now;
55841
+ try {
55842
+ backfillJsonlAgentId(db2, entry.filePath, entry.agentId, log);
55843
+ } catch (bfErr) {
55844
+ log?.(`subagent-watcher: backfill retry error ${entry.agentId}: ${bfErr.message}`);
55845
+ }
55846
+ }
55794
55847
  } else {
55795
55848
  bumpSubagentActivity(db2, { id: existing.id, ts: now });
55796
55849
  isForeground = existing.background === 0;
@@ -55913,6 +55966,23 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
55913
55966
  }
55914
55967
  }
55915
55968
  }
55969
+ } else if (ev.kind === "sub_agent_nested_spawn") {
55970
+ if (db2 != null && ev.toolUseId != null && ev.toolUseId.length > 0) {
55971
+ try {
55972
+ const nestedInput = ev.input ?? {};
55973
+ recordNestedSubagentDispatch(db2, {
55974
+ toolUseId: ev.toolUseId,
55975
+ parentJsonlAgentId: entry.agentId,
55976
+ agentType: typeof nestedInput.subagent_type === "string" ? nestedInput.subagent_type : null,
55977
+ description: typeof nestedInput.description === "string" ? nestedInput.description : null,
55978
+ background: nestedInput.run_in_background === true,
55979
+ now
55980
+ });
55981
+ log?.(`subagent-watcher: nested dispatch recorded parent=${entry.agentId} toolUseId=${ev.toolUseId}`);
55982
+ } catch (dbErr) {
55983
+ log?.(`subagent-watcher: nested dispatch record error ${entry.agentId}: ${dbErr.message}`);
55984
+ }
55985
+ }
55916
55986
  } else if (ev.kind === "sub_agent_text") {
55917
55987
  entry.lastSummaryLine = clipNarrative(ev.text);
55918
55988
  entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX);
@@ -55984,7 +56054,7 @@ function startSubagentWatcher(config) {
55984
56054
  clearTimeout(ref.ref);
55985
56055
  });
55986
56056
  const fs2 = config.fs ?? {
55987
- existsSync: existsSync31,
56057
+ existsSync: existsSync30,
55988
56058
  readdirSync: readdirSync5,
55989
56059
  statSync: statSync9,
55990
56060
  openSync: openSync5,
@@ -56064,6 +56134,15 @@ function startSubagentWatcher(config) {
56064
56134
  log?.(`subagent-watcher: backfill error for ${agentId}: ${err.message}`);
56065
56135
  }
56066
56136
  }
56137
+ entry.toolCount = 0;
56138
+ entry.lastTool = null;
56139
+ entry.pendingNarrative = null;
56140
+ tail.cursor = 0;
56141
+ tail.pendingPartial = "";
56142
+ tail.hasEmittedStart = false;
56143
+ readSubTail(entry, tail, n, (desc) => {
56144
+ log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`);
56145
+ }, fs2, log, db2, parentStateDir, config.onUnstall, undefined, config.onProgress);
56067
56146
  }
56068
56147
  }
56069
56148
  if (isHistorical && entry.state === "done") {
@@ -56172,7 +56251,7 @@ function startSubagentWatcher(config) {
56172
56251
  if (entry.stallNotified)
56173
56252
  continue;
56174
56253
  const idleMs = n - entry.lastActivityAt;
56175
- const threshold = entry.toolCount === 0 ? silentSynthesisStallThresholdMs : stallThresholdMs;
56254
+ const threshold = entry.toolCount === 0 || isLongRunningTool(entry.lastTool?.name) ? silentSynthesisStallThresholdMs : stallThresholdMs;
56176
56255
  if (idleMs >= threshold) {
56177
56256
  entry.stallNotified = true;
56178
56257
  entry.stalledAt = n;
@@ -56448,7 +56527,7 @@ function determineRestartReason(opts) {
56448
56527
  init_boot_card();
56449
56528
 
56450
56529
  // gateway/update-announce.ts
56451
- import { existsSync as existsSync36, mkdirSync as mkdirSync25, openSync as openSync6, closeSync as closeSync6, readFileSync as readFileSync34 } from "node:fs";
56530
+ import { existsSync as existsSync35, mkdirSync as mkdirSync25, openSync as openSync6, closeSync as closeSync6, readFileSync as readFileSync34 } from "node:fs";
56452
56531
  import { join as join32 } from "node:path";
56453
56532
  import { homedir as homedir12 } from "node:os";
56454
56533
 
@@ -56577,7 +56656,7 @@ function readAndFilter(raw, filters, limit) {
56577
56656
  var DEFAULT_LOOKBACK_MS = 10 * 60 * 1000;
56578
56657
  function readLastTerminalUpdateAudit(opts = {}) {
56579
56658
  const path = opts.auditLogPath ?? defaultAuditLogPath();
56580
- const exists = opts.exists ?? existsSync36;
56659
+ const exists = opts.exists ?? existsSync35;
56581
56660
  const readFile = opts.readFile ?? ((p) => readFileSync34(p, "utf-8"));
56582
56661
  if (!exists(path))
56583
56662
  return null;
@@ -56667,7 +56746,7 @@ function maybeRenderUpdateAnnouncement(opts = {}) {
56667
56746
 
56668
56747
  // issues-card.ts
56669
56748
  init_card_format();
56670
- import { readFileSync as readFileSync35, writeFileSync as writeFileSync25 } from "node:fs";
56749
+ import { readFileSync as readFileSync35, writeFileSync as writeFileSync24 } from "node:fs";
56671
56750
  var SEVERITY_EMOJI = {
56672
56751
  info: "\u2139\ufe0f",
56673
56752
  warn: "\u26a0\ufe0f",
@@ -56775,7 +56854,7 @@ function readPersistedMessageId(path, log) {
56775
56854
  }
56776
56855
  function writePersistedMessageId(path, messageId, log) {
56777
56856
  try {
56778
- writeFileSync25(path, JSON.stringify({ messageId }) + `
56857
+ writeFileSync24(path, JSON.stringify({ messageId }) + `
56779
56858
  `, { mode: 384 });
56780
56859
  } catch (err) {
56781
56860
  log(`issues-card: persist write failed (${err.message})`);
@@ -56868,13 +56947,13 @@ function createIssuesCardHandle(opts) {
56868
56947
  }
56869
56948
 
56870
56949
  // issues-watcher.ts
56871
- import { existsSync as existsSync38, statSync as statSync11 } from "node:fs";
56950
+ import { existsSync as existsSync37, statSync as statSync11 } from "node:fs";
56872
56951
  import { join as join34 } from "node:path";
56873
56952
 
56874
56953
  // ../src/issues/store.ts
56875
56954
  import {
56876
56955
  closeSync as closeSync7,
56877
- existsSync as existsSync37,
56956
+ existsSync as existsSync36,
56878
56957
  mkdirSync as mkdirSync26,
56879
56958
  openSync as openSync7,
56880
56959
  readdirSync as readdirSync7,
@@ -56882,8 +56961,8 @@ import {
56882
56961
  renameSync as renameSync13,
56883
56962
  statSync as statSync10,
56884
56963
  unlinkSync as unlinkSync13,
56885
- writeFileSync as writeFileSync26,
56886
- writeSync as writeSync3
56964
+ writeFileSync as writeFileSync25,
56965
+ writeSync as writeSync4
56887
56966
  } from "node:fs";
56888
56967
  import { join as join33 } from "node:path";
56889
56968
  import { randomBytes as randomBytes6 } from "node:crypto";
@@ -56905,7 +56984,7 @@ var ISSUES_FILE = "issues.jsonl";
56905
56984
  var ISSUES_LOCK = "issues.lock";
56906
56985
  function readAll(stateDir) {
56907
56986
  const path = join33(stateDir, ISSUES_FILE);
56908
- if (!existsSync37(path))
56987
+ if (!existsSync36(path))
56909
56988
  return [];
56910
56989
  let raw;
56911
56990
  try {
@@ -56941,7 +57020,7 @@ function list(stateDir, opts = {}) {
56941
57020
  });
56942
57021
  }
56943
57022
  function resolve6(stateDir, fingerprint, nowFn = Date.now) {
56944
- if (!existsSync37(join33(stateDir, ISSUES_FILE)))
57023
+ if (!existsSync36(join33(stateDir, ISSUES_FILE)))
56945
57024
  return 0;
56946
57025
  return withLock(stateDir, () => {
56947
57026
  const all = readAll(stateDir);
@@ -56965,7 +57044,7 @@ function writeAll(stateDir, events) {
56965
57044
  const body = events.length === 0 ? "" : events.map((e) => JSON.stringify(e)).join(`
56966
57045
  `) + `
56967
57046
  `;
56968
- writeFileSync26(tmp, body, "utf-8");
57047
+ writeFileSync25(tmp, body, "utf-8");
56969
57048
  renameSync13(tmp, path);
56970
57049
  }
56971
57050
  var ORPHAN_TMP_TTL_MS = 60000;
@@ -57000,7 +57079,7 @@ function withLock(stateDir, fn) {
57000
57079
  try {
57001
57080
  fd = openSync7(lockPath, "wx");
57002
57081
  try {
57003
- writeSync3(fd, String(process.pid));
57082
+ writeSync4(fd, String(process.pid));
57004
57083
  } catch {}
57005
57084
  } catch (err) {
57006
57085
  const e = err;
@@ -57126,7 +57205,7 @@ function startIssuesWatcher(opts) {
57126
57205
  };
57127
57206
  }
57128
57207
  function defaultSignatureProvider(path) {
57129
- if (!existsSync38(path))
57208
+ if (!existsSync37(path))
57130
57209
  return null;
57131
57210
  try {
57132
57211
  const stat = statSync11(path);
@@ -58127,7 +58206,7 @@ function extractFlowItems(line) {
58127
58206
 
58128
58207
  // credits-watch.ts
58129
58208
  init_card_format();
58130
- import { readFileSync as readFileSync37, writeFileSync as writeFileSync27, existsSync as existsSync39, mkdirSync as mkdirSync27 } from "fs";
58209
+ import { readFileSync as readFileSync37, writeFileSync as writeFileSync26, existsSync as existsSync38, mkdirSync as mkdirSync27 } from "fs";
58131
58210
  import { join as join35 } from "path";
58132
58211
  var STATE_FILE = "credits-watch.json";
58133
58212
  var DEFAULT_CREDIT_FATAL_REASONS = new Set;
@@ -58151,7 +58230,7 @@ function emptyCreditState() {
58151
58230
  }
58152
58231
  function readClaudeJsonOverage(claudeConfigDir) {
58153
58232
  const path = join35(claudeConfigDir, ".claude.json");
58154
- if (!existsSync39(path))
58233
+ if (!existsSync38(path))
58155
58234
  return null;
58156
58235
  let raw;
58157
58236
  try {
@@ -58234,7 +58313,7 @@ function humanizeReason(reason) {
58234
58313
  }
58235
58314
  function loadCreditState(stateDir) {
58236
58315
  const path = join35(stateDir, STATE_FILE);
58237
- if (!existsSync39(path))
58316
+ if (!existsSync38(path))
58238
58317
  return emptyCreditState();
58239
58318
  try {
58240
58319
  const raw = readFileSync37(path, "utf-8");
@@ -58251,12 +58330,12 @@ function loadCreditState(stateDir) {
58251
58330
  function saveCreditState(stateDir, state4) {
58252
58331
  mkdirSync27(stateDir, { recursive: true });
58253
58332
  const path = join35(stateDir, STATE_FILE);
58254
- writeFileSync27(path, JSON.stringify(state4, null, 2) + `
58333
+ writeFileSync26(path, JSON.stringify(state4, null, 2) + `
58255
58334
  `, { mode: 384 });
58256
58335
  }
58257
58336
 
58258
58337
  // quota-watch.ts
58259
- import { readFileSync as readFileSync38, writeFileSync as writeFileSync28, existsSync as existsSync40, mkdirSync as mkdirSync28 } from "fs";
58338
+ import { readFileSync as readFileSync38, writeFileSync as writeFileSync27, existsSync as existsSync39, mkdirSync as mkdirSync28 } from "fs";
58260
58339
  import { join as join36 } from "path";
58261
58340
  init_card_format();
58262
58341
  var STATE_FILE2 = "quota-watch.json";
@@ -58453,7 +58532,7 @@ function buildRecoveryMessage(agentName3, snap) {
58453
58532
  }
58454
58533
  function loadQuotaWatchState(stateDir) {
58455
58534
  const path = join36(stateDir, STATE_FILE2);
58456
- if (!existsSync40(path))
58535
+ if (!existsSync39(path))
58457
58536
  return emptyQuotaWatchState();
58458
58537
  try {
58459
58538
  const raw = readFileSync38(path, "utf-8");
@@ -58475,7 +58554,7 @@ function loadQuotaWatchState(stateDir) {
58475
58554
  function saveQuotaWatchState(stateDir, state4) {
58476
58555
  mkdirSync28(stateDir, { recursive: true });
58477
58556
  const path = join36(stateDir, STATE_FILE2);
58478
- writeFileSync28(path, JSON.stringify(state4, null, 2) + `
58557
+ writeFileSync27(path, JSON.stringify(state4, null, 2) + `
58479
58558
  `, { mode: 384 });
58480
58559
  }
58481
58560
  function patchQuotaWatchState(current, accountLabel, accountState) {
@@ -58505,27 +58584,27 @@ function maskVaultKey(name) {
58505
58584
  // gateway/turn-active-marker.ts
58506
58585
  import {
58507
58586
  closeSync as closeSync8,
58508
- existsSync as existsSync41,
58587
+ existsSync as existsSync40,
58509
58588
  mkdirSync as mkdirSync29,
58510
58589
  openSync as openSync8,
58511
58590
  readFileSync as readFileSync39,
58512
58591
  statSync as statSync12,
58513
58592
  unlinkSync as unlinkSync14,
58514
58593
  utimesSync as utimesSync2,
58515
- writeFileSync as writeFileSync29
58594
+ writeFileSync as writeFileSync28
58516
58595
  } from "node:fs";
58517
58596
  import { join as join37 } from "node:path";
58518
58597
  var TURN_ACTIVE_MARKER_FILE2 = "turn-active.json";
58519
58598
  function writeTurnActiveMarker(stateDir, marker) {
58520
58599
  try {
58521
58600
  mkdirSync29(stateDir, { recursive: true });
58522
- writeFileSync29(join37(stateDir, TURN_ACTIVE_MARKER_FILE2), JSON.stringify(marker, null, 2) + `
58601
+ writeFileSync28(join37(stateDir, TURN_ACTIVE_MARKER_FILE2), JSON.stringify(marker, null, 2) + `
58523
58602
  `, { mode: 384 });
58524
58603
  } catch {}
58525
58604
  }
58526
58605
  function touchTurnActiveMarker2(stateDir) {
58527
58606
  const path = join37(stateDir, TURN_ACTIVE_MARKER_FILE2);
58528
- if (!existsSync41(path))
58607
+ if (!existsSync40(path))
58529
58608
  return;
58530
58609
  const now = new Date;
58531
58610
  try {
@@ -58544,7 +58623,7 @@ function removeTurnActiveMarker(stateDir) {
58544
58623
  }
58545
58624
  function sweepStaleTurnActiveMarker(stateDir, opts) {
58546
58625
  const path = join37(stateDir, TURN_ACTIVE_MARKER_FILE2);
58547
- if (!existsSync41(path))
58626
+ if (!existsSync40(path))
58548
58627
  return false;
58549
58628
  const now = opts.now ?? Date.now();
58550
58629
  try {
@@ -58584,11 +58663,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
58584
58663
  }
58585
58664
 
58586
58665
  // ../src/build-info.ts
58587
- var VERSION = "0.15.48";
58588
- var COMMIT_SHA = "eee77212";
58589
- var COMMIT_DATE = "2026-07-04T12:23:42+10:00";
58590
- var LATEST_PR = null;
58591
- var COMMITS_AHEAD_OF_TAG = 2;
58666
+ var VERSION = "0.17.0";
58667
+ var COMMIT_SHA = "6bb8a20";
58668
+ var COMMIT_DATE = "2026-07-05T13:24:36+10:00";
58669
+ var LATEST_PR = 2825;
58670
+ var COMMITS_AHEAD_OF_TAG = 0;
58592
58671
 
58593
58672
  // gateway/boot-version.ts
58594
58673
  function formatRelativeAgo(iso) {
@@ -59630,6 +59709,7 @@ var SCHEMA_SQL = `
59630
59709
  assistant_reply_preview TEXT,
59631
59710
  tool_call_count INTEGER,
59632
59711
  interrupt_reason TEXT,
59712
+ resumed_at INTEGER,
59633
59713
  created_at INTEGER NOT NULL,
59634
59714
  updated_at INTEGER NOT NULL
59635
59715
  );
@@ -59643,12 +59723,15 @@ var PHASE1_MIGRATIONS = [
59643
59723
  var PHASE2_MIGRATIONS = [
59644
59724
  `ALTER TABLE turns ADD COLUMN interrupt_reason TEXT`
59645
59725
  ];
59726
+ var PHASE3_MIGRATIONS = [
59727
+ `ALTER TABLE turns ADD COLUMN resumed_at INTEGER`
59728
+ ];
59646
59729
  function applySchema(db2) {
59647
59730
  db2.exec("PRAGMA journal_mode = WAL");
59648
59731
  db2.exec("PRAGMA synchronous = NORMAL");
59649
59732
  db2.exec("PRAGMA busy_timeout = 5000");
59650
59733
  db2.exec(SCHEMA_SQL);
59651
- for (const sql of [...PHASE1_MIGRATIONS, ...PHASE2_MIGRATIONS]) {
59734
+ for (const sql of [...PHASE1_MIGRATIONS, ...PHASE2_MIGRATIONS, ...PHASE3_MIGRATIONS]) {
59652
59735
  try {
59653
59736
  db2.exec(sql);
59654
59737
  } catch (err) {
@@ -59690,6 +59773,7 @@ function mapRow(row) {
59690
59773
  assistant_reply_preview: row.assistant_reply_preview,
59691
59774
  tool_call_count: row.tool_call_count,
59692
59775
  interrupt_reason: row.interrupt_reason,
59776
+ resumed_at: row.resumed_at,
59693
59777
  created_at: row.created_at,
59694
59778
  updated_at: row.updated_at
59695
59779
  };
@@ -59753,6 +59837,14 @@ var INTERRUPTED_VIA = new Set([
59753
59837
  "timeout",
59754
59838
  "unknown"
59755
59839
  ]);
59840
+ function markTurnResumed(db2, turnKey, now = Date.now()) {
59841
+ db2.prepare(`
59842
+ UPDATE turns
59843
+ SET resumed_at = ?,
59844
+ updated_at = ?
59845
+ WHERE turn_key = ? AND resumed_at IS NULL
59846
+ `).run(now, now, turnKey);
59847
+ }
59756
59848
  function findLatestTurnIfInterrupted(db2) {
59757
59849
  const row = db2.prepare(`
59758
59850
  SELECT * FROM turns
@@ -59762,6 +59854,8 @@ function findLatestTurnIfInterrupted(db2) {
59762
59854
  if (!row)
59763
59855
  return null;
59764
59856
  const turn = mapRow(row);
59857
+ if (turn.resumed_at != null)
59858
+ return null;
59765
59859
  if (turn.ended_at == null)
59766
59860
  return turn;
59767
59861
  if (turn.ended_via != null && INTERRUPTED_VIA.has(turn.ended_via))
@@ -59885,7 +59979,8 @@ var SUBAGENTS_SCHEMA_SQL = `
59885
59979
  ended_at INTEGER,
59886
59980
  status TEXT NOT NULL,
59887
59981
  result_summary TEXT,
59888
- jsonl_agent_id TEXT
59982
+ jsonl_agent_id TEXT,
59983
+ parent_agent_id TEXT
59889
59984
  );
59890
59985
  CREATE INDEX IF NOT EXISTS subagents_turn ON subagents(parent_turn_key);
59891
59986
  CREATE INDEX IF NOT EXISTS subagents_status ON subagents(status);
@@ -59897,6 +59992,10 @@ function applySubagentsSchema(db2) {
59897
59992
  if (!hasJsonlId) {
59898
59993
  db2.exec("ALTER TABLE subagents ADD COLUMN jsonl_agent_id TEXT");
59899
59994
  }
59995
+ const hasParentAgentId = cols.some((c) => c.name === "parent_agent_id");
59996
+ if (!hasParentAgentId) {
59997
+ db2.exec("ALTER TABLE subagents ADD COLUMN parent_agent_id TEXT");
59998
+ }
59900
59999
  db2.exec("CREATE INDEX IF NOT EXISTS subagents_jsonl_id ON subagents(jsonl_agent_id)");
59901
60000
  }
59902
60001
  function mapSubagentRow(row) {
@@ -59912,19 +60011,38 @@ function mapSubagentRow(row) {
59912
60011
  ended_at: row.ended_at,
59913
60012
  status: row.status,
59914
60013
  result_summary: row.result_summary,
59915
- jsonl_agent_id: row.jsonl_agent_id
60014
+ jsonl_agent_id: row.jsonl_agent_id,
60015
+ parent_agent_id: row.parent_agent_id ?? null
59916
60016
  };
59917
60017
  }
59918
60018
  function getSubagentByJsonlId(db2, jsonlAgentId) {
59919
60019
  const row = db2.prepare("SELECT * FROM subagents WHERE jsonl_agent_id = ?").get(jsonlAgentId);
59920
60020
  return row ? mapSubagentRow(row) : null;
59921
60021
  }
60022
+ function resolveSubagentOriginTurnKey(db2, jsonlAgentId, maxHops = 5) {
60023
+ const seen = new Set;
60024
+ let currentJsonlId = jsonlAgentId;
60025
+ for (let hop = 0;hop <= maxHops && currentJsonlId != null; hop++) {
60026
+ if (seen.has(currentJsonlId))
60027
+ return null;
60028
+ seen.add(currentJsonlId);
60029
+ const row = db2.prepare("SELECT parent_turn_key, parent_agent_id FROM subagents WHERE jsonl_agent_id = ? LIMIT 1").get(currentJsonlId);
60030
+ if (row == null)
60031
+ return null;
60032
+ if (row.parent_turn_key != null && row.parent_turn_key.length > 0)
60033
+ return row.parent_turn_key;
60034
+ currentJsonlId = row.parent_agent_id ?? null;
60035
+ }
60036
+ return null;
60037
+ }
59922
60038
 
59923
60039
  // gateway/worker-feed-dispatch.ts
59924
60040
  function resolveWorkerFeedDispatch(sub, watcherDescription) {
59925
60041
  return {
59926
60042
  isBackground: sub?.background ?? false,
59927
- feedDescription: (sub?.description ?? "") || watcherDescription
60043
+ feedDescription: (sub?.description ?? "") || watcherDescription,
60044
+ hasRow: sub != null,
60045
+ isNested: sub?.parent_agent_id != null
59928
60046
  };
59929
60047
  }
59930
60048
 
@@ -59943,35 +60061,19 @@ function isOrphanSubagentStatusEnabled(envVal) {
59943
60061
  return envVal !== "0";
59944
60062
  }
59945
60063
 
59946
- // gateway/resolve-calling-subagent.ts
59947
- function resolveCallingSubagent(opts) {
59948
- if (opts.db == null)
59949
- return null;
59950
- try {
59951
- if (opts.agentIdHint != null) {
59952
- const row = opts.db.prepare("SELECT jsonl_agent_id FROM subagents WHERE jsonl_agent_id = ? AND status = 'running'").get(opts.agentIdHint);
59953
- if (row?.jsonl_agent_id)
59954
- return { agentId: row.jsonl_agent_id };
59955
- }
59956
- if (opts.toolUseIdHint != null) {
59957
- const row = opts.db.prepare("SELECT jsonl_agent_id FROM subagents WHERE id = ? AND status = 'running'").get(opts.toolUseIdHint);
59958
- if (row?.jsonl_agent_id)
59959
- return { agentId: row.jsonl_agent_id };
59960
- }
59961
- const turnRow = opts.db.prepare("SELECT turn_key FROM turns WHERE chat_id = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1").get(opts.chatId);
59962
- if (turnRow?.turn_key == null)
59963
- return null;
59964
- const candidates = opts.db.prepare("SELECT jsonl_agent_id FROM subagents WHERE parent_turn_key = ? AND status = 'running' AND jsonl_agent_id IS NOT NULL ORDER BY started_at DESC").all(turnRow.turn_key);
59965
- if (candidates.length === 0)
59966
- return null;
59967
- if (candidates.length > 1) {
59968
- console.warn(`progress_update: heuristic resolution selected most-recent of ${candidates.length} running sub-agents (chat=${opts.chatId}); pass agent_id explicitly to avoid mis-attribution`);
59969
- }
59970
- return { agentId: candidates[0].jsonl_agent_id };
59971
- } catch (err) {
59972
- console.warn("progress_update: resolveCallingSubagent SQL error", err);
59973
- return null;
59974
- }
60064
+ // status-query-telemetry.ts
60065
+ function deriveStatusQueryTelemetry(s) {
60066
+ const idle = !s.turnActive && s.runningWorkers <= 0 && s.workerFeedSize <= 0 && s.pendingAsync <= 0;
60067
+ const stage = idle ? "idle" : s.turnActive ? "turn-active" : "background-work";
60068
+ return { stage, idle };
60069
+ }
60070
+ function formatStatusQueryLine(agentName3, chatId, thread, s, t) {
60071
+ return `telegram gateway: status-query agent=${agentName3} chat_id=${chatId} thread=${thread} ` + `stage=${t.stage} turn_age_s=${s.turnAgeS} running_workers=${s.runningWorkers} ` + `worker_feed=${s.workerFeedSize} pending_async=${s.pendingAsync}
60072
+ `;
60073
+ }
60074
+ function formatStatusQueryUxFailureLine(agentName3, chatId, thread) {
60075
+ return `telegram gateway: ux-failure: status-query agent=${agentName3} chat_id=${chatId} thread=${thread} ` + `stage=idle turn_age_s=-1 running_workers=0 worker_feed=0 pending_async=0
60076
+ `;
59975
60077
  }
59976
60078
 
59977
60079
  // gateway/gateway.ts
@@ -60277,7 +60379,7 @@ function saveAccess(a) {
60277
60379
  return;
60278
60380
  mkdirSync32(STATE_DIR, { recursive: true, mode: 448 });
60279
60381
  const tmp = ACCESS_FILE + ".tmp";
60280
- writeFileSync31(tmp, JSON.stringify(a, null, 2) + `
60382
+ writeFileSync30(tmp, JSON.stringify(a, null, 2) + `
60281
60383
  `, { mode: 384 });
60282
60384
  renameSync14(tmp, ACCESS_FILE);
60283
60385
  }
@@ -60314,7 +60416,7 @@ try {
60314
60416
  let markerAgeMs = null;
60315
60417
  try {
60316
60418
  const markerPath = join40(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
60317
- if (existsSync44(markerPath)) {
60419
+ if (existsSync43(markerPath)) {
60318
60420
  const st = statSync15(markerPath);
60319
60421
  markerAgeMs = Date.now() - st.mtimeMs;
60320
60422
  try {
@@ -60400,13 +60502,13 @@ try {
60400
60502
  pending2.interrupt_reason != null ? `SWITCHROOM_PENDING_INTERRUPT_REASON=${pending2.interrupt_reason}` : `SWITCHROOM_PENDING_INTERRUPT_REASON=`
60401
60503
  ];
60402
60504
  const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
60403
- writeFileSync31(pendingEnvTmp, lines.join(`
60505
+ writeFileSync30(pendingEnvTmp, lines.join(`
60404
60506
  `) + `
60405
60507
  `, { mode: 384 });
60406
60508
  renameSync14(pendingEnvTmp, pendingEnvPath);
60407
60509
  process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"}
60408
60510
  `);
60409
- } else if (existsSync44(pendingEnvPath)) {
60511
+ } else if (existsSync43(pendingEnvPath)) {
60410
60512
  rmSync4(pendingEnvPath, { force: true });
60411
60513
  process.stderr.write(`telegram gateway: pending-turn env cleared (clean previous shutdown)
60412
60514
  `);
@@ -60424,10 +60526,10 @@ function resolveSubagentOriginChat(agentId) {
60424
60526
  if (turnsDb == null)
60425
60527
  return null;
60426
60528
  try {
60427
- const sub = getSubagentByJsonlId(turnsDb, agentId);
60428
- if (sub?.parent_turn_key == null)
60529
+ const originKey = resolveSubagentOriginTurnKey(turnsDb, agentId);
60530
+ if (originKey == null)
60429
60531
  return null;
60430
- const turn = getTurnByKey(turnsDb, sub.parent_turn_key);
60532
+ const turn = getTurnByKey(turnsDb, originKey);
60431
60533
  if (turn == null || turn.chat_id.length === 0)
60432
60534
  return null;
60433
60535
  const threadNum = turn.thread_id != null && turn.thread_id.length > 0 ? Number(turn.thread_id) : NaN;
@@ -60506,6 +60608,7 @@ var chatAvailableReactions = new Map;
60506
60608
  var chatProbesInFlight = new Set;
60507
60609
  var activeTurnStartedAt = new Map;
60508
60610
  var claudeBusyKeys = new Set;
60611
+ var claudeBusyKeySince = new Map;
60509
60612
  var reactionTransitionCounts = new Map;
60510
60613
  var firstTextReplyLogged = new Set;
60511
60614
  function markClaudeBusyForInbound(m) {
@@ -60516,9 +60619,20 @@ function markClaudeBusyForInbound(m) {
60516
60619
  tid = n;
60517
60620
  }
60518
60621
  const key = chatKey2(m.chatId, tid);
60519
- claudeBusyKeys.add(key);
60622
+ markBusyKeyLockstep(claudeBusyKeys, claudeBusyKeySince, key, Date.now());
60520
60623
  return key;
60521
60624
  }
60625
+ var _busyOrphanTtlRaw = process.env.SWITCHROOM_BUSY_ORPHAN_TTL_MS;
60626
+ var _busyOrphanTtlParsed = _busyOrphanTtlRaw != null && _busyOrphanTtlRaw !== "" ? Number(_busyOrphanTtlRaw) : 360000;
60627
+ var CLAUDE_BUSY_ORPHAN_TTL_MS = Number.isFinite(_busyOrphanTtlParsed) && _busyOrphanTtlParsed > 0 ? _busyOrphanTtlParsed : 360000;
60628
+ function reapOrphanBusyKeysNow(now) {
60629
+ reapOrphanBusyKeys(claudeBusyKeys, claudeBusyKeySince, now, {
60630
+ ttlMs: CLAUDE_BUSY_ORPHAN_TTL_MS,
60631
+ hasPendingDelivery: (key) => deliveryQueue.pending.has(key),
60632
+ log: (msg) => process.stderr.write(`${msg}
60633
+ `)
60634
+ });
60635
+ }
60522
60636
  var DELIVERY_CONFIRM_ENABLED = process.env.SWITCHROOM_INBOUND_DELIVERY_CONFIRM !== "0";
60523
60637
  var _deliveryTimeoutRaw = process.env.SWITCHROOM_INBOUND_DELIVERY_TIMEOUT_MS;
60524
60638
  var _deliveryTimeoutParsed = _deliveryTimeoutRaw != null && _deliveryTimeoutRaw !== "" ? Number(_deliveryTimeoutRaw) : 15000;
@@ -60572,9 +60686,9 @@ function noteAgentOutputAt(key, ts) {
60572
60686
  var OBLIGATION_STORE_PATH = join40(STATE_DIR, "obligations.json");
60573
60687
  var obligationStoreFs = {
60574
60688
  readFileSync: (p) => readFileSync41(p, "utf8"),
60575
- writeFileSync: (p, d) => writeFileSync31(p, d),
60689
+ writeFileSync: (p, d) => writeFileSync30(p, d),
60576
60690
  renameSync: (a, b) => renameSync14(a, b),
60577
- existsSync: (p) => existsSync44(p)
60691
+ existsSync: (p) => existsSync43(p)
60578
60692
  };
60579
60693
  var obligationLedger = new ObligationLedger(OBLIGATION_REPRESENT_MAX, {
60580
60694
  onChange: STATIC || !OBLIGATION_LEDGER_ENABLED ? undefined : (snapshot) => persistObligations(OBLIGATION_STORE_PATH, obligationStoreFs, snapshot)
@@ -60608,15 +60722,10 @@ var FEED_LIVENESS_OPEN_MS = (() => {
60608
60722
  return Number.isFinite(n) && n > 0 ? n : 1200;
60609
60723
  })();
60610
60724
  var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
60611
- function formatFeedElapsed3(ms) {
60612
- const s = Math.floor(ms / 1000);
60613
- if (s < 60)
60614
- return `${s}s`;
60615
- const m = Math.floor(s / 60);
60616
- return `${m}m${(s % 60).toString().padStart(2, "0")}s`;
60617
- }
60618
60725
  function turnInFlightForGate() {
60619
- return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
60726
+ if (!isDeliveryCutoverEnabled())
60727
+ return claudeBusyKeys.size > 0;
60728
+ return probeGateParity(isMachineInTurn(), claudeBusyKeys.size);
60620
60729
  }
60621
60730
  function deliverResumeSyntheticOrBuffer(agent, inbound) {
60622
60731
  const decision = decideInboundDelivery({
@@ -60936,7 +61045,7 @@ var pendingPtyPartial = null;
60936
61045
  function statusKey(chatId, threadId) {
60937
61046
  return chatKey2(chatId, threadId);
60938
61047
  }
60939
- function streamKey3(chatId, threadId) {
61048
+ function streamKey2(chatId, threadId) {
60940
61049
  return chatKey2(chatId, threadId);
60941
61050
  }
60942
61051
  var pendingCrossTurnGate = new Map;
@@ -61002,6 +61111,7 @@ function purgeReactionTracking(key, endingTurn) {
61002
61111
  reapQueuedStatus(pqChatId, Number.isFinite(pqThread) ? pqThread : undefined);
61003
61112
  }
61004
61113
  claudeBusyKeys.delete(key);
61114
+ claudeBusyKeySince.delete(key);
61005
61115
  reactionTransitionCounts.delete(key);
61006
61116
  firstTextReplyLogged.delete(key);
61007
61117
  if (endingTurn != null) {
@@ -61064,6 +61174,7 @@ function releaseTurnBufferGate(key, endingTurn) {
61064
61174
  return;
61065
61175
  activeTurnStartedAt.delete(key);
61066
61176
  claudeBusyKeys.delete(key);
61177
+ claudeBusyKeySince.delete(key);
61067
61178
  shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted: true });
61068
61179
  drainBufferedIfAllowed(endingTurn, "reply-released-gate");
61069
61180
  }
@@ -62024,24 +62135,12 @@ var statusPinChatIds = new Map;
62024
62135
  var STATUS_PIN_STORE_PATH = join40(STATE_DIR, "status-pins.json");
62025
62136
  var statusPinStoreFs = {
62026
62137
  readFileSync: (p) => readFileSync41(p, "utf8"),
62027
- writeFileSync: (p, d) => writeFileSync31(p, d),
62138
+ writeFileSync: (p, d) => writeFileSync30(p, d),
62028
62139
  renameSync: (a, b) => renameSync14(a, b),
62029
- existsSync: (p) => existsSync44(p)
62140
+ existsSync: (p) => existsSync43(p)
62030
62141
  };
62031
62142
  var statusPinPersistEnabled = !STATIC && PIN_STATUS_WHILE_WORKING;
62032
- function snapshotStatusPins() {
62033
- const snapshot = [];
62034
- for (const [pinKey, state4] of statusPinState) {
62035
- const chatId = statusPinChatIds.get(pinKey);
62036
- if (chatId == null)
62037
- continue;
62038
- snapshot.push({ pinKey, chatId, messageId: state4.messageId });
62039
- }
62040
- return snapshot;
62041
- }
62042
- function snapshotStatusPinsExcept(exceptKey) {
62043
- return snapshotStatusPins().filter((p) => p.pinKey !== exceptKey);
62044
- }
62143
+ var bannerPinPersistEnabled = !STATIC;
62045
62144
  function statusPinApi() {
62046
62145
  return {
62047
62146
  pinChatMessage: (chat_id, message_id, opts) => robustApiCall(() => lockedBot.api.pinChatMessage(chat_id, message_id, opts), { chat_id: String(chat_id), verb: "status-pin.pin" }),
@@ -62049,7 +62148,7 @@ function statusPinApi() {
62049
62148
  };
62050
62149
  }
62051
62150
  async function statusPinBootCleanup() {
62052
- if (!statusPinPersistEnabled)
62151
+ if (!statusPinPersistEnabled && !bannerPinPersistEnabled)
62053
62152
  return;
62054
62153
  const api = statusPinApi();
62055
62154
  const { cleared, total } = await runStatusPinBootCleanup({
@@ -62107,7 +62206,6 @@ async function reconcileStatusPinInner(pinKey, chatId, desired) {
62107
62206
  pinKey,
62108
62207
  chatId,
62109
62208
  op,
62110
- snapshotOthers: () => snapshotStatusPinsExcept(pinKey),
62111
62209
  applyPin: runReconcile
62112
62210
  });
62113
62211
  if (next == null) {
@@ -62459,14 +62557,35 @@ async function redeliverStrandedInbound(p) {
62459
62557
  forgetDelivery(deliveryQueue, p.key);
62460
62558
  }
62461
62559
  }
62560
+ function sweepSuspendedTargets() {
62561
+ const keys = new Set;
62562
+ const chats = new Set;
62563
+ for (const p of pendingPermissions.values()) {
62564
+ for (const c of p.cards) {
62565
+ if (c.threadId != null)
62566
+ keys.add(chatKey2(c.chatId, c.threadId));
62567
+ else
62568
+ chats.add(c.chatId);
62569
+ }
62570
+ }
62571
+ for (const a of pendingAskUser.values()) {
62572
+ if (a.threadId != null)
62573
+ keys.add(chatKey2(a.chatId, a.threadId));
62574
+ else
62575
+ chats.add(a.chatId);
62576
+ }
62577
+ return { keys, chats };
62578
+ }
62462
62579
  var _deliveryConfirmSweep = setInterval(() => {
62463
62580
  if (!DELIVERY_CONFIRM_ENABLED)
62464
62581
  return;
62465
62582
  if (currentTurn != null)
62466
62583
  return;
62467
- if (pendingPermissions.size > 0 || pendingAskUser.size > 0)
62468
- return;
62584
+ reapOrphanBusyKeysNow(Date.now());
62585
+ const suspended = sweepSuspendedTargets();
62469
62586
  for (const p of sweep(deliveryQueue, Date.now(), DELIVERY_CONFIRM_TIMEOUT_MS)) {
62587
+ if (isRedeliverySuspended(p.key, suspended))
62588
+ continue;
62470
62589
  redeliverStrandedInbound(p);
62471
62590
  }
62472
62591
  }, DELIVERY_CONFIRM_SWEEP_MS);
@@ -62490,13 +62609,33 @@ var inboundSpool = STATIC ? undefined : createInboundSpool({
62490
62609
  fs: {
62491
62610
  appendFileSync: (p, d) => appendFileSync6(p, d),
62492
62611
  readFileSync: (p) => readFileSync41(p, "utf8"),
62493
- writeFileSync: (p, d) => writeFileSync31(p, d),
62612
+ writeFileSync: (p, d) => writeFileSync30(p, d),
62494
62613
  renameSync: (a, b) => renameSync14(a, b),
62495
- existsSync: (p) => existsSync44(p),
62614
+ existsSync: (p) => existsSync43(p),
62496
62615
  statSizeSync: (p) => statSync15(p).size
62616
+ },
62617
+ onDegraded: (info) => {
62618
+ process.stderr.write(`telegram gateway: inbound-spool durability ${info.degraded ? "DEGRADED to in-memory-only" : "RECOVERED"} path=${info.path} consecutiveFailures=${info.consecutiveFailures}${info.error != null ? ` error=${info.error}` : ""}
62619
+ `);
62620
+ }
62621
+ });
62622
+ var EVICT_NOTICE_COOLDOWN_MS = 300000;
62623
+ var evictNoticeByChat = new Map;
62624
+ var pendingInboundBuffer = createPendingInboundBuffer({
62625
+ spool: inboundSpool,
62626
+ onEvict: (_agent, evicted) => {
62627
+ const chat = evicted.chatId;
62628
+ const evThread = typeof evicted.meta?.threadId === "string" && evicted.meta.threadId ? Number(evicted.meta.threadId) : undefined;
62629
+ const key = `${chat}:${evThread ?? "-"}`;
62630
+ const last = evictNoticeByChat.get(key);
62631
+ const nowMs2 = Date.now();
62632
+ if (last != null && nowMs2 - last < EVICT_NOTICE_COOLDOWN_MS)
62633
+ return;
62634
+ evictNoticeByChat.set(key, nowMs2);
62635
+ const threadOpts = evThread != null ? { message_thread_id: evThread } : {};
62636
+ 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" });
62497
62637
  }
62498
62638
  });
62499
- var pendingInboundBuffer = createPendingInboundBuffer({ spool: inboundSpool });
62500
62639
  function agentHasInFlightBackgroundWork(now) {
62501
62640
  if (countRunningWorkers() > 0)
62502
62641
  return true;
@@ -62546,6 +62685,11 @@ function obligationSweep() {
62546
62685
  pendingCrossTurnGate.set(o.originTurnId, { sinceMs: o.openedAt });
62547
62686
  const attempt = obligationLedger.markRepresented(o.originTurnId);
62548
62687
  process.stderr.write(`telegram gateway: obligation re-presented origin=${o.originTurnId} attempt=${attempt}/${OBLIGATION_REPRESENT_MAX}
62688
+ `);
62689
+ return;
62690
+ }
62691
+ if (shouldDeferEscalationForBridge({ bridgeAlive: ipcServer.getClient(agent)?.isAlive() === true })) {
62692
+ process.stderr.write(`telegram gateway: obligation escalation deferred \u2014 bridge down (nudge waits for reconnect) origin=${o.originTurnId}
62549
62693
  `);
62550
62694
  return;
62551
62695
  }
@@ -62575,6 +62719,15 @@ if (bootResumeInbound != null) {
62575
62719
  } else {
62576
62720
  pendingInboundBuffer.push(bootResumeInbound.agent, bootResumeInbound.msg);
62577
62721
  }
62722
+ const resumeTurnKey = bootResumeInbound.msg.meta?.resume_turn_key;
62723
+ if (turnsDb != null && typeof resumeTurnKey === "string" && resumeTurnKey.length > 0) {
62724
+ try {
62725
+ markTurnResumed(turnsDb, resumeTurnKey);
62726
+ } catch (err) {
62727
+ process.stderr.write(`telegram gateway: markTurnResumed failed turnKey=${resumeTurnKey}: ${err.message}
62728
+ `);
62729
+ }
62730
+ }
62578
62731
  }
62579
62732
  if (inboundSpool != null) {
62580
62733
  const replay = inboundSpool.liveEntries();
@@ -62759,6 +62912,7 @@ var ipcServer = createIpcServer({
62759
62912
  activeReactionMsgIds,
62760
62913
  activeTurnStartedAt,
62761
62914
  claudeBusyKeys,
62915
+ claudeBusyKeySince,
62762
62916
  activeDraftStreams,
62763
62917
  clearActiveReactions: () => {
62764
62918
  const ad = resolveAgentDirFromEnv();
@@ -62800,9 +62954,6 @@ var ipcServer = createIpcServer({
62800
62954
  if (msg.activeFile)
62801
62955
  lastSessionActiveFile = msg.activeFile;
62802
62956
  const ev = msg.event;
62803
- const chatHint = msg.chatId || null;
62804
- const threadHint = msg.threadId != null ? String(msg.threadId) : undefined;
62805
- progressDriver?.ingest(ev, chatHint, threadHint);
62806
62957
  handleSessionEvent(ev);
62807
62958
  toolFlightTracker.onEvent(ev);
62808
62959
  if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
@@ -62872,7 +63023,8 @@ var ipcServer = createIpcServer({
62872
63023
  }), { threadId, chat_id: chatId, verb: "permission_request" }).then((sent) => {
62873
63024
  const pend = pendingPermissions.get(requestId);
62874
63025
  if (pend && sent && typeof sent.message_id === "number") {
62875
- pend.cards.push({ chatId, messageId: sent.message_id });
63026
+ const landedThreadId = sent.message_thread_id ?? undefined;
63027
+ pend.cards.push({ chatId, messageId: sent.message_id, threadId: landedThreadId });
62876
63028
  permCardStore.add({
62877
63029
  requestId,
62878
63030
  chatId,
@@ -63383,20 +63535,21 @@ if (!STATIC) {
63383
63535
  process.stderr.write(`telegram gateway: idle-drain flushed ${r.redelivered}/${r.drained} buffered inbound for ${selfAgent}${r.rebuffered > 0 ? ` (${r.rebuffered} re-buffered)` : ""}
63384
63536
  `);
63385
63537
  }
63386
- inboundSpool?.sweepEscalations((e, { postNotice }) => {
63538
+ inboundSpool?.sweepEscalations((e, { postNotice, droppedCount }) => {
63387
63539
  const chat = e.msg.chatId;
63388
63540
  const escThread = typeof e.msg.meta?.threadId === "string" && e.msg.meta.threadId ? Number(e.msg.meta.threadId) : undefined;
63389
63541
  const threadOpts = escThread != null ? { message_thread_id: escThread } : {};
63390
63542
  reapQueuedStatus(chat, escThread);
63391
63543
  if (!postNotice)
63392
63544
  return;
63393
- swallowingApiCall(() => bot.api.sendMessage(chat, "\u26A0\uFE0F I couldn't deliver an earlier message to the agent after repeated retries (it survived restarts but the agent never picked it up). Please resend it.", { ...threadOpts }), { chat_id: chat, verb: "inbound-spool-escalation" });
63545
+ const n = droppedCount > 0 ? droppedCount : 1;
63546
+ const noticeText = n === 1 ? "\u26A0\uFE0F I couldn't deliver an earlier message to the agent after repeated retries (it survived restarts but the agent never picked it up). Please resend it." : `\u26A0\uFE0F I couldn't deliver ${n} earlier messages to the agent after repeated retries (they survived restarts but the agent never picked them up). Please resend them.`;
63547
+ swallowingApiCall(() => bot.api.sendMessage(chat, noticeText, { ...threadOpts }), { chat_id: chat, verb: "inbound-spool-escalation" });
63394
63548
  });
63395
63549
  }, IDLE_DRAIN_INTERVAL_MS).unref();
63396
63550
  }
63397
63551
  var ALLOWED_TOOLS = new Set([
63398
63552
  "reply",
63399
- "stream_reply",
63400
63553
  "progress_update",
63401
63554
  "react",
63402
63555
  "download_attachment",
@@ -63425,8 +63578,6 @@ async function executeToolCall(tool, args) {
63425
63578
  switch (tool) {
63426
63579
  case "reply":
63427
63580
  return executeReply(args);
63428
- case "stream_reply":
63429
- return executeStreamReply(args);
63430
63581
  case "progress_update":
63431
63582
  return executeProgressUpdate(args);
63432
63583
  case "react":
@@ -63895,7 +64046,7 @@ ${url}`;
63895
64046
  }
63896
64047
  }
63897
64048
  }
63898
- const replySKey = streamKey3(chat_id, threadId);
64049
+ const replySKey = streamKey2(chat_id, threadId);
63899
64050
  suppressPtyPreview.add(replySKey);
63900
64051
  let previewMessageId = null;
63901
64052
  const openStream = activeDraftStreams.get(replySKey);
@@ -64293,221 +64444,6 @@ ${url}`;
64293
64444
  }
64294
64445
  return { content: [{ type: "text", text: result }] };
64295
64446
  }
64296
- async function executeStreamReply(args) {
64297
- const turn = currentTurn;
64298
- if (!args.chat_id)
64299
- throw new Error("stream_reply: chat_id is required");
64300
- if (args.text == null || args.text === "")
64301
- throw new Error("stream_reply: text is required and cannot be empty");
64302
- {
64303
- const _rawChatId = String(args.chat_id ?? "");
64304
- const resolved = resolveChatIdFallback(_rawChatId, loadAccess(), turn?.sessionChatId, lastActiveTurnChatId, turn != null);
64305
- if (resolved.tier !== "raw") {
64306
- process.stderr.write(`telegram gateway: stream_reply: model passed chat_id "${_rawChatId}" (not allowlisted) \u2014 ` + `routing to ${resolved.tier} turn chat "${resolved.chatId}"
64307
- `);
64308
- args.chat_id = resolved.chatId;
64309
- }
64310
- }
64311
- let streamRoutedOriginTurn = null;
64312
- let streamOriginVia = null;
64313
- if (TURN_ORIGIN_ROUTING_ENABLED) {
64314
- const echoedTurn = findTurnByOriginId(args.origin_turn_id);
64315
- const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(String(args.chat_id), args.reply_to) : null;
64316
- const originTurn = echoedTurn ?? quotedTurn;
64317
- streamRoutedOriginTurn = originTurn ?? null;
64318
- streamOriginVia = originTurn == null ? null : echoedTurn != null ? "echo" : "quoted";
64319
- }
64320
- if (args.message_thread_id == null) {
64321
- let injected;
64322
- if (TURN_ORIGIN_ROUTING_ENABLED) {
64323
- injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, streamRoutedOriginTurn, streamOriginVia, turn, "stream_reply");
64324
- } else {
64325
- injected = turn?.sessionThreadId;
64326
- }
64327
- if (injected != null)
64328
- args.message_thread_id = String(injected);
64329
- }
64330
- args.text = redactOutboundText(args.text, "stream_reply");
64331
- {
64332
- args.text = normalizePunctuation(args.text);
64333
- const scrub = scrubVoice(args.text);
64334
- if (scrub.replaced > 0) {
64335
- args.text = scrub.scrubbed;
64336
- emitRuntimeMetric({
64337
- kind: "voice_scrub_applied",
64338
- chatKey: statusKey(String(args.chat_id ?? ""), args.message_thread_id != null ? Number(args.message_thread_id) : undefined),
64339
- replaced: scrub.replaced,
64340
- site: "stream_reply"
64341
- });
64342
- }
64343
- }
64344
- if (args.done === true) {
64345
- const sChatId = String(args.chat_id ?? "");
64346
- const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
64347
- const sText = args.text;
64348
- const dup = outboundDedup.check(sChatId, sThreadId, sText, Date.now(), currentTurn?.registryKey ?? null);
64349
- if (dup != null) {
64350
- process.stderr.write(`telegram gateway: stream_reply: deduped (#546) chatId=${sChatId} ageMs=${dup.ageMs} preview=${JSON.stringify(dup.preview)}
64351
- `);
64352
- return { content: [{ type: "text", text: "sent (deduped \u2014 same content sent via earlier path)" }] };
64353
- }
64354
- }
64355
- const access = loadAccess();
64356
- const streamChatId = String(args.chat_id ?? "");
64357
- const streamIsPrivate = isDmChatId(streamChatId);
64358
- const streamIsForumTopic = args.message_thread_id != null && args.message_thread_id !== "";
64359
- let streamReplyMarkup;
64360
- let streamButtonMeta;
64361
- const rawStreamKeyboard = args.inline_keyboard;
64362
- if (rawStreamKeyboard != null && Boolean(args.done)) {
64363
- const validationErrors = validateInlineKeyboard(rawStreamKeyboard);
64364
- if (validationErrors.length > 0) {
64365
- const summary = validationErrors.map((e) => `${e.path}.${e.field}: ${e.reason}`).join("; ");
64366
- throw new Error(`inline_keyboard validation failed: ${summary}`);
64367
- }
64368
- streamButtonMeta = extractAgentButtonMeta(rawStreamKeyboard);
64369
- streamReplyMarkup = { inline_keyboard: wrapAgentCallbacks(rawStreamKeyboard) };
64370
- }
64371
- {
64372
- const streamThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
64373
- const sKeyBefore = streamKey3(streamChatId, streamThreadId);
64374
- if (!activeDraftStreams.has(sKeyBefore)) {
64375
- const sKey = statusKey(streamChatId, streamThreadId);
64376
- noteOutbound(sKey, Date.now());
64377
- noteOutbound2(sKey, Date.now());
64378
- shadowEmit({ kind: "modelOutbound", key: sKey, at: Date.now() });
64379
- if (turn != null && !firstTextReplyLogged.has(sKey)) {
64380
- firstTextReplyLogged.add(sKey);
64381
- logStreamingEvent({
64382
- kind: "turn_reply_timing",
64383
- chatId: streamChatId,
64384
- threadId: streamThreadId,
64385
- turnId: turn.turnId,
64386
- timeToFirstTextReplyMs: Date.now() - turn.gatewayReceiveAt
64387
- });
64388
- }
64389
- if (isFinalAnswerReply({
64390
- text: args.text ?? "",
64391
- disableNotification: args.disable_notification === true,
64392
- done: args.done === true
64393
- })) {
64394
- clearSilentEndState(sKey);
64395
- }
64396
- }
64397
- }
64398
- if (turn != null && isSubstantiveFinalReply({
64399
- text: args.text ?? "",
64400
- disableNotification: args.disable_notification === true,
64401
- done: args.done === true
64402
- })) {
64403
- const ea = emissionAuthorityFor(turn);
64404
- ea.markSubstantiveFinalDelivered(() => {
64405
- turn.finalAnswerEverDelivered = true;
64406
- if (turn.finalAnswerDeliveredAt == null)
64407
- turn.finalAnswerDeliveredAt = Date.now();
64408
- });
64409
- ea.finalizeCard(() => {
64410
- clearActivitySummary(turn);
64411
- });
64412
- }
64413
- const result = await handleStreamReply({
64414
- chat_id: streamChatId,
64415
- text: args.text,
64416
- done: Boolean(args.done),
64417
- message_thread_id: args.message_thread_id,
64418
- format: args.format,
64419
- reply_to: args.reply_to,
64420
- quote: args.quote,
64421
- ...args.protect_content === true ? { protect_content: true } : {},
64422
- ...args.quote_text != null ? { quote_text: args.quote_text } : {},
64423
- ...streamReplyMarkup != null ? { reply_markup: streamReplyMarkup } : {},
64424
- ...args.disable_notification === true ? { disable_notification: true } : {}
64425
- }, { activeDraftStreams, suppressPtyPreview }, {
64426
- bot: lockedBot,
64427
- retry: robustApiCall,
64428
- repairEscapedWhitespace,
64429
- normalizeParagraphBreaks,
64430
- normalizePunctuation,
64431
- stripExcessBold,
64432
- addParagraphSpacers,
64433
- assertAllowedChat,
64434
- resolveThreadId,
64435
- disableLinkPreview: access.disableLinkPreview !== false,
64436
- defaultFormat: access.parseMode ?? "html",
64437
- logStreamingEvent,
64438
- isPrivateChat: streamIsPrivate,
64439
- isForumTopic: streamIsForumTopic,
64440
- recordOutboundDelivered: (chatId, threadId) => {
64441
- progressDriver?.recordOutboundDelivered(chatId, threadId != null ? String(threadId) : undefined);
64442
- },
64443
- forceCompleteTurn: (chatId, threadId) => {
64444
- progressDriver?.forceCompleteTurn({
64445
- chatId,
64446
- threadId: threadId != null ? String(threadId) : undefined
64447
- });
64448
- },
64449
- historyEnabled: HISTORY_ENABLED,
64450
- recordOutbound,
64451
- ...HISTORY_ENABLED ? { getLatestInboundMessageId } : {},
64452
- writeError: (line) => process.stderr.write(line),
64453
- ...STREAM_THROTTLE_MS_OVERRIDE != null ? { throttleMs: STREAM_THROTTLE_MS_OVERRIDE } : {},
64454
- progressCardActive: streamMode === "checklist"
64455
- });
64456
- if (result.messageId != null) {
64457
- try {
64458
- progressDriver?.recordOutboundDelivered(String(args.chat_id ?? ""), args.message_thread_id);
64459
- } catch {}
64460
- try {
64461
- const threadIdNum2 = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
64462
- noteSignal(statusKey(String(args.chat_id ?? ""), threadIdNum2), Date.now());
64463
- } catch {}
64464
- }
64465
- if (args.done === true && result.messageId != null && streamButtonMeta != null && streamButtonMeta.size > 0) {
64466
- rememberAgentButtonMeta(String(args.chat_id ?? ""), result.messageId, streamButtonMeta);
64467
- }
64468
- if (args.done === true && result.messageId != null) {
64469
- const sChatId = String(args.chat_id ?? "");
64470
- const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
64471
- outboundDedup.record(sChatId, sThreadId, args.text, Date.now(), currentTurn?.registryKey ?? null);
64472
- const streamFormat = args.format ?? (access.parseMode ?? "html");
64473
- const streamLiteralText = streamFormat === "text";
64474
- clearPending(statusKey(sChatId, sThreadId), "reply_finalize");
64475
- noteOutbound3(statusKey(sChatId, sThreadId), {
64476
- messageId: result.messageId,
64477
- text: args.text,
64478
- literalText: streamLiteralText
64479
- });
64480
- }
64481
- if (turn != null && isFinalAnswerReply({
64482
- text: args.text ?? "",
64483
- disableNotification: args.disable_notification === true,
64484
- done: args.done === true
64485
- })) {
64486
- turn.finalAnswerDelivered = true;
64487
- turn.finalAnswerSubstantive = isSubstantiveFinalReply({
64488
- text: args.text ?? "",
64489
- disableNotification: args.disable_notification === true,
64490
- done: args.done === true
64491
- });
64492
- if (turn.finalAnswerSubstantive)
64493
- turn.finalAnswerEverDelivered = true;
64494
- if (turn.finalAnswerSubstantive && turn.finalAnswerDeliveredAt == null)
64495
- turn.finalAnswerDeliveredAt = Date.now();
64496
- if (turn.finalAnswerSubstantive)
64497
- closeObligationOnSubstantiveReply(args, turn, streamRoutedOriginTurn);
64498
- const streamThreadIdForClear = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
64499
- clearSilentEndState(statusKey(streamChatId, streamThreadIdForClear));
64500
- }
64501
- {
64502
- const sChat = String(args.chat_id ?? "");
64503
- const sThread = resolveThreadId(sChat, args.message_thread_id);
64504
- releaseTurnBufferGate(statusKey(sChat, sThread), turn ?? undefined);
64505
- if (turn?.finalAnswerDelivered === true) {
64506
- reapQueuedStatus(turn.sessionChatId, turn.sessionThreadId);
64507
- }
64508
- }
64509
- return { content: [{ type: "text", text: `${result.status} (id: ${result.messageId ?? "pending"})` }] };
64510
- }
64511
64447
  async function executeProgressUpdate(args) {
64512
64448
  if (!args.chat_id)
64513
64449
  throw new Error("progress_update: chat_id is required");
@@ -64518,6 +64454,7 @@ async function executeProgressUpdate(args) {
64518
64454
  const threadId = resolveThreadId(chat_id, args.message_thread_id);
64519
64455
  const key = statusKey(chat_id, threadId);
64520
64456
  assertAllowedChat(chat_id);
64457
+ text2 = redactOutboundText(text2, "progress_update");
64521
64458
  if (text2.length > 300) {
64522
64459
  text2 = text2.slice(0, 299) + "\u2026";
64523
64460
  }
@@ -64551,38 +64488,6 @@ async function executeProgressUpdate(args) {
64551
64488
  }
64552
64489
  progressUpdateTurnCount.set(key, currentCount + 1);
64553
64490
  }
64554
- const agentIdHint = typeof args.agent_id === "string" && args.agent_id || null;
64555
- const toolUseIdHint = typeof args.tool_use_id === "string" && args.tool_use_id || null;
64556
- const subAgent = resolveCallingSubagent({
64557
- db: turnsDb,
64558
- chatId: chat_id,
64559
- threadId,
64560
- agentIdHint,
64561
- toolUseIdHint
64562
- });
64563
- if (subAgent != null && progressDriver != null) {
64564
- const cardText = text2.length > 200 ? text2.slice(0, 199) + "\u2026" : text2;
64565
- const result = progressDriver.recordSubAgentNarrative({
64566
- chatId: chat_id,
64567
- threadId: threadId != null ? String(threadId) : undefined,
64568
- agentId: subAgent.agentId,
64569
- text: cardText
64570
- });
64571
- if (result.ok) {
64572
- progressUpdateLastSent.set(key, now);
64573
- try {
64574
- noteSignal(key, Date.now());
64575
- } catch {}
64576
- return {
64577
- content: [
64578
- {
64579
- type: "text",
64580
- text: JSON.stringify({ ok: true, mode: "card", agent_id: subAgent.agentId })
64581
- }
64582
- ]
64583
- };
64584
- }
64585
- }
64586
64491
  const access = loadAccess();
64587
64492
  const literalText = (access.parseMode ?? "html") === "text";
64588
64493
  const sendOpts = {
@@ -64764,7 +64669,7 @@ async function publishToTelegraph(text2, shortName, authorName) {
64764
64669
  const accountPath = join40(STATE_DIR, "telegraph-account.json");
64765
64670
  let account = null;
64766
64671
  try {
64767
- if (existsSync44(accountPath)) {
64672
+ if (existsSync43(accountPath)) {
64768
64673
  const raw = readFileSync41(accountPath, "utf-8");
64769
64674
  const parsed = JSON.parse(raw);
64770
64675
  if (parsed.shortName && parsed.accessToken) {
@@ -64785,7 +64690,7 @@ async function publishToTelegraph(text2, shortName, authorName) {
64785
64690
  account = created.value;
64786
64691
  try {
64787
64692
  mkdirSync32(STATE_DIR, { recursive: true, mode: 448 });
64788
- writeFileSync31(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
64693
+ writeFileSync30(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
64789
64694
  } catch (err) {
64790
64695
  process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
64791
64696
  `);
@@ -65227,7 +65132,7 @@ async function executeDownloadAttachment(args) {
65227
65132
  });
65228
65133
  mkdirSync32(INBOX_DIR, { recursive: true, mode: 448 });
65229
65134
  assertInsideInbox(INBOX_DIR, dlPath);
65230
- writeFileSync31(dlPath, buf, { mode: 384 });
65135
+ writeFileSync30(dlPath, buf, { mode: 384 });
65231
65136
  return { content: [{ type: "text", text: dlPath }] };
65232
65137
  }
65233
65138
  async function executeEditMessage(args) {
@@ -65402,7 +65307,7 @@ function resetOrphanedReplyTimeout() {
65402
65307
  currentSessionChatId: turn.sessionChatId,
65403
65308
  capturedTextCount: turn.capturedText.length,
65404
65309
  replyCalled: turn.replyCalled,
65405
- progressCardActive: progressDriver != null
65310
+ progressCardActive: false
65406
65311
  })) {
65407
65312
  turn.orphanedReplyTimeoutId = setTimeout(() => {
65408
65313
  const t = currentTurn;
@@ -65413,7 +65318,7 @@ function resetOrphanedReplyTimeout() {
65413
65318
  currentSessionChatId: t.sessionChatId,
65414
65319
  capturedTextCount: t.capturedText.length,
65415
65320
  replyCalled: t.replyCalled,
65416
- progressCardActive: progressDriver != null
65321
+ progressCardActive: false
65417
65322
  })) {
65418
65323
  const turnKey = statusKey(t.sessionChatId, t.sessionThreadId);
65419
65324
  const working = isLegitimatelyWorking(turnKey);
@@ -65583,7 +65488,7 @@ function openLivenessFeedIfDue(turn) {
65583
65488
  toolCount: turn.labeledToolCount,
65584
65489
  state: "running"
65585
65490
  };
65586
- const rendered = renderActivityFeedWithNested(lines, [], false, ` \xB7 ${formatFeedElapsed3(age)}`, undefined, livenessHeader);
65491
+ const rendered = renderActivityFeedWithNested(lines, [], false, formatStepSuffix2(age), undefined, livenessHeader);
65587
65492
  if (rendered == null)
65588
65493
  return;
65589
65494
  turn.activityPendingRender = rendered;
@@ -65646,7 +65551,7 @@ function feedHeartbeatTick() {
65646
65551
  };
65647
65552
  const lines = turn.mirrorLines.length > 0 ? turn.mirrorLines : ["Working in background\u2026"];
65648
65553
  const elapsed2 = Date.now() - subagentAt;
65649
- const rendered2 = renderActivityFeedWithNested(lines, [], false, ` \xB7 ${formatFeedElapsed3(elapsed2)}`, undefined, livenessHeader);
65554
+ const rendered2 = renderActivityFeedWithNested(lines, [], false, formatStepSuffix2(elapsed2), undefined, livenessHeader);
65650
65555
  if (rendered2 == null)
65651
65556
  return;
65652
65557
  turn.activityPendingRender = rendered2;
@@ -65671,7 +65576,7 @@ function feedHeartbeatTick() {
65671
65576
  const elapsed = Date.now() - turn.lastToolLabelAt;
65672
65577
  if (elapsed < FEED_HEARTBEAT_MIN_STALE_MS)
65673
65578
  return;
65674
- const rendered = composeTurnActivity(turn, false, ` \xB7 ${formatFeedElapsed3(elapsed)}`);
65579
+ const rendered = composeTurnActivity(turn, false, formatStepSuffix2(elapsed));
65675
65580
  if (rendered == null)
65676
65581
  return;
65677
65582
  turn.activityPendingRender = rendered;
@@ -65799,7 +65704,7 @@ function handleSessionEvent(ev) {
65799
65704
  rememberRecentTurn(next);
65800
65705
  promoteQueuedStatus(ev.chatId, enqThreadIdNum);
65801
65706
  if (DELIVERY_CONFIRM_ENABLED) {
65802
- ackDelivery(deliveryQueue, chatKey2(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null), ev.messageId);
65707
+ ackDelivery(deliveryQueue, chatKey2(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null), ev.messageId, ev.rawContent);
65803
65708
  }
65804
65709
  shadowEmit({
65805
65710
  kind: "turnStart",
@@ -66149,7 +66054,7 @@ function handleSessionEvent(ev) {
66149
66054
  unpinProgressCardForChat?.(chatId, threadId);
66150
66055
  finalizeStatusReaction(chatId, threadId, "done");
66151
66056
  {
66152
- const sKey = streamKey3(chatId, threadId);
66057
+ const sKey = streamKey2(chatId, threadId);
66153
66058
  const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
66154
66059
  logStreamingEvent({
66155
66060
  kind: "turn_end",
@@ -66194,10 +66099,12 @@ function handleSessionEvent(ev) {
66194
66099
  }
66195
66100
  if (flushDecision.kind === "flush") {
66196
66101
  let capturedText = flushDecision.text;
66102
+ capturedText = normalizeParagraphBreaks(repairEscapedWhitespace(capturedText));
66197
66103
  const backstopChatId = chatId;
66198
66104
  const backstopThreadId = threadId;
66199
66105
  const backstopCtrl = ctrl;
66200
66106
  capturedText = redactOutboundText(capturedText, "turn_flush");
66107
+ capturedText = stripExcessBold(normalizePunctuation(capturedText));
66201
66108
  {
66202
66109
  const scrub = scrubVoice(capturedText);
66203
66110
  if (scrub.replaced > 0) {
@@ -66247,7 +66154,8 @@ function handleSessionEvent(ev) {
66247
66154
  link_preview_options: { is_disabled: true }
66248
66155
  };
66249
66156
  const limit = RICH_MESSAGE_MAX_CHARS2;
66250
- const htmlChunks = splitMarkdownChunks(capturedText, limit);
66157
+ const renderedText = addParagraphSpacers(capturedText);
66158
+ const htmlChunks = splitMarkdownChunks(renderedText, limit);
66251
66159
  const sentIds = [];
66252
66160
  try {
66253
66161
  let firstSendUsedEdit = false;
@@ -66336,7 +66244,7 @@ function handleSessionEvent(ev) {
66336
66244
  }
66337
66245
  finalizeStatusReaction(chatId, threadId, terminalReason);
66338
66246
  {
66339
- const sKey = streamKey3(chatId, threadId);
66247
+ const sKey = streamKey2(chatId, threadId);
66340
66248
  const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
66341
66249
  logStreamingEvent({
66342
66250
  kind: "turn_end",
@@ -66736,15 +66644,23 @@ async function handleInbound(ctx, text2, downloadImage, attachment, extraAttachm
66736
66644
  }
66737
66645
  if (STATUS_QUERY_RE.test(text2)) {
66738
66646
  try {
66739
- const threadKey = messageThreadId != null ? String(messageThreadId) : undefined;
66740
- const cardState = progressDriver?.peek(chat_id, threadKey);
66741
- const turnAgeS = cardState?.turnStartedAt ? Math.max(0, Math.floor((Date.now() - cardState.turnStartedAt) / 1000)) : -1;
66742
- const stage = cardState?.stage ?? "idle";
66743
- const itemCount = cardState?.items.length ?? 0;
66744
- const subAgentCount = cardState?.subAgents.size ?? 0;
66647
+ const thread = messageThreadId != null ? String(messageThreadId) : "none";
66648
+ const key = statusKey(chat_id, messageThreadId);
66649
+ const turnStartedAt = activeTurnStartedAt.get(key);
66650
+ const turnActive = turnStartedAt != null;
66651
+ const surfaces = {
66652
+ turnActive,
66653
+ turnAgeS: turnActive ? Math.max(0, Math.floor((Date.now() - turnStartedAt) / 1000)) : -1,
66654
+ runningWorkers: countRunningWorkers(),
66655
+ workerFeedSize: workerActivityFeed?.size ?? 0,
66656
+ pendingAsync: hasPendingAsyncDispatch(key) ? 1 : 0
66657
+ };
66658
+ const telemetry = deriveStatusQueryTelemetry(surfaces);
66745
66659
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME ?? "-";
66746
- process.stderr.write(`telegram gateway: ux-failure: status-query agent=${agentName3} chat_id=${chat_id} thread=${threadKey ?? "none"} card_stage=${stage} card_turn_age_s=${turnAgeS} card_items=${itemCount} card_subagents=${subAgentCount}
66747
- `);
66660
+ process.stderr.write(formatStatusQueryLine(agentName3, String(chat_id), thread, surfaces, telemetry));
66661
+ if (telemetry.idle) {
66662
+ process.stderr.write(formatStatusQueryUxFailureLine(agentName3, String(chat_id), thread));
66663
+ }
66748
66664
  } catch (err) {
66749
66665
  process.stderr.write(`telegram gateway: status-query telemetry failed: ${err.message}
66750
66666
  `);
@@ -67113,7 +67029,7 @@ ${preBlock(write.output)}`;
67113
67029
  bot.api.setMessageReaction(chat_id, msgId, [{ type: "emoji", emoji: "\uD83D\uDC40" }]).catch(() => {});
67114
67030
  logStreamingEvent({ kind: "inbound_ack", chatId: chat_id, messageId: msgId, ackDelayMs: Date.now() - inboundReceivedAt });
67115
67031
  } else {
67116
- const sKey = streamKey3(chat_id, messageThreadId);
67032
+ const sKey = streamKey2(chat_id, messageThreadId);
67117
67033
  const priorStream = activeDraftStreams.get(sKey);
67118
67034
  if (priorStream && !priorStream.isFinal()) {
67119
67035
  activeDraftStreams.delete(sKey);
@@ -67486,7 +67402,7 @@ function writeRestartMarker(marker) {
67486
67402
  if (!p)
67487
67403
  return;
67488
67404
  try {
67489
- writeFileSync31(p, JSON.stringify(marker));
67405
+ writeFileSync30(p, JSON.stringify(marker));
67490
67406
  lastPlannedRestartAt = Date.now();
67491
67407
  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}
67492
67408
  `);
@@ -67654,7 +67570,7 @@ var _dockerReachable;
67654
67570
  function isDockerReachable() {
67655
67571
  if (_dockerReachable !== undefined)
67656
67572
  return _dockerReachable;
67657
- if (!existsSync44("/var/run/docker.sock")) {
67573
+ if (!existsSync43("/var/run/docker.sock")) {
67658
67574
  _dockerReachable = false;
67659
67575
  return _dockerReachable;
67660
67576
  }
@@ -67676,7 +67592,7 @@ function spawnSwitchroomDetached(args, onFailure) {
67676
67592
  try {
67677
67593
  mkdirSync32(STATE_DIR, { recursive: true });
67678
67594
  outFd = openSync9(logPath, "a");
67679
- writeFileSync31(logPath, `
67595
+ writeFileSync30(logPath, `
67680
67596
  [${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
67681
67597
  `, { flag: "a" });
67682
67598
  } catch {}
@@ -68070,7 +67986,7 @@ bot.use(async (ctx, next) => {
68070
67986
  function readRecentDenialsForAgent(agentName3, windowMs, limit) {
68071
67987
  try {
68072
67988
  const auditPath = join40(homedir14(), ".switchroom", "vault-audit.log");
68073
- if (!existsSync44(auditPath))
67989
+ if (!existsSync43(auditPath))
68074
67990
  return [];
68075
67991
  const raw = readFileSync41(auditPath, "utf8");
68076
67992
  return recentDenialsFromAuditLog(raw, { agentName: agentName3, windowMs, limit });
@@ -68498,7 +68414,7 @@ function flushAgentHandoff(agentDir) {
68498
68414
  for (const fname of [".handoff.md", ".handoff-topic"]) {
68499
68415
  const p = join40(agentDir, fname);
68500
68416
  try {
68501
- if (existsSync44(p)) {
68417
+ if (existsSync43(p)) {
68502
68418
  unlinkSync16(p);
68503
68419
  removed++;
68504
68420
  }
@@ -68554,7 +68470,7 @@ async function handleNewCommand(ctx) {
68554
68470
  writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
68555
68471
  if (agentDir != null) {
68556
68472
  try {
68557
- writeFileSync31(join40(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
68473
+ writeFileSync30(join40(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
68558
68474
  `, "utf8");
68559
68475
  } catch (err) {
68560
68476
  process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
@@ -68918,15 +68834,15 @@ bot.command("interrupt", async (ctx) => {
68918
68834
  });
68919
68835
  var lockoutOps = {
68920
68836
  readFileSync: (p, enc) => readFileSync41(p, enc),
68921
- writeFileSync: (p, data, opts) => writeFileSync31(p, data, opts),
68922
- existsSync: (p) => existsSync44(p),
68837
+ writeFileSync: (p, data, opts) => writeFileSync30(p, data, opts),
68838
+ existsSync: (p) => existsSync43(p),
68923
68839
  mkdirSync: (p, opts) => mkdirSync32(p, opts),
68924
68840
  joinPath: (...parts) => join40(...parts)
68925
68841
  };
68926
68842
  var FLEET_FALLBACK_DEDUP_MS = 30000;
68927
68843
  function isAuthBrokerSocketReachable() {
68928
68844
  try {
68929
- return existsSync44(resolveAuthBrokerSocketPath2());
68845
+ return existsSync43(resolveAuthBrokerSocketPath2());
68930
68846
  } catch {
68931
68847
  return false;
68932
68848
  }
@@ -69582,7 +69498,7 @@ async function handleVaultRecentDenialCallback(ctx, data) {
69582
69498
  const tokenPath = join40(homedir14(), ".switchroom", "agents", agentName3, ".vault-token");
69583
69499
  try {
69584
69500
  mkdirSync32(join40(homedir14(), ".switchroom", "agents", agentName3), { recursive: true });
69585
- writeFileSync31(tokenPath, token, { mode: 384 });
69501
+ writeFileSync30(tokenPath, token, { mode: 384 });
69586
69502
  } catch (err) {
69587
69503
  await switchroomReply(ctx, `**Grant created (${escapeHtmlForTg2(id)}) but token write failed:** ${escapeHtmlForTg2(String(err))}
69588
69504
  _Recover with: \`switchroom vault grant ${escapeHtmlForTg2(agentName3)} --keys ${escapeHtmlForTg2(keyName)} --duration 30d\` on the host._`, { html: true });
@@ -69660,7 +69576,7 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
69660
69576
  const tokenPath = join40(homedir14(), ".switchroom", "agents", pending2.agent, ".vault-token");
69661
69577
  try {
69662
69578
  mkdirSync32(join40(homedir14(), ".switchroom", "agents", pending2.agent), { recursive: true });
69663
- writeFileSync31(tokenPath, token, { mode: 384 });
69579
+ writeFileSync30(tokenPath, token, { mode: 384 });
69664
69580
  } catch (err) {
69665
69581
  await switchroomReply(ctx, `**Grant created (${escapeHtmlForTg2(id)}) but token write failed:** ${escapeHtmlForTg2(String(err))}
69666
69582
  _Recover with: \`switchroom vault grant ${escapeHtmlForTg2(pending2.agent)} --keys ${escapeHtmlForTg2(pending2.key)} --duration ${Math.round(pending2.ttl_seconds / 86400)}d\` on the host._`, { html: true });
@@ -70232,7 +70148,7 @@ async function executeGrantWizard(ctx, chatId, state4) {
70232
70148
  const tokenPath = join40(homedir14(), ".switchroom", "agents", state4.agent, ".vault-token");
70233
70149
  try {
70234
70150
  mkdirSync32(join40(homedir14(), ".switchroom", "agents", state4.agent), { recursive: true });
70235
- writeFileSync31(tokenPath, token, { mode: 384 });
70151
+ writeFileSync30(tokenPath, token, { mode: 384 });
70236
70152
  } catch (err) {
70237
70153
  await switchroomReply(ctx, `**Grant created but token write failed:** ${escapeHtmlForTg2(String(err))}`, { html: true });
70238
70154
  return;
@@ -71962,7 +71878,7 @@ bot.on("message:photo", async (ctx) => {
71962
71878
  });
71963
71879
  mkdirSync32(INBOX_DIR, { recursive: true, mode: 448 });
71964
71880
  assertInsideInbox(INBOX_DIR, dlPath);
71965
- writeFileSync31(dlPath, buf, { mode: 384 });
71881
+ writeFileSync30(dlPath, buf, { mode: 384 });
71966
71882
  return dlPath;
71967
71883
  } catch (err) {
71968
71884
  const msg = err instanceof Error ? err.message : "unknown error";
@@ -73152,28 +73068,6 @@ var didOneTimeSetup = false;
73152
73068
  parentStateDir: STATE_DIR,
73153
73069
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
73154
73070
  `),
73155
- onStall: (agentId, idleMs, description) => {
73156
- progressDriver?.onSubAgentStall(agentId, idleMs, description);
73157
- },
73158
- onUnstall: (agentId, description) => {
73159
- progressDriver?.onSubAgentUnstall?.(agentId, description);
73160
- },
73161
- onStallTerminal: (agentId) => {
73162
- try {
73163
- const fleets = progressDriver?.peekAllFleets() ?? [];
73164
- for (const f of fleets) {
73165
- if (f.fleet.has(agentId)) {
73166
- progressDriver?.ingest({ kind: "sub_agent_turn_end", agentId }, f.chatId ?? null, undefined);
73167
- return;
73168
- }
73169
- }
73170
- process.stderr.write(`telegram gateway: subagent-watcher: onStallTerminal ${agentId} \u2014 no fleet entry to retarget; synthetic sub_agent_turn_end skipped
73171
- `);
73172
- } catch (err) {
73173
- process.stderr.write(`telegram gateway: subagent-watcher: onStallTerminal ${agentId} ingest error: ${err.message}
73174
- `);
73175
- }
73176
- },
73177
73071
  onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs }) => {
73178
73072
  deferredDoneReactions.promote();
73179
73073
  let fleetChatId = "";
@@ -73193,6 +73087,20 @@ var didOneTimeSetup = false;
73193
73087
  } catch {}
73194
73088
  }
73195
73089
  const isBackground = dispatch.isBackground;
73090
+ if (dispatch.isNested) {
73091
+ if (workerFeedEnabled) {
73092
+ workerActivityFeed?.finish(agentId, {
73093
+ description: dispatch.feedDescription,
73094
+ lastTool: null,
73095
+ toolCount,
73096
+ latestSummary: resultText,
73097
+ elapsedMs: durationMs,
73098
+ state: outcome === "failed" ? "failed" : "done"
73099
+ });
73100
+ reconcileWorkerPin(agentId, null, false);
73101
+ }
73102
+ return;
73103
+ }
73196
73104
  if (!isBackground) {
73197
73105
  const turn = currentTurn;
73198
73106
  if (turn != null && turn.foregroundSubAgents.has(agentId)) {
@@ -73302,11 +73210,12 @@ var didOneTimeSetup = false;
73302
73210
  dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description);
73303
73211
  } catch {}
73304
73212
  }
73305
- const isBackground = dispatch.isBackground;
73213
+ const isBackground = dispatch.isBackground || dispatch.isNested;
73214
+ const stepLine = progressLine != null && progressLine.length > 0 ? progressLine : latestSummary;
73306
73215
  if (!isBackground) {
73307
73216
  const surface = resolveSubagentStatusSurface({
73308
73217
  isBackground: false,
73309
- liveTurnPresent: currentTurn != null,
73218
+ liveTurnPresent: currentTurn != null && dispatch.hasRow,
73310
73219
  workerFeedEnabled,
73311
73220
  orphanStatusEnabled
73312
73221
  });
@@ -73317,7 +73226,7 @@ var didOneTimeSetup = false;
73317
73226
  description: dispatch.feedDescription,
73318
73227
  lastTool,
73319
73228
  toolCount,
73320
- latestSummary,
73229
+ latestSummary: stepLine,
73321
73230
  elapsedMs,
73322
73231
  state: "running"
73323
73232
  }, origin?.threadId)?.then(() => reconcileWorkerPin(agentId, wkChat, true));
@@ -73341,7 +73250,7 @@ var didOneTimeSetup = false;
73341
73250
  narrative = [];
73342
73251
  turn.foregroundSubAgents.set(agentId, narrative);
73343
73252
  }
73344
- if (narrative[narrative.length - 1] !== child) {
73253
+ if (!narrative.includes(child)) {
73345
73254
  narrative.push(child);
73346
73255
  if (narrative.length > FOREGROUND_SUBAGENT_ACCUM_MAX) {
73347
73256
  narrative.splice(0, narrative.length - FOREGROUND_SUBAGENT_ACCUM_MAX);
@@ -73375,7 +73284,7 @@ var didOneTimeSetup = false;
73375
73284
  description: dispatch.feedDescription,
73376
73285
  lastTool,
73377
73286
  toolCount,
73378
- latestSummary,
73287
+ latestSummary: stepLine,
73379
73288
  elapsedMs,
73380
73289
  state: "running"
73381
73290
  }, origin?.threadId)?.then(() => reconcileWorkerPin(agentId, wkChat, true));