@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
@@ -12,11 +12,14 @@
12
12
  * without reaching up into the daemon orchestrator.
13
13
  */
14
14
  import {
15
+ isMemorySpotlightText,
15
16
  MEMORY_SPOTLIGHT_PREFIX,
16
17
  MEMORY_SPOTLIGHT_SUFFIX,
17
18
  } from "../plugins/defaults/memory/memory-marker.js";
18
19
  import type { Message } from "../providers/types.js";
19
20
 
21
+ export { isMemorySpotlightText };
22
+
20
23
  /**
21
24
  * A matcher for an injected text block. A plain string matches by prefix
22
25
  * (`startsWith`). A `{ prefix, suffix }` wrapper requires BOTH the opening
@@ -115,69 +118,27 @@ export function stripTailUserTextBlocksByPrefix(
115
118
  }
116
119
 
117
120
  /**
118
- * Full-wrapper matcher for the memory-v3 ephemeral `<memory_spotlight>` block.
119
- * Shared by the per-turn scoped strip ({@link stripSpotlightInjections}) and
120
- * the compaction pipeline below so both recognize exactly the same wrapper.
121
+ * Full-wrapper matcher for the memory-v3 per-turn `<memory_spotlight>` block.
122
+ * Shared by the leftover cleanup ({@link stripSpotlightInjections}), the
123
+ * tail-only assembly strip, and the compaction pipeline so every path
124
+ * recognizes exactly the same wrapper.
121
125
  */
122
- const MEMORY_SPOTLIGHT_MATCHER: InjectionMatcher = {
126
+ export const MEMORY_SPOTLIGHT_MATCHER: InjectionMatcher = {
123
127
  prefix: MEMORY_SPOTLIGHT_PREFIX,
124
128
  suffix: MEMORY_SPOTLIGHT_SUFFIX,
125
129
  };
126
130
 
127
131
  /**
128
- * Remove memory-v3 `<memory_spotlight>` blocks from every user message — and
129
- * ONLY those blocks. The spotlight is ephemeral by contract: runtime assembly
130
- * strip-and-replaces it each turn (the previous turn's spotlight is stale),
131
- * while the frozen `<memory>` card blocks stay byte-identical in history for
132
- * prompt caching. This is deliberately a scoped, single-id strip — the old
133
- * whole-layer `stripAllMemoryInjections` replace is gone.
132
+ * Remove leftover `<memory_spotlight>` blocks from every user message, and
133
+ * only those blocks. Compaction and overflow recovery rewrite history, so
134
+ * they strip every runtime injection including historical spotlights.
135
+ * Frozen `<memory>` card blocks stay byte-identical. Assembly uses the
136
+ * tail-scoped variant so live history keeps each prior turn's spotlight.
134
137
  */
135
138
  export function stripSpotlightInjections(messages: Message[]): Message[] {
136
139
  return stripUserTextBlocksByPrefix(messages, [MEMORY_SPOTLIGHT_MATCHER]);
137
140
  }
138
141
 
139
- /**
140
- * Whether the TURN-STARTING user message carries a memory-v3
141
- * `<memory_spotlight>` block.
142
- *
143
- * The spotlight is the only injected block that is strip-and-replaced from
144
- * every user message each turn, so its presence is exactly what makes that
145
- * message volatile across turns. Every other injected block (turn context,
146
- * workspace, `<info>`, `<memory>` cards, NOW.md) is frozen into history and
147
- * re-renders byte-identically, so a message without a spotlight is a stable
148
- * cache anchor. Providers consume this through the `mutableLatestUserMessage`
149
- * config field when placing cache breakpoints.
150
- *
151
- * The turn start is the most recent user message carrying TEXT content, which
152
- * is the same message the Anthropic client anchors on. Tool-result messages
153
- * are skipped: they are user-role but carry no injected blocks, and letting
154
- * them answer this question would flip the signal to `false` partway through a
155
- * tool loop. The provider would then mark the very same turn-start block at a
156
- * different TTL than it did on the turn's first request, billing a second
157
- * write for one reusable prefix. Anchoring on the turn start keeps the signal
158
- * constant for every request in the turn.
159
- *
160
- * Detection uses {@link MEMORY_SPOTLIGHT_MATCHER}, the same full-wrapper
161
- * matcher {@link stripSpotlightInjections} strips with, so the two can never
162
- * disagree about which blocks are ephemeral.
163
- */
164
- export function turnStartUserMessageHasSpotlight(messages: Message[]): boolean {
165
- for (let i = messages.length - 1; i >= 0; i--) {
166
- const message = messages[i];
167
- if (message.role !== "user") {
168
- continue;
169
- }
170
- const textBlocks = message.content.filter((block) => block.type === "text");
171
- if (textBlocks.length === 0) {
172
- continue;
173
- }
174
- return textBlocks.some((block) =>
175
- textBlockMatchesInjection(block.text, [MEMORY_SPOTLIGHT_MATCHER]),
176
- );
177
- }
178
- return false;
179
- }
180
-
181
142
  /** `<NOW.md>` scratchpad prefixes (current tag, pre-line-limit variant, legacy `<now_scratchpad>`) — shared with `stripNowScratchpad` so the two strip paths can't drift. */
182
143
  export const NOW_SCRATCHPAD_STRIP_PREFIXES: InjectionMatcher[] = [
183
144
  "<NOW.md Always keep this up to date",
@@ -217,10 +178,10 @@ export const RUNTIME_INJECTION_PREFIXES: InjectionMatcher[] = [
217
178
  // matches the full-wrapper requirement in `countMemoryPrefixBlocks`.
218
179
  { prefix: "<memory>\n", suffix: "\n</memory>" },
219
180
  { prefix: "<info>\n", suffix: "\n</info>" },
220
- // The memory-v3 ephemeral spotlight block. Normally strip-and-replaced every
221
- // turn by `stripSpotlightInjections`, but registered here too so compaction
222
- // and overflow recovery remove a stale spotlight along with the rest of the
223
- // runtime injections. Full-wrapper shape for the same reason as `<memory>`.
181
+ // Memory-v3 per-turn spotlight blocks. Compaction rewrites history, so
182
+ // it strips them along with the other runtime injections and re-injects
183
+ // a fresh copy on the new tail. Full-wrapper shape for the same reason
184
+ // as `<memory>`.
224
185
  MEMORY_SPOTLIGHT_MATCHER,
225
186
  "<voice_call_control>",
226
187
  "<workspace_top_level>", // backward-compat: strip legacy workspace blocks
@@ -312,6 +312,45 @@ describe("mergeConsecutiveAssistantMessages", () => {
312
312
  expect(mergedIdMap.get("anchor")).toEqual(["tail"]);
313
313
  });
314
314
 
315
+ test("a row deleted on its channel never merges, in either direction", () => {
316
+ // The deletion lives in the provider envelope, not in `messageKind`, and
317
+ // a merged run keeps the anchor's metadata: folding would drop the
318
+ // deletion or claim it over text the channel still shows.
319
+ const deleted = makeMsg(
320
+ "assistant",
321
+ JSON.stringify([{ type: "text", text: "gone from Slack" }]),
322
+ {
323
+ id: "deleted",
324
+ metadata: JSON.stringify({
325
+ slackMeta: JSON.stringify({
326
+ source: "slack",
327
+ channelId: "C1",
328
+ channelTs: "1725100000.000100",
329
+ eventKind: "message",
330
+ deletedAt: 1725100001000,
331
+ }),
332
+ }),
333
+ },
334
+ );
335
+ const { messages, mergedIdMap } = mergeConsecutiveAssistantMessages([
336
+ makeMsg("user", "hi"),
337
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "A" }]), {
338
+ id: "before",
339
+ }),
340
+ deleted,
341
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "B" }]), {
342
+ id: "after",
343
+ }),
344
+ ]);
345
+ expect(messages.map((m) => m.id)).toEqual([
346
+ messages[0].id,
347
+ "before",
348
+ "deleted",
349
+ "after",
350
+ ]);
351
+ expect(mergedIdMap.size).toBe(0);
352
+ });
353
+
315
354
  test("leaves a single assistant row unchanged", () => {
316
355
  const messages = [
317
356
  makeMsg("user", "hi"),
@@ -40,10 +40,11 @@ const log = getLogger("message-consolidation");
40
40
 
41
41
  /**
42
42
  * True when an assistant row is a standalone display turn (system card,
43
- * provider-error notice, or deliberate-silence marker) that never merges with adjacent assistant rows in
43
+ * provider-error notice, deliberate-silence marker, reaction, or a row
44
+ * deleted on its channel) that never merges with adjacent assistant rows in
44
45
  * either direction. Merging one into an adjacent run would let the anchor's
45
- * metadata win and drop the `messageKind` marker from the wire (or stamp it
46
- * onto a bubble containing real assistant text).
46
+ * metadata win and drop the marker from the wire (or stamp it onto a bubble
47
+ * containing real assistant text).
47
48
  */
48
49
  function isStandaloneAssistantRow(msg: MessageRow): boolean {
49
50
  return isStandaloneAssistantMessage(msg.role, msg.metadata);
@@ -38,6 +38,11 @@ import {
38
38
  expectedScopesForStoredToken,
39
39
  scopeDifference,
40
40
  } from "../oauth/scope-utils.js";
41
+ import { credentialKey } from "../security/credential-key.js";
42
+ import {
43
+ getSecureKeyAsync,
44
+ getSecureKeyResultAsync,
45
+ } from "../security/secure-keys.js";
41
46
  import {
42
47
  TokenExpiredError,
43
48
  withValidToken,
@@ -610,6 +615,131 @@ async function checkManagedProvider(
610
615
  return results;
611
616
  }
612
617
 
618
+ /**
619
+ * Connection id for the Vellum-managed assistant credential.
620
+ *
621
+ * The credential is a single workspace-wide slot rather than one row per
622
+ * account, so it carries the credential-store key as its id instead of a
623
+ * connection uuid.
624
+ */
625
+ const ASSISTANT_API_KEY_CONNECTION_ID = "vellum:assistant_api_key";
626
+
627
+ /**
628
+ * Health of the platform-provisioned assistant API key, the credential that
629
+ * authenticates Vellum-managed inference.
630
+ *
631
+ * Deliberately not part of {@link checkAllCredentials} yet. Adding it there
632
+ * makes the heartbeat raise a `credential.health_alert` for this credential,
633
+ * and that alert has nowhere to send the reader until the notification carries
634
+ * the repair. It is used on demand for now, by the route a client calls to
635
+ * confirm a credential it just wrote.
636
+ *
637
+ * This is the one credential the assistant cannot mint for itself: the
638
+ * platform issues it and a signed-in client writes it in. For a self-hosted
639
+ * or local assistant the platform deliberately does not self-heal a dead key
640
+ * (`recover_expired_assistant_api_key` excludes registrations of that kind,
641
+ * because the client owns the reprovision flow), so its health has to be
642
+ * observed here and acted on by a client.
643
+ *
644
+ * Statuses map onto the shared vocabulary:
645
+ * - `missing_token`: no key is stored, so managed inference cannot run.
646
+ * - `revoked`: the platform settled that the stored key is not accepted.
647
+ * - `unreachable`: the credential store or the platform did not answer, so
648
+ * health is unknown. The heartbeat drops these before notifying, which is
649
+ * what keeps a transient outage from raising a credential alert.
650
+ *
651
+ * `canAutoRecover` is true for both failure statuses: recovery needs no
652
+ * re-authorization from the user, only a signed-in client to rotate the key.
653
+ */
654
+ export async function checkAssistantApiKey(): Promise<CredentialHealthResult | null> {
655
+ const base: Omit<
656
+ CredentialHealthResult,
657
+ "status" | "details" | "canAutoRecover"
658
+ > = {
659
+ connectionId: ASSISTANT_API_KEY_CONNECTION_ID,
660
+ provider: "vellum",
661
+ accountInfo: null,
662
+ missingScopes: [],
663
+ };
664
+
665
+ const stored = await getSecureKeyResultAsync(
666
+ credentialKey("vellum", "assistant_api_key"),
667
+ );
668
+
669
+ if (stored.unreachable) {
670
+ return {
671
+ ...base,
672
+ status: "unreachable",
673
+ details:
674
+ "The credential store is unreachable, so Vellum's managed credentials could not be checked.",
675
+ canAutoRecover: false,
676
+ };
677
+ }
678
+
679
+ if (stored.value == null) {
680
+ // A workspace that was never connected to the platform has no managed
681
+ // credential and is not unhealthy. Only an install that already carries
682
+ // the rest of a platform identity is missing something.
683
+ const baseUrl = await getSecureKeyAsync(
684
+ credentialKey("vellum", "platform_base_url"),
685
+ );
686
+ if (!baseUrl) {
687
+ return null;
688
+ }
689
+ return {
690
+ ...base,
691
+ status: "missing_token",
692
+ details:
693
+ "No Vellum-managed credentials are stored yet. Log in to the Vellum platform to set them up.",
694
+ canAutoRecover: true,
695
+ };
696
+ }
697
+
698
+ const { VellumPlatformClient } = await import("../platform/client.js");
699
+ const client = await VellumPlatformClient.create();
700
+ if (!client) {
701
+ return {
702
+ ...base,
703
+ status: "unreachable",
704
+ details:
705
+ "The Vellum platform client could not be built, so Vellum's managed credentials could not be checked.",
706
+ canAutoRecover: false,
707
+ };
708
+ }
709
+
710
+ const verdict = await client.verifyCredential();
711
+ if (verdict === "rejected") {
712
+ return {
713
+ ...base,
714
+ status: "revoked",
715
+ // Names the recovery the app actually performs. A replacement is
716
+ // provisioned only when someone asks for one, from the repair action or
717
+ // by logging in from the command line, so the copy points at that
718
+ // action rather than at reconnecting something the reader never
719
+ // connected.
720
+ details:
721
+ "Vellum's managed credentials stopped working, so chat and background tasks that use them are paused. Restoring takes a moment and will not affect anything else.",
722
+ canAutoRecover: true,
723
+ };
724
+ }
725
+ if (verdict === "unknown") {
726
+ return {
727
+ ...base,
728
+ status: "unreachable",
729
+ details:
730
+ "Vellum did not answer the managed-credential check. Health is unknown until the next check.",
731
+ canAutoRecover: false,
732
+ };
733
+ }
734
+
735
+ return {
736
+ ...base,
737
+ status: "healthy",
738
+ details: "Vellum's managed credentials are working.",
739
+ canAutoRecover: true,
740
+ };
741
+ }
742
+
613
743
  // ── Public API ────────────────────────────────────────────────────────
614
744
 
615
745
  /**
@@ -171,6 +171,37 @@ describe("isToolActiveForContext - client OS eligibility", () => {
171
171
  }
172
172
  });
173
173
 
174
+ test("falls back to linux client OS from the linux transport interface", () => {
175
+ const skillId = "linux-transport-client-os-test-skill";
176
+ registerSkillTools(skillId, [
177
+ finalizeTool({
178
+ name: "linux_transport_os_test_tool",
179
+ supportedClientOs: ["linux"],
180
+ }),
181
+ ]);
182
+
183
+ try {
184
+ expect(
185
+ isToolActiveForContext(
186
+ "linux_transport_os_test_tool",
187
+ makeCtx({
188
+ transportInterface: "linux",
189
+ }),
190
+ ),
191
+ ).toBe(true);
192
+ expect(
193
+ isToolActiveForContext(
194
+ "linux_transport_os_test_tool",
195
+ makeCtx({
196
+ transportInterface: "windows",
197
+ }),
198
+ ),
199
+ ).toBe(false);
200
+ } finally {
201
+ unregisterSkillTools(skillId);
202
+ }
203
+ });
204
+
174
205
  test("uses pinned client OS without falling through to live context", () => {
175
206
  const skillId = "pinned-client-os-test-skill";
176
207
  registerSkillTools(skillId, [
@@ -26,8 +26,6 @@ import type { ProviderMessageMetadata } from "../messaging/provider-message-meta
26
26
  import {
27
27
  formatSlackTimezoneLabel,
28
28
  isSlackTs,
29
- type SlackMessageMetadata,
30
- writeSlackMetadata,
31
29
  } from "../messaging/providers/slack/message-metadata.js";
32
30
  import {
33
31
  recordCompactionEndBestEffort,
@@ -1345,9 +1343,9 @@ function resolveAssistantReplyTimestampTimezone(ctx: Conversation): string {
1345
1343
  * snapshot that handleMessageComplete used to compute at end-of-turn. All
1346
1344
  * inputs (channel context, trust context, turnStartedAt) are stable across
1347
1345
  * the LLM call, so building this once at reserve is equivalent to building
1348
- * it at complete. Slack reply rows further stamp a `slackMeta` sub-object —
1349
- * the `channelTs` field stays absent here and is back-filled by
1350
- * `deliverReplyViaCallback` after the gateway returns the ts.
1346
+ * it at complete. The envelope's `messageId` stays absent here and is
1347
+ * back-filled by `deliverReplyViaCallback` after the transport returns the
1348
+ * sent message's id.
1351
1349
  */
1352
1350
  function buildAssistantChannelMetadata(
1353
1351
  state: EventHandlerState,
@@ -1363,61 +1361,59 @@ function buildAssistantChannelMetadata(
1363
1361
  sentAt: state.turnStartedAt,
1364
1362
  };
1365
1363
 
1366
- if (deps.turnChannelContext.assistantMessageChannel === "slack") {
1367
- // The whole envelope resolves from one turn-local snapshot: the actor the
1368
- // provenance above names is the actor whose channel and thread the row
1369
- // routes to. Reading the live slot here instead would let a concurrent
1370
- // inbound repoint the reply mid-turn, and could stamp one actor's class
1371
- // beside another actor's routing identity.
1372
- const rowTrust = turnOrRestingTrust(deps.ctx);
1373
- const channelId = rowTrust?.requesterChatId;
1374
- if (channelId) {
1375
- // This turn's own inbound thread id (the same field guardian-approval
1376
- // cards read). Deliberately not the shared conversation binding: on a
1377
- // legacy flat→thread aliased Slack conversation a concurrent inbound
1378
- // can rewrite the binding's externalThreadId mid-turn.
1379
- const turnThreadTs = rowTrust?.sourceThreadId;
1380
- const threadTs = isSlackTs(turnThreadTs) ? turnThreadTs : undefined;
1381
- const timestampTimezone = resolveAssistantReplyTimestampTimezone(
1382
- deps.ctx,
1383
- );
1384
- const timestampTimezoneLabel = formatSlackTimezoneLabel(
1385
- timestampTimezone,
1386
- { nowMs: state.turnStartedAt },
1387
- );
1388
- const partialSlackMeta: Partial<SlackMessageMetadata> = {
1389
- source: "slack",
1390
- eventKind: "message",
1391
- channelId,
1392
- ...(threadTs ? { threadTs } : {}),
1393
- timestampTimezone,
1394
- ...(timestampTimezoneLabel ? { timestampTimezoneLabel } : {}),
1395
- };
1396
- // `channelTs` is filled in by the post-send reconciliation step in
1397
- // `deliverReplyViaCallback`; cast through the Partial to satisfy
1398
- // the writer's type at this pre-send boundary.
1399
- metadata.slackMeta = writeSlackMetadata(
1400
- partialSlackMeta as SlackMessageMetadata,
1401
- );
1402
- }
1403
- } else if (deps.turnChannelContext.assistantMessageChannel !== "vellum") {
1404
- // Every other channel row describes itself in the neutral envelope
1405
- // `readProviderMetadata` serves, the shape a plugin channel writes too.
1406
- // `messageId` stays absent here and is back-filled by the post-send
1407
- // reconciliation once the transport returns the sent message's id, which
1408
- // is what lets a later reaction on this row resolve back to it. No
1409
- // `threadId`: a value there asserts a thread exists, and the reply's
1410
- // routing thread is delivery state, not a fact about this row.
1411
- const chatId = turnOrRestingTrust(deps.ctx)?.requesterChatId;
1412
- if (chatId) {
1413
- metadata.providerMeta = JSON.stringify({
1414
- source: deps.turnChannelContext.assistantMessageChannel,
1415
- conversationExternalId: chatId,
1416
- eventKind: "message",
1417
- } satisfies ProviderMessageMetadata);
1418
- }
1364
+ const channel = deps.turnChannelContext.assistantMessageChannel;
1365
+ if (channel === "vellum") {
1366
+ // A vellum turn is not a provider message: no envelope.
1367
+ return metadata;
1419
1368
  }
1420
-
1369
+ // Every channel row describes itself in the neutral envelope
1370
+ // `readProviderMetadata` serves, the shape a plugin channel writes too.
1371
+ // `messageId` stays absent here and is back-filled by the post-send
1372
+ // reconciliation once the transport returns the sent message's id, which
1373
+ // is what lets a later reaction or delete on this row resolve back to it.
1374
+ //
1375
+ // The whole envelope resolves from one turn-local snapshot: the actor the
1376
+ // provenance above names is the actor whose channel and thread the row
1377
+ // routes to. Reading the live slot here instead would let a concurrent
1378
+ // inbound repoint the reply mid-turn, and could stamp one actor's class
1379
+ // beside another actor's routing identity.
1380
+ const rowTrust = turnOrRestingTrust(deps.ctx);
1381
+ const chatId = rowTrust?.requesterChatId;
1382
+ if (!chatId) {
1383
+ return metadata;
1384
+ }
1385
+ const envelope: ProviderMessageMetadata = {
1386
+ source: channel,
1387
+ conversationExternalId: chatId,
1388
+ eventKind: "message",
1389
+ };
1390
+ if (channel === "slack") {
1391
+ // Slack's own facts ride the schema's passthrough and come back through
1392
+ // the envelope's Slack view (`slackViewOfProviderMetadata`). The thread
1393
+ // is this turn's own inbound thread id (the same field guardian-approval
1394
+ // cards read), stated because the reply is posted in it and the Slack
1395
+ // transcript is assembled by thread. Deliberately not the shared
1396
+ // conversation binding: on a legacy flat-to-thread aliased Slack
1397
+ // conversation a concurrent inbound can rewrite the binding's
1398
+ // externalThreadId mid-turn.
1399
+ const turnThreadTs = rowTrust?.sourceThreadId;
1400
+ const threadTs = isSlackTs(turnThreadTs) ? turnThreadTs : undefined;
1401
+ const timestampTimezone = resolveAssistantReplyTimestampTimezone(deps.ctx);
1402
+ const timestampTimezoneLabel = formatSlackTimezoneLabel(timestampTimezone, {
1403
+ nowMs: state.turnStartedAt,
1404
+ });
1405
+ metadata.providerMeta = JSON.stringify({
1406
+ ...envelope,
1407
+ ...(threadTs ? { threadId: threadTs } : {}),
1408
+ ...(timestampTimezone ? { timestampTimezone } : {}),
1409
+ ...(timestampTimezoneLabel ? { timestampTimezoneLabel } : {}),
1410
+ });
1411
+ return metadata;
1412
+ }
1413
+ // No `threadId` on the other channels: a value there asserts a thread
1414
+ // exists, and the reply's routing thread is delivery state, not a fact
1415
+ // about this row.
1416
+ metadata.providerMeta = JSON.stringify(envelope);
1421
1417
  return metadata;
1422
1418
  }
1423
1419
 
@@ -359,6 +359,8 @@ function connectionResolutionUserMessage(
359
359
  return `No provider connection is configured${usedBy}. Ask me to set one up right here, or add an API key in ${fixPath}.`;
360
360
  case "unroutable_managed_model":
361
361
  return `The model "${error.model ?? "<unset>"}"${usedBy} isn't served by the Vellum managed route. Pick a model from the Vellum catalog, or choose a concrete provider in ${fixPath}.`;
362
+ case "adapter_unavailable":
363
+ return `${connection}${usedBy} could not serve model "${error.model ?? "<unset>"}". This model is only available on the Vellum GPU route and was not sent through another provider.`;
362
364
  case "missing_credential":
363
365
  // Provider-neutral: api_key connections store keys, oauth_subscription
364
366
  // connections store login tokens — the fix differs but the location