@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,26 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ const INDEX = "idx_channel_inbound_events_message_id";
5
+
6
+ /**
7
+ * Index the inbound-event to message link.
8
+ *
9
+ * Three queries filter on `message_id`: the sibling lookups on the
10
+ * redelivery path (`getSiblingStreamedReplyTs`,
11
+ * `isDeduplicatedDeliveryOwnedBySibling`) and the NOT EXISTS prefilter in
12
+ * `findMessageByProviderMessageId` that skips rows the inbound-event
13
+ * index already resolves. Without this index each is a full table scan.
14
+ *
15
+ * Partial, on linked rows only: unlinked events (crash-window orphans,
16
+ * reactions before linking) contribute nothing to it.
17
+ *
18
+ * Idempotent via IF NOT EXISTS.
19
+ */
20
+ export function migrateChannelInboundMessageIdIndex(database: DrizzleDb): void {
21
+ getSqliteFrom(database).exec(
22
+ `CREATE INDEX IF NOT EXISTS ${INDEX}
23
+ ON channel_inbound_events (message_id)
24
+ WHERE message_id IS NOT NULL`,
25
+ );
26
+ }
@@ -0,0 +1,52 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ const TABLE = "channel_outbound_posts";
5
+
6
+ /**
7
+ * Create `channel_outbound_posts`: the provider-id index for messages the
8
+ * assistant itself posted, the outbound counterpart of
9
+ * `channel_inbound_events`' provider-id resolution.
10
+ *
11
+ * Its own table rather than more envelope fields on `messages`, because one
12
+ * stored reply fans out to many provider posts (a reply split at tool
13
+ * boundaries or length limits) and resolution by id must be exact: reactions
14
+ * and deletions name a provider post, and a JSON-array scan over recent rows
15
+ * is a recency heuristic, not a resolution contract. The row's
16
+ * `providerMeta` envelope stays the self-description every reader consumes;
17
+ * this table is the derived index over its id facts, written by the same
18
+ * post-send reconciliation that writes them.
19
+ *
20
+ * Composite primary key: a provider post id is unique within its channel's
21
+ * chat, and the triple is exactly how an inbound reaction or delete
22
+ * addresses it. Secondary indexes on the two FK columns because SQLite does
23
+ * not index FK columns automatically and both parents cascade deletes.
24
+ *
25
+ * No backfill: rows delivered before this table resolve through the
26
+ * transitional envelope-scan fallback in `findMessageByProviderMessageId`
27
+ * until they age out of relevance.
28
+ *
29
+ * Idempotent via IF NOT EXISTS.
30
+ */
31
+ export function migrateCreateChannelOutboundPosts(database: DrizzleDb): void {
32
+ const sqlite = getSqliteFrom(database);
33
+ sqlite.exec(
34
+ `CREATE TABLE IF NOT EXISTS ${TABLE} (
35
+ source_channel TEXT NOT NULL,
36
+ external_chat_id TEXT NOT NULL,
37
+ provider_message_id TEXT NOT NULL,
38
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
39
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
40
+ created_at INTEGER NOT NULL,
41
+ PRIMARY KEY (source_channel, external_chat_id, provider_message_id)
42
+ )`,
43
+ );
44
+ sqlite.exec(
45
+ `CREATE INDEX IF NOT EXISTS idx_channel_outbound_posts_message_id
46
+ ON ${TABLE} (message_id)`,
47
+ );
48
+ sqlite.exec(
49
+ `CREATE INDEX IF NOT EXISTS idx_channel_outbound_posts_conversation_id
50
+ ON ${TABLE} (conversation_id)`,
51
+ );
52
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Migration 375 creates `channel_outbound_posts`, the provider-id resolution
3
+ * index for messages the assistant itself posted: the outbound counterpart
4
+ * of `channel_inbound_events`. Keyed by the (channel, chat, provider id)
5
+ * triple, which is exactly how an inbound reaction or delete addresses a
6
+ * post.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import { drizzle } from "drizzle-orm/bun-sqlite";
13
+
14
+ import { getSqliteFrom } from "../../db-connection.js";
15
+ import * as schema from "../../schema.js";
16
+ import { migrateCreateChannelOutboundPosts } from "../375-create-channel-outbound-posts.js";
17
+
18
+ function createTestDb() {
19
+ const sqlite = new Database(":memory:");
20
+ sqlite.exec("PRAGMA journal_mode=WAL");
21
+ return drizzle(sqlite, { schema });
22
+ }
23
+
24
+ function tableNames(db: ReturnType<typeof createTestDb>): string[] {
25
+ const rows = getSqliteFrom(db)
26
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
27
+ .all() as { name: string }[];
28
+ return rows.map((row) => row.name);
29
+ }
30
+
31
+ function indexNames(db: ReturnType<typeof createTestDb>): string[] {
32
+ const rows = getSqliteFrom(db)
33
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'index'")
34
+ .all() as { name: string }[];
35
+ return rows.map((row) => row.name);
36
+ }
37
+
38
+ describe("migrateCreateChannelOutboundPosts", () => {
39
+ test("creates the table and its FK indexes", () => {
40
+ const db = createTestDb();
41
+
42
+ migrateCreateChannelOutboundPosts(db);
43
+
44
+ expect(tableNames(db)).toContain("channel_outbound_posts");
45
+ // SQLite does not index FK columns automatically, and both parents
46
+ // cascade deletes through them.
47
+ expect(indexNames(db)).toContain("idx_channel_outbound_posts_message_id");
48
+ expect(indexNames(db)).toContain(
49
+ "idx_channel_outbound_posts_conversation_id",
50
+ );
51
+ });
52
+
53
+ test("re-running is a no-op", () => {
54
+ const db = createTestDb();
55
+
56
+ migrateCreateChannelOutboundPosts(db);
57
+ migrateCreateChannelOutboundPosts(db);
58
+
59
+ expect(tableNames(db)).toContain("channel_outbound_posts");
60
+ });
61
+
62
+ test("the triple is the primary key: one row per provider post", () => {
63
+ const db = createTestDb();
64
+ migrateCreateChannelOutboundPosts(db);
65
+ const sqlite = getSqliteFrom(db);
66
+ const insert = () =>
67
+ sqlite
68
+ .prepare(
69
+ `INSERT OR IGNORE INTO channel_outbound_posts
70
+ (source_channel, external_chat_id, provider_message_id,
71
+ message_id, conversation_id, created_at)
72
+ VALUES ('discord', 'chat-1', 'post-1', 'row-1', 'conv-1', 1)`,
73
+ )
74
+ .run();
75
+
76
+ insert();
77
+ insert();
78
+
79
+ const count = sqlite
80
+ .prepare("SELECT COUNT(*) AS n FROM channel_outbound_posts")
81
+ .get() as { n: number };
82
+ expect(count.n).toBe(1);
83
+ });
84
+ });
@@ -2,6 +2,7 @@ import { sql } from "drizzle-orm";
2
2
  import {
3
3
  index,
4
4
  integer,
5
+ primaryKey,
5
6
  real,
6
7
  sqliteTable,
7
8
  text,
@@ -282,6 +283,30 @@ export const conversationCompactionEvents = sqliteTable(
282
283
  ],
283
284
  );
284
285
 
286
+ /**
287
+ * Lifecycle of a channel inbound event's reply delivery. `pending` is the only
288
+ * non-terminal value, so every path that finishes a turn has to move the row
289
+ * off it, including the paths that finish WITHOUT delivering: recovery reads a
290
+ * lingering `pending` as a reply still owed. See `runtime/AGENTS.md`.
291
+ */
292
+ export const CHANNEL_DELIVERY_STATUSES = [
293
+ "pending",
294
+ "delivered",
295
+ "failed",
296
+ "dead_letter",
297
+ ] as const;
298
+ export type ChannelDeliveryStatus = (typeof CHANNEL_DELIVERY_STATUSES)[number];
299
+
300
+ /** Lifecycle of a channel inbound event's agent-turn processing. */
301
+ export const CHANNEL_PROCESSING_STATUSES = [
302
+ "pending",
303
+ "processed",
304
+ "failed",
305
+ "dead_letter",
306
+ ] as const;
307
+ export type ChannelProcessingStatus =
308
+ (typeof CHANNEL_PROCESSING_STATUSES)[number];
309
+
285
310
  export const channelInboundEvents = sqliteTable("channel_inbound_events", {
286
311
  id: text("id").primaryKey(),
287
312
  sourceChannel: text("source_channel").notNull(),
@@ -294,8 +319,14 @@ export const channelInboundEvents = sqliteTable("channel_inbound_events", {
294
319
  messageId: text("message_id").references(() => messages.id, {
295
320
  onDelete: "cascade",
296
321
  }),
297
- deliveryStatus: text("delivery_status").notNull().default("pending"),
298
- processingStatus: text("processing_status").notNull().default("pending"),
322
+ deliveryStatus: text("delivery_status", { enum: CHANNEL_DELIVERY_STATUSES })
323
+ .notNull()
324
+ .default("pending"),
325
+ processingStatus: text("processing_status", {
326
+ enum: CHANNEL_PROCESSING_STATUSES,
327
+ })
328
+ .notNull()
329
+ .default("pending"),
299
330
  processingAttempts: integer("processing_attempts").notNull().default(0),
300
331
  deliveryAttempts: integer("delivery_attempts").notNull().default(0),
301
332
  lastProcessingError: text("last_processing_error"),
@@ -307,3 +338,41 @@ export const channelInboundEvents = sqliteTable("channel_inbound_events", {
307
338
  createdAt: integer("created_at").notNull(),
308
339
  updatedAt: integer("updated_at").notNull(),
309
340
  });
341
+
342
+ /**
343
+ * Provider-id index for messages the assistant itself posted: the outbound
344
+ * counterpart of `channelInboundEvents`' provider-id resolution. One stored
345
+ * reply fans out to many provider posts, and a reaction or deletion names a
346
+ * post, so resolution is keyed by the (channel, chat, provider id) triple.
347
+ * Written by the post-send reconciliation alongside the row's `providerMeta`
348
+ * envelope, which remains the row's self-description; this table is the
349
+ * derived index over its id facts.
350
+ */
351
+ export const channelOutboundPosts = sqliteTable(
352
+ "channel_outbound_posts",
353
+ {
354
+ sourceChannel: text("source_channel").notNull(),
355
+ externalChatId: text("external_chat_id").notNull(),
356
+ providerMessageId: text("provider_message_id").notNull(),
357
+ messageId: text("message_id")
358
+ .notNull()
359
+ .references(() => messages.id, { onDelete: "cascade" }),
360
+ conversationId: text("conversation_id")
361
+ .notNull()
362
+ .references(() => conversations.id, { onDelete: "cascade" }),
363
+ createdAt: integer("created_at").notNull(),
364
+ },
365
+ (table) => [
366
+ primaryKey({
367
+ columns: [
368
+ table.sourceChannel,
369
+ table.externalChatId,
370
+ table.providerMessageId,
371
+ ],
372
+ }),
373
+ index("idx_channel_outbound_posts_message_id").on(table.messageId),
374
+ index("idx_channel_outbound_posts_conversation_id").on(
375
+ table.conversationId,
376
+ ),
377
+ ],
378
+ );
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ diffDrizzleSchemaContract,
5
+ listDrizzleTables,
6
+ schemaContractIsClean,
7
+ } from "./schema-contract.js";
8
+
9
+ describe("listDrizzleTables", () => {
10
+ test("includes conversations.fork_strategy from the source catalog", () => {
11
+ const conversations = listDrizzleTables().find(
12
+ (table) => table.name === "conversations",
13
+ );
14
+ expect(conversations).toBeDefined();
15
+ expect(conversations!.columns).toContain("fork_strategy");
16
+ expect(conversations!.columns).toContain("id");
17
+ });
18
+
19
+ test("de-duplicates aliased table exports", () => {
20
+ const names = listDrizzleTables().map((table) => table.name);
21
+ expect(names.filter((name) => name === "cron_jobs")).toEqual(["cron_jobs"]);
22
+ });
23
+ });
24
+
25
+ describe("diffDrizzleSchemaContract", () => {
26
+ test("reports a missing column on a table that exists", () => {
27
+ const conversations = listDrizzleTables().find(
28
+ (table) => table.name === "conversations",
29
+ );
30
+ expect(conversations).toBeDefined();
31
+ const liveColumns = new Set(
32
+ conversations!.columns.filter((column) => column !== "fork_strategy"),
33
+ );
34
+ const live = new Map<string, Set<string>>(
35
+ listDrizzleTables().map((table) => [
36
+ table.name,
37
+ table.name === "conversations" ? liveColumns : new Set(table.columns),
38
+ ]),
39
+ );
40
+
41
+ const report = diffDrizzleSchemaContract(live);
42
+ expect(schemaContractIsClean(report)).toBe(false);
43
+ expect(report.missingTables).toEqual([]);
44
+ expect(report.missingColumns).toEqual([
45
+ { table: "conversations", column: "fork_strategy" },
46
+ ]);
47
+ });
48
+
49
+ test("ignores extra SQLite columns that Drizzle does not own", () => {
50
+ const live = new Map<string, Set<string>>(
51
+ listDrizzleTables().map((table) => {
52
+ const columns = new Set(table.columns);
53
+ if (table.name === "conversations") {
54
+ columns.add("group_id");
55
+ }
56
+ return [table.name, columns];
57
+ }),
58
+ );
59
+
60
+ const report = diffDrizzleSchemaContract(live);
61
+ expect(schemaContractIsClean(report)).toBe(true);
62
+ expect(report.missingColumns).toEqual([]);
63
+ });
64
+
65
+ test("reports a drizzle table that is absent from every live file", () => {
66
+ const live = new Map<string, Set<string>>(
67
+ listDrizzleTables()
68
+ .filter((table) => table.name !== "conversations")
69
+ .map((table) => [table.name, new Set(table.columns)]),
70
+ );
71
+
72
+ const report = diffDrizzleSchemaContract(live);
73
+ expect(report.missingTables).toEqual(["conversations"]);
74
+ expect(
75
+ report.missingColumns.some((miss) => miss.table === "conversations"),
76
+ ).toBe(false);
77
+ });
78
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Compare the Drizzle table/column catalog in source to a live SQLite schema.
3
+ *
4
+ * Used by `assistant db status` to surface a binary that selects a
5
+ * column the live database never received. Extra SQLite tables or
6
+ * columns are ignored. Those are often raw-SQL fields (`group_id`) that
7
+ * Drizzle does not own.
8
+ *
9
+ * The catalog is the union of every `sqliteTable` exported from
10
+ * `schema/index.ts`. Tables live across the main, logs, memory, and
11
+ * telemetry files; callers merge those files before calling
12
+ * {@link diffDrizzleSchemaContract}.
13
+ */
14
+
15
+ import { getTableColumns, getTableName, is, Table } from "drizzle-orm";
16
+
17
+ import * as schema from "./schema/index.js";
18
+
19
+ export interface SchemaColumnMiss {
20
+ table: string;
21
+ column: string;
22
+ }
23
+
24
+ export interface SchemaContractReport {
25
+ /** Drizzle tables with no matching SQLite table in the live files. */
26
+ missingTables: string[];
27
+ /** Drizzle columns whose table exists in SQLite but the column does not. */
28
+ missingColumns: SchemaColumnMiss[];
29
+ }
30
+
31
+ export interface DrizzleTableColumns {
32
+ name: string;
33
+ columns: string[];
34
+ }
35
+
36
+ /** Every Drizzle table name and its SQL column names, de-duplicated. */
37
+ export function listDrizzleTables(): DrizzleTableColumns[] {
38
+ const byName = new Map<string, string[]>();
39
+ for (const value of Object.values(schema)) {
40
+ if (!is(value, Table)) {
41
+ continue;
42
+ }
43
+ const name = getTableName(value);
44
+ if (!name) {
45
+ continue;
46
+ }
47
+ const columns = Object.values(getTableColumns(value)).map(
48
+ (column) => column.name,
49
+ );
50
+ byName.set(name, columns);
51
+ }
52
+ return [...byName.entries()]
53
+ .map(([name, columns]) => ({ name, columns: [...columns].sort() }))
54
+ .sort((a, b) => a.name.localeCompare(b.name));
55
+ }
56
+
57
+ /**
58
+ * Diff Drizzle's catalog against a merged live schema (table name → columns).
59
+ * Tables and columns present only in SQLite are not reported.
60
+ */
61
+ export function diffDrizzleSchemaContract(
62
+ liveColumnsByTable: Map<string, ReadonlySet<string>>,
63
+ ): SchemaContractReport {
64
+ const missingTables: string[] = [];
65
+ const missingColumns: SchemaColumnMiss[] = [];
66
+
67
+ for (const table of listDrizzleTables()) {
68
+ const live = liveColumnsByTable.get(table.name);
69
+ if (!live) {
70
+ missingTables.push(table.name);
71
+ continue;
72
+ }
73
+ for (const column of table.columns) {
74
+ if (!live.has(column)) {
75
+ missingColumns.push({ table: table.name, column });
76
+ }
77
+ }
78
+ }
79
+
80
+ missingTables.sort();
81
+ missingColumns.sort((a, b) => {
82
+ const tableCmp = a.table.localeCompare(b.table);
83
+ if (tableCmp !== 0) {
84
+ return tableCmp;
85
+ }
86
+ return a.column.localeCompare(b.column);
87
+ });
88
+
89
+ return { missingTables, missingColumns };
90
+ }
91
+
92
+ export function schemaContractIsClean(report: SchemaContractReport): boolean {
93
+ return (
94
+ report.missingTables.length === 0 && report.missingColumns.length === 0
95
+ );
96
+ }
@@ -481,6 +481,8 @@ import { migrateAcpSessionHistoryAuthErrorCode } from "./migrations/370-acp-sess
481
481
  import { migrateAcpSessionHistoryAuthErrorCredential } from "./migrations/371-acp-session-history-auth-error-credential.js";
482
482
  import { migrateCreateAcpRefusedCredentials } from "./migrations/372-create-acp-refused-credentials.js";
483
483
  import { migrateAcpAuthMarkerIndex } from "./migrations/373-acp-auth-marker-index.js";
484
+ import { migrateChannelInboundMessageIdIndex } from "./migrations/374-channel-inbound-message-id-index.js";
485
+ import { migrateCreateChannelOutboundPosts } from "./migrations/375-create-channel-outbound-posts.js";
484
486
  import type { MigrationStep } from "./migrations/run-migrations.js";
485
487
 
486
488
  export const migrationSteps: MigrationStep[] = [
@@ -1596,4 +1598,6 @@ export const migrationSteps: MigrationStep[] = [
1596
1598
  migrateAcpSessionHistoryAuthErrorCredential,
1597
1599
  migrateCreateAcpRefusedCredentials,
1598
1600
  migrateAcpAuthMarkerIndex,
1601
+ migrateChannelInboundMessageIdIndex,
1602
+ migrateCreateChannelOutboundPosts,
1599
1603
  ];
@@ -5,6 +5,8 @@
5
5
  * authenticated fetch for all platform API calls.
6
6
  */
7
7
 
8
+ import type { PlatformCredentialVerificationStatus } from "@vellumai/service-contracts/platform-credential";
9
+
8
10
  import { getPlatformAssistantId } from "../config/env.js";
9
11
  import { resolveManagedProxyContext } from "../providers/platform-proxy/context.js";
10
12
  import { credentialKey } from "../security/credential-key.js";
@@ -106,6 +108,9 @@ async function resolvePlatformClientConfig(): Promise<PlatformClientConfig | nul
106
108
  // credential backend must answer from cache instead of stalling the banner.
107
109
  const CONFIGURED_PROBE_DEADLINE_MS = 500;
108
110
 
111
+ /** Bound for the credential check; the heartbeat waits on it. */
112
+ const CREDENTIAL_VERIFY_TIMEOUT_MS = 5_000;
113
+
109
114
  let lastKnownConfigured: boolean | null = null;
110
115
  // Single-flight slot with rotation: concurrent probes share one resolution,
111
116
  // while a flight some caller already gave up on is replaced so a hung
@@ -308,6 +313,56 @@ export class VellumPlatformClient {
308
313
  }
309
314
  }
310
315
 
316
+ /**
317
+ * Ask the platform whether this client's assistant API key still
318
+ * authenticates.
319
+ *
320
+ * `"rejected"` is a settled negative: the platform answered, and the answer
321
+ * was that the credential is not accepted (401/403, which is how
322
+ * `AssistantAPIKeyAuthentication` reports a revoked, expired, or unknown
323
+ * key). Only that verdict may drive a rotation, because rotating on a
324
+ * network blip would replace a working credential for no reason.
325
+ * `"unknown"` covers every unsettled case (no assistant id, transport
326
+ * failure, server error) and means "ask again later", never "broken".
327
+ *
328
+ * The carrier is the owner-consent endpoint. Nothing here reads consent:
329
+ * the request is a side-effect-free authenticated GET this client already
330
+ * makes, and the only thing inspected is whether authentication succeeded.
331
+ * The platform exposes no dedicated credential-verification endpoint, so
332
+ * the carrier is named here rather than left implicit at call sites.
333
+ */
334
+ async verifyCredential(): Promise<PlatformCredentialVerificationStatus> {
335
+ if (!this.assistantId) {
336
+ return "unknown";
337
+ }
338
+
339
+ try {
340
+ const res = await this.fetch(
341
+ `/v1/assistants/${this.assistantId}/owner-consent/`,
342
+ // The heartbeat awaits this check, so an unanswered request must
343
+ // settle as unknown rather than hold the credential pass open.
344
+ { signal: AbortSignal.timeout(CREDENTIAL_VERIFY_TIMEOUT_MS) },
345
+ );
346
+ if (res.ok) {
347
+ return "valid";
348
+ }
349
+ if (res.status === 401 || res.status === 403) {
350
+ return "rejected";
351
+ }
352
+ log.debug(
353
+ { status: res.status },
354
+ "credential verification returned an unsettled status",
355
+ );
356
+ return "unknown";
357
+ } catch (err) {
358
+ log.debug(
359
+ { err },
360
+ "credential verification could not reach the platform",
361
+ );
362
+ return "unknown";
363
+ }
364
+ }
365
+
311
366
  get baseUrl(): string {
312
367
  return this.platformBaseUrl;
313
368
  }
@@ -137,6 +137,52 @@ describe("readPluginMcpServers", () => {
137
137
  });
138
138
  });
139
139
 
140
+ test("defaults an omitted type to streamable-http", () => {
141
+ const dir = makePluginsDir({
142
+ openseo: {
143
+ mcpJson: JSON.stringify({
144
+ mcpServers: {
145
+ openseo: { url: "https://app.openseo.so/mcp" },
146
+ },
147
+ }),
148
+ },
149
+ });
150
+ const { servers, issues } = readPluginMcpServers({
151
+ workspacePluginsDir: dir,
152
+ });
153
+
154
+ expect(issues).toEqual([]);
155
+ expect(servers).toHaveLength(1);
156
+ expect(servers[0].config.transport).toEqual({
157
+ type: "streamable-http",
158
+ url: "https://app.openseo.so/mcp",
159
+ });
160
+ });
161
+
162
+ test("treats the Claude-style http alias as streamable-http", () => {
163
+ const dir = makePluginsDir({
164
+ openseo: {
165
+ mcpJson: JSON.stringify({
166
+ mcpServers: {
167
+ openseo: {
168
+ type: "http",
169
+ url: "https://app.openseo.so/mcp",
170
+ },
171
+ },
172
+ }),
173
+ },
174
+ });
175
+ const { servers, issues } = readPluginMcpServers({
176
+ workspacePluginsDir: dir,
177
+ });
178
+
179
+ expect(issues).toEqual([]);
180
+ expect(servers[0].config.transport).toEqual({
181
+ type: "streamable-http",
182
+ url: "https://app.openseo.so/mcp",
183
+ });
184
+ });
185
+
140
186
  test("defaults plugin servers to low risk", () => {
141
187
  // mcp.json has no risk field, so a host default applies. It is `low`
142
188
  // because the review happens at install time — curated marketplace,
@@ -34,7 +34,8 @@
34
34
  *
35
35
  * Gaps between slots leave room for future injectors to slot in at granular
36
36
  * positions (e.g. `25` between unified-turn-context and pkb) without
37
- * renumbering. The memory-v3 injectors sit far above this range (1000 / 1001).
37
+ * renumbering. The memory-v3 injectors sit far above this range (1000 cards
38
+ * / 1001 outbound spotlight).
38
39
  */
39
40
  export const DEFAULT_INJECTOR_ORDER = {
40
41
  diskPressureWarning: 5,
@@ -44,8 +44,8 @@ const SIGHT_FRAME_CONTENT = JSON.stringify([
44
44
  ]);
45
45
 
46
46
  /**
47
- * What `persistLiveVoiceSightFrame` writes: the gate sampled this, nobody
48
- * spoke it, so `scripted` rides along with the tag.
47
+ * What `persistAmbientSightFrame` writes on the voice surface: the gate
48
+ * sampled this, nobody spoke it, so `scripted` rides along with the tag.
49
49
  */
50
50
  function sightFrameMetadata(attachmentId: string): string {
51
51
  return JSON.stringify({
@@ -4,7 +4,7 @@ import {
4
4
  searchMessageIdsLexical,
5
5
  } from "@vellumai/plugin-api";
6
6
 
7
- import { readSlackMetadata } from "../../../../../messaging/providers/slack/message-metadata.js";
7
+ import { readSlackMetadataFromMessageMetadata } from "../../../../../messaging/providers/slack/message-metadata.js";
8
8
  import { AUTO_ANALYSIS_SOURCE } from "../../../../../persistence/auto-analysis-constants.js";
9
9
  import { isLexicalBackfillComplete } from "../../../../../persistence/checkpoints.js";
10
10
  import { rawAll } from "../../../../../persistence/raw-query.js";
@@ -296,25 +296,23 @@ function parseSlackRecallMetadata(rawMetadata: string | null): {
296
296
  return null;
297
297
  }
298
298
 
299
+ const slackMeta = readSlackMetadataFromMessageMetadata(rawMetadata);
300
+ if (!slackMeta) {
301
+ return null;
302
+ }
303
+ // The trust class sits beside the envelope, on the row's top-level
304
+ // metadata. Parsed here rather than through the host's JSON helper, which
305
+ // is outside the plugin's import boundary.
299
306
  let parsed: unknown;
300
307
  try {
301
308
  parsed = JSON.parse(rawMetadata);
302
309
  } catch {
303
- return null;
304
- }
305
-
306
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
307
- return null;
308
- }
309
-
310
- const metadata = parsed as Record<string, unknown>;
311
- if (typeof metadata.slackMeta !== "string") {
312
- return null;
313
- }
314
- const slackMeta = readSlackMetadata(metadata.slackMeta);
315
- if (!slackMeta) {
316
- return null;
310
+ parsed = null;
317
311
  }
312
+ const metadata =
313
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
314
+ ? (parsed as Record<string, unknown>)
315
+ : {};
318
316
 
319
317
  return {
320
318
  ...(slackMeta.displayName ? { displayName: slackMeta.displayName } : {}),