switchroom 0.18.32 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/auth-broker/index.js +17 -1
  2. package/dist/cli/switchroom.js +847 -729
  3. package/dist/host-control/main.js +18 -2
  4. package/dist/vault/approvals/kernel-server.js +17 -1
  5. package/dist/vault/broker/server.js +44 -2
  6. package/package.json +2 -2
  7. package/profiles/_base/start.sh.hbs +105 -18
  8. package/telegram-plugin/dist/gateway/gateway.js +60612 -56998
  9. package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
  10. package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
  11. package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
  12. package/telegram-plugin/gateway/boot-card.ts +169 -1
  13. package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
  14. package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
  15. package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
  16. package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
  17. package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
  18. package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
  19. package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
  20. package/telegram-plugin/gateway/gateway.ts +1347 -6758
  21. package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
  22. package/telegram-plugin/gateway/inbound-router.ts +400 -0
  23. package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
  24. package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
  25. package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
  26. package/telegram-plugin/gateway/model-command.ts +23 -0
  27. package/telegram-plugin/gateway/narrative-lane.ts +865 -0
  28. package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
  29. package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
  30. package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
  31. package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
  32. package/telegram-plugin/gateway/secret-request-card.ts +45 -0
  33. package/telegram-plugin/gateway/stream-render.ts +2166 -0
  34. package/telegram-plugin/gateway/turn-end.ts +606 -0
  35. package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
  36. package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
  37. package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
  38. package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
  39. package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
  40. package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
  41. package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
  42. package/telegram-plugin/narrative-dedup.ts +24 -1
  43. package/telegram-plugin/narrative-flush.ts +2 -2
  44. package/telegram-plugin/render/render.ts +25 -1
  45. package/telegram-plugin/status-no-truncate.ts +13 -0
  46. package/telegram-plugin/subagent-watcher.ts +186 -3
  47. package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
  48. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
  49. package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
  50. package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
  51. package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
  52. package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
  53. package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
  54. package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
  55. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +15 -6
  56. package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
  57. package/telegram-plugin/tests/button-tap-turn-gated.test.ts +18 -9
  58. package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
  59. package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
  60. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
  61. package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
  62. package/telegram-plugin/tests/emission-authority-facade.test.ts +47 -10
  63. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +27 -9
  64. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
  65. package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +39 -18
  66. package/telegram-plugin/tests/gateway-boot-smoke.test.ts +160 -0
  67. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +3 -7
  68. package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
  69. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +8 -2
  70. package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
  71. package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
  72. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
  73. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +14 -3
  74. package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
  75. package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
  76. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
  77. package/telegram-plugin/tests/model-command.test.ts +30 -0
  78. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +27 -9
  79. package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
  80. package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
  81. package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
  82. package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
  83. package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +16 -7
  84. package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
  85. package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
  86. package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
  87. package/telegram-plugin/tests/render/render.test.ts +42 -0
  88. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
  89. package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
  90. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +28 -18
  91. package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
  92. package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
  93. package/telegram-plugin/tests/stop-command.test.ts +22 -12
  94. package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
  95. package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
  96. package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +14 -0
  97. package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
  98. package/telegram-plugin/tests/turn-flush-safety.test.ts +183 -5
  99. package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +9 -4
  100. package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
  101. package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
  102. package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
  103. package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
  104. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
  105. package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
  106. package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
  107. package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
  108. package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
  109. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +110 -20
  110. package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
  111. package/telegram-plugin/tool-activity-summary.ts +83 -35
  112. package/telegram-plugin/turn-flush-safety.ts +80 -14
  113. package/telegram-plugin/uat/restart-capability.ts +76 -0
  114. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
  115. package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
  116. package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
  117. package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
  118. package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
  119. package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
  120. package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
  121. package/telegram-plugin/worker-activity-feed.ts +10 -4
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `checklist_tasks_done` / `checklist_tasks_added` service-message forwarding
3
+ * (switchroom#2996 P6 cluster F).
4
+ *
5
+ * Telegram emits these service messages when users tick or add tasks in a
6
+ * native checklist. They arrive as part of the `message` update type (no extra
7
+ * `allowed_updates` config needed). We route them to the agent as a new channel
8
+ * event with kind="checklist_task_changed" so the agent can react to user
9
+ * actions on a checklist it sent.
10
+ *
11
+ * Extracted verbatim from gateway.ts (Amendment 9 — no module-global reads).
12
+ * The two `bot.on('message:checklist_tasks_*')` registration lines stay in
13
+ * gateway.ts (order is a load-bearing invariant) and delegate here.
14
+ */
15
+
16
+ import type { Context } from 'grammy'
17
+ import type { InboundMessage } from './ipc-protocol.js'
18
+
19
+ type ChecklistTaskUpdate = {
20
+ message_checklist?: {
21
+ title?: string
22
+ tasks?: Array<{ id?: number; text?: string; is_completed?: boolean }>
23
+ }
24
+ checklist_tasks_done?: Array<{ id?: number; user?: { id?: number; username?: string }; done?: boolean }>
25
+ checklist_tasks_added?: Array<{ id?: number; text?: string; user?: { id?: number; username?: string } }>
26
+ }
27
+
28
+ export interface ChecklistHandlerDeps {
29
+ /** Access config — only `allowFrom` is consulted (same gate as inbound). */
30
+ loadAccess: () => { allowFrom: string[] }
31
+ /** Broadcast a channel event to connected bridges. */
32
+ broadcast: (msg: InboundMessage) => void
33
+ /** stderr log sink. */
34
+ log: (line: string) => void
35
+ }
36
+
37
+ export function handleChecklistUpdate(
38
+ ctx: Context,
39
+ kind: 'checklist_tasks_done' | 'checklist_tasks_added',
40
+ deps: ChecklistHandlerDeps,
41
+ ): void {
42
+ try {
43
+ const msg = ctx.message as (typeof ctx.message & ChecklistTaskUpdate) | undefined
44
+ if (!msg) return
45
+
46
+ const chat = ctx.chat
47
+ if (!chat) return
48
+
49
+ const chat_id = String(chat.id)
50
+ const access = deps.loadAccess()
51
+
52
+ // Only notify if this chat is allowlisted — same guard as inbound user messages.
53
+ // Closes #472 finding #13. Pre-fix the `&& access.allowFrom.length > 0`
54
+ // tail made this fail-OPEN when the allowlist was empty: every chat's
55
+ // checklist tasks would forward to the agent. Sibling guards (the
56
+ // inbound-message gate at line ~588 and the operator-event broadcast
57
+ // at line ~1221) are both fail-closed for empty allowlists. The
58
+ // empty-allowlist case is the most likely state for a misconfiguration
59
+ // (e.g. /unpair just ran), so fail-OPEN is the worst default.
60
+ if (!access.allowFrom.includes(chat_id)) return
61
+
62
+ const message_id = String(msg.message_id)
63
+ const ts = msg.date ?? Math.floor(Date.now() / 1000)
64
+
65
+ // Extract task updates depending on service message type
66
+ const tasksDone = msg.checklist_tasks_done ?? []
67
+ const tasksAdded = msg.checklist_tasks_added ?? []
68
+ const allTasks = kind === 'checklist_tasks_done' ? tasksDone : tasksAdded
69
+
70
+ // Build per-task channel events and broadcast each to connected bridges.
71
+ for (const task of allTasks) {
72
+ const taskId = task.id != null ? String(task.id) : '?'
73
+ const user = (task.user as { username?: string; id?: number } | undefined)
74
+ const userName = user?.username ?? (user?.id != null ? String(user.id) : 'unknown')
75
+ const state = kind === 'checklist_tasks_done'
76
+ ? ((task as { done?: boolean }).done === false ? 'undone' : 'done')
77
+ : 'added'
78
+
79
+ const inboundMsg: InboundMessage = {
80
+ type: 'inbound',
81
+ chatId: chat_id,
82
+ messageId: Number(message_id),
83
+ user: userName,
84
+ userId: user?.id ?? 0,
85
+ ts,
86
+ text: `(checklist task ${state}: id=${taskId})`,
87
+ meta: {
88
+ chat_id,
89
+ message_id,
90
+ kind: 'checklist_task_changed',
91
+ task_id: taskId,
92
+ state,
93
+ user: userName,
94
+ user_id: user?.id != null ? String(user.id) : '0',
95
+ ts: new Date(ts * 1000).toISOString(),
96
+ },
97
+ }
98
+ // allow-broadcast: informational checklist task notification, not turn-driving
99
+ deps.broadcast(inboundMsg)
100
+ deps.log(
101
+ `telegram gateway: checklist ${kind}: chat_id=${chat_id} message_id=${message_id} task_id=${taskId} state=${state} user=${userName}\n`,
102
+ )
103
+ }
104
+ } catch (err) {
105
+ deps.log(`telegram gateway: checklist handler error (${kind}): ${err}\n`)
106
+ }
107
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * delivery-confirm-wiring.ts — the deliver-until-acked SWEEP wiring
3
+ * (#2996 P8 PR-C1).
4
+ *
5
+ * Extracted VERBATIM from gateway.ts: the 5 s confirm-sweep tick body
6
+ * (`runDeliveryConfirmSweep`), the strand re-delivery (`redeliverStrandedInbound`)
7
+ * and the #2787 Mechanism A card-suspension scoper (`sweepSuspendedTargets`).
8
+ * The gateway keeps thin same-name wrappers and OWNS the `setInterval`
9
+ * (the P0c rule — a test import must stay side-effect-free).
10
+ *
11
+ * DI contract: state stays in gateway.ts (`deliveryQueue`, the pending-card
12
+ * maps, `pendingInboundBuffer`); volatile handles cross as getters. The idle
13
+ * gate deliberately reads `getCurrentTurnNull()` — the turn ATOM, not the
14
+ * machine's eager in-turn state (the documented trap: `turnInFlightForGate`
15
+ * stays set through a strand until the TTL tick, so it is NOT a usable idle
16
+ * signal here). Design §3 PR-C1 risk row; harness case 4 pins mid-turn
17
+ * suppression.
18
+ *
19
+ * `trackDelivery` enrolment callsites stay in gateway.ts unchanged — the two
20
+ * twin-body sites live inside the P7-owned handleInbound window and are P7's
21
+ * to delete (design §2 PR-C1 note).
22
+ */
23
+ import type { InboundMessage } from './ipc-protocol.js'
24
+ import type { DeliveryConfirmWiringDeps } from './gateway.js'
25
+ import {
26
+ trackDelivery,
27
+ sweep as sweepDeliveryQueue,
28
+ forgetDelivery,
29
+ isRedeliverySuspended,
30
+ type PendingDelivery,
31
+ } from './inbound-delivery-confirm.js'
32
+ import { chatKey } from './chat-key.js'
33
+
34
+ export function createDeliveryConfirmWiring(deps: DeliveryConfirmWiringDeps) {
35
+ const {
36
+ DELIVERY_CONFIRM_ENABLED,
37
+ DELIVERY_CONFIRM_TIMEOUT_MS,
38
+ getCurrentTurnNull,
39
+ sendToAgent,
40
+ deliveryQueue,
41
+ pendingInboundBuffer,
42
+ pendingPermissions,
43
+ pendingAskUser,
44
+ } = deps
45
+
46
+ // Re-deliver stranded inbounds until claude acks (the marko drop-wedge).
47
+ // Every few seconds, re-send any inbound that was handed to claude but never
48
+ // acked by an `enqueue` — it stranded unsubmitted in the composer. Re-clear
49
+ // the composer so the re-sent notification lands on a clean line, then
50
+ // re-send. Reuses the same delivery primitives; the message is never dropped.
51
+ // (Refs ipcServer / pendingInboundBuffer declared below — resolved at fire
52
+ // time, after module init.) unref so the interval never holds the process.
53
+ async function redeliverStrandedInbound(p: PendingDelivery<InboundMessage>): Promise<void> {
54
+ const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? ''
55
+ process.stderr.write(
56
+ `telegram gateway: inbound strand (no enqueue ack) key=${p.key} — re-clearing composer + re-delivering\n`,
57
+ )
58
+ try {
59
+ const { clearAgentComposer } = await import('../../src/agents/tmux.js')
60
+ if (selfAgent) clearAgentComposer({ agentName: selfAgent })
61
+ } catch { /* best-effort; re-deliver regardless */ }
62
+ const ok = sendToAgent(selfAgent, p.inbound)
63
+ if (ok) {
64
+ // Survive an ack that raced the `await import` above: only `enqueue`
65
+ // clears tracking, so if a concurrent ack removed the entry, re-affirm
66
+ // it — never drop. Idempotent.
67
+ if (!deliveryQueue.pending.has(p.key)) {
68
+ trackDelivery(deliveryQueue, p.key, p.inbound, Date.now(), p.messageId)
69
+ }
70
+ } else {
71
+ // Bridge offline between attempts — hand off to the offline buffer
72
+ // (bridgeUp drains it) and stop tracking here; the spool owns it now.
73
+ pendingInboundBuffer.push(selfAgent, p.inbound)
74
+ forgetDelivery(deliveryQueue, p.key)
75
+ }
76
+ }
77
+ // #2787 Mechanism A — which chats/topics currently hold a live permission or
78
+ // ask_user card. A pending card is a live interaction for ITS OWN chat, so the
79
+ // confirm sweep must not re-clear the composer + re-send there. But the OLD guard
80
+ // (`pendingPermissions.size > 0 || pendingAskUser.size > 0 → return`) suspended
81
+ // the sweep GLOBALLY: one card parked in a single topic (or in the operator DM,
82
+ // a different chatId entirely) froze re-delivery of every stranded inbound across
83
+ // EVERY topic until that one card resolved. This scopes the suspension to the
84
+ // card's own target. Returns per-topic chatKeys where the topic is known and bare
85
+ // chatIds where it isn't (a card fanned to operator DMs records chatId only, and
86
+ // a whole-chat suspension is the safe conservative fallback there); the callsite
87
+ // tests a delivery entry against both renderings.
88
+ function sweepSuspendedTargets(): { keys: Set<string>; chats: Set<string> } {
89
+ const keys = new Set<string>()
90
+ const chats = new Set<string>()
91
+ for (const p of pendingPermissions.values()) {
92
+ for (const c of p.cards) {
93
+ if (c.threadId != null) keys.add(chatKey(c.chatId, c.threadId))
94
+ else chats.add(c.chatId)
95
+ }
96
+ // A permission whose card send hasn't resolved yet (cards still empty) has
97
+ // no recorded target — suspend nothing for it; the next sweep sees it once
98
+ // the send resolves, and the currentTurn guard already covers the in-turn
99
+ // window a fresh permission request lives in.
100
+ }
101
+ for (const a of pendingAskUser.values()) {
102
+ if (a.threadId != null) keys.add(chatKey(a.chatId, a.threadId))
103
+ else chats.add(a.chatId)
104
+ }
105
+ return { keys, chats }
106
+ }
107
+
108
+ // #2996 P0c/P8 PR-C1: the sweep TICK body (named in PR-A so the harness can
109
+ // drive it) — the GATEWAY still owns the `setInterval` (the P0c gate rule:
110
+ // modules export the tick body; the gateway registers the timer).
111
+ function runDeliveryConfirmSweep(): void {
112
+ if (!DELIVERY_CONFIRM_ENABLED) return
113
+ // Re-deliver ONLY when claude is genuinely idle. `currentTurn` is set solely
114
+ // by the enqueue session-event and nulled at turn-end, so `currentTurn != null`
115
+ // means a real turn is in flight — re-clearing the composer + re-sending now
116
+ // would clobber it (the exact mid-turn wedge this queue exists to prevent).
117
+ // NB: the machine's in-turn state (turnInFlightForGate) is advanced EAGERLY
118
+ // at delivery and stays set through a strand until the TTL tick, so it is
119
+ // NOT a usable "idle" signal here.
120
+ if (!getCurrentTurnNull()) return
121
+ // #2787 Mechanism A: a pending permission / ask_user card suspends re-delivery
122
+ // ONLY for its own chat/topic — never globally. Skip just the stranded entries
123
+ // whose target holds a live card; sweep the rest so unrelated topics keep
124
+ // getting re-delivered.
125
+ const suspended = sweepSuspendedTargets()
126
+ for (const p of sweepDeliveryQueue(deliveryQueue, Date.now(), DELIVERY_CONFIRM_TIMEOUT_MS)) {
127
+ if (isRedeliverySuspended(p.key, suspended)) continue
128
+ void redeliverStrandedInbound(p)
129
+ }
130
+ }
131
+
132
+ return { runDeliveryConfirmSweep, redeliverStrandedInbound, sweepSuspendedTargets }
133
+ }