@vellumai/assistant 0.11.1 → 0.11.2-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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -186,15 +186,29 @@ export function seedInferenceProfiles(
186
186
  }
187
187
  }
188
188
 
189
- // Profile ordering ensure all seeded profiles appear in the order array.
189
+ // Profile ordering: ensure all seeded profiles appear in the order array.
190
+ // A managed key absent from a workspace's order is placed next to the
191
+ // sibling that precedes it in the catalog rather than appended, so the
192
+ // managed profiles stay grouped in catalog order even on a workspace whose
193
+ // order already lists custom profiles. Entries already in the order keep
194
+ // their relative positions, so a user arrangement survives.
190
195
  const profileOrder = Array.isArray(llm.profileOrder)
191
196
  ? (llm.profileOrder as string[])
192
197
  : [];
193
- const orderSet = new Set(profileOrder);
194
- for (const name of Object.keys(MANAGED_PROFILE_TEMPLATES)) {
195
- if (!orderSet.has(name)) {
198
+ const managedNames = Object.keys(MANAGED_PROFILE_TEMPLATES);
199
+ for (const [index, name] of managedNames.entries()) {
200
+ if (profileOrder.includes(name)) {
201
+ continue;
202
+ }
203
+ const anchor = managedNames
204
+ .slice(0, index)
205
+ .reverse()
206
+ .map((sibling) => profileOrder.indexOf(sibling))
207
+ .find((position) => position >= 0);
208
+ if (anchor === undefined) {
196
209
  profileOrder.push(name);
197
- orderSet.add(name);
210
+ } else {
211
+ profileOrder.splice(anchor + 1, 0, name);
198
212
  }
199
213
  }
200
214
  llm.profileOrder = profileOrder;
@@ -14,8 +14,8 @@ import {
14
14
  } from "../security/untrusted-content.js";
15
15
  import { safeStringSlice } from "../util/unicode.js";
16
16
 
17
- /** Minimum content length (chars) before a tool result is eligible for truncation. ~2000 tokens at 4 chars/token. */
18
- export const THRESHOLD_CHARS = 8_000;
17
+ /** Minimum content length (chars) before a tool result is eligible for truncation. ~6250 tokens at 4 chars/token. */
18
+ export const THRESHOLD_CHARS = 25_000;
19
19
 
20
20
  /** Target size (chars) for the truncated stub. ~300 tokens at 4 chars/token. */
21
21
  export const TARGET_CHARS = 1_200;
@@ -381,3 +381,57 @@ describe("system-card boundaries", () => {
381
381
  expect(findDisplayTurnEndIndex(rows, 0)).toBe(0);
382
382
  });
383
383
  });
384
+
385
+ describe("provider-error boundaries", () => {
386
+ const errorMeta = JSON.stringify({
387
+ messageKind: "provider_error",
388
+ providerErrorCode: "PROVIDER_BILLING",
389
+ providerErrorCategory: "credits_exhausted",
390
+ });
391
+
392
+ test("a provider-error row never merges into the preceding assistant run", () => {
393
+ const { messages } = mergeConsecutiveAssistantMessages([
394
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "reply" }])),
395
+ makeMsg(
396
+ "assistant",
397
+ JSON.stringify([{ type: "text", text: "You're out of credits." }]),
398
+ { metadata: errorMeta },
399
+ ),
400
+ ]);
401
+ expect(messages).toHaveLength(2);
402
+ expect(messages[1]!.metadata).toBe(errorMeta);
403
+ });
404
+
405
+ test("an assistant row never merges into a preceding provider-error row", () => {
406
+ const { messages } = mergeConsecutiveAssistantMessages([
407
+ makeMsg(
408
+ "assistant",
409
+ JSON.stringify([{ type: "text", text: "You're out of credits." }]),
410
+ { metadata: errorMeta },
411
+ ),
412
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "reply" }])),
413
+ ]);
414
+ expect(messages).toHaveLength(2);
415
+ expect(messages[0]!.metadata).toBe(errorMeta);
416
+ });
417
+
418
+ test("findDisplayTurnEndIndex treats a provider-error row as a single-row turn", () => {
419
+ const rows = [
420
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "error" }]), {
421
+ metadata: errorMeta,
422
+ }),
423
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "reply" }])),
424
+ ];
425
+ expect(findDisplayTurnEndIndex(rows, 0)).toBe(0);
426
+ });
427
+
428
+ test("findDisplayTurnEndIndex stops an assistant run before a provider-error row", () => {
429
+ const rows = [
430
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "reply" }])),
431
+ makeMsg("assistant", JSON.stringify([{ type: "text", text: "error" }]), {
432
+ metadata: errorMeta,
433
+ }),
434
+ ];
435
+ expect(findDisplayTurnEndIndex(rows, 0)).toBe(0);
436
+ });
437
+ });
@@ -32,20 +32,21 @@
32
32
  */
33
33
 
34
34
  import type { MessageRow } from "../persistence/conversation-crud.js";
35
- import { isSystemCardMessage } from "../persistence/conversation-crud.js";
35
+ import { isStandaloneAssistantMessage } from "../persistence/conversation-crud.js";
36
36
  import type { ContentBlock } from "../providers/types.js";
37
37
  import { getLogger } from "../util/logger.js";
38
38
 
39
39
  const log = getLogger("message-consolidation");
40
40
 
41
41
  /**
42
- * True when a row is a daemon-authored system card (`messageKind:
43
- * "system_card"` metadata). Cards are standalone display turns: they never
44
- * fold into an adjacent assistant run and adjacent assistant rows never
45
- * fold into them.
42
+ * True when an assistant row is a standalone display turn (system card or
43
+ * provider-error notice) that never merges with adjacent assistant rows in
44
+ * 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
47
  */
47
- export function isSystemCardRow(msg: MessageRow): boolean {
48
- return isSystemCardMessage(msg.role, msg.metadata);
48
+ function isStandaloneAssistantRow(msg: MessageRow): boolean {
49
+ return isStandaloneAssistantMessage(msg.role, msg.metadata);
49
50
  }
50
51
 
51
52
  // ── Block predicates ────────────────────────────────────────────────
@@ -120,8 +121,9 @@ export function findDisplayTurnEndIndex(
120
121
  if (messages[startIdx]?.role !== "assistant") {
121
122
  return startIdx;
122
123
  }
123
- // A system card is a single-row display turn — never spans neighbours.
124
- if (isSystemCardRow(messages[startIdx]!)) {
124
+ // System cards and provider-error rows are single-row display turns that
125
+ // never span neighbours.
126
+ if (isStandaloneAssistantRow(messages[startIdx]!)) {
125
127
  return startIdx;
126
128
  }
127
129
 
@@ -131,7 +133,7 @@ export function findDisplayTurnEndIndex(
131
133
  if (!next) {
132
134
  break;
133
135
  }
134
- if (next.role === "assistant" && !isSystemCardRow(next)) {
136
+ if (next.role === "assistant" && !isStandaloneAssistantRow(next)) {
135
137
  endIdx += 1;
136
138
  continue;
137
139
  }
@@ -297,15 +299,15 @@ export function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
297
299
 
298
300
  for (const msg of messages) {
299
301
  const lastIdx = result.length - 1;
300
- // System cards stay standalone in both directions: a card never folds
301
- // into the preceding assistant run, and the following assistant row
302
- // never folds into a card.
302
+ // System cards and provider-error rows stay standalone in both
303
+ // directions: neither folds into the preceding assistant run, and the
304
+ // following assistant row never folds into them.
303
305
  const isConsecutiveAssistant =
304
306
  msg.role === "assistant" &&
305
- !isSystemCardRow(msg) &&
307
+ !isStandaloneAssistantRow(msg) &&
306
308
  lastIdx >= 0 &&
307
309
  result[lastIdx].role === "assistant" &&
308
- !isSystemCardRow(result[lastIdx]);
310
+ !isStandaloneAssistantRow(result[lastIdx]);
309
311
 
310
312
  if (!isConsecutiveAssistant) {
311
313
  result.push(msg);
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Wiring tests for the `chat.assistant_reply` emit in `runDeferredTurnTail`:
3
+ * which turn outcomes reach the producer, what the tail hands it, and that the
4
+ * emit can neither block nor break turn finalization.
5
+ */
6
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
7
+
8
+ import { makeMockLogger } from "../../__tests__/helpers/mock-logger.js";
9
+ import { setConfig } from "../../__tests__/helpers/set-config.js";
10
+ import type { InflightContentWriter } from "../inflight-message-content.js";
11
+
12
+ // The finalize module's import graph reaches the memory indexer; keep it inert
13
+ // so no embedding backend is touched.
14
+ setConfig("memory", { enabled: false, v2: { enabled: false } });
15
+
16
+ const CONVERSATION_ID = "conv-tail-1";
17
+ const ASSISTANT_MESSAGE_ID = "msg-assistant-1";
18
+ const USER_MESSAGE_ID = "msg-user-1";
19
+
20
+ interface EmitCall {
21
+ conversationId: string;
22
+ assistantMessageId: string;
23
+ userMessageId: string | undefined;
24
+ replyDeliveredInAppOnly?: boolean;
25
+ }
26
+
27
+ const emitCalls: EmitCall[] = [];
28
+ /** Ordered trace of the tail's deferred work, for the ordering assertion. */
29
+ const trace: string[] = [];
30
+ let producerBehavior: "resolve" | "reject" | "hang" = "resolve";
31
+
32
+ mock.module("../../notifications/assistant-reply-producer.js", () => ({
33
+ emitAssistantReplyNotification: (params: EmitCall): Promise<void> => {
34
+ emitCalls.push({
35
+ conversationId: params.conversationId,
36
+ assistantMessageId: params.assistantMessageId,
37
+ userMessageId: params.userMessageId,
38
+ ...("replyDeliveredInAppOnly" in params
39
+ ? { replyDeliveredInAppOnly: params.replyDeliveredInAppOnly }
40
+ : {}),
41
+ });
42
+ trace.push("emit");
43
+ if (producerBehavior === "reject") {
44
+ const rejected = Promise.reject(new Error("simulated producer failure"));
45
+ // The call site fires the producer without awaiting it, so nothing in
46
+ // the tail observes this rejection. Settle it here so the failure stays
47
+ // a test fixture rather than a process-level unhandled rejection.
48
+ rejected.catch(() => {});
49
+ return rejected;
50
+ }
51
+ if (producerBehavior === "hang") {
52
+ return new Promise<void>(() => {});
53
+ }
54
+ return Promise.resolve();
55
+ },
56
+ }));
57
+
58
+ const realCrud = await import("../../persistence/conversation-crud.js");
59
+ mock.module("../../persistence/conversation-crud.js", () => ({
60
+ ...realCrud,
61
+ // No conversation row: the tail's truncation and disk-sync steps
62
+ // short-circuit, keeping these tests on the notify wiring.
63
+ getConversation: () => null,
64
+ }));
65
+
66
+ const { runDeferredTurnTail } =
67
+ await import("../conversation-turn-finalize.js");
68
+
69
+ const rlog = makeMockLogger() as Parameters<
70
+ typeof runDeferredTurnTail
71
+ >[0]["rlog"];
72
+
73
+ async function runTail(overrides: {
74
+ turnCompleted: boolean;
75
+ lastAssistantMessageId?: string | undefined;
76
+ deferredFinalizeEffects?: ReadonlyArray<() => Promise<void>>;
77
+ userMessageId?: string | undefined;
78
+ replyDeliveredInAppOnly?: boolean;
79
+ }): Promise<void> {
80
+ await runDeferredTurnTail({
81
+ ctx: { conversationId: CONVERSATION_ID, messages: [] },
82
+ state: {
83
+ deferredFinalizeEffects: overrides.deferredFinalizeEffects ?? [],
84
+ lastAssistantMessageId:
85
+ "lastAssistantMessageId" in overrides
86
+ ? overrides.lastAssistantMessageId
87
+ : ASSISTANT_MESSAGE_ID,
88
+ inflightWriters: new Map<string, InflightContentWriter>(),
89
+ },
90
+ rlog,
91
+ generationCompletedAt: Date.now(),
92
+ turnCompleted: overrides.turnCompleted,
93
+ userMessageId:
94
+ "userMessageId" in overrides ? overrides.userMessageId : USER_MESSAGE_ID,
95
+ ...(overrides.replyDeliveredInAppOnly
96
+ ? { replyDeliveredInAppOnly: true }
97
+ : {}),
98
+ });
99
+ }
100
+
101
+ beforeEach(() => {
102
+ emitCalls.length = 0;
103
+ trace.length = 0;
104
+ producerBehavior = "resolve";
105
+ });
106
+
107
+ describe("runDeferredTurnTail assistant-reply notification", () => {
108
+ test("emits once for a completed turn", async () => {
109
+ await runTail({ turnCompleted: true });
110
+
111
+ expect(emitCalls).toEqual([
112
+ {
113
+ conversationId: CONVERSATION_ID,
114
+ assistantMessageId: ASSISTANT_MESSAGE_ID,
115
+ userMessageId: USER_MESSAGE_ID,
116
+ },
117
+ ]);
118
+ });
119
+
120
+ test("forwards the app-only delivery marker to the producer", async () => {
121
+ await runTail({ turnCompleted: true, replyDeliveredInAppOnly: true });
122
+
123
+ expect(emitCalls).toEqual([
124
+ {
125
+ conversationId: CONVERSATION_ID,
126
+ assistantMessageId: ASSISTANT_MESSAGE_ID,
127
+ userMessageId: USER_MESSAGE_ID,
128
+ replyDeliveredInAppOnly: true,
129
+ },
130
+ ]);
131
+ });
132
+
133
+ test("omits the app-only delivery marker for an ordinary turn", async () => {
134
+ await runTail({ turnCompleted: true });
135
+
136
+ expect(emitCalls[0]).not.toHaveProperty("replyDeliveredInAppOnly");
137
+ });
138
+
139
+ test("stays silent for a handed-off or cancelled turn", async () => {
140
+ await runTail({ turnCompleted: false });
141
+
142
+ expect(emitCalls).toEqual([]);
143
+ });
144
+
145
+ test("stays silent when the turn produced no assistant row", async () => {
146
+ await runTail({ turnCompleted: true, lastAssistantMessageId: undefined });
147
+
148
+ expect(emitCalls).toEqual([]);
149
+ });
150
+
151
+ test("emits after the deferred finalize effects have run", async () => {
152
+ await runTail({
153
+ turnCompleted: true,
154
+ deferredFinalizeEffects: [
155
+ async () => {
156
+ trace.push("effect-1");
157
+ },
158
+ async () => {
159
+ trace.push("effect-2");
160
+ },
161
+ ],
162
+ });
163
+
164
+ expect(trace).toEqual(["effect-1", "effect-2", "emit"]);
165
+ });
166
+
167
+ test("completes without awaiting the producer, even when it never settles", async () => {
168
+ producerBehavior = "hang";
169
+
170
+ await runTail({ turnCompleted: true });
171
+
172
+ expect(emitCalls).toHaveLength(1);
173
+ });
174
+
175
+ test("survives a failing producer", async () => {
176
+ producerBehavior = "reject";
177
+
178
+ await runTail({ turnCompleted: true });
179
+
180
+ expect(emitCalls).toHaveLength(1);
181
+ });
182
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * `runDeferredTurnTail` runs after the terminal SSE, so the user can delete the
3
+ * conversation while its awaited bookkeeping is still draining. These tests pin
4
+ * that the tail's two filesystem steps (tool-result spooling and the JSONL disk
5
+ * mirror) read the conversation fresh and skip once it is gone, rather than
6
+ * rebuilding a deleted conversation's directory from in-memory history.
7
+ */
8
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ import { makeMockLogger } from "../../__tests__/helpers/mock-logger.js";
11
+ import { setConfig } from "../../__tests__/helpers/set-config.js";
12
+ import type { ConversationRow } from "../../persistence/conversation-crud.js";
13
+ import type { Message } from "../../providers/types.js";
14
+ import type { InflightContentWriter } from "../inflight-message-content.js";
15
+
16
+ // The finalize module's import graph reaches the memory indexer; keep it inert
17
+ // so no embedding backend is touched.
18
+ setConfig("memory", { enabled: false, v2: { enabled: false } });
19
+
20
+ const CONVERSATION_ID = "conv-tail-delete-1";
21
+ const ASSISTANT_MESSAGE_ID = "msg-assistant-1";
22
+ const USER_MESSAGE_ID = "msg-user-1";
23
+ const CONVERSATION_DIR = "/tmp/vellum-test/conv-tail-delete-1";
24
+
25
+ /** Flipped by a deferred finalize effect to model a mid-tail deletion. */
26
+ let conversationDeleted = false;
27
+ let getConversationCalls = 0;
28
+ const resolvedDirCalls: string[] = [];
29
+ const truncateCalls: string[] = [];
30
+ const diskSyncCalls: string[] = [];
31
+
32
+ const TRUNCATED_MESSAGES: Message[] = [{ role: "user", content: [] }];
33
+
34
+ mock.module("../../notifications/assistant-reply-producer.js", () => ({
35
+ emitAssistantReplyNotification: (): Promise<void> => Promise.resolve(),
36
+ }));
37
+
38
+ function makeConversation(): ConversationRow {
39
+ return {
40
+ id: CONVERSATION_ID,
41
+ title: null,
42
+ createdAt: 1700000000000,
43
+ updatedAt: 1700000000000,
44
+ totalInputTokens: 0,
45
+ totalOutputTokens: 0,
46
+ totalEstimatedCost: 0,
47
+ contextSummary: null,
48
+ contextCompactedMessageCount: 0,
49
+ contextCompactedAt: null,
50
+ historyStrippedAt: null,
51
+ slackContextCompactionWatermarkTs: null,
52
+ slackContextCompactionWatermarkAt: null,
53
+ conversationType: "chat",
54
+ source: "user",
55
+ originChannel: null,
56
+ originInterface: null,
57
+ forkParentConversationId: null,
58
+ forkParentMessageId: null,
59
+ isAutoTitle: 0,
60
+ scheduleJobId: null,
61
+ lastMessageAt: null,
62
+ archivedAt: null,
63
+ surfacedAt: null,
64
+ inferenceProfile: null,
65
+ enabledPlugins: null,
66
+ inferenceProfileSessionId: null,
67
+ inferenceProfileExpiresAt: null,
68
+ lastNotifiedInferenceProfile: null,
69
+ processingStartedAt: null,
70
+ };
71
+ }
72
+
73
+ const realCrud = await import("../../persistence/conversation-crud.js");
74
+ mock.module("../../persistence/conversation-crud.js", () => ({
75
+ ...realCrud,
76
+ getConversation: (): ConversationRow | null => {
77
+ getConversationCalls += 1;
78
+ return conversationDeleted ? null : makeConversation();
79
+ },
80
+ }));
81
+
82
+ const realDirectories =
83
+ await import("../../persistence/conversation-directories.js");
84
+ mock.module("../../persistence/conversation-directories.js", () => ({
85
+ ...realDirectories,
86
+ getResolvedConversationDirPath: (id: string): string => {
87
+ resolvedDirCalls.push(id);
88
+ return CONVERSATION_DIR;
89
+ },
90
+ }));
91
+
92
+ const realTruncation =
93
+ await import("../../context/post-turn-tool-result-truncation.js");
94
+ mock.module("../../context/post-turn-tool-result-truncation.js", () => ({
95
+ ...realTruncation,
96
+ derefToolResultReReads: (messages: Message[]) => ({
97
+ messages,
98
+ dereferencedCount: 0,
99
+ }),
100
+ postTurnTruncateToolResults: (
101
+ _messages: Message[],
102
+ options: { conversationDir: string },
103
+ ) => {
104
+ truncateCalls.push(options.conversationDir);
105
+ return { messages: TRUNCATED_MESSAGES, truncatedCount: 0 };
106
+ },
107
+ }));
108
+
109
+ const realDiskView =
110
+ await import("../../persistence/conversation-disk-view.js");
111
+ mock.module("../../persistence/conversation-disk-view.js", () => ({
112
+ ...realDiskView,
113
+ syncMessageToDisk: (_conversationId: string, messageId: string): void => {
114
+ diskSyncCalls.push(messageId);
115
+ },
116
+ }));
117
+
118
+ const { runDeferredTurnTail } =
119
+ await import("../conversation-turn-finalize.js");
120
+
121
+ const rlog = makeMockLogger() as Parameters<
122
+ typeof runDeferredTurnTail
123
+ >[0]["rlog"];
124
+
125
+ /** Runs the tail with one deferred effect standing in for the turn's bookkeeping. */
126
+ async function runTail(
127
+ deferredEffect: () => Promise<void>,
128
+ ): Promise<Message[]> {
129
+ const ctx = { conversationId: CONVERSATION_ID, messages: [] as Message[] };
130
+ await runDeferredTurnTail({
131
+ ctx,
132
+ state: {
133
+ deferredFinalizeEffects: [deferredEffect],
134
+ lastAssistantMessageId: ASSISTANT_MESSAGE_ID,
135
+ inflightWriters: new Map<string, InflightContentWriter>(),
136
+ },
137
+ rlog,
138
+ generationCompletedAt: Date.now(),
139
+ turnCompleted: true,
140
+ userMessageId: USER_MESSAGE_ID,
141
+ });
142
+ return ctx.messages;
143
+ }
144
+
145
+ beforeEach(() => {
146
+ conversationDeleted = false;
147
+ getConversationCalls = 0;
148
+ resolvedDirCalls.length = 0;
149
+ truncateCalls.length = 0;
150
+ diskSyncCalls.length = 0;
151
+ });
152
+
153
+ describe("runDeferredTurnTail conversation deleted mid-tail", () => {
154
+ test("skips the filesystem steps when the conversation is gone", async () => {
155
+ const messages = await runTail(async () => {
156
+ conversationDeleted = true;
157
+ });
158
+
159
+ expect(resolvedDirCalls).toEqual([]);
160
+ expect(truncateCalls).toEqual([]);
161
+ expect(diskSyncCalls).toEqual([]);
162
+ expect(messages).toEqual([]);
163
+ });
164
+
165
+ test("reads the conversation once more before the filesystem steps", async () => {
166
+ await runTail(async () => {
167
+ conversationDeleted = true;
168
+ });
169
+
170
+ expect(getConversationCalls).toBe(2);
171
+ });
172
+
173
+ test("runs the filesystem steps when the conversation survives", async () => {
174
+ const messages = await runTail(async () => {});
175
+
176
+ expect(resolvedDirCalls).toEqual([CONVERSATION_ID]);
177
+ expect(truncateCalls).toEqual([CONVERSATION_DIR]);
178
+ expect(diskSyncCalls).toEqual([ASSISTANT_MESSAGE_ID]);
179
+ expect(messages).toEqual(TRUNCATED_MESSAGES);
180
+ });
181
+ });