switchroom 0.13.9 → 0.13.11

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 (29) hide show
  1. package/dist/cli/switchroom.js +38 -14
  2. package/dist/host-control/main.js +222 -7
  3. package/examples/switchroom.yaml +25 -7
  4. package/package.json +1 -1
  5. package/profiles/_shared/telegram-style.md.hbs +1 -1
  6. package/telegram-plugin/dist/bridge/bridge.js +23 -4
  7. package/telegram-plugin/dist/gateway/gateway.js +540 -147
  8. package/telegram-plugin/dist/server.js +23 -4
  9. package/telegram-plugin/gateway/config-approval-handler.test.ts +246 -0
  10. package/telegram-plugin/gateway/config-approval-handler.ts +284 -0
  11. package/telegram-plugin/gateway/gateway.ts +218 -25
  12. package/telegram-plugin/gateway/ipc-protocol.ts +72 -2
  13. package/telegram-plugin/gateway/ipc-server.ts +101 -0
  14. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +185 -0
  15. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +69 -0
  16. package/telegram-plugin/model-unavailable.ts +11 -1
  17. package/telegram-plugin/operator-events.fixtures.json +14 -24
  18. package/telegram-plugin/operator-events.ts +11 -2
  19. package/telegram-plugin/session-tail.ts +71 -4
  20. package/telegram-plugin/subagent-watcher.ts +39 -0
  21. package/telegram-plugin/tests/model-unavailable.test.ts +15 -2
  22. package/telegram-plugin/tests/operator-events-session-tail.test.ts +53 -2
  23. package/telegram-plugin/tests/operator-events.test.ts +14 -7
  24. package/telegram-plugin/tests/subagent-handback-decision.test.ts +112 -0
  25. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +105 -0
  26. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +61 -0
  27. package/telegram-plugin/tests/subagent-watcher.test.ts +67 -1
  28. package/telegram-plugin/uat/scenarios/jtbd-subagent-handback-dm.test.ts +95 -0
  29. package/profiles/default/CLAUDE.md +0 -193
@@ -27225,7 +27225,7 @@ var init_secretlint_source = __esm(() => {
27225
27225
  function escapeHtml8(s) {
27226
27226
  return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
27227
27227
  }
27228
- function truncate2(s, n) {
27228
+ function truncate3(s, n) {
27229
27229
  return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
27230
27230
  }
27231
27231
 
@@ -29017,6 +29017,161 @@ var init_materialize_bot_token = __esm(() => {
29017
29017
  };
29018
29018
  });
29019
29019
 
29020
+ // gateway/config-approval-handler.ts
29021
+ var exports_config_approval_handler = {};
29022
+ __export(exports_config_approval_handler, {
29023
+ resolvePendingConfigApproval: () => resolvePendingConfigApproval,
29024
+ parseConfigApprovalCallback: () => parseConfigApprovalCallback,
29025
+ handleRequestConfigFinalize: () => handleRequestConfigFinalize,
29026
+ handleRequestConfigApproval: () => handleRequestConfigApproval,
29027
+ buildConfigApprovalCardBody: () => buildConfigApprovalCardBody,
29028
+ _resetPendingConfigApprovalsForTest: () => _resetPendingConfigApprovalsForTest,
29029
+ _peekPendingConfigApprovalForTest: () => _peekPendingConfigApprovalForTest
29030
+ });
29031
+ function buildConfigApprovalCardBody(args) {
29032
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29033
+ return `\uD83D\uDEE0 <b>Config edit proposed</b>
29034
+ ` + `Agent: <code>${esc(args.agentName)}</code>
29035
+ ` + `Reason: ${esc(args.reason)}
29036
+
29037
+ ` + `<pre>${esc(args.unifiedDiff)}</pre>`;
29038
+ }
29039
+ async function handleRequestConfigApproval(client3, msg, deps) {
29040
+ const reply = (verdict, reason) => {
29041
+ try {
29042
+ client3.send({
29043
+ type: "config_approval_resolved",
29044
+ requestId: msg.requestId,
29045
+ verdict,
29046
+ ...reason ? { reason } : {}
29047
+ });
29048
+ } catch (err) {
29049
+ deps.log?.(`config_approval_resolved send failed (requestId=${msg.requestId}): ${err.message}`);
29050
+ }
29051
+ };
29052
+ if (msg.agentName !== deps.agentName) {
29053
+ reply("deny", `gateway serves '${deps.agentName}', not '${msg.agentName}'`);
29054
+ return;
29055
+ }
29056
+ const target = deps.loadTargetChat();
29057
+ if (target === null) {
29058
+ reply("deny", "no target chat available \u2014 operator not paired?");
29059
+ return;
29060
+ }
29061
+ const body = buildConfigApprovalCardBody({
29062
+ agentName: msg.agentName,
29063
+ reason: msg.reason,
29064
+ unifiedDiff: msg.unifiedDiff
29065
+ });
29066
+ const replyMarkup = deps.buildKeyboard(msg.requestId);
29067
+ const posted = await deps.postCard({
29068
+ chatId: target.chatId,
29069
+ ...target.threadId !== undefined ? { threadId: target.threadId } : {},
29070
+ text: body,
29071
+ replyMarkup
29072
+ });
29073
+ if (posted === null) {
29074
+ reply("deny", "Telegram sendMessage failed");
29075
+ return;
29076
+ }
29077
+ const entry = {
29078
+ requestId: msg.requestId,
29079
+ client: client3,
29080
+ chatId: target.chatId,
29081
+ ...target.threadId !== undefined ? { threadId: target.threadId } : {},
29082
+ messageId: posted.messageId,
29083
+ timer: null,
29084
+ resolved: false
29085
+ };
29086
+ entry.timer = setTimeout(() => {
29087
+ resolvePendingConfigApproval(msg.requestId, "timeout", deps).catch((err) => deps.log?.(`config approval timeout handler threw (requestId=${msg.requestId}): ${err.message}`));
29088
+ }, msg.timeoutMs);
29089
+ pending.set(msg.requestId, entry);
29090
+ deps.log?.(`config_approval_posted requestId=${msg.requestId} agent=${msg.agentName} messageId=${posted.messageId}`);
29091
+ }
29092
+ async function resolvePendingConfigApproval(requestId, verdict, deps) {
29093
+ const entry = pending.get(requestId);
29094
+ if (!entry || entry.resolved)
29095
+ return false;
29096
+ entry.resolved = true;
29097
+ if (entry.timer !== null) {
29098
+ clearTimeout(entry.timer);
29099
+ entry.timer = null;
29100
+ }
29101
+ try {
29102
+ entry.client.send({
29103
+ type: "config_approval_resolved",
29104
+ requestId,
29105
+ verdict
29106
+ });
29107
+ } catch (err) {
29108
+ deps.log?.(`config_approval_resolved send failed (requestId=${requestId}): ${err.message}`);
29109
+ }
29110
+ const interim = verdict === "approve" ? "\uD83D\uDC40 <b>Applying\u2026</b>" : verdict === "deny" ? "\uD83D\uDEAB <b>Denied</b>" : "\u23f1 <b>Expired</b>";
29111
+ try {
29112
+ await deps.editCard({
29113
+ chatId: entry.chatId,
29114
+ messageId: entry.messageId,
29115
+ text: interim
29116
+ });
29117
+ } catch (err) {
29118
+ deps.log?.(`config approval card edit failed (requestId=${requestId}): ${err.message}`);
29119
+ }
29120
+ return true;
29121
+ }
29122
+ async function handleRequestConfigFinalize(_client, msg, deps) {
29123
+ const entry = pending.get(msg.requestId);
29124
+ if (!entry) {
29125
+ deps.log?.(`config_finalize: no pending entry for requestId=${msg.requestId} (likely already cleaned up)`);
29126
+ return;
29127
+ }
29128
+ pending.delete(msg.requestId);
29129
+ const body = msg.outcome === "applied" ? `\u2705 <b>Applied</b>${msg.detail ? `
29130
+ ${escapeHtml11(msg.detail)}` : ""}` : `\u26a0\ufe0f <b>Reconcile failed; rolled back</b>${msg.detail ? `
29131
+ ${escapeHtml11(msg.detail)}` : ""}`;
29132
+ try {
29133
+ await deps.editCard({
29134
+ chatId: entry.chatId,
29135
+ messageId: entry.messageId,
29136
+ text: body
29137
+ });
29138
+ } catch (err) {
29139
+ deps.log?.(`config finalize card edit failed (requestId=${msg.requestId}): ${err.message}`);
29140
+ }
29141
+ }
29142
+ function escapeHtml11(s) {
29143
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29144
+ }
29145
+ function _resetPendingConfigApprovalsForTest() {
29146
+ for (const entry of pending.values()) {
29147
+ if (entry.timer !== null)
29148
+ clearTimeout(entry.timer);
29149
+ }
29150
+ pending.clear();
29151
+ }
29152
+ function _peekPendingConfigApprovalForTest(requestId) {
29153
+ return pending.get(requestId);
29154
+ }
29155
+ function parseConfigApprovalCallback(data) {
29156
+ if (!data.startsWith("cfg:"))
29157
+ return null;
29158
+ const rest = data.slice(4);
29159
+ const colon = rest.lastIndexOf(":");
29160
+ if (colon < 0)
29161
+ return null;
29162
+ const requestId = rest.slice(0, colon);
29163
+ const choice = rest.slice(colon + 1);
29164
+ if (requestId.length === 0 || requestId.length > 64)
29165
+ return null;
29166
+ if (choice !== "approve" && choice !== "deny")
29167
+ return null;
29168
+ return { requestId, choice };
29169
+ }
29170
+ var pending;
29171
+ var init_config_approval_handler = __esm(() => {
29172
+ pending = new Map;
29173
+ });
29174
+
29020
29175
  // ../src/agents/tmux.ts
29021
29176
  var exports_tmux = {};
29022
29177
  __export(exports_tmux, {
@@ -29229,17 +29384,17 @@ function registerApprovalsCommands(bot, opts) {
29229
29384
  return;
29230
29385
  }
29231
29386
  if (decisions.length === 0) {
29232
- await ctx.reply(agentFilter ? `No active approvals for <code>${escapeHtml11(agentFilter)}</code>.` : "No active approvals.", { parse_mode: "HTML" });
29387
+ await ctx.reply(agentFilter ? `No active approvals for <code>${escapeHtml12(agentFilter)}</code>.` : "No active approvals.", { parse_mode: "HTML" });
29233
29388
  return;
29234
29389
  }
29235
29390
  const byAgent = new Map;
29236
29391
  for (const d of decisions)
29237
29392
  byAgent.set(d.agent_unit, (byAgent.get(d.agent_unit) ?? 0) + 1);
29238
- const summary = Array.from(byAgent.entries()).map(([a, n]) => `\u2022 <b>${escapeHtml11(a)}</b>: ${n}`).join(`
29393
+ const summary = Array.from(byAgent.entries()).map(([a, n]) => `\u2022 <b>${escapeHtml12(a)}</b>: ${n}`).join(`
29239
29394
  `);
29240
29395
  const detail = decisions.slice(0, 20).map((d) => {
29241
29396
  const ttl = d.ttl_expires_at === null ? "always" : `until ${new Date(d.ttl_expires_at).toISOString().slice(0, 16).replace("T", " ")}`;
29242
- return `<code>${escapeHtml11(d.id.slice(0, 8))}</code> ` + `${escapeHtml11(d.agent_unit)} \u2192 ` + `<code>${escapeHtml11(d.scope)}</code> ` + `(${escapeHtml11(d.action)}, ${ttl}) ` + `\u00b7 /approvals revoke ${escapeHtml11(d.id)}`;
29397
+ return `<code>${escapeHtml12(d.id.slice(0, 8))}</code> ` + `${escapeHtml12(d.agent_unit)} \u2192 ` + `<code>${escapeHtml12(d.scope)}</code> ` + `(${escapeHtml12(d.action)}, ${ttl}) ` + `\u00b7 /approvals revoke ${escapeHtml12(d.id)}`;
29243
29398
  }).join(`
29244
29399
  `);
29245
29400
  await ctx.reply(`<b>Active approvals</b>
@@ -29265,13 +29420,13 @@ ${detail}`, {
29265
29420
  await ctx.reply("Approval kernel unreachable.");
29266
29421
  return;
29267
29422
  }
29268
- await ctx.reply(ok ? `Revoked <code>${escapeHtml11(id)}</code>.` : `No such active decision <code>${escapeHtml11(id)}</code>.`, { parse_mode: "HTML" });
29423
+ await ctx.reply(ok ? `Revoked <code>${escapeHtml12(id)}</code>.` : `No such active decision <code>${escapeHtml12(id)}</code>.`, { parse_mode: "HTML" });
29269
29424
  return;
29270
29425
  }
29271
- await ctx.reply(`Unknown subcommand <code>${escapeHtml11(sub)}</code>. ` + `Use <code>/approvals list</code> or <code>/approvals revoke &lt;id&gt;</code>. ` + `(<code>add</code> and <code>stats</code> are coming in a follow-up.)`, { parse_mode: "HTML" });
29426
+ await ctx.reply(`Unknown subcommand <code>${escapeHtml12(sub)}</code>. ` + `Use <code>/approvals list</code> or <code>/approvals revoke &lt;id&gt;</code>. ` + `(<code>add</code> and <code>stats</code> are coming in a follow-up.)`, { parse_mode: "HTML" });
29272
29427
  });
29273
29428
  }
29274
- function escapeHtml11(s) {
29429
+ function escapeHtml12(s) {
29275
29430
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29276
29431
  }
29277
29432
  var init_approvals_commands = __esm(() => {
@@ -29353,16 +29508,16 @@ function renderAccountRow2(snap, opts) {
29353
29508
  const lines = [];
29354
29509
  const marker = snap.isActive ? "\u25cf " : "";
29355
29510
  if (!snap.quota) {
29356
- lines.push(`${marker}<code>${escapeHtml12(snap.label)}</code> <i>quota probe failed</i>`);
29511
+ lines.push(`${marker}<code>${escapeHtml13(snap.label)}</code> <i>quota probe failed</i>`);
29357
29512
  if (snap.quotaError) {
29358
- lines.push(` <i>${escapeHtml12(snap.quotaError)}</i>`);
29513
+ lines.push(` <i>${escapeHtml13(snap.quotaError)}</i>`);
29359
29514
  }
29360
29515
  return lines;
29361
29516
  }
29362
29517
  const q = snap.quota;
29363
29518
  const fiveStr = fmtPct2(q.fiveHourUtilizationPct);
29364
29519
  const sevenStr = fmtPct2(q.sevenDayUtilizationPct);
29365
- lines.push(`${marker}<code>${escapeHtml12(snap.label)}</code> ${fiveStr} / ${sevenStr}`);
29520
+ lines.push(`${marker}<code>${escapeHtml13(snap.label)}</code> ${fiveStr} / ${sevenStr}`);
29366
29521
  const health = classifyHealth2(snap);
29367
29522
  if (health === "blocked") {
29368
29523
  const win = bindingWindow2(q);
@@ -29468,13 +29623,13 @@ function renderFallbackAnnouncement2(input) {
29468
29623
  const limitWord = input.oldQuota ? limitWordFor2(input.oldQuota) : "quota";
29469
29624
  const headerLimit = limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
29470
29625
  if (!input.newLabel) {
29471
- lines.push(`\uD83D\uDD34 <b>All accounts blocked \u00b7 ${headerLimit} on ${escapeHtml12(input.oldLabel)}</b>`);
29626
+ lines.push(`\uD83D\uDD34 <b>All accounts blocked \u00b7 ${headerLimit} on ${escapeHtml13(input.oldLabel)}</b>`);
29472
29627
  lines.push("");
29473
- lines.push(`Triggered by: agent <b>${escapeHtml12(input.triggerAgent)}</b>`);
29628
+ lines.push(`Triggered by: agent <b>${escapeHtml13(input.triggerAgent)}</b>`);
29474
29629
  if (input.oldQuota) {
29475
29630
  const recovery = recoveryAtFor2(input.oldQuota);
29476
29631
  if (recovery) {
29477
- lines.push(`${escapeHtml12(input.oldLabel)} recovers ${formatAbsolute2(recovery, tz)} ` + `(in ${formatRelative2(recovery, now)})`);
29632
+ lines.push(`${escapeHtml13(input.oldLabel)} recovers ${formatAbsolute2(recovery, tz)} ` + `(in ${formatRelative2(recovery, now)})`);
29478
29633
  }
29479
29634
  }
29480
29635
  lines.push("");
@@ -29482,15 +29637,15 @@ function renderFallbackAnnouncement2(input) {
29482
29637
  return lines.join(`
29483
29638
  `);
29484
29639
  }
29485
- lines.push(`\u2713 <b>Switched fleet \u00b7 ${headerLimit} on ${escapeHtml12(input.oldLabel)}</b>`);
29640
+ lines.push(`\u2713 <b>Switched fleet \u00b7 ${headerLimit} on ${escapeHtml13(input.oldLabel)}</b>`);
29486
29641
  lines.push("");
29487
- lines.push(`<code>${escapeHtml12(input.oldLabel)}</code> \u2192 <code>${escapeHtml12(input.newLabel)}</code>`);
29488
- lines.push(`Triggered by: agent <b>${escapeHtml12(input.triggerAgent)}</b>`);
29642
+ lines.push(`<code>${escapeHtml13(input.oldLabel)}</code> \u2192 <code>${escapeHtml13(input.newLabel)}</code>`);
29643
+ lines.push(`Triggered by: agent <b>${escapeHtml13(input.triggerAgent)}</b>`);
29489
29644
  lines.push("");
29490
29645
  if (input.oldQuota) {
29491
29646
  const recovery = recoveryAtFor2(input.oldQuota);
29492
29647
  if (recovery) {
29493
- lines.push(`<code>${escapeHtml12(input.oldLabel)}</code> recovers ` + `${formatAbsolute2(recovery, tz)} (in ${formatRelative2(recovery, now)})`);
29648
+ lines.push(`<code>${escapeHtml13(input.oldLabel)}</code> recovers ` + `${formatAbsolute2(recovery, tz)} (in ${formatRelative2(recovery, now)})`);
29494
29649
  }
29495
29650
  }
29496
29651
  if (input.newQuota) {
@@ -29498,7 +29653,7 @@ function renderFallbackAnnouncement2(input) {
29498
29653
  const sevenStr = fmtPct2(input.newQuota.sevenDayUtilizationPct);
29499
29654
  const hasHeadroom = input.newQuota.fiveHourUtilizationPct < THROTTLING_THRESHOLD_PCT2 && input.newQuota.sevenDayUtilizationPct < THROTTLING_THRESHOLD_PCT2;
29500
29655
  const headroomStr = hasHeadroom ? "<i>(plenty of headroom)</i>" : "<i>(near limit \u2014 watch this)</i>";
29501
- lines.push(`<code>${escapeHtml12(input.newLabel)}</code> now: ${fiveStr} of 5h \u00b7 ${sevenStr} of 7d ${headroomStr}`);
29656
+ lines.push(`<code>${escapeHtml13(input.newLabel)}</code> now: ${fiveStr} of 5h \u00b7 ${sevenStr} of 7d ${headroomStr}`);
29502
29657
  } else {
29503
29658
  lines.push(`<i>(quota probe for new account is pending \u2014 will reflect on next /auth)</i>`);
29504
29659
  }
@@ -29557,7 +29712,7 @@ function switchPriority2(s) {
29557
29712
  return 2;
29558
29713
  return 3;
29559
29714
  }
29560
- function escapeHtml12(s) {
29715
+ function escapeHtml13(s) {
29561
29716
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29562
29717
  }
29563
29718
  function buildSnapshotsFromState2(state4, quotas) {
@@ -39477,7 +39632,8 @@ function resolveModelUnavailableFromOperatorEvent(ev) {
39477
39632
  return detectModelUnavailable(detail) ?? { kind: "quota_exhausted", raw: detail };
39478
39633
  }
39479
39634
  if (ev.kind === "rate-limited") {
39480
- return detectModelUnavailable(detail) ?? { kind: "overload", raw: detail };
39635
+ const detected = detectModelUnavailable(detail);
39636
+ return detected?.kind === "quota_exhausted" ? detected : null;
39481
39637
  }
39482
39638
  if (ev.kind === "unknown-5xx") {
39483
39639
  return detectModelUnavailable(detail) ?? { kind: "overload", raw: detail };
@@ -43135,6 +43291,28 @@ function validateClientMessage(msg) {
43135
43291
  const inb = m.inbound;
43136
43292
  return inb.type === "inbound" && typeof inb.chatId === "string" && inb.chatId.length > 0 && typeof inb.text === "string" && typeof inb.messageId === "number" && typeof inb.user === "string" && typeof inb.userId === "number" && typeof inb.ts === "number" && typeof inb.meta === "object" && inb.meta !== null;
43137
43293
  }
43294
+ case "request_config_approval": {
43295
+ if (typeof m.requestId !== "string" || m.requestId.length === 0 || m.requestId.length > 64)
43296
+ return false;
43297
+ if (typeof m.agentName !== "string" || !AGENT_NAME_RE3.test(m.agentName))
43298
+ return false;
43299
+ if (typeof m.reason !== "string" || m.reason.length === 0 || m.reason.length > 500)
43300
+ return false;
43301
+ if (typeof m.unifiedDiff !== "string" || m.unifiedDiff.length === 0)
43302
+ return false;
43303
+ if (typeof m.timeoutMs !== "number" || !Number.isFinite(m.timeoutMs) || m.timeoutMs <= 0)
43304
+ return false;
43305
+ return true;
43306
+ }
43307
+ case "request_config_finalize": {
43308
+ if (typeof m.requestId !== "string" || m.requestId.length === 0 || m.requestId.length > 64)
43309
+ return false;
43310
+ if (m.outcome !== "applied" && m.outcome !== "reconcile_failed_rolled_back")
43311
+ return false;
43312
+ if (m.detail !== undefined && (typeof m.detail !== "string" || m.detail.length > 500))
43313
+ return false;
43314
+ return true;
43315
+ }
43138
43316
  case "request_drive_approval": {
43139
43317
  if (typeof m.correlationId !== "string" || m.correlationId.length === 0 || m.correlationId.length > 64)
43140
43318
  return false;
@@ -43164,6 +43342,8 @@ function createIpcServer(options) {
43164
43342
  onPtyPartial,
43165
43343
  onInjectInbound,
43166
43344
  onRequestDriveApproval,
43345
+ onRequestConfigApproval,
43346
+ onRequestConfigFinalize,
43167
43347
  log = () => {},
43168
43348
  heartbeatTimeoutMs = 30000
43169
43349
  } = options;
@@ -43268,6 +43448,37 @@ function createIpcServer(options) {
43268
43448
  } catch {}
43269
43449
  }
43270
43450
  break;
43451
+ case "request_config_approval":
43452
+ if (onRequestConfigApproval) {
43453
+ onRequestConfigApproval(client3, msg).catch((err) => {
43454
+ log(`request_config_approval handler threw (client=${client3.id}): ${err.message}`);
43455
+ try {
43456
+ client3.send({
43457
+ type: "config_approval_resolved",
43458
+ requestId: msg.requestId,
43459
+ verdict: "deny",
43460
+ reason: `gateway handler error: ${err.message}`
43461
+ });
43462
+ } catch {}
43463
+ });
43464
+ } else {
43465
+ try {
43466
+ client3.send({
43467
+ type: "config_approval_resolved",
43468
+ requestId: msg.requestId,
43469
+ verdict: "deny",
43470
+ reason: "gateway not configured for config-edit approval"
43471
+ });
43472
+ } catch {}
43473
+ }
43474
+ break;
43475
+ case "request_config_finalize":
43476
+ if (onRequestConfigFinalize) {
43477
+ onRequestConfigFinalize(client3, msg).catch((err) => {
43478
+ log(`request_config_finalize handler threw (client=${client3.id}): ${err.message}`);
43479
+ });
43480
+ }
43481
+ break;
43271
43482
  case "update_placeholder":
43272
43483
  if (!loggedLegacyUpdatePlaceholder.has(client3.id)) {
43273
43484
  loggedLegacyUpdatePlaceholder.add(client3.id);
@@ -44530,6 +44741,74 @@ function buildVaultSaveDiscardedInbound(opts) {
44530
44741
  };
44531
44742
  }
44532
44743
 
44744
+ // gateway/subagent-handback-inbound-builder.ts
44745
+ var HANDBACK_RESULT_MAX = 3000;
44746
+ var HANDBACK_DESC_MAX = 200;
44747
+ function truncate2(s, max) {
44748
+ const t = s.trim();
44749
+ return t.length > max ? t.slice(0, max) + "\u2026" : t;
44750
+ }
44751
+ function buildSubagentHandbackInbound(opts) {
44752
+ const ts = opts.nowMs ?? Date.now();
44753
+ const desc = truncate2(opts.ctx.taskDescription, HANDBACK_DESC_MAX) || "(no description)";
44754
+ const result = truncate2(opts.ctx.resultText, HANDBACK_RESULT_MAX);
44755
+ const text = opts.ctx.outcome === "failed" ? `\uD83E\uDD1D A background worker you dispatched has FAILED.
44756
+
44757
+ ` + `Task: ${desc}
44758
+
44759
+ ` + (result ? `What it reported before failing:
44760
+ ${result}
44761
+
44762
+ ` : "") + `This is beat 4 \u2014 the handback. Tell the user plainly that the ` + `delegated work did not complete, what is known, and your ` + `recommended next step \u2014 one \`reply\` in your own voice. Do not ` + `stay silent.` : `\uD83E\uDD1D A background worker you dispatched has finished.
44763
+
44764
+ ` + `Task: ${desc}
44765
+
44766
+ ` + (result ? `What the worker reported:
44767
+ ${result}
44768
+
44769
+ ` : `The worker left no summary text.
44770
+
44771
+ `) + `This is beat 4 \u2014 the handback. Synthesise this for the user ` + `now: one \`reply\` in your own voice covering what the worker ` + `found and your recommended next step. Do NOT paste the raw ` + `report and do NOT stay silent \u2014 the user dispatched this and ` + `is waiting to hear back.`;
44772
+ return {
44773
+ type: "inbound",
44774
+ chatId: opts.ctx.chatId,
44775
+ messageId: ts,
44776
+ user: "subagent-watcher",
44777
+ userId: 0,
44778
+ ts,
44779
+ text,
44780
+ meta: {
44781
+ source: "subagent_handback",
44782
+ outcome: opts.ctx.outcome
44783
+ }
44784
+ };
44785
+ }
44786
+ function decideSubagentHandback(input) {
44787
+ if (input.handbackEnvValue === "0") {
44788
+ return { deliver: false, reason: "env-disabled" };
44789
+ }
44790
+ if (input.outcome !== "completed" && input.outcome !== "failed") {
44791
+ return { deliver: false, reason: "outcome-not-terminal" };
44792
+ }
44793
+ if (!input.isBackground) {
44794
+ return { deliver: false, reason: "foreground" };
44795
+ }
44796
+ const chatId = input.fleetChatId || input.ownerChatId;
44797
+ if (!chatId) {
44798
+ return { deliver: false, reason: "no-chat" };
44799
+ }
44800
+ const inbound = buildSubagentHandbackInbound({
44801
+ ctx: {
44802
+ chatId,
44803
+ taskDescription: input.taskDescription,
44804
+ resultText: input.resultText,
44805
+ outcome: input.outcome
44806
+ },
44807
+ ...input.nowMs !== undefined ? { nowMs: input.nowMs } : {}
44808
+ });
44809
+ return { deliver: true, chatId, inbound };
44810
+ }
44811
+
44533
44812
  // gateway/poll-health.ts
44534
44813
  var DEFAULT_LOG = (msg) => {
44535
44814
  process.stderr.write(msg.endsWith(`
@@ -46106,6 +46385,7 @@ var DEFAULT_RESCAN_MS = 1000;
46106
46385
  var DEFAULT_STALL_THRESHOLD_MS = 60000;
46107
46386
  var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
46108
46387
  var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
46388
+ var SUBAGENT_RESULT_TEXT_MAX = 3000;
46109
46389
  function parseEnvMs(varName) {
46110
46390
  const raw = process.env[varName];
46111
46391
  if (raw == null || raw === "")
@@ -46231,6 +46511,7 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
46231
46511
  } else if (ev.kind === "sub_agent_text") {
46232
46512
  entry.lastSummaryLine = ev.text.split(`
46233
46513
  `)[0].trim().slice(0, 120);
46514
+ entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX);
46234
46515
  } else if (ev.kind === "sub_agent_turn_end") {
46235
46516
  if (entry.state === "running") {
46236
46517
  entry.state = "done";
@@ -46322,6 +46603,7 @@ function startSubagentWatcher(config) {
46322
46603
  completionNotified: false,
46323
46604
  stallTerminalSynthesised: false,
46324
46605
  lastSummaryLine: "",
46606
+ lastResultText: "",
46325
46607
  lastTool: null,
46326
46608
  historical: isHistorical
46327
46609
  };
@@ -46372,8 +46654,8 @@ function startSubagentWatcher(config) {
46372
46654
  return;
46373
46655
  if (entry.state === "done" && !entry.completionNotified) {
46374
46656
  entry.completionNotified = true;
46375
- const desc = escapeHtml8(truncate2(entry.description, 80));
46376
- const summary = entry.lastSummaryLine ? ` \u2014 ${escapeHtml8(truncate2(entry.lastSummaryLine, 120))}` : "";
46657
+ const desc = escapeHtml8(truncate3(entry.description, 80));
46658
+ const summary = entry.lastSummaryLine ? ` \u2014 ${escapeHtml8(truncate3(entry.lastSummaryLine, 120))}` : "";
46377
46659
  const tools = entry.toolCount > 0 ? ` (${entry.toolCount} tools)` : "";
46378
46660
  try {
46379
46661
  config.sendNotification(`\u2713 Worker done: ${desc}${tools}${summary}`);
@@ -46387,7 +46669,9 @@ function startSubagentWatcher(config) {
46387
46669
  state: entry.state,
46388
46670
  outcome: entry.historical ? "orphan" : "completed",
46389
46671
  toolCount: entry.toolCount,
46390
- durationMs: nowFn() - entry.dispatchedAt
46672
+ durationMs: nowFn() - entry.dispatchedAt,
46673
+ description: entry.description,
46674
+ resultText: entry.lastResultText
46391
46675
  });
46392
46676
  } catch (cbErr) {
46393
46677
  log?.(`subagent-watcher: onFinish callback error ${agentId}: ${cbErr.message}`);
@@ -46404,7 +46688,9 @@ function startSubagentWatcher(config) {
46404
46688
  state: entry.state,
46405
46689
  outcome: "failed",
46406
46690
  toolCount: entry.toolCount,
46407
- durationMs: nowFn() - entry.dispatchedAt
46691
+ durationMs: nowFn() - entry.dispatchedAt,
46692
+ description: entry.description,
46693
+ resultText: entry.lastResultText
46408
46694
  });
46409
46695
  } catch (cbErr) {
46410
46696
  log?.(`subagent-watcher: onFinish callback error ${agentId}: ${cbErr.message}`);
@@ -46457,7 +46743,7 @@ function startSubagentWatcher(config) {
46457
46743
  if (idleMs >= threshold) {
46458
46744
  entry.stallNotified = true;
46459
46745
  entry.stalledAt = n;
46460
- const desc = escapeHtml8(truncate2(entry.description, 80));
46746
+ const desc = escapeHtml8(truncate3(entry.description, 80));
46461
46747
  const idleSec = Math.floor(idleMs / 1000);
46462
46748
  log?.(`subagent-watcher: stall detected for ${entry.agentId} (idle ${idleSec}s): ${desc}`);
46463
46749
  if (db2 != null) {
@@ -47419,7 +47705,7 @@ function summarizeToolForTitle(toolName, inputPreview) {
47419
47705
  }
47420
47706
  case "Bash": {
47421
47707
  const command = readString(input, "command");
47422
- return command ? `${toolName}: ${truncate3(command, COMMAND_TITLE_MAX)}` : toolName;
47708
+ return command ? `${toolName}: ${truncate4(command, COMMAND_TITLE_MAX)}` : toolName;
47423
47709
  }
47424
47710
  case "Read":
47425
47711
  case "Edit":
@@ -47427,17 +47713,17 @@ function summarizeToolForTitle(toolName, inputPreview) {
47427
47713
  case "MultiEdit":
47428
47714
  case "NotebookEdit": {
47429
47715
  const filePath = readString(input, "file_path") ?? readString(input, "notebook_path");
47430
- return filePath ? `${toolName}: ${truncate3(basename5(filePath), PATH_TITLE_MAX)}` : toolName;
47716
+ return filePath ? `${toolName}: ${truncate4(basename5(filePath), PATH_TITLE_MAX)}` : toolName;
47431
47717
  }
47432
47718
  case "Glob":
47433
47719
  case "Grep": {
47434
47720
  const pattern = readString(input, "pattern");
47435
- return pattern ? `${toolName}: ${truncate3(pattern, COMMAND_TITLE_MAX)}` : toolName;
47721
+ return pattern ? `${toolName}: ${truncate4(pattern, COMMAND_TITLE_MAX)}` : toolName;
47436
47722
  }
47437
47723
  case "WebFetch":
47438
47724
  case "WebSearch": {
47439
47725
  const query2 = readString(input, "url") ?? readString(input, "query");
47440
- return query2 ? `${toolName}: ${truncate3(query2, COMMAND_TITLE_MAX)}` : toolName;
47726
+ return query2 ? `${toolName}: ${truncate4(query2, COMMAND_TITLE_MAX)}` : toolName;
47441
47727
  }
47442
47728
  default:
47443
47729
  return toolName;
@@ -47470,7 +47756,7 @@ function skillBasenameFromPath(input) {
47470
47756
  const basename6 = lastSlash >= 0 ? trimmed.slice(lastSlash + 1) : trimmed;
47471
47757
  return basename6.length > 0 ? basename6 : null;
47472
47758
  }
47473
- function truncate3(text, max) {
47759
+ function truncate4(text, max) {
47474
47760
  const collapsed = text.replace(/\s+/g, " ").trim();
47475
47761
  if (collapsed.length <= max)
47476
47762
  return collapsed;
@@ -47741,11 +48027,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
47741
48027
  }
47742
48028
 
47743
48029
  // ../src/build-info.ts
47744
- var VERSION = "0.13.9";
47745
- var COMMIT_SHA = "b3a077a6";
47746
- var COMMIT_DATE = "2026-05-22T10:38:19+10:00";
48030
+ var VERSION = "0.13.11";
48031
+ var COMMIT_SHA = "5984798c";
48032
+ var COMMIT_DATE = "2026-05-22T15:59:07+10:00";
47747
48033
  var LATEST_PR = null;
47748
- var COMMITS_AHEAD_OF_TAG = 1;
48034
+ var COMMITS_AHEAD_OF_TAG = 3;
47749
48035
 
47750
48036
  // gateway/boot-version.ts
47751
48037
  function formatRelativeAgo(iso) {
@@ -48562,23 +48848,23 @@ try {
48562
48848
  }
48563
48849
  const pendingEnvPath = join32(agentDir, ".pending-turn.env");
48564
48850
  try {
48565
- const pending = findMostRecentInterruptedTurn(turnsDb);
48566
- if (pending != null) {
48851
+ const pending2 = findMostRecentInterruptedTurn(turnsDb);
48852
+ if (pending2 != null) {
48567
48853
  const lines = [
48568
48854
  `SWITCHROOM_PENDING_TURN=true`,
48569
- `SWITCHROOM_PENDING_TURN_KEY=${pending.turn_key}`,
48570
- `SWITCHROOM_PENDING_CHAT_ID=${pending.chat_id}`,
48571
- pending.thread_id != null ? `SWITCHROOM_PENDING_THREAD_ID=${pending.thread_id}` : `SWITCHROOM_PENDING_THREAD_ID=`,
48572
- pending.last_user_msg_id != null ? `SWITCHROOM_PENDING_USER_MSG_ID=${pending.last_user_msg_id}` : `SWITCHROOM_PENDING_USER_MSG_ID=`,
48573
- `SWITCHROOM_PENDING_ENDED_VIA=${pending.ended_via ?? "unknown"}`,
48574
- `SWITCHROOM_PENDING_STARTED_AT=${pending.started_at}`
48855
+ `SWITCHROOM_PENDING_TURN_KEY=${pending2.turn_key}`,
48856
+ `SWITCHROOM_PENDING_CHAT_ID=${pending2.chat_id}`,
48857
+ pending2.thread_id != null ? `SWITCHROOM_PENDING_THREAD_ID=${pending2.thread_id}` : `SWITCHROOM_PENDING_THREAD_ID=`,
48858
+ pending2.last_user_msg_id != null ? `SWITCHROOM_PENDING_USER_MSG_ID=${pending2.last_user_msg_id}` : `SWITCHROOM_PENDING_USER_MSG_ID=`,
48859
+ `SWITCHROOM_PENDING_ENDED_VIA=${pending2.ended_via ?? "unknown"}`,
48860
+ `SWITCHROOM_PENDING_STARTED_AT=${pending2.started_at}`
48575
48861
  ];
48576
48862
  const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
48577
48863
  writeFileSync21(pendingEnvTmp, lines.join(`
48578
48864
  `) + `
48579
48865
  `, { mode: 384 });
48580
48866
  renameSync12(pendingEnvTmp, pendingEnvPath);
48581
- process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending.turn_key} endedVia=${pending.ended_via ?? "open"}
48867
+ process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"}
48582
48868
  `);
48583
48869
  } else if (existsSync34(pendingEnvPath)) {
48584
48870
  rmSync4(pendingEnvPath, { force: true });
@@ -49368,7 +49654,7 @@ function emitGatewayOperatorEvent(event) {
49368
49654
  let renderedText;
49369
49655
  let renderedKeyboard;
49370
49656
  if (modelUnavailable) {
49371
- const isAutoKind = modelUnavailable.kind === "quota_exhausted" || modelUnavailable.kind === "overload";
49657
+ const isAutoKind = modelUnavailable.kind === "quota_exhausted";
49372
49658
  const willActuallyFire = isAutoKind && wouldFireFleetAutoFallback();
49373
49659
  process.stderr.write(`telegram gateway: operator-event suppressing-raw-stderr-for-model-unavailable agent=${agent} kind=${kind} detected=${modelUnavailable.kind} autoKind=${isAutoKind} willFire=${willActuallyFire}
49374
49660
  `);
@@ -49658,8 +49944,8 @@ var ipcServer = createIpcServer({
49658
49944
  client: client3
49659
49945
  });
49660
49946
  } else {
49661
- const pending = pendingInboundBuffer.drain(client3.agentName);
49662
- for (const msg of pending) {
49947
+ const pending2 = pendingInboundBuffer.drain(client3.agentName);
49948
+ for (const msg of pending2) {
49663
49949
  try {
49664
49950
  client3.send(msg);
49665
49951
  inboundSpool?.ack(msg);
@@ -49762,9 +50048,9 @@ var ipcServer = createIpcServer({
49762
50048
  }
49763
50049
  },
49764
50050
  onClientDisconnected(client3) {
49765
- process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
49766
- `);
49767
50051
  if (client3.agentName != null) {
50052
+ process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
50053
+ `);
49768
50054
  shadowEmit({ kind: "bridgeDown", at: Date.now() });
49769
50055
  }
49770
50056
  flushOnAgentDisconnect({
@@ -49954,6 +50240,68 @@ ${reminder}
49954
50240
  },
49955
50241
  buildCard: ({ preview, suggestRequestId }) => buildDiffPreviewCard({ preview, suggestRequestId }),
49956
50242
  log: (m) => process.stderr.write(`telegram gateway: drive-approval \u2014 ${m}
50243
+ `)
50244
+ });
50245
+ },
50246
+ async onRequestConfigApproval(client3, msg) {
50247
+ const { handleRequestConfigApproval: handleRequestConfigApproval2 } = await Promise.resolve().then(() => (init_config_approval_handler(), exports_config_approval_handler));
50248
+ const { InlineKeyboard: InlineKeyboard6 } = await Promise.resolve().then(() => __toESM(require_mod(), 1));
50249
+ await handleRequestConfigApproval2(client3, msg, {
50250
+ agentName: getMyAgentName(),
50251
+ loadTargetChat: () => {
50252
+ const access = loadAccess();
50253
+ const operator = access.allowFrom[0];
50254
+ if (operator === undefined)
50255
+ return null;
50256
+ return { chatId: operator };
50257
+ },
50258
+ buildKeyboard: (requestId) => new InlineKeyboard6().text("\u2705 Approve", `cfg:${requestId}:approve`).text("\uD83D\uDEAB Deny", `cfg:${requestId}:deny`),
50259
+ postCard: async (args) => {
50260
+ try {
50261
+ const sent = await robustApiCall(() => bot.api.sendMessage(args.chatId, args.text, {
50262
+ parse_mode: "HTML",
50263
+ ...args.threadId !== undefined ? { message_thread_id: args.threadId } : {},
50264
+ reply_markup: args.replyMarkup
50265
+ }), {
50266
+ chat_id: String(args.chatId),
50267
+ verb: "config-approval-card",
50268
+ ...args.threadId !== undefined ? { threadId: args.threadId } : {}
50269
+ });
50270
+ return { messageId: sent.message_id };
50271
+ } catch (err) {
50272
+ process.stderr.write(`telegram gateway: config-approval postCard failed: ${err.message}
50273
+ `);
50274
+ return null;
50275
+ }
50276
+ },
50277
+ editCard: async (args) => {
50278
+ try {
50279
+ await robustApiCall(() => bot.api.editMessageText(args.chatId, args.messageId, args.text, {
50280
+ parse_mode: "HTML"
50281
+ }), { chat_id: String(args.chatId), verb: "config-approval-edit" });
50282
+ } catch (err) {
50283
+ process.stderr.write(`telegram gateway: config-approval editCard failed: ${err.message}
50284
+ `);
50285
+ }
50286
+ },
50287
+ log: (m) => process.stderr.write(`telegram gateway: config-approval \u2014 ${m}
50288
+ `)
50289
+ });
50290
+ },
50291
+ async onRequestConfigFinalize(client3, msg) {
50292
+ const { handleRequestConfigFinalize: handleRequestConfigFinalize2 } = await Promise.resolve().then(() => (init_config_approval_handler(), exports_config_approval_handler));
50293
+ await handleRequestConfigFinalize2(client3, msg, {
50294
+ editCard: async (args) => {
50295
+ try {
50296
+ await robustApiCall(() => bot.api.editMessageText(args.chatId, args.messageId, args.text, {
50297
+ parse_mode: "HTML"
50298
+ }), { chat_id: String(args.chatId), verb: "config-approval-finalize" });
50299
+ } catch (err) {
50300
+ process.stderr.write(`telegram gateway: config-finalize editCard failed: ${err.message}
50301
+ `);
50302
+ }
50303
+ },
50304
+ log: (m) => process.stderr.write(`telegram gateway: config-finalize \u2014 ${m}
49957
50305
  `)
49958
50306
  });
49959
50307
  },
@@ -50847,7 +51195,7 @@ async function executeVaultRequestSave(args) {
50847
51195
  }
50848
51196
  const agentSlug = process.env.SWITCHROOM_AGENT_NAME || "agent";
50849
51197
  const stageId = randomBytes6(4).toString("hex");
50850
- const pending = {
51198
+ const pending2 = {
50851
51199
  agent: agentSlug,
50852
51200
  chat_id,
50853
51201
  key,
@@ -50856,16 +51204,16 @@ async function executeVaultRequestSave(args) {
50856
51204
  why,
50857
51205
  staged_at: Date.now()
50858
51206
  };
50859
- pendingVaultRequestSaves.set(stageId, pending);
51207
+ pendingVaultRequestSaves.set(stageId, pending2);
50860
51208
  sweepPendingVaultRequestSaves();
50861
- const text = renderVaultRequestSaveCard(pending, agentSlug);
51209
+ const text = renderVaultRequestSaveCard(pending2, agentSlug);
50862
51210
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
50863
51211
  const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
50864
51212
  parse_mode: "HTML",
50865
51213
  reply_markup: buildVaultRequestSaveKeyboard(stageId),
50866
51214
  ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
50867
51215
  }), { threadId, chat_id, verb: "vault_request_save.card" });
50868
- pending.card_message_id = sent.message_id;
51216
+ pending2.card_message_id = sent.message_id;
50869
51217
  return {
50870
51218
  content: [
50871
51219
  {
@@ -50950,7 +51298,7 @@ async function executeVaultRequestAccess(args) {
50950
51298
  } catch {}
50951
51299
  }
50952
51300
  const stageId = randomBytes6(4).toString("hex");
50953
- const pending = {
51301
+ const pending2 = {
50954
51302
  agent: agentSlug,
50955
51303
  chat_id,
50956
51304
  key,
@@ -50959,16 +51307,16 @@ async function executeVaultRequestAccess(args) {
50959
51307
  ttl_seconds,
50960
51308
  staged_at: Date.now()
50961
51309
  };
50962
- pendingVaultRequestAccesses.set(stageId, pending);
51310
+ pendingVaultRequestAccesses.set(stageId, pending2);
50963
51311
  sweepPendingVaultRequestAccesses();
50964
- const text = renderVaultRequestAccessCard(pending);
51312
+ const text = renderVaultRequestAccessCard(pending2);
50965
51313
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
50966
51314
  const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
50967
51315
  parse_mode: "HTML",
50968
51316
  reply_markup: buildVaultRequestAccessKeyboard(stageId),
50969
51317
  ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
50970
51318
  }), { threadId, chat_id, verb: "vault_request_access.card" });
50971
- pending.card_message_id = sent.message_id;
51319
+ pending2.card_message_id = sent.message_id;
50972
51320
  return {
50973
51321
  content: [
50974
51322
  {
@@ -51282,9 +51630,9 @@ function handleSessionEvent(ev) {
51282
51630
  });
51283
51631
  }
51284
51632
  if (pendingPtyPartial != null) {
51285
- const pending = pendingPtyPartial;
51633
+ const pending2 = pendingPtyPartial;
51286
51634
  pendingPtyPartial = null;
51287
- handlePtyPartial(pending);
51635
+ handlePtyPartial(pending2);
51288
51636
  }
51289
51637
  }
51290
51638
  return;
@@ -54096,15 +54444,15 @@ async function handleVaultRecentDenialCallback(ctx, data) {
54096
54444
  parseMode: "HTML"
54097
54445
  });
54098
54446
  }
54099
- async function performVaultAccessApproval(ctx, pending, stageId, senderId, attestation) {
54447
+ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, attestation) {
54100
54448
  const brokerAuthOpts = attestation.kind === "passphrase" ? { passphrase: attestation.passphrase } : { attest_via_posture: true };
54101
- if (pending.scope === "read") {
54449
+ if (pending2.scope === "read") {
54102
54450
  try {
54103
54451
  const visible = await listViaBroker();
54104
- if (visible !== null && visible.includes(pending.key)) {
54452
+ if (visible !== null && visible.includes(pending2.key)) {
54105
54453
  pendingVaultRequestAccesses.delete(stageId);
54106
- if (pending.card_message_id != null) {
54107
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2139\uFE0F <b>${escapeHtmlForTg(pending.agent)}</b> already has standing-ACL access to <code>${escapeHtmlForTg(pending.key)}</code> (schedule.secrets[]). ` + `<b>No grant minted</b> \u2014 a token would shadow the standing ACL. ` + `The agent can read it directly.`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54454
+ if (pending2.card_message_id != null) {
54455
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\u2139\uFE0F <b>${escapeHtmlForTg(pending2.agent)}</b> already has standing-ACL access to <code>${escapeHtmlForTg(pending2.key)}</code> (schedule.secrets[]). ` + `<b>No grant minted</b> \u2014 a token would shadow the standing ACL. ` + `The agent can read it directly.`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54108
54456
  }
54109
54457
  return;
54110
54458
  }
@@ -54112,8 +54460,8 @@ async function performVaultAccessApproval(ctx, pending, stageId, senderId, attes
54112
54460
  }
54113
54461
  let existingReadKeys = [];
54114
54462
  let existingWriteKeys = [];
54115
- if (pending.scope === "read" || pending.scope === "write") {
54116
- const list2 = await listGrantsViaBroker(pending.agent, brokerAuthOpts);
54463
+ if (pending2.scope === "read" || pending2.scope === "write") {
54464
+ const list2 = await listGrantsViaBroker(pending2.agent, brokerAuthOpts);
54117
54465
  if (list2.kind === "ok") {
54118
54466
  const now = Math.floor(Date.now() / 1000);
54119
54467
  const active = list2.grants.filter((g) => g.expires_at === null || g.expires_at > now).sort((a, b) => {
@@ -54130,15 +54478,15 @@ async function performVaultAccessApproval(ctx, pending, stageId, senderId, attes
54130
54478
  }
54131
54479
  const readKeys = new Set(existingReadKeys);
54132
54480
  const writeKeys = new Set(existingWriteKeys);
54133
- if (pending.scope === "read")
54134
- readKeys.add(pending.key);
54135
- if (pending.scope === "write")
54136
- writeKeys.add(pending.key);
54481
+ if (pending2.scope === "read")
54482
+ readKeys.add(pending2.key);
54483
+ if (pending2.scope === "write")
54484
+ writeKeys.add(pending2.key);
54137
54485
  const mintArgs = {
54138
- agent: pending.agent,
54486
+ agent: pending2.agent,
54139
54487
  keys: Array.from(readKeys),
54140
- ttl_seconds: pending.ttl_seconds,
54141
- description: `auto-mint via vault_request_access (#1012, scope=${pending.scope}, by op ${senderId}` + (existingReadKeys.length + existingWriteKeys.length > 0 ? `, unioned with prior grant` : ``) + `)`,
54488
+ ttl_seconds: pending2.ttl_seconds,
54489
+ description: `auto-mint via vault_request_access (#1012, scope=${pending2.scope}, by op ${senderId}` + (existingReadKeys.length + existingWriteKeys.length > 0 ? `, unioned with prior grant` : ``) + `)`,
54142
54490
  ...writeKeys.size > 0 ? { write_keys: Array.from(writeKeys) } : {},
54143
54491
  ...brokerAuthOpts
54144
54492
  };
@@ -54149,45 +54497,45 @@ async function performVaultAccessApproval(ctx, pending, stageId, senderId, attes
54149
54497
  }
54150
54498
  if (result.kind === "error") {
54151
54499
  pendingVaultRequestAccesses.delete(stageId);
54152
- if (pending.card_message_id != null) {
54153
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `<b>mint_grant failed:</b> ${escapeHtmlForTg(result.msg)}`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54500
+ if (pending2.card_message_id != null) {
54501
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `<b>mint_grant failed:</b> ${escapeHtmlForTg(result.msg)}`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54154
54502
  }
54155
54503
  return;
54156
54504
  }
54157
54505
  const { token, id } = result;
54158
- const tokenPath = join32(homedir12(), ".switchroom", "agents", pending.agent, ".vault-token");
54506
+ const tokenPath = join32(homedir12(), ".switchroom", "agents", pending2.agent, ".vault-token");
54159
54507
  try {
54160
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", pending.agent), { recursive: true });
54508
+ mkdirSync21(join32(homedir12(), ".switchroom", "agents", pending2.agent), { recursive: true });
54161
54509
  writeFileSync21(tokenPath, token, { mode: 384 });
54162
54510
  } catch (err) {
54163
54511
  await switchroomReply(ctx, `<b>Grant created (${escapeHtmlForTg(id)}) but token write failed:</b> ${escapeHtmlForTg(String(err))}
54164
- <i>Recover with: <code>switchroom vault grant ${escapeHtmlForTg(pending.agent)} --keys ${escapeHtmlForTg(pending.key)} --duration ${Math.round(pending.ttl_seconds / 86400)}d</code> on the host.</i>`, { html: true });
54512
+ <i>Recover with: <code>switchroom vault grant ${escapeHtmlForTg(pending2.agent)} --keys ${escapeHtmlForTg(pending2.key)} --duration ${Math.round(pending2.ttl_seconds / 86400)}d</code> on the host.</i>`, { html: true });
54165
54513
  return;
54166
54514
  }
54167
54515
  pendingVaultRequestAccesses.delete(stageId);
54168
- if (pending.card_message_id != null) {
54169
- const days = Math.round(pending.ttl_seconds / 86400);
54516
+ if (pending2.card_message_id != null) {
54517
+ const days = Math.round(pending2.ttl_seconds / 86400);
54170
54518
  const footer = VAULT_APPROVAL_AUTH_MODE === "telegram-id" ? `
54171
54519
  <i>Approver verified by Telegram identity \u2014 broker auto-unlocked at startup.</i>` : "";
54172
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2705 Granted <b>${escapeHtmlForTg(pending.agent)}</b> ${pending.scope} access to <code>${escapeHtmlForTg(pending.key)}</code> for ${days}d. (grant <code>${escapeHtmlForTg(id)}</code>)` + footer, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54520
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\u2705 Granted <b>${escapeHtmlForTg(pending2.agent)}</b> ${pending2.scope} access to <code>${escapeHtmlForTg(pending2.key)}</code> for ${days}d. (grant <code>${escapeHtmlForTg(id)}</code>)` + footer, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54173
54521
  }
54174
54522
  const synthetic = buildVaultGrantApprovedInbound({
54175
54523
  ctx: {
54176
- agent: pending.agent,
54177
- key: pending.key,
54178
- scope: pending.scope,
54179
- chat_id: pending.chat_id,
54180
- ttl_seconds: pending.ttl_seconds
54524
+ agent: pending2.agent,
54525
+ key: pending2.key,
54526
+ scope: pending2.scope,
54527
+ chat_id: pending2.chat_id,
54528
+ ttl_seconds: pending2.ttl_seconds
54181
54529
  },
54182
54530
  grantId: id,
54183
54531
  stageId,
54184
54532
  operatorId: senderId
54185
54533
  });
54186
- const delivered = ipcServer.sendToAgent(pending.agent, synthetic);
54187
- process.stderr.write(`telegram gateway: vault_grant_approved injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${delivered}
54534
+ const delivered = ipcServer.sendToAgent(pending2.agent, synthetic);
54535
+ process.stderr.write(`telegram gateway: vault_grant_approved injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${delivered}
54188
54536
  `);
54189
54537
  if (!delivered) {
54190
- pendingInboundBuffer.push(pending.agent, synthetic);
54538
+ pendingInboundBuffer.push(pending2.agent, synthetic);
54191
54539
  }
54192
54540
  }
54193
54541
  async function handleVaultRequestAccessCallback(ctx, data) {
@@ -54204,8 +54552,8 @@ async function handleVaultRequestAccessCallback(ctx, data) {
54204
54552
  }
54205
54553
  const action = parts[1];
54206
54554
  const stageId = parts.slice(2).join(":");
54207
- const pending = pendingVaultRequestAccesses.get(stageId);
54208
- if (!pending) {
54555
+ const pending2 = pendingVaultRequestAccesses.get(stageId);
54556
+ if (!pending2) {
54209
54557
  await ctx.answerCallbackQuery({ text: "Card expired \u2014 ask the agent to re-request." }).catch(() => {});
54210
54558
  if (ctx.callbackQuery?.message) {
54211
54559
  await ctx.api.editMessageText(ctx.callbackQuery.message.chat.id, ctx.callbackQuery.message.message_id, "\u231B <i>This access-request card expired before you tapped. Ask the agent to re-issue if the need still stands.</i>", { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
@@ -54215,66 +54563,66 @@ async function handleVaultRequestAccessCallback(ctx, data) {
54215
54563
  if (action === "deny") {
54216
54564
  pendingVaultRequestAccesses.delete(stageId);
54217
54565
  await ctx.answerCallbackQuery({ text: "\uD83D\uDEAB Denied" }).catch(() => {});
54218
- if (pending.card_message_id != null) {
54219
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\uD83D\uDEAB <i>Denied. <b>${escapeHtmlForTg(pending.agent)}</b> will not get access to <code>${escapeHtmlForTg(pending.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54566
+ if (pending2.card_message_id != null) {
54567
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\uD83D\uDEAB <i>Denied. <b>${escapeHtmlForTg(pending2.agent)}</b> will not get access to <code>${escapeHtmlForTg(pending2.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54220
54568
  }
54221
54569
  const denyInbound = buildVaultGrantDeniedInbound({
54222
54570
  ctx: {
54223
- agent: pending.agent,
54224
- key: pending.key,
54225
- scope: pending.scope,
54226
- chat_id: pending.chat_id,
54227
- ttl_seconds: pending.ttl_seconds
54571
+ agent: pending2.agent,
54572
+ key: pending2.key,
54573
+ scope: pending2.scope,
54574
+ chat_id: pending2.chat_id,
54575
+ ttl_seconds: pending2.ttl_seconds
54228
54576
  },
54229
54577
  stageId,
54230
54578
  operatorId: senderId
54231
54579
  });
54232
- const denyDelivered = ipcServer.sendToAgent(pending.agent, denyInbound);
54233
- process.stderr.write(`telegram gateway: vault_grant_denied injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${denyDelivered}
54580
+ const denyDelivered = ipcServer.sendToAgent(pending2.agent, denyInbound);
54581
+ process.stderr.write(`telegram gateway: vault_grant_denied injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${denyDelivered}
54234
54582
  `);
54235
54583
  if (!denyDelivered) {
54236
- pendingInboundBuffer.push(pending.agent, denyInbound);
54584
+ pendingInboundBuffer.push(pending2.agent, denyInbound);
54237
54585
  }
54238
54586
  return;
54239
54587
  }
54240
54588
  if (action === "approve") {
54241
54589
  if (VAULT_APPROVAL_AUTH_MODE === "telegram-id") {
54242
54590
  const username = ctx.from?.username ?? ctx.from?.first_name ?? `id=${senderId}`;
54243
- if (pending.card_message_id != null) {
54244
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2705 Approved by @${escapeHtmlForTg(username)} \u2014 minting\u2026`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54591
+ if (pending2.card_message_id != null) {
54592
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\u2705 Approved by @${escapeHtmlForTg(username)} \u2014 minting\u2026`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54245
54593
  }
54246
54594
  await ctx.answerCallbackQuery({ text: "\u23F3 Minting grant\u2026" }).catch(() => {});
54247
- await performVaultAccessApproval(ctx, pending, stageId, senderId, { kind: "posture" });
54595
+ await performVaultAccessApproval(ctx, pending2, stageId, senderId, { kind: "posture" });
54248
54596
  return;
54249
54597
  }
54250
- const cached = vaultPassphraseCache.get(pending.chat_id);
54598
+ const cached = vaultPassphraseCache.get(pending2.chat_id);
54251
54599
  if (!cached || cached.expiresAt <= Date.now()) {
54252
- if (pending.card_message_id == null) {
54600
+ if (pending2.card_message_id == null) {
54253
54601
  await ctx.answerCallbackQuery({ text: "Card missing \u2014 ask the agent to re-issue." }).catch(() => {});
54254
54602
  return;
54255
54603
  }
54256
- const existing = pendingVaultOps.get(pending.chat_id);
54604
+ const existing = pendingVaultOps.get(pending2.chat_id);
54257
54605
  const newItem = {
54258
54606
  stageId,
54259
- cardChatId: pending.chat_id,
54260
- cardMessageId: pending.card_message_id,
54607
+ cardChatId: pending2.chat_id,
54608
+ cardMessageId: pending2.card_message_id,
54261
54609
  senderId
54262
54610
  };
54263
54611
  const items = existing?.kind === "passphrase-for-access-approve" ? [...existing.items.filter((it) => it.stageId !== stageId), newItem] : [newItem];
54264
- pendingVaultOps.set(pending.chat_id, {
54612
+ pendingVaultOps.set(pending2.chat_id, {
54265
54613
  kind: "passphrase-for-access-approve",
54266
54614
  items,
54267
54615
  startedAt: existing?.kind === "passphrase-for-access-approve" ? existing.startedAt : Date.now()
54268
54616
  });
54269
54617
  const joiningBatch = items.length > 1;
54270
54618
  await ctx.answerCallbackQuery({ text: joiningBatch ? `\uD83D\uDD10 Queued \u2014 one passphrase covers ${items.length} cards` : "\uD83D\uDD10 Send your passphrase\u2026" }).catch(() => {});
54271
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, joiningBatch ? `\uD83D\uDD10 <b>Queued behind an earlier card.</b> Type your passphrase as your next message \u2014 it covers <b>${items.length}</b> pending approvals in this chat (one entry mints all grants, no re-type per card).` : `\uD83D\uDD10 <b>Vault is locked.</b> Reply with your passphrase as your next message \u2014 we'll unlock, mint the grant for <b>${escapeHtmlForTg(pending.agent)}</b>, and delete the passphrase message in one step.
54619
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, joiningBatch ? `\uD83D\uDD10 <b>Queued behind an earlier card.</b> Type your passphrase as your next message \u2014 it covers <b>${items.length}</b> pending approvals in this chat (one entry mints all grants, no re-type per card).` : `\uD83D\uDD10 <b>Vault is locked.</b> Reply with your passphrase as your next message \u2014 we'll unlock, mint the grant for <b>${escapeHtmlForTg(pending2.agent)}</b>, and delete the passphrase message in one step.
54272
54620
 
54273
54621
  <i>Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54274
54622
  return;
54275
54623
  }
54276
54624
  await ctx.answerCallbackQuery({ text: "\u23F3 Minting grant\u2026" }).catch(() => {});
54277
- await performVaultAccessApproval(ctx, pending, stageId, senderId, { kind: "passphrase", passphrase: cached.passphrase });
54625
+ await performVaultAccessApproval(ctx, pending2, stageId, senderId, { kind: "passphrase", passphrase: cached.passphrase });
54278
54626
  return;
54279
54627
  }
54280
54628
  await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
@@ -54293,8 +54641,8 @@ async function handleVaultRequestSaveCallback(ctx, data) {
54293
54641
  }
54294
54642
  const action = parts[1];
54295
54643
  const stageId = parts.slice(2).join(":");
54296
- const pending = pendingVaultRequestSaves.get(stageId);
54297
- if (!pending) {
54644
+ const pending2 = pendingVaultRequestSaves.get(stageId);
54645
+ if (!pending2) {
54298
54646
  await ctx.answerCallbackQuery({ text: "Card expired \u2014 ask the agent to re-send." }).catch(() => {});
54299
54647
  if (ctx.callbackQuery?.message) {
54300
54648
  await ctx.api.editMessageText(ctx.callbackQuery.message.chat.id, ctx.callbackQuery.message.message_id, "\u231B <i>This vault-save card expired before you tapped. Ask the agent to re-issue if you still want to save.</i>", { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
@@ -54304,23 +54652,23 @@ async function handleVaultRequestSaveCallback(ctx, data) {
54304
54652
  if (action === "discard") {
54305
54653
  pendingVaultRequestSaves.delete(stageId);
54306
54654
  await ctx.answerCallbackQuery({ text: "\uD83D\uDEAB Discarded" }).catch(() => {});
54307
- if (pending.card_message_id != null) {
54308
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\uD83D\uDEAB <i>Discarded. The secret was not written to the vault.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54655
+ if (pending2.card_message_id != null) {
54656
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\uD83D\uDEAB <i>Discarded. The secret was not written to the vault.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54309
54657
  }
54310
54658
  const discardInbound = buildVaultSaveDiscardedInbound({
54311
- ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
54659
+ ctx: { agent: pending2.agent, key: pending2.key, chat_id: pending2.chat_id },
54312
54660
  stageId,
54313
54661
  operatorId: senderId
54314
54662
  });
54315
- const dDelivered = ipcServer.sendToAgent(pending.agent, discardInbound);
54316
- process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${dDelivered}
54663
+ const dDelivered = ipcServer.sendToAgent(pending2.agent, discardInbound);
54664
+ process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${dDelivered}
54317
54665
  `);
54318
54666
  if (!dDelivered)
54319
- pendingInboundBuffer.push(pending.agent, discardInbound);
54667
+ pendingInboundBuffer.push(pending2.agent, discardInbound);
54320
54668
  return;
54321
54669
  }
54322
54670
  if (action === "rename") {
54323
- pendingVaultOps.set(pending.chat_id, {
54671
+ pendingVaultOps.set(pending2.chat_id, {
54324
54672
  kind: "rename-vault-save",
54325
54673
  stageId,
54326
54674
  startedAt: Date.now()
@@ -54329,7 +54677,7 @@ async function handleVaultRequestSaveCallback(ctx, data) {
54329
54677
  const baseText = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
54330
54678
  const statusLine = `
54331
54679
 
54332
- \u270F\uFE0F <b>Rename mode</b> \u2014 send the new key name as your next message. ` + `The current proposed key is <code>${escapeHtmlForTg(pending.key)}</code>.`;
54680
+ \u270F\uFE0F <b>Rename mode</b> \u2014 send the new key name as your next message. ` + `The current proposed key is <code>${escapeHtmlForTg(pending2.key)}</code>.`;
54333
54681
  await finalizeCallback(ctx, {
54334
54682
  ackText: "Send the new key name as your next message.",
54335
54683
  newText: baseText ? `${baseText}${statusLine}` : statusLine,
@@ -54339,22 +54687,22 @@ async function handleVaultRequestSaveCallback(ctx, data) {
54339
54687
  }
54340
54688
  if (action === "save") {
54341
54689
  await ctx.answerCallbackQuery({ text: "\u23F3 Saving\u2026" }).catch(() => {});
54342
- const cached = vaultPassphraseCache.get(pending.chat_id);
54690
+ const cached = vaultPassphraseCache.get(pending2.chat_id);
54343
54691
  if (!cached || cached.expiresAt <= Date.now()) {
54344
- if (pending.card_message_id != null) {
54345
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\uD83D\uDD12 <b>Vault is locked.</b> Run <code>/vault unlock</code> (or any /vault command) to cache the passphrase, then tap Save again on the next card.`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54692
+ if (pending2.card_message_id != null) {
54693
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\uD83D\uDD12 <b>Vault is locked.</b> Run <code>/vault unlock</code> (or any /vault command) to cache the passphrase, then tap Save again on the next card.`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54346
54694
  }
54347
54695
  pendingVaultRequestSaves.delete(stageId);
54348
54696
  return;
54349
54697
  }
54350
- const write = defaultVaultWrite(pending.key, pending.value, cached.passphrase);
54698
+ const write = defaultVaultWrite(pending2.key, pending2.value, cached.passphrase);
54351
54699
  if (!write.ok) {
54352
54700
  const parsed = parseVaultCliError(write.output);
54353
- const rendered = renderVaultCliError(parsed, { verb: "save", key: pending.key });
54701
+ const rendered = renderVaultCliError(parsed, { verb: "save", key: pending2.key });
54354
54702
  const body = rendered.suppressRaw ? rendered.html : `\u26A0\uFE0F vault write failed:
54355
54703
  <pre>${escapeHtmlForTg(write.output)}</pre>`;
54356
- if (pending.card_message_id != null) {
54357
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `${body}
54704
+ if (pending2.card_message_id != null) {
54705
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `${body}
54358
54706
 
54359
54707
  <i>Tap a fresh card after fixing the underlying issue.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54360
54708
  }
@@ -54362,33 +54710,33 @@ async function handleVaultRequestSaveCallback(ctx, data) {
54362
54710
  const failReason = (write.output || "vault write error").split(`
54363
54711
  `)[0].slice(0, 200);
54364
54712
  const failInbound = buildVaultSaveFailedInbound({
54365
- ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
54713
+ ctx: { agent: pending2.agent, key: pending2.key, chat_id: pending2.chat_id },
54366
54714
  stageId,
54367
54715
  operatorId: senderId,
54368
54716
  reason: failReason
54369
54717
  });
54370
- const fDelivered = ipcServer.sendToAgent(pending.agent, failInbound);
54371
- process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${fDelivered}
54718
+ const fDelivered = ipcServer.sendToAgent(pending2.agent, failInbound);
54719
+ process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${fDelivered}
54372
54720
  `);
54373
54721
  if (!fDelivered)
54374
- pendingInboundBuffer.push(pending.agent, failInbound);
54722
+ pendingInboundBuffer.push(pending2.agent, failInbound);
54375
54723
  return;
54376
54724
  }
54377
54725
  pendingVaultRequestSaves.delete(stageId);
54378
- if (pending.card_message_id != null) {
54379
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2705 saved as <code>vault:${escapeHtmlForTg(pending.key)}</code> (masked: <code>${escapeHtmlForTg(maskToken2(pending.value))}</code>)
54380
- <i>The agent can now reference this as <code>vault:${escapeHtmlForTg(pending.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54726
+ if (pending2.card_message_id != null) {
54727
+ await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\u2705 saved as <code>vault:${escapeHtmlForTg(pending2.key)}</code> (masked: <code>${escapeHtmlForTg(maskToken2(pending2.value))}</code>)
54728
+ <i>The agent can now reference this as <code>vault:${escapeHtmlForTg(pending2.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
54381
54729
  }
54382
54730
  const okInbound = buildVaultSaveCompletedInbound({
54383
- ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
54731
+ ctx: { agent: pending2.agent, key: pending2.key, chat_id: pending2.chat_id },
54384
54732
  stageId,
54385
54733
  operatorId: senderId
54386
54734
  });
54387
- const okDelivered = ipcServer.sendToAgent(pending.agent, okInbound);
54388
- process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${okDelivered}
54735
+ const okDelivered = ipcServer.sendToAgent(pending2.agent, okInbound);
54736
+ process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${okDelivered}
54389
54737
  `);
54390
54738
  if (!okDelivered)
54391
- pendingInboundBuffer.push(pending.agent, okInbound);
54739
+ pendingInboundBuffer.push(pending2.agent, okInbound);
54392
54740
  return;
54393
54741
  }
54394
54742
  await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
@@ -55650,6 +55998,35 @@ bot.on("callback_query:data", async (ctx) => {
55650
55998
  await handleApprovalCallback2(ctx, data);
55651
55999
  return;
55652
56000
  }
56001
+ if (data.startsWith("cfg:")) {
56002
+ const access2 = loadAccess();
56003
+ const senderId2 = String(ctx.from?.id ?? "");
56004
+ if (!access2.allowFrom.includes(senderId2)) {
56005
+ await ctx.answerCallbackQuery({ text: "Not authorized." });
56006
+ return;
56007
+ }
56008
+ const { parseConfigApprovalCallback: parseConfigApprovalCallback2, resolvePendingConfigApproval: resolvePendingConfigApproval2 } = await Promise.resolve().then(() => (init_config_approval_handler(), exports_config_approval_handler));
56009
+ const parsed = parseConfigApprovalCallback2(data);
56010
+ if (parsed === null) {
56011
+ await ctx.answerCallbackQuery({ text: "Malformed callback." });
56012
+ return;
56013
+ }
56014
+ const resolved = await resolvePendingConfigApproval2(parsed.requestId, parsed.choice, {
56015
+ editCard: async (args) => {
56016
+ try {
56017
+ await robustApiCall(() => bot.api.editMessageText(args.chatId, args.messageId, args.text, {
56018
+ parse_mode: "HTML"
56019
+ }));
56020
+ } catch {}
56021
+ },
56022
+ log: (m2) => process.stderr.write(`telegram gateway: config-approval cb \u2014 ${m2}
56023
+ `)
56024
+ });
56025
+ await ctx.answerCallbackQuery({
56026
+ text: resolved ? parsed.choice === "approve" ? "Approving\u2026" : "Denied" : "Already resolved."
56027
+ });
56028
+ return;
56029
+ }
55653
56030
  if (data.startsWith("drvpick:")) {
55654
56031
  const access2 = loadAccess();
55655
56032
  const senderId2 = String(ctx.from?.id ?? "");
@@ -56490,7 +56867,7 @@ async function handleMessageReaction(ctx) {
56490
56867
  `);
56491
56868
  return;
56492
56869
  }
56493
- const pending = {
56870
+ const pending2 = {
56494
56871
  targetMessageId: message_id,
56495
56872
  emoji,
56496
56873
  action,
@@ -56500,7 +56877,7 @@ async function handleMessageReaction(ctx) {
56500
56877
  user: reacter.first_name ?? reacter.username ?? String(reacter.id),
56501
56878
  ...typeof update.message_thread_id === "number" ? { threadId: update.message_thread_id } : {}
56502
56879
  };
56503
- getReactionDebounce().enqueue(update.chat.id, pending);
56880
+ getReactionDebounce().enqueue(update.chat.id, pending2);
56504
56881
  } catch (err) {
56505
56882
  process.stderr.write(`telegram gateway: message_reaction handler error: ${err}
56506
56883
  `);
@@ -56969,16 +57346,14 @@ var didOneTimeSetup = false;
56969
57346
  `);
56970
57347
  }
56971
57348
  },
56972
- onFinish: ({ agentId, outcome, toolCount, durationMs }) => {
56973
- let parentTurnKey = "";
56974
- let chatId = "";
57349
+ onFinish: ({ agentId, outcome, description, resultText }) => {
57350
+ let fleetChatId = "";
56975
57351
  let isBackground = false;
56976
57352
  try {
56977
57353
  const fleets = progressDriver?.peekAllFleets() ?? [];
56978
57354
  for (const f of fleets) {
56979
57355
  if (f.fleet.has(agentId)) {
56980
- parentTurnKey = f.turnKey;
56981
- chatId = f.chatId ?? "";
57356
+ fleetChatId = f.chatId ?? "";
56982
57357
  break;
56983
57358
  }
56984
57359
  }
@@ -56990,7 +57365,25 @@ var didOneTimeSetup = false;
56990
57365
  isBackground = row.background === 1;
56991
57366
  } catch {}
56992
57367
  }
56993
- const finalOutcome = isBackground ? "background" : outcome === "completed" ? "completed" : "orphan";
57368
+ const decision = decideSubagentHandback({
57369
+ handbackEnvValue: process.env.SWITCHROOM_SUBAGENT_HANDBACK,
57370
+ outcome,
57371
+ isBackground,
57372
+ fleetChatId,
57373
+ ownerChatId: loadAccess().allowFrom[0] ?? "",
57374
+ taskDescription: description,
57375
+ resultText
57376
+ });
57377
+ if (!decision.deliver) {
57378
+ if (decision.reason === "no-chat") {
57379
+ process.stderr.write(`telegram gateway: subagent-handback ${agentId} \u2014 no chat to deliver to; skipped
57380
+ `);
57381
+ }
57382
+ return;
57383
+ }
57384
+ pendingInboundBuffer.push(process.env.SWITCHROOM_AGENT_NAME ?? "", decision.inbound);
57385
+ process.stderr.write(`telegram gateway: subagent-handback queued agent=${agentId} outcome=${outcome} chat=${decision.chatId} resultChars=${resultText.length}
57386
+ `);
56994
57387
  }
56995
57388
  });
56996
57389
  process.stderr.write(`telegram gateway: subagent-watcher active