switchroom 0.18.11 → 0.18.13

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 (150) hide show
  1. package/dist/agent-scheduler/index.js +37 -5
  2. package/dist/auth-broker/index.js +116 -78
  3. package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
  4. package/dist/cli/ms-365-write-pretool.mjs +31 -8
  5. package/dist/cli/notion-write-pretool.mjs +38 -6
  6. package/dist/cli/skill-validate-pretool.mjs +144 -2847
  7. package/dist/cli/switchroom.js +3529 -4543
  8. package/dist/cli/ui/index.html +163 -17
  9. package/dist/host-control/main.js +605 -2847
  10. package/dist/vault/approvals/kernel-server.js +120 -13
  11. package/dist/vault/broker/server.js +259 -157
  12. package/package.json +3 -4
  13. package/profiles/_base/start.sh.hbs +65 -0
  14. package/profiles/_shared/vault-protocol.md.hbs +3 -1
  15. package/profiles/coding/CLAUDE.md.hbs +1 -1
  16. package/profiles/default/CLAUDE.md.hbs +2 -2
  17. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  18. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  19. package/telegram-plugin/bridge/bridge.ts +37 -0
  20. package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
  21. package/telegram-plugin/dist/bridge/bridge.js +122 -4
  22. package/telegram-plugin/dist/gateway/gateway.js +4213 -3288
  23. package/telegram-plugin/dist/server.js +139 -5
  24. package/telegram-plugin/flood-circuit-breaker.ts +493 -21
  25. package/telegram-plugin/format.ts +19 -0
  26. package/telegram-plugin/gateway/approval-hold.ts +602 -0
  27. package/telegram-plugin/gateway/auth-command.ts +92 -2
  28. package/telegram-plugin/gateway/auth-loopback-relay.ts +670 -0
  29. package/telegram-plugin/gateway/boot-card.ts +12 -5
  30. package/telegram-plugin/gateway/callback-query-handlers.ts +88 -1
  31. package/telegram-plugin/gateway/config-approval-handler.ts +6 -1
  32. package/telegram-plugin/gateway/disconnect-flush.ts +19 -0
  33. package/telegram-plugin/gateway/dm-pin-sweep.test.ts +251 -0
  34. package/telegram-plugin/gateway/dm-pin-sweep.ts +178 -0
  35. package/telegram-plugin/gateway/gateway.ts +1695 -230
  36. package/telegram-plugin/gateway/hostd-dispatch.ts +23 -0
  37. package/telegram-plugin/gateway/idle-clear.ts +90 -6
  38. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +26 -5
  39. package/telegram-plugin/gateway/inject-handler.ts +8 -0
  40. package/telegram-plugin/gateway/ipc-protocol.ts +46 -3
  41. package/telegram-plugin/gateway/ipc-server.ts +43 -0
  42. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +145 -37
  43. package/telegram-plugin/gateway/model-command.ts +9 -3
  44. package/telegram-plugin/gateway/pending-session-command.ts +13 -1
  45. package/telegram-plugin/gateway/permission-ttl-sweep.ts +66 -0
  46. package/telegram-plugin/gateway/pre-approval-check.ts +74 -0
  47. package/telegram-plugin/gateway/queued-card-store.ts +217 -0
  48. package/telegram-plugin/gateway/session-model-file.ts +26 -1
  49. package/telegram-plugin/gateway/turn-end-gate-backstop.ts +59 -0
  50. package/telegram-plugin/gateway/turn-end-gate.ts +95 -0
  51. package/telegram-plugin/gateway/turn-typing-loop.ts +10 -2
  52. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +13 -0
  53. package/telegram-plugin/history.ts +51 -0
  54. package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
  55. package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
  56. package/telegram-plugin/hooks/hooks.json +9 -0
  57. package/telegram-plugin/inline-keyboard-callbacks.ts +303 -2
  58. package/telegram-plugin/model-unavailable.ts +41 -11
  59. package/telegram-plugin/operator-events.ts +23 -0
  60. package/telegram-plugin/outbound-field-redact.ts +69 -0
  61. package/telegram-plugin/package.json +0 -1
  62. package/telegram-plugin/permission-rule.ts +1 -0
  63. package/telegram-plugin/permission-title.ts +1 -0
  64. package/telegram-plugin/render/render.ts +32 -14
  65. package/telegram-plugin/retry-api-call.ts +212 -2
  66. package/telegram-plugin/scoped-approval.ts +11 -2
  67. package/telegram-plugin/secret-detect/chunker.ts +18 -4
  68. package/telegram-plugin/secret-detect/index.ts +12 -56
  69. package/telegram-plugin/send-gate-degraded.test.ts +574 -0
  70. package/telegram-plugin/send-gate-observability.test.ts +470 -0
  71. package/telegram-plugin/send-gate-observability.ts +355 -0
  72. package/telegram-plugin/send-gate.test.ts +717 -0
  73. package/telegram-plugin/send-gate.ts +1056 -0
  74. package/telegram-plugin/session-tail.ts +82 -7
  75. package/telegram-plugin/shared/bot-runtime.ts +17 -5
  76. package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
  77. package/telegram-plugin/status-pin-driver.ts +52 -7
  78. package/telegram-plugin/status-pin.ts +81 -0
  79. package/telegram-plugin/subagent-watcher.ts +173 -18
  80. package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
  81. package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
  82. package/telegram-plugin/tests/approval-hold-outcome.test.ts +327 -0
  83. package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
  84. package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
  85. package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
  86. package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
  87. package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
  88. package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
  89. package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
  90. package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
  91. package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
  92. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  93. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
  94. package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
  95. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
  96. package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
  97. package/telegram-plugin/tests/history.test.ts +115 -0
  98. package/telegram-plugin/tests/idle-clear.test.ts +233 -3
  99. package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
  100. package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
  101. package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +448 -0
  102. package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
  103. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
  104. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
  105. package/telegram-plugin/tests/model-command.test.ts +14 -0
  106. package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
  107. package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
  108. package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
  109. package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
  110. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
  111. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
  112. package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
  113. package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
  114. package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
  115. package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
  116. package/telegram-plugin/tests/render/render.test.ts +88 -0
  117. package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
  118. package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
  119. package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
  120. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
  121. package/telegram-plugin/tests/session-model-file.test.ts +50 -0
  122. package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
  123. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
  124. package/telegram-plugin/tests/status-pin.test.ts +275 -1
  125. package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
  126. package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
  127. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
  128. package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
  129. package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
  130. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
  131. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
  132. package/telegram-plugin/typing-emitter.ts +224 -0
  133. package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
  134. package/telegram-plugin/welcome-text.ts +42 -0
  135. package/telegram-plugin/worktree-watch-cwds.ts +194 -5
  136. package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
  137. package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
  138. package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
  139. package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
  140. package/vendor/hindsight-memory/scripts/session_end.py +14 -3
  141. package/vendor/hindsight-memory/scripts/session_start.py +21 -0
  142. package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
  143. package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
  144. package/vendor/hindsight-memory/tests/test_pending.py +44 -0
  145. package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
  146. package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
  147. package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
  148. package/telegram-plugin/channel-envelope-safety.ts +0 -56
  149. package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
  150. package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
@@ -1685,12 +1685,22 @@
1685
1685
  const safe = (p, fallback) =>
1686
1686
  p.then(r => r.ok ? r.json() : fallback).catch(() => fallback);
1687
1687
  try {
1688
- const [approvals, grants] = await Promise.all([
1688
+ const [blocked, blockedStatus, approvals, grants] = await Promise.all([
1689
+ // Blocked approvals lead the tab — an agent held with no way to ask
1690
+ // is the only thing here that is actively costing the operator.
1691
+ // The fallback is a `null` SENTINEL, never `[]`: a 500 / auth
1692
+ // failure / network error must not render as "nothing is blocked".
1693
+ // That would be a positive claim we did not verify, over an agent
1694
+ // that may be hanging right now.
1695
+ safe(fetch(`${API}/api/blocked-approvals`, { headers: authHeaders() }), null),
1696
+ // Companion read: how many records we FAILED to read (0600 files).
1697
+ // Same rule — null means we don't know, which is not "all clear".
1698
+ safe(fetch(`${API}/api/blocked-approvals/status`, { headers: authHeaders() }), null),
1689
1699
  safe(fetch(`${API}/api/approvals`, { headers: authHeaders() }), null),
1690
1700
  safe(fetch(`${API}/api/grants`, { headers: authHeaders() }),
1691
1701
  { reachable: false, grants: [], error: 'Failed to fetch standing grants.' }),
1692
1702
  ]);
1693
- renderApprovals(approvals, grants);
1703
+ renderApprovals(blocked, blockedStatus, approvals, grants);
1694
1704
  clearError();
1695
1705
  } catch (err) {
1696
1706
  showError(`Failed to fetch approvals: ${err.message}`);
@@ -2063,15 +2073,50 @@
2063
2073
  const apprReachable = !!(appr && appr.reachable !== false);
2064
2074
  const apprDecisions = (appr && appr.decisions) || [];
2065
2075
  const activeGrants = apprDecisions.filter(x => !x.revoked_at && !(x.ttl_expires_at && x.ttl_expires_at < Date.now())).length;
2076
+ // Agents HELD on an approval that couldn't be delivered to Telegram.
2077
+ // Surfaced twice on purpose: as critical rows in "Needs attention"
2078
+ // above (server-derived), and as a count here — a blocked agent must be
2079
+ // visible on the default tab without drilling in (#3084).
2080
+ const blockedAppr = Array.isArray(summary.blockedApprovals) ? summary.blockedApprovals : [];
2081
+ // Records that EXIST but could not be read (0600, IO error). Non-zero
2082
+ // means an empty `blockedAppr` proves nothing, so nothing below may
2083
+ // claim "none blocked" or "all nominal".
2084
+ const blockedUnreadable = Number(summary.blockedApprovalsUnreadable) || 0;
2066
2085
  const schedEntries = (sched && Array.isArray(sched.entries)) ? sched.entries : [];
2067
2086
  const schedAgents = new Set(schedEntries.map(e => e.agent)).size;
2068
- const services = [
2069
- svc('auth-broker', !!b.reachable, b.reachable ? `active ${escapeHtml(b.active || '—')} · ${b.accounts ?? '?'} accts` : 'unreachable'),
2070
- svc('hindsight', !!hs.running, hs.running ? `${escapeHtml(hs.model || '?')} · ${hs.mcpStateless ? 'stateless' : 'stateful'}` : 'not running'),
2071
- svc('hostd', !!hd.auditLogPresent, hd.auditLogPresent ? `${(hd.recent || []).length} recent verbs` : 'no audit log'),
2072
- svc('schedule', schedEntries.length > 0, `${schedEntries.length} crons · ${schedAgents} agents`),
2073
- svc('approvals', apprReachable, apprReachable ? `${activeGrants} active grants` : 'unreachable'),
2074
- ].join('');
2087
+ // Approvals is NOT ok if an agent is blocked (the leash is jammed and
2088
+ // someone is waiting on Ken) OR if we could not read the records at all
2089
+ // (we cannot claim "none blocked" without having looked).
2090
+ const apprOk = apprReachable && blockedAppr.length === 0 && blockedUnreadable === 0;
2091
+ const apprMeta = blockedUnreadable > 0
2092
+ ? `<b>cannot read ${blockedUnreadable} blocked record(s)</b> · an agent may be held`
2093
+ : blockedAppr.length > 0
2094
+ ? `<b>${blockedAppr.length} agent${blockedAppr.length === 1 ? '' : 's'} blocked</b> — ${escapeHtml(blockedAppr.map(b => b.agent).join(', '))}`
2095
+ : apprReachable ? `${activeGrants} active grants · none blocked` : 'unreachable';
2096
+
2097
+ // Build the rail from specs so the BAND can be derived from whether
2098
+ // every service is actually ok. The band used to hardcode "all nominal"
2099
+ // even with hindsight down and hostd red — the same claim-health-you-
2100
+ // haven't-verified bug this PR exists to kill, so it dies here too
2101
+ // rather than being carved out for one service.
2102
+ const svcSpecs = [
2103
+ { name: 'auth-broker', ok: !!b.reachable, meta: b.reachable ? `active ${escapeHtml(b.active || '—')} · ${b.accounts ?? '?'} accts` : 'unreachable' },
2104
+ { name: 'hindsight', ok: !!hs.running, meta: hs.running ? `${escapeHtml(hs.model || '?')} · ${hs.mcpStateless ? 'stateless' : 'stateful'}` : 'not running' },
2105
+ { name: 'hostd', ok: !!hd.auditLogPresent, meta: hd.auditLogPresent ? `${(hd.recent || []).length} recent verbs` : 'no audit log' },
2106
+ { name: 'schedule', ok: schedEntries.length > 0, meta: `${schedEntries.length} crons · ${schedAgents} agents` },
2107
+ { name: 'approvals', ok: apprOk, meta: apprMeta },
2108
+ ];
2109
+ const services = svcSpecs.map(s => svc(s.name, s.ok, s.meta)).join('');
2110
+ const badSvcs = svcSpecs.filter(s => !s.ok);
2111
+ // Blocked/unreadable approvals get named explicitly in the band — that
2112
+ // is the one the operator has to act on right now.
2113
+ const bandNote = blockedUnreadable > 0
2114
+ ? `cannot read ${blockedUnreadable} blocked record(s)`
2115
+ : blockedAppr.length > 0
2116
+ ? `${blockedAppr.length} blocked on approval`
2117
+ : badSvcs.length > 0
2118
+ ? `${badSvcs.length} degraded — ${escapeHtml(badSvcs.map(s => s.name).join(', '))}`
2119
+ : 'all nominal';
2075
2120
 
2076
2121
  el.innerHTML = `
2077
2122
  ${verdict}${asOf}
@@ -2079,7 +2124,7 @@
2079
2124
  <div class="sm-attn-list">${attnHtml}</div>
2080
2125
  <div class="sm-band">Fleet vitals</div>
2081
2126
  <div class="sm-vitals">${agentsCard}${quotaCard}</div>
2082
- <div class="sm-band">Services <span class="n">all nominal</span></div>
2127
+ <div class="sm-band">Services <span class="n">${bandNote}</span></div>
2083
2128
  <div class="sm-services">${services}</div>`;
2084
2129
  }
2085
2130
 
@@ -3137,15 +3182,116 @@
3137
3182
  container.innerHTML = degradedBanner + truncatedNote + posture + cards;
3138
3183
  }
3139
3184
 
3140
- function renderApprovals(data, grants) {
3185
+ function renderApprovals(blocked, blockedStatus, data, grants) {
3141
3186
  const container = document.getElementById('approvals');
3142
- // Two independent sections: the operator's REAL standing capability
3143
- // grants (broker grants DB the actually-useful one), then the
3144
- // kernel decision ledger (honestly empty on most installs). They
3145
- // degrade independently one being unreachable never blanks the
3146
- // other.
3187
+ // Three independent sections, most-urgent first: agents HELD on an
3188
+ // approval they couldn't deliver (the only live, costing thing here),
3189
+ // then the operator's REAL standing capability grants (broker grants
3190
+ // DB), then the kernel decision ledger (honestly empty on most
3191
+ // installs). They degrade independently — one being unreachable never
3192
+ // blanks the others.
3147
3193
  container.innerHTML =
3148
- renderStandingGrants(grants) + renderApprovalDecisions(data);
3194
+ renderBlockedApprovals(blocked, blockedStatus) +
3195
+ renderStandingGrants(grants) +
3196
+ renderApprovalDecisions(data);
3197
+ }
3198
+
3199
+ // Humanized hold duration. Mirrors formatBlockedFor() in
3200
+ // src/web/blocked-approvals-read.ts so the Summary row and this table
3201
+ // read identically ("blocked 47m"). Clamps a future timestamp to 0.
3202
+ function formatBlockedFor(ms) {
3203
+ const t = Math.max(0, Math.floor(ms / 1000));
3204
+ if (t < 60) return `${t}s`;
3205
+ const m = Math.floor(t / 60);
3206
+ if (m < 60) return `${m}m`;
3207
+ const h = Math.floor(m / 60);
3208
+ if (h < 24) return (m % 60 > 0) ? `${h}h ${m % 60}m` : `${h}h`;
3209
+ const d = Math.floor(h / 24);
3210
+ return (h % 24 > 0) ? `${d}d ${h % 24}h` : `${d}d`;
3211
+ }
3212
+
3213
+ // Agents held on an approval whose card could not be delivered to
3214
+ // Telegram (flood ban, API outage). The agent HOLDS — it never
3215
+ // auto-approves and never auto-denies — so it waits indefinitely. This
3216
+ // section is the whole point: a held agent has to be SEEN here, not
3217
+ // discovered an hour later (#3084).
3218
+ //
3219
+ // The record is metadata only; it never carries the raw tool input, and
3220
+ // the server hard-whitelists the fields. Empty is stated plainly rather
3221
+ // than implying health we haven't verified.
3222
+ function renderBlockedApprovals(blocked, blockedStatus) {
3223
+ const dim = (s) => `<span style="color:var(--text-dim)">${escapeHtml(s)}</span>`;
3224
+ const rows = Array.isArray(blocked) ? blocked : [];
3225
+ const heading = `<h3 style="margin:0 0 0.5rem;font-size:0.95rem">Blocked approvals${rows.length ? ` <span class="usage-pill primary">${rows.length}</span>` : ''}</h3>`;
3226
+
3227
+ // Did we actually LOOK? Three distinct states, and only one of them may
3228
+ // print an all-clear:
3229
+ // - fetch failed (`blocked` is the null sentinel) → we don't know
3230
+ // - status says records were unreadable (0600, IO) → we don't know
3231
+ // - read clean and empty → nothing is blocked
3232
+ // Collapsing the first two into the third is the failure this whole
3233
+ // surface exists to prevent: a confident "all clear" that really means
3234
+ // "I couldn't look", printed over an agent that is hanging.
3235
+ const fetchFailed = !Array.isArray(blocked);
3236
+ const unreadable = (blockedStatus && Number(blockedStatus.unreadable)) || 0;
3237
+ const blind = fetchFailed || unreadable > 0;
3238
+
3239
+ // The warning is a BANNER, not an early return: an unreadable record
3240
+ // must never hide the records we CAN read. Both can be true at once.
3241
+ const warning = blind
3242
+ ? renderProblem({
3243
+ title: 'Could not read the blocked-approval records. An agent may be held.',
3244
+ detail: (fetchFailed
3245
+ ? 'The dashboard could not fetch the blocked-approval records. '
3246
+ : `${unreadable} blocked-approval record(s) exist but could not be read (wrong file mode, or an IO error). `) +
3247
+ 'This is not an all-clear: an agent may be blocked on an approval right now and this page cannot see it.',
3248
+ remediation: {
3249
+ kind: 'command',
3250
+ label: 'Records must be mode 0644. Check on the host:',
3251
+ value: 'ls -l ~/.switchroom/blocked-approvals/',
3252
+ },
3253
+ })
3254
+ : '';
3255
+
3256
+ if (rows.length === 0) {
3257
+ // Only an unblinded, clean read may print an all-clear.
3258
+ return heading + warning + (blind
3259
+ ? ''
3260
+ : '<div class="loading" style="color:var(--text-dim)">No agent is blocked on an undeliverable approval.</div>');
3261
+ }
3262
+ const now = Date.now();
3263
+ const body = rows.map(b => {
3264
+ const held = formatBlockedFor(now - (Number(b.blockedSince) || now));
3265
+ const retryCell = (typeof b.retryableAt === 'number')
3266
+ ? (b.retryableAt > now
3267
+ ? `in ${escapeHtml(formatBlockedFor(b.retryableAt - now))} <span style="color:var(--text-dim)">(${escapeHtml(formatTimestamp(b.retryableAt))})</span>`
3268
+ : `${escapeHtml(formatTimestamp(b.retryableAt))} <span style="color:var(--text-dim)">(due)</span>`)
3269
+ : dim('unknown');
3270
+ return `
3271
+ <tr>
3272
+ <td><span class="status-dot inactive" style="display:inline-block;vertical-align:middle"></span> ${escapeHtml(b.agent || '—')}</td>
3273
+ <td><b>blocked ${escapeHtml(held)}</b></td>
3274
+ <td>${b.action ? escapeHtml(b.action) : dim('—')}</td>
3275
+ <td><span class="chip">${escapeHtml(b.toolName || '—')}</span></td>
3276
+ <td>${escapeHtml(b.reason || '—')}</td>
3277
+ <td>${retryCell}</td>
3278
+ <td title="${escapeHtml(b.requestId || '')}">${b.requestId ? escapeHtml(b.requestId) : dim('—')}</td>
3279
+ </tr>`;
3280
+ }).join('');
3281
+ return heading + warning + `
3282
+ <div style="margin-bottom:0.6rem;font-size:0.8rem;color:var(--text-dim)">
3283
+ These agents are waiting on you and cannot ask. The approval card could not be delivered to Telegram.
3284
+ Nothing is auto-approved and nothing is auto-denied; each one holds until it can reach you.
3285
+ </div>
3286
+ <div class="accounts-table-wrap">
3287
+ <table class="accounts-table">
3288
+ <thead><tr>
3289
+ <th>Agent</th><th>Held for</th><th>Waiting to</th>
3290
+ <th>Tool</th><th>Why undelivered</th><th>Retry</th><th>Request</th>
3291
+ </tr></thead>
3292
+ <tbody>${body}</tbody>
3293
+ </table>
3294
+ </div>`;
3149
3295
  }
3150
3296
 
3151
3297
  // Standing capability grants from the vault-broker grants DB — what