@vellumai/assistant 0.11.8 → 0.11.9-staging.2

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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -0,0 +1,198 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+
5
+ import type { WorkspaceMigration } from "./types.js";
6
+
7
+ /**
8
+ * Repair the retired Fireworks MiniMax M2.7 model ID in workspace LLM
9
+ * config.
10
+ *
11
+ * Fireworks has no serverless deployment of
12
+ * `accounts/fireworks/models/minimax-m2p7`: the model page claims
13
+ * serverless support, but chat/completions calls fail with a 404 "Model
14
+ * not found, inaccessible, and/or not deployed". Existing configs can pin
15
+ * the ID in `llm.default`, `llm.callSites.*`, and `llm.profiles.*`.
16
+ *
17
+ * Repair those leaves only on an exact stale match, replacing with
18
+ * `accounts/fireworks/models/minimax-m3`: the only MiniMax model Fireworks
19
+ * serves serverless, at the same per-token pricing. Because m3 is also the
20
+ * balanced-intent template's model, a repaired hand-edited `custom-*`
21
+ * profile can read as unedited to `ensureByokDefaultProfiles` and collapse
22
+ * into the balanced default. That is acceptable: the collapsed copy routes
23
+ * to the same model this migration writes.
24
+ *
25
+ * Provider guard: the stale ID belongs to the `fireworks` provider and also
26
+ * appears in managed profiles stamped `provider: "vellum"` (which route
27
+ * Fireworks-account model IDs through the managed proxy). Under the entries
28
+ * model (migration 145) `provider` can also hold a `provider_connections`
29
+ * entry name whose row kind drives dispatch. A fragment is repaired when
30
+ * its `provider` is `"fireworks"`, `"vellum"`, absent, or an entry name
31
+ * whose row kind is one of those. Any other provider is left untouched: an
32
+ * `openai-compatible` endpoint may legitimately serve a model by the stale
33
+ * name.
34
+ */
35
+ export const repairRetiredFireworksMinimaxM2p7ModelIdMigration: WorkspaceMigration =
36
+ {
37
+ id: "152-repair-retired-fireworks-minimax-m2p7-model-id",
38
+ description:
39
+ "Repair retired Fireworks accounts/fireworks/models/minimax-m2p7 model ID in workspace LLM config",
40
+ run(workspaceDir: string): void {
41
+ const configPath = join(workspaceDir, "config.json");
42
+ if (!existsSync(configPath)) {
43
+ return;
44
+ }
45
+
46
+ // Read outside the parse catch: a transient filesystem error (EIO,
47
+ // EACCES) must reach the runner so the migration retries, while
48
+ // malformed JSON is a permanent state this migration cannot repair.
49
+ const rawText = readFileSync(configPath, "utf-8");
50
+
51
+ let config: Record<string, unknown>;
52
+ try {
53
+ const raw = JSON.parse(rawText);
54
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
55
+ return;
56
+ }
57
+ config = raw as Record<string, unknown>;
58
+ } catch {
59
+ return;
60
+ }
61
+
62
+ const llm = readObject(config.llm);
63
+ if (llm === null) {
64
+ return;
65
+ }
66
+
67
+ // Entry rows load lazily: only a stale fragment whose provider is
68
+ // neither a repairable vendor nor absent needs them. An unreadable DB
69
+ // then fails the run (retried next boot) rather than checkpointing a
70
+ // pass that skips entry-bound profiles.
71
+ let rows: Map<string, string> | null | undefined;
72
+ const isRepairableProvider = (provider: unknown): boolean => {
73
+ if (provider === undefined) {
74
+ return true;
75
+ }
76
+ if (typeof provider !== "string") {
77
+ return false;
78
+ }
79
+ if (REPAIRABLE_PROVIDERS.has(provider)) {
80
+ return true;
81
+ }
82
+ if (rows === undefined) {
83
+ rows = readConnectionRows(workspaceDir);
84
+ }
85
+ if (rows === null) {
86
+ throw new Error(
87
+ "provider_connections is not readable; retrying the model-ID repair on the next run",
88
+ );
89
+ }
90
+ const kind = rows.get(provider);
91
+ return kind !== undefined && REPAIRABLE_PROVIDERS.has(kind);
92
+ };
93
+
94
+ let changed = false;
95
+
96
+ changed =
97
+ repairFragment(readObject(llm.default), isRepairableProvider) ||
98
+ changed;
99
+
100
+ const callSites = readObject(llm.callSites);
101
+ if (callSites !== null) {
102
+ for (const rawConfig of Object.values(callSites)) {
103
+ changed =
104
+ repairFragment(readObject(rawConfig), isRepairableProvider) ||
105
+ changed;
106
+ }
107
+ }
108
+
109
+ const profiles = readObject(llm.profiles);
110
+ if (profiles !== null) {
111
+ for (const rawProfile of Object.values(profiles)) {
112
+ changed =
113
+ repairFragment(readObject(rawProfile), isRepairableProvider) ||
114
+ changed;
115
+ }
116
+ }
117
+
118
+ if (!changed) {
119
+ return;
120
+ }
121
+
122
+ // Write-then-rename so an interrupted write cannot leave config.json
123
+ // truncated: a torn in-place write would parse as invalid JSON on the
124
+ // retry, which the catch above treats as "nothing to do", letting the
125
+ // runner checkpoint the migration as completed against a corrupt file.
126
+ const tmpPath = `${configPath}.migration-152.tmp`;
127
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
128
+ renameSync(tmpPath, configPath);
129
+ },
130
+ // The exact-match rewrite is idempotent, so a transient failure (full
131
+ // disk, I/O error) is safe to retry on later startups.
132
+ retryFailedCheckpoint: true,
133
+ down(_workspaceDir: string): void {
134
+ // Forward-only: reintroducing the retired model ID would break
135
+ // Fireworks calls.
136
+ },
137
+ };
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Helpers: self-contained per workspace migrations AGENTS.md
141
+ // ---------------------------------------------------------------------------
142
+
143
+ const STALE_MODEL_ID = "accounts/fireworks/models/minimax-m2p7";
144
+ const REPLACEMENT_MODEL_ID = "accounts/fireworks/models/minimax-m3";
145
+ const REPAIRABLE_PROVIDERS = new Set(["fireworks", "vellum"]);
146
+
147
+ function repairFragment(
148
+ fragment: Record<string, unknown> | null,
149
+ isRepairableProvider: (provider: unknown) => boolean,
150
+ ): boolean {
151
+ if (fragment === null) {
152
+ return false;
153
+ }
154
+ if (fragment.model !== STALE_MODEL_ID) {
155
+ return false;
156
+ }
157
+ if (!isRepairableProvider(fragment.provider)) {
158
+ return false;
159
+ }
160
+ fragment.model = REPLACEMENT_MODEL_ID;
161
+ return true;
162
+ }
163
+
164
+ /**
165
+ * Connection name -> provider kind, or null when the DB or table is not
166
+ * readable. The caller fails the run on null: entry-name providers must be
167
+ * judged against real rows, never guessed. An absent DB file is a real
168
+ * state (no rows, so every entry name is dangling and stays untouched).
169
+ */
170
+ function readConnectionRows(workspaceDir: string): Map<string, string> | null {
171
+ const dbPath = join(workspaceDir, "data", "db", "assistant.db");
172
+ if (!existsSync(dbPath)) {
173
+ return new Map();
174
+ }
175
+ let db: Database;
176
+ try {
177
+ db = new Database(dbPath);
178
+ } catch {
179
+ return null;
180
+ }
181
+ try {
182
+ const rows = db
183
+ .query(`SELECT name, provider FROM provider_connections`)
184
+ .all() as Array<{ name: string; provider: string }>;
185
+ return new Map(rows.map((r) => [r.name, r.provider]));
186
+ } catch {
187
+ return null;
188
+ } finally {
189
+ db.close();
190
+ }
191
+ }
192
+
193
+ function readObject(value: unknown): Record<string, unknown> | null {
194
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
195
+ return null;
196
+ }
197
+ return value as Record<string, unknown>;
198
+ }
@@ -4,7 +4,6 @@ import { join } from "node:path";
4
4
  import { describe, expect, test } from "bun:test";
5
5
 
6
6
  import { sttFluxProviderToModelFamilyMigration } from "../150-stt-flux-provider-to-model-family.js";
7
- import { WORKSPACE_MIGRATIONS } from "../registry.js";
8
7
 
9
8
  function workspaceWith(config: unknown): string {
10
9
  const dir = mkdtempSync(join(tmpdir(), "stt-flux-migration-"));
@@ -17,15 +16,6 @@ function readConfig(dir: string): any {
17
16
  }
18
17
 
19
18
  describe("150-stt-flux-provider-to-model-family", () => {
20
- test("is registered as the newest migration", () => {
21
- expect(sttFluxProviderToModelFamilyMigration.id).toBe(
22
- "150-stt-flux-provider-to-model-family",
23
- );
24
- expect(WORKSPACE_MIGRATIONS.at(-1)?.id).toBe(
25
- "150-stt-flux-provider-to-model-family",
26
- );
27
- });
28
-
29
19
  test("rewrites a Flux provider id to its parent plus a model family", () => {
30
20
  const dir = workspaceWith({
31
21
  services: { stt: { provider: "deepgram-flux", providers: {} } },
@@ -148,6 +148,8 @@ import { renameCollidingBackupProfileNamesMigration } from "./147-rename-collidi
148
148
  import { stripUnsupportedFallbackProfilesMigration } from "./148-strip-unsupported-fallback-profiles.js";
149
149
  import { repointBackupProfileSelectionsMigration } from "./149-repoint-backup-profile-selections.js";
150
150
  import { sttFluxProviderToModelFamilyMigration } from "./150-stt-flux-provider-to-model-family.js";
151
+ import { repairRenamedFireworksDeepseekProModelIdMigration } from "./151-repair-renamed-fireworks-deepseek-pro-model-id.js";
152
+ import { repairRetiredFireworksMinimaxM2p7ModelIdMigration } from "./152-repair-retired-fireworks-minimax-m2p7-model-id.js";
151
153
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
152
154
  import type { WorkspaceMigration } from "./types.js";
153
155
 
@@ -311,4 +313,6 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
311
313
  stripUnsupportedFallbackProfilesMigration,
312
314
  repointBackupProfileSelectionsMigration,
313
315
  sttFluxProviderToModelFamilyMigration,
316
+ repairRenamedFireworksDeepseekProModelIdMigration,
317
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration,
314
318
  ];
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env sh
2
- # Runs pip inside the persistent apt chroot. Must be invoked under `unshare -m`
3
- # (see docker-kata-pip.sh): the bind mounts below live in a private mount
4
- # namespace so they disappear with the pip process and are never visible to
5
- # other processes — a lingering /data bind inside the chroot would create a
6
- # path cycle (/data/system/data/system/...) for anything walking /data.
7
- set -eu
8
-
9
- DATA_ROOT="$1"
10
- CALLER_CWD="$2"
11
- PIP_BIN="$3"
12
- shift 3
13
-
14
- for dir in /workspace /data /tmp /var/tmp; do
15
- [ -d "${dir}" ] || continue
16
- mkdir -p "${DATA_ROOT}${dir}"
17
- mount --bind "${dir}" "${DATA_ROOT}${dir}" 2>/dev/null || true
18
- done
19
-
20
- # chroot(1) always chdirs to /; restore the caller's cwd when it exists inside
21
- # the chroot so relative paths keep working.
22
- exec chroot "${DATA_ROOT}" /bin/sh -c 'cd "$1" 2>/dev/null || cd /; shift; exec "$@"' sh "${CALLER_CWD}" "${PIP_BIN}" "$@"
@@ -1,97 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { reactionDecisionForEmoji } from "../runtime/routes/channel-route-shared.js";
4
-
5
- // =============================================================================
6
- // reactionDecisionForEmoji
7
- // =============================================================================
8
-
9
- describe("reactionDecisionForEmoji", () => {
10
- test("maps +1 emoji to approve_once", () => {
11
- const result = reactionDecisionForEmoji("+1");
12
- expect(result).toEqual({
13
- action: "approve_once",
14
- source: "reaction",
15
- });
16
- });
17
-
18
- test("maps thumbsup emoji to approve_once", () => {
19
- const result = reactionDecisionForEmoji("thumbsup");
20
- expect(result).toEqual({
21
- action: "approve_once",
22
- source: "reaction",
23
- });
24
- });
25
-
26
- test("maps -1 emoji to reject", () => {
27
- const result = reactionDecisionForEmoji("-1");
28
- expect(result).toEqual({
29
- action: "reject",
30
- source: "reaction",
31
- });
32
- });
33
-
34
- test("maps thumbsdown emoji to reject", () => {
35
- const result = reactionDecisionForEmoji("thumbsdown");
36
- expect(result).toEqual({
37
- action: "reject",
38
- source: "reaction",
39
- });
40
- });
41
-
42
- test("alarm_clock emoji maps to approve_once (legacy compat)", () => {
43
- const result = reactionDecisionForEmoji("alarm_clock");
44
- expect(result).toEqual({
45
- action: "approve_once",
46
- source: "reaction",
47
- });
48
- });
49
-
50
- test("white_check_mark emoji maps to approve_once (legacy compat)", () => {
51
- const result = reactionDecisionForEmoji("white_check_mark");
52
- expect(result).toEqual({
53
- action: "approve_once",
54
- source: "reaction",
55
- });
56
- });
57
-
58
- test("unicode vocabulary: thumbs up, check mark, and alarm clock approve", () => {
59
- for (const emoji of ["\u{1F44D}", "\u2705", "\u23F0"]) {
60
- expect(reactionDecisionForEmoji(emoji)).toEqual({
61
- action: "approve_once",
62
- source: "reaction",
63
- });
64
- }
65
- });
66
-
67
- test("unicode vocabulary: thumbs down rejects", () => {
68
- expect(reactionDecisionForEmoji("\u{1F44E}")).toEqual({
69
- action: "reject",
70
- source: "reaction",
71
- });
72
- });
73
-
74
- test("a Discord custom emoji in mention form never resolves a decision", () => {
75
- // A guild custom emoji can be named after a Slack decision word; the
76
- // gateway forwards it as its mention form, which must stay unmapped.
77
- expect(
78
- reactionDecisionForEmoji("<:white_check_mark:999888777>"),
79
- ).toBeNull();
80
- expect(reactionDecisionForEmoji("<:thumbsup:999888777>")).toBeNull();
81
- });
82
-
83
- test("returns null for unknown emoji", () => {
84
- const result = reactionDecisionForEmoji("tada");
85
- expect(result).toBeNull();
86
- });
87
-
88
- test("returns null for an unmapped emoji", () => {
89
- const result = reactionDecisionForEmoji("eyes");
90
- expect(result).toBeNull();
91
- });
92
-
93
- test("returns null for an empty emoji name", () => {
94
- const result = reactionDecisionForEmoji("");
95
- expect(result).toBeNull();
96
- });
97
- });
@@ -1,307 +0,0 @@
1
- /**
2
- * Tests for guardian approval-by-reaction on the guardian decision pipeline.
3
- *
4
- * A Slack emoji reaction (✅ / ❌) on a delivered approval card is routed
5
- * through `routeGuardianReply` exactly like a button press or text reply. The
6
- * emoji maps to an action and the reacted card's message id (`reactedMessageTs`)
7
- * resolves the target request via its canonical delivery record — so the
8
- * decision is applied precisely even when several cards are pending in the same
9
- * chat, with no clarification prompt.
10
- */
11
-
12
- import { beforeEach, describe, expect, mock, test } from "bun:test";
13
-
14
- const _conversationMocks = new Map<string, unknown>();
15
- mock.module("../daemon/conversation-registry.js", () => ({
16
- findConversation: (id: string) => _conversationMocks.get(id),
17
- }));
18
-
19
- import { createGuardianGatewaySim } from "./guardian-gateway-sim.js";
20
-
21
- const sim = createGuardianGatewaySim();
22
- mock.module("../channels/gateway-guardian-requests.js", () => sim.module);
23
-
24
- import type { Conversation } from "../daemon/conversation.js";
25
- import { initializeDb } from "../persistence/db-init.js";
26
- import {
27
- inferDecisionActionFromFreeText,
28
- routeGuardianReply,
29
- } from "../runtime/guardian-reply-router.js";
30
- import * as pendingInteractions from "../runtime/pending-interactions.js";
31
-
32
- await initializeDb();
33
-
34
- // ---------------------------------------------------------------------------
35
- // Constants & helpers
36
- // ---------------------------------------------------------------------------
37
-
38
- const GUARDIAN_USER = "U_GUARDIAN";
39
- const PRINCIPAL = "principal-1";
40
- const CHAT_ID = "D_GUARDIAN_DM";
41
- const TOOL_NAME = "execute_shell";
42
-
43
- function resetTables(): void {
44
- sim.reset();
45
- pendingInteractions.clear();
46
- _conversationMocks.clear();
47
- }
48
-
49
- /**
50
- * Seed a pending tool-approval (gateway request + delivery + pending
51
- * interaction with a mock conversation) and return the mocked
52
- * `handleConfirmationResponse` so callers can assert it was driven.
53
- */
54
- function seedApproval(opts: {
55
- requestId: string;
56
- ts: string;
57
- chatId?: string;
58
- principal?: string;
59
- }): ReturnType<typeof mock> {
60
- const chatId = opts.chatId ?? CHAT_ID;
61
- const principal = opts.principal ?? PRINCIPAL;
62
- const conversationId = `conv-${opts.requestId}`;
63
-
64
- sim.seedRequest({
65
- id: opts.requestId,
66
- kind: "tool_approval",
67
- sourceChannel: "slack",
68
- sourceConversationId: conversationId,
69
- requesterExternalUserId: "requester-1",
70
- requesterChatId: chatId,
71
- guardianExternalUserId: GUARDIAN_USER,
72
- guardianPrincipalId: principal,
73
- toolName: TOOL_NAME,
74
- status: "pending",
75
- expiresAt: Date.now() + 300_000,
76
- });
77
-
78
- sim.seedDelivery({
79
- requestId: opts.requestId,
80
- destinationChannel: "slack",
81
- destinationChatId: chatId,
82
- destinationMessageId: opts.ts,
83
- });
84
-
85
- const handleConfirmationResponse = mock(() => {});
86
- _conversationMocks.set(conversationId, {
87
- handleConfirmationResponse,
88
- ensureActorScopedHistory: async () => {},
89
- } as unknown as Conversation);
90
- pendingInteractions.register(opts.requestId, {
91
- conversationId,
92
- kind: "confirmation",
93
- confirmationDetails: {
94
- toolName: TOOL_NAME,
95
- input: { command: "echo hi" },
96
- riskLevel: "high",
97
- allowlistOptions: [{ label: "t", description: "t", pattern: "t" }],
98
- scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
99
- },
100
- });
101
- return handleConfirmationResponse;
102
- }
103
-
104
- function reaction(opts: {
105
- emoji: string;
106
- reactedMessageTs?: string;
107
- chatId?: string;
108
- principal?: string;
109
- }): Parameters<typeof routeGuardianReply>[0] {
110
- return {
111
- messageText: "",
112
- channel: "slack",
113
- actor: {
114
- actorPrincipalId: opts.principal ?? PRINCIPAL,
115
- actorExternalUserId: GUARDIAN_USER,
116
- channel: "slack",
117
- guardianPrincipalId: opts.principal ?? PRINCIPAL,
118
- },
119
- conversationId: "guardian-conv",
120
- reaction: {
121
- op: "added" as const,
122
- emoji: opts.emoji,
123
- targetMessageId: opts.reactedMessageTs ?? "",
124
- },
125
- reactedMessageTs: opts.reactedMessageTs,
126
- channelDeliveryContext: {
127
- replyCallbackUrl: "https://gateway.test/deliver",
128
- guardianChatId: opts.chatId ?? CHAT_ID,
129
- assistantId: "self",
130
- },
131
- };
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // Reaction routing
136
- // ---------------------------------------------------------------------------
137
-
138
- describe("routeGuardianReply / reactions", () => {
139
- beforeEach(resetTables);
140
-
141
- test("✅ on a delivered card approves the matching request", async () => {
142
- const hcr = seedApproval({ requestId: "req-1", ts: "111.1" });
143
-
144
- const result = await routeGuardianReply(
145
- reaction({ emoji: "white_check_mark", reactedMessageTs: "111.1" }),
146
- );
147
-
148
- expect(result.consumed).toBe(true);
149
- expect(result.decisionApplied).toBe(true);
150
- expect(result.type).toBe("canonical_decision_applied");
151
- expect(sim.getRequest("req-1")?.status).toBe("approved");
152
- expect(hcr).toHaveBeenCalledTimes(1);
153
- expect(hcr.mock.calls[0]?.[0]).toBe("req-1");
154
- expect(hcr.mock.calls[0]?.[1]).toBe("allow");
155
- });
156
-
157
- test("❌ on a delivered card rejects the matching request", async () => {
158
- const hcr = seedApproval({ requestId: "req-1", ts: "111.1" });
159
-
160
- const result = await routeGuardianReply(
161
- reaction({ emoji: "-1", reactedMessageTs: "111.1" }),
162
- );
163
-
164
- expect(result.decisionApplied).toBe(true);
165
- expect(sim.getRequest("req-1")?.status).toBe("denied");
166
- expect(hcr.mock.calls[0]?.[1]).toBe("deny");
167
- });
168
-
169
- test("a person-addressed delivery resolves through the reactor's own id", async () => {
170
- // Person-addressed adapters (Discord) record the guardian's user id as
171
- // the destination, while the reaction arrives addressed by its chat, so
172
- // the chat probe misses and the actor-id probe must find the card.
173
- const hcr = seedApproval({
174
- requestId: "req-person",
175
- ts: "300.3",
176
- chatId: GUARDIAN_USER,
177
- });
178
-
179
- const result = await routeGuardianReply(
180
- reaction({
181
- emoji: "white_check_mark",
182
- reactedMessageTs: "300.3",
183
- chatId: "DM_CHANNEL_9",
184
- }),
185
- );
186
-
187
- expect(result.consumed).toBe(true);
188
- expect(result.decisionApplied).toBe(true);
189
- expect(sim.getRequest("req-person")?.status).toBe("approved");
190
- expect(hcr).toHaveBeenCalledTimes(1);
191
- });
192
-
193
- test("reacting on a specific card resolves only that card (disambiguates N>1)", async () => {
194
- const hcrA = seedApproval({ requestId: "req-A", ts: "100.1" });
195
- const hcrB = seedApproval({ requestId: "req-B", ts: "200.2" });
196
-
197
- // React on card B's message — only B should resolve.
198
- const result = await routeGuardianReply(
199
- reaction({ emoji: "white_check_mark", reactedMessageTs: "200.2" }),
200
- );
201
-
202
- expect(result.requestId).toBe("req-B");
203
- expect(sim.getRequest("req-B")?.status).toBe("approved");
204
- expect(sim.getRequest("req-A")?.status).toBe("pending");
205
- expect(hcrB).toHaveBeenCalledTimes(1);
206
- expect(hcrA).not.toHaveBeenCalled();
207
- });
208
-
209
- test("a reaction on an unknown message is not consumed (left for transcript)", async () => {
210
- seedApproval({ requestId: "req-1", ts: "111.1" });
211
-
212
- const result = await routeGuardianReply(
213
- reaction({ emoji: "white_check_mark", reactedMessageTs: "no-such-ts" }),
214
- );
215
-
216
- expect(result.consumed).toBe(false);
217
- expect(result.type).toBe("not_consumed");
218
- expect(sim.getRequest("req-1")?.status).toBe("pending");
219
- });
220
-
221
- test("an unknown emoji is not consumed", async () => {
222
- seedApproval({ requestId: "req-1", ts: "111.1" });
223
-
224
- const result = await routeGuardianReply(
225
- reaction({ emoji: "tada", reactedMessageTs: "111.1" }),
226
- );
227
-
228
- expect(result.consumed).toBe(false);
229
- expect(sim.getRequest("req-1")?.status).toBe("pending");
230
- });
231
-
232
- test("a reaction without a reacted message id is not consumed", async () => {
233
- seedApproval({ requestId: "req-1", ts: "111.1" });
234
-
235
- const result = await routeGuardianReply(
236
- reaction({ emoji: "white_check_mark" }),
237
- );
238
-
239
- expect(result.consumed).toBe(false);
240
- });
241
-
242
- test("a reaction from a different principal is surfaced, not silently dropped", async () => {
243
- const hcr = seedApproval({ requestId: "req-1", ts: "111.1" });
244
-
245
- const result = await routeGuardianReply(
246
- reaction({
247
- emoji: "white_check_mark",
248
- reactedMessageTs: "111.1",
249
- principal: "someone-else",
250
- }),
251
- );
252
-
253
- // Consumed with a user-facing reply, request untouched, side effect not run.
254
- expect(result.consumed).toBe(true);
255
- expect(result.decisionApplied).toBe(false);
256
- expect(result.replyText).toMatch(/permission/i);
257
- expect(sim.getRequest("req-1")?.status).toBe("pending");
258
- expect(hcr).not.toHaveBeenCalled();
259
- });
260
- });
261
-
262
- // ---------------------------------------------------------------------------
263
- // Free-text decision inference (no request code)
264
- // ---------------------------------------------------------------------------
265
-
266
- describe("inferDecisionActionFromFreeText / introduction verbs", () => {
267
- // Regression: a bare "block" on an access request must map to the block
268
- // action (revoke), not the weaker generic reject (leave_unverified).
269
- test('"block" maps to block for access requests, reject elsewhere', () => {
270
- expect(inferDecisionActionFromFreeText("block", "access_request")).toBe(
271
- "block",
272
- );
273
- expect(inferDecisionActionFromFreeText("Block!", "access_request")).toBe(
274
- "block",
275
- );
276
- expect(inferDecisionActionFromFreeText("block")).toBe("reject");
277
- expect(inferDecisionActionFromFreeText("block", "tool_approval")).toBe(
278
- "reject",
279
- );
280
- });
281
-
282
- test("trust and verify verbs are recognized for access requests", () => {
283
- expect(inferDecisionActionFromFreeText("trust", "access_request")).toBe(
284
- "trust",
285
- );
286
- expect(inferDecisionActionFromFreeText("verify", "access_request")).toBe(
287
- "verify_code",
288
- );
289
- // Unknown outside access requests — falls through to null (no cue).
290
- expect(inferDecisionActionFromFreeText("trust")).toBeNull();
291
- });
292
-
293
- test("generic vocabulary keeps its semantics per kind", () => {
294
- expect(inferDecisionActionFromFreeText("no", "access_request")).toBe(
295
- "leave_unverified",
296
- );
297
- expect(inferDecisionActionFromFreeText("reject", "access_request")).toBe(
298
- "leave_unverified",
299
- );
300
- expect(inferDecisionActionFromFreeText("yes", "access_request")).toBe(
301
- "approve_once",
302
- );
303
- expect(inferDecisionActionFromFreeText("no", "tool_approval")).toBe(
304
- "reject",
305
- );
306
- });
307
- });