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,256 @@
1
+ /**
2
+ * Media-envelope inbound handlers (switchroom#2996 P6 cluster A).
3
+ *
4
+ * The "envelope" `message:*` content types that carry structured metadata the
5
+ * gateway renders into a compact text envelope for the agent — contact,
6
+ * location, venue, poll, web_app_data, users_shared, chat_shared — plus the
7
+ * two money/identity outliers that are deliberately NOT forwarded to the agent
8
+ * (successful_payment → log-and-ack, passport_data → refuse), plus the
9
+ * situational ack-only leaves (dice/game/story/paid_media → log-and-react,
10
+ * switchroom#2996 P6 cluster B).
11
+ *
12
+ * Extracted VERBATIM out of gateway.ts (the P0a `registerGatewayHandlers`
13
+ * block) so the rendering logic is unit-testable against a fake ctx + mock
14
+ * dispatch deps, instead of living inline in a side-effecting module that
15
+ * cannot be imported. The `bot.on('message:*')` registration lines stay in
16
+ * gateway.ts (their ORDER is a load-bearing invariant pinned by
17
+ * gateway-handler-registration-wiring.test.ts); each now delegates to the
18
+ * matching handler here.
19
+ *
20
+ * Dispatch surfaces (`handleInbound`, `handleAckOnly`, `handleRefusal`) and
21
+ * the stderr log sink are injected via `MediaEnvelopeDeps` — no gateway
22
+ * module-scope globals are read here (switchroom#2996 Amendment 9).
23
+ */
24
+
25
+ import type { Context, Filter } from 'grammy'
26
+
27
+ /**
28
+ * Sanitize a user-supplied string for inclusion in a text envelope: strips
29
+ * the characters that could break the envelope framing or inject markup
30
+ * (`<>[]`, CR/LF, `;`). Shared with the attachment handlers (photo/document/
31
+ * audio/video/animation) that still live inline in gateway.ts — imported
32
+ * there — so it lives here as the single owner.
33
+ */
34
+ export function safeName(s: string | undefined): string | undefined {
35
+ return s?.replace(/[<>\[\]\r\n;]/g, '_')
36
+ }
37
+
38
+ /**
39
+ * Polite, fixed refusal for inbound Telegram Passport data — a regulated
40
+ * identity-document flow we explicitly do not process (never forwarded to the
41
+ * LLM-driven agent).
42
+ */
43
+ export const PASSPORT_REFUSAL_TEXT =
44
+ "Sorry, I don't handle Telegram Passport data. Please share identity documents through a supported channel — your agent does not process encrypted Passport payloads."
45
+
46
+ /**
47
+ * Injected dispatch + logging surfaces. `handleInbound` is the normal inbound
48
+ * pipeline (same access gating / forward-origin parsing as every content
49
+ * handler); `handleAckOnly` reacts without forwarding; `handleRefusal` replies
50
+ * a refusal and never forwards; `log` is the stderr sink.
51
+ */
52
+ export interface MediaEnvelopeDeps {
53
+ handleInbound: (ctx: Context, text: string, downloadImage: undefined) => Promise<void>
54
+ handleAckOnly: (
55
+ ctx: Context,
56
+ kind: string,
57
+ opts?: { emoji?: string; warn?: boolean },
58
+ ) => Promise<void>
59
+ handleRefusal: (ctx: Context, kind: string, refusalText: string) => Promise<void>
60
+ log: (line: string) => void
61
+ }
62
+
63
+ export async function handleContactMessage(
64
+ ctx: Filter<Context, 'message:contact'>,
65
+ deps: MediaEnvelopeDeps,
66
+ ): Promise<void> {
67
+ try {
68
+ const c = ctx.message.contact
69
+ const phone = safeName(c.phone_number) ?? '?'
70
+ const first = safeName(c.first_name) ?? ''
71
+ const last = safeName(c.last_name) ?? ''
72
+ const name = [first, last].filter(Boolean).join(' ') || '?'
73
+ const userIdPart = c.user_id != null ? ` user_id=${c.user_id}` : ''
74
+ const text = `(contact: name="${name}" phone="${phone}"${userIdPart})`
75
+ deps.log(`telegram gateway: inbound contact from chat=${ctx.chat?.id ?? '?'}\n`)
76
+ await deps.handleInbound(ctx, text, undefined)
77
+ } catch (err) {
78
+ deps.log(`telegram gateway: contact handler error: ${(err as Error).message}\n`)
79
+ }
80
+ }
81
+
82
+ export async function handleLocationMessage(
83
+ ctx: Filter<Context, 'message:location'>,
84
+ deps: MediaEnvelopeDeps,
85
+ ): Promise<void> {
86
+ try {
87
+ const loc = ctx.message.location
88
+ const lat = typeof loc.latitude === 'number' ? loc.latitude.toFixed(6) : '?'
89
+ const lon = typeof loc.longitude === 'number' ? loc.longitude.toFixed(6) : '?'
90
+ const live = (loc as { live_period?: number }).live_period != null
91
+ ? ` live_period=${(loc as { live_period?: number }).live_period}s`
92
+ : ''
93
+ const text = `(location: lat=${lat} lon=${lon}${live})`
94
+ deps.log(`telegram gateway: inbound location from chat=${ctx.chat?.id ?? '?'}\n`)
95
+ await deps.handleInbound(ctx, text, undefined)
96
+ } catch (err) {
97
+ deps.log(`telegram gateway: location handler error: ${(err as Error).message}\n`)
98
+ }
99
+ }
100
+
101
+ export async function handleVenueMessage(
102
+ ctx: Filter<Context, 'message:venue'>,
103
+ deps: MediaEnvelopeDeps,
104
+ ): Promise<void> {
105
+ try {
106
+ const v = ctx.message.venue
107
+ const title = safeName(v.title) ?? '?'
108
+ const address = safeName(v.address) ?? '?'
109
+ const lat = typeof v.location?.latitude === 'number' ? v.location.latitude.toFixed(6) : '?'
110
+ const lon = typeof v.location?.longitude === 'number' ? v.location.longitude.toFixed(6) : '?'
111
+ const text = `(venue: title="${title}" address="${address}" lat=${lat} lon=${lon})`
112
+ deps.log(`telegram gateway: inbound venue from chat=${ctx.chat?.id ?? '?'}\n`)
113
+ await deps.handleInbound(ctx, text, undefined)
114
+ } catch (err) {
115
+ deps.log(`telegram gateway: venue handler error: ${(err as Error).message}\n`)
116
+ }
117
+ }
118
+
119
+ export async function handlePollMessage(
120
+ ctx: Filter<Context, 'message:poll'>,
121
+ deps: MediaEnvelopeDeps,
122
+ ): Promise<void> {
123
+ try {
124
+ const p = ctx.message.poll
125
+ const q = safeName(p.question) ?? '?'
126
+ const optsCount = Array.isArray(p.options) ? p.options.length : 0
127
+ const optsList = Array.isArray(p.options)
128
+ ? p.options.slice(0, 10).map(o => safeName((o as { text?: string }).text) ?? '?').join(' | ')
129
+ : ''
130
+ const anon = p.is_anonymous ? ' anonymous' : ''
131
+ const text = `(poll: question="${q}" options=${optsCount}${anon}${optsList ? ` choices=[${optsList}]` : ''})`
132
+ deps.log(`telegram gateway: inbound poll from chat=${ctx.chat?.id ?? '?'}\n`)
133
+ await deps.handleInbound(ctx, text, undefined)
134
+ } catch (err) {
135
+ deps.log(`telegram gateway: poll handler error: ${(err as Error).message}\n`)
136
+ }
137
+ }
138
+
139
+ export async function handleWebAppDataMessage(
140
+ ctx: Filter<Context, 'message:web_app_data'>,
141
+ deps: MediaEnvelopeDeps,
142
+ ): Promise<void> {
143
+ try {
144
+ const w = ctx.message.web_app_data
145
+ // web_app_data.data is arbitrary user-supplied string from the
146
+ // mini-app — pass it through but cap length so a malicious mini-app
147
+ // can't blast the agent with multi-MB payloads.
148
+ const raw = typeof w.data === 'string' ? w.data : ''
149
+ const data = raw.length > 4096 ? raw.slice(0, 4096) + '…(truncated)' : raw
150
+ const button = safeName(w.button_text) ?? '?'
151
+ const text = `(web_app_data: button="${button}" data=${JSON.stringify(data)})`
152
+ deps.log(`telegram gateway: inbound web_app_data from chat=${ctx.chat?.id ?? '?'} button="${button}" bytes=${raw.length}\n`)
153
+ await deps.handleInbound(ctx, text, undefined)
154
+ } catch (err) {
155
+ deps.log(`telegram gateway: web_app_data handler error: ${(err as Error).message}\n`)
156
+ }
157
+ }
158
+
159
+ export async function handleUsersSharedMessage(
160
+ ctx: Filter<Context, 'message:users_shared'>,
161
+ deps: MediaEnvelopeDeps,
162
+ ): Promise<void> {
163
+ try {
164
+ const u = ctx.message.users_shared
165
+ const users = Array.isArray(u.users) ? u.users : []
166
+ const ids = users.map(usr => String((usr as { user_id?: number }).user_id ?? '?')).join(',')
167
+ const text = `(users_shared: request_id=${u.request_id ?? '?'} user_ids=[${ids}] count=${users.length})`
168
+ deps.log(`telegram gateway: inbound users_shared from chat=${ctx.chat?.id ?? '?'} count=${users.length}\n`)
169
+ await deps.handleInbound(ctx, text, undefined)
170
+ } catch (err) {
171
+ deps.log(`telegram gateway: users_shared handler error: ${(err as Error).message}\n`)
172
+ }
173
+ }
174
+
175
+ export async function handleChatSharedMessage(
176
+ ctx: Filter<Context, 'message:chat_shared'>,
177
+ deps: MediaEnvelopeDeps,
178
+ ): Promise<void> {
179
+ try {
180
+ const c = ctx.message.chat_shared
181
+ const title = safeName((c as { title?: string }).title) ?? ''
182
+ const titlePart = title ? ` title="${title}"` : ''
183
+ const text = `(chat_shared: request_id=${c.request_id ?? '?'} chat_id=${c.chat_id ?? '?'}${titlePart})`
184
+ deps.log(`telegram gateway: inbound chat_shared from chat=${ctx.chat?.id ?? '?'} shared_chat_id=${c.chat_id ?? '?'}\n`)
185
+ await deps.handleInbound(ctx, text, undefined)
186
+ } catch (err) {
187
+ deps.log(`telegram gateway: chat_shared handler error: ${(err as Error).message}\n`)
188
+ }
189
+ }
190
+
191
+ export async function handleSuccessfulPaymentMessage(
192
+ ctx: Filter<Context, 'message:successful_payment'>,
193
+ deps: MediaEnvelopeDeps,
194
+ ): Promise<void> {
195
+ // Money has changed hands — log loudly with the structured fields a
196
+ // reconciliation script would want. Do NOT forward to the agent; an
197
+ // LLM should not be in the loop for confirming receipts.
198
+ try {
199
+ const p = ctx.message.successful_payment
200
+ deps.log(
201
+ `telegram gateway: inbound successful_payment from chat=${ctx.chat?.id ?? '?'} ` +
202
+ `currency=${p.currency ?? '?'} total_amount=${p.total_amount ?? '?'} ` +
203
+ `payload="${safeName(p.invoice_payload) ?? ''}" ` +
204
+ `provider_charge=${safeName(p.provider_payment_charge_id) ?? '?'} ` +
205
+ `telegram_charge=${safeName(p.telegram_payment_charge_id) ?? '?'}\n`,
206
+ )
207
+ } catch (err) {
208
+ deps.log(`telegram gateway: successful_payment log failed: ${(err as Error).message}\n`)
209
+ }
210
+ await deps.handleAckOnly(ctx, 'successful_payment', { warn: true })
211
+ }
212
+
213
+ export async function handleDiceMessage(
214
+ ctx: Filter<Context, 'message:dice'>,
215
+ deps: MediaEnvelopeDeps,
216
+ ): Promise<void> {
217
+ deps.log(`telegram gateway: inbound dice from chat=${ctx.chat?.id ?? '?'}\n`)
218
+ await deps.handleAckOnly(ctx, 'dice', { emoji: '🎲' })
219
+ }
220
+
221
+ export async function handleGameMessage(
222
+ ctx: Filter<Context, 'message:game'>,
223
+ deps: MediaEnvelopeDeps,
224
+ ): Promise<void> {
225
+ deps.log(`telegram gateway: inbound game from chat=${ctx.chat?.id ?? '?'}\n`)
226
+ await deps.handleAckOnly(ctx, 'game')
227
+ }
228
+
229
+ export async function handleStoryMessage(
230
+ ctx: Filter<Context, 'message:story'>,
231
+ deps: MediaEnvelopeDeps,
232
+ ): Promise<void> {
233
+ deps.log(`telegram gateway: inbound story from chat=${ctx.chat?.id ?? '?'}\n`)
234
+ await deps.handleAckOnly(ctx, 'story')
235
+ }
236
+
237
+ export async function handlePaidMediaMessage(
238
+ ctx: Filter<Context, 'message:paid_media'>,
239
+ deps: MediaEnvelopeDeps,
240
+ ): Promise<void> {
241
+ deps.log(`telegram gateway: inbound paid_media from chat=${ctx.chat?.id ?? '?'}\n`)
242
+ await deps.handleAckOnly(ctx, 'paid_media', { warn: true })
243
+ }
244
+
245
+ export async function handlePassportDataMessage(
246
+ ctx: Filter<Context, 'message:passport_data'>,
247
+ deps: MediaEnvelopeDeps,
248
+ ): Promise<void> {
249
+ // Telegram Passport is a regulated identity-document flow. Forwarding
250
+ // the encrypted credentials to an LLM-driven agent would be reckless;
251
+ // we explicitly refuse to handle it and tell the user so they can
252
+ // route via the proper channel. Logged at SECURITY level so operators
253
+ // can see if a passport_data inbound ever lands here (an outlier worth
254
+ // investigating).
255
+ await deps.handleRefusal(ctx, 'passport_data', PASSPORT_REFUSAL_TEXT)
256
+ }
@@ -67,3 +67,19 @@ export function renderMentalModelProposeCard(
67
67
  // richMessage, bypassing the switchroomReply chokepoint.
68
68
  return hardenCardBreaks(lines.join("\n"));
69
69
  }
70
+
71
+ /**
72
+ * Inline keyboard for the mental-model proposal card (hindsight Phase 5).
73
+ * Callback prefix `mmp:`, handled by handleMentalModelProposeCallback in
74
+ * callback-query-handlers.ts. Extracted verbatim from gateway.ts (#2996 P5).
75
+ */
76
+ export function buildMentalModelProposeKeyboard(stageId: string): { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> } {
77
+ return {
78
+ inline_keyboard: [
79
+ [
80
+ { text: '✅ Approve', callback_data: `mmp:approve:${stageId}` },
81
+ { text: '🚫 Deny', callback_data: `mmp:deny:${stageId}` },
82
+ ],
83
+ ],
84
+ }
85
+ }
@@ -666,6 +666,16 @@ export const SR_MODEL_LABELS: Record<string, string> = {
666
666
  'sr-gemini-flash-lite': 'Gemini 3.1 Flash Lite',
667
667
  'sr-minimax-m3': 'MiniMax M3',
668
668
  'sr-deepseek-v4-flash': 'DeepSeek V4 Flash',
669
+ // xAI Grok / Moonshot Kimi / OpenAI GPT-5.6 (added 2026-07-19, pairs with the
670
+ // live litellm sr-* model_name additions). The `.5`/`.7`/`.6` version dots are
671
+ // valid model-arg chars (MODEL_ARG_RE) and match the sr-* ids in the proxy config.
672
+ 'sr-grok-4.5': 'Grok 4.5',
673
+ 'sr-grok-4.3': 'Grok 4.3',
674
+ 'sr-kimi-k3': 'Kimi K3',
675
+ 'sr-kimi-k2.7-code': 'Kimi K2.7 Code',
676
+ 'sr-gpt-5.6-sol': 'GPT-5.6 Sol',
677
+ 'sr-gpt-5.6-terra': 'GPT-5.6 Terra',
678
+ 'sr-gpt-5.6-luna': 'GPT-5.6 Luna',
669
679
  }
670
680
 
671
681
  /**
@@ -681,6 +691,19 @@ export const SR_MODEL_ALIASES: Record<string, string> = {
681
691
  r1: 'sr-deepseek-r1',
682
692
  glm: 'sr-glm-5',
683
693
  codex: 'sr-codex-5.5',
694
+ // Flagship keyboard buttons for the three providers added 2026-07-19. Each
695
+ // points at that provider's top tier; the cheaper tiers (grok-4.3,
696
+ // kimi-k2.7-code, gpt-5.6-terra/luna) stay typeable via `/model sr-<id>` and
697
+ // keep a friendly label above, matching the "labels are a superset of buttons"
698
+ // design (SR_MODEL_LABELS doc comment).
699
+ grok: 'sr-grok-4.5',
700
+ kimi: 'sr-kimi-k3',
701
+ gpt: 'sr-gpt-5.6-sol',
702
+ // Per-tier buttons for the GPT-5.6 line so each is a one-word shortcut
703
+ // (`gpt` stays the flagship-Sol default). Added 2026-07-19.
704
+ sol: 'sr-gpt-5.6-sol',
705
+ terra: 'sr-gpt-5.6-terra',
706
+ luna: 'sr-gpt-5.6-luna',
684
707
  }
685
708
 
686
709
  /** Expand a short alias (case-insensitive) to its full sr-* id, or return the original. */