@vellumai/assistant 0.9.0 → 0.10.0-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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Regression test for descriptor hygiene in the vbundle export path.
3
+ *
4
+ * `streamExportVBundle` opens every file in the workspace twice — once to hash
5
+ * it (Pass 1) and once to stream it into the tar (Pass 2). This test pins the
6
+ * invariant that those reads release their descriptors: the number of open
7
+ * descriptors held by the process must not grow proportionally to the number
8
+ * of files exported. Without that guarantee a workspace-sized export exhausts
9
+ * the daemon's descriptor limit (EMFILE) and every subsequent subprocess spawn
10
+ * fails.
11
+ *
12
+ * Descriptor counting uses `/proc/self/fd`, which only exists on Linux. The
13
+ * assertion is skipped on platforms without it (e.g. macOS dev machines); CI
14
+ * runs on Linux, so the regression stays covered there.
15
+ */
16
+
17
+ import {
18
+ existsSync,
19
+ mkdirSync,
20
+ readdirSync,
21
+ rmSync,
22
+ writeFileSync,
23
+ } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { join } from "node:path";
26
+ import { describe, expect, test } from "bun:test";
27
+
28
+ import { assertNotLiveDb } from "../../../__tests__/assert-not-live-db.js";
29
+ import { streamExportVBundle } from "../vbundle-builder.js";
30
+ import { defaultV1Options } from "./v1-test-helpers.js";
31
+
32
+ const PROC_SELF_FD = "/proc/self/fd";
33
+ const hasProcFd = existsSync(PROC_SELF_FD);
34
+
35
+ /** Count the descriptors currently open by this process. */
36
+ function openFdCount(): number {
37
+ return readdirSync(PROC_SELF_FD).length;
38
+ }
39
+
40
+ /**
41
+ * Build a workspace with enough distinct files (across nested directories) that
42
+ * a per-file descriptor leak would be obvious against the assertion bound.
43
+ */
44
+ function createPopulatedWorkspace(fileCount: number): {
45
+ dir: string;
46
+ cleanup: () => void;
47
+ } {
48
+ const dir = join(
49
+ tmpdir(),
50
+ `vbundle-fd-leak-${Date.now()}-${Math.random().toString(36).slice(2)}`,
51
+ );
52
+ mkdirSync(dir, { recursive: true });
53
+ writeFileSync(join(dir, "config.json"), JSON.stringify({ test: true }));
54
+ const dbDir = join(dir, "data", "db");
55
+ mkdirSync(dbDir, { recursive: true });
56
+ writeFileSync(join(dbDir, "assistant.db"), "fake-db-content");
57
+
58
+ // A spread of files under a few nested directories, each with non-trivial
59
+ // (multi-chunk) content so the read loop iterates more than once.
60
+ const filler = "x".repeat(200 * 1024);
61
+ for (let i = 0; i < fileCount; i++) {
62
+ const sub = join(dir, "data", "files", `dir-${i % 5}`);
63
+ mkdirSync(sub, { recursive: true });
64
+ writeFileSync(join(sub, `file-${i}.txt`), `${filler}\n${i}`);
65
+ }
66
+
67
+ return {
68
+ dir,
69
+ cleanup: () => {
70
+ assertNotLiveDb(dir);
71
+ rmSync(dir, { recursive: true, force: true });
72
+ },
73
+ };
74
+ }
75
+
76
+ describe("streamExportVBundle — descriptor lifecycle", () => {
77
+ test.skipIf(!hasProcFd)(
78
+ "does not leak a file descriptor per exported file",
79
+ async () => {
80
+ const fileCount = 60;
81
+ const workspace = createPopulatedWorkspace(fileCount);
82
+
83
+ // Warm-up export: first run can open and cache descriptors that legitimately
84
+ // persist (loggers, the daemon DB, etc.). Measuring the delta around a
85
+ // second export isolates per-file leakage from one-time setup.
86
+ let warmup: Awaited<ReturnType<typeof streamExportVBundle>> | undefined;
87
+ try {
88
+ warmup = await streamExportVBundle({
89
+ workspaceDir: workspace.dir,
90
+ ...defaultV1Options(),
91
+ });
92
+ } finally {
93
+ await warmup?.cleanup();
94
+ }
95
+
96
+ const before = openFdCount();
97
+
98
+ let result: Awaited<ReturnType<typeof streamExportVBundle>> | undefined;
99
+ try {
100
+ result = await streamExportVBundle({
101
+ workspaceDir: workspace.dir,
102
+ ...defaultV1Options(),
103
+ });
104
+ // Manifest covers every file we wrote — proves the walk actually read
105
+ // them (otherwise a no-op export would trivially leak nothing).
106
+ expect(result.manifest.contents.length).toBeGreaterThanOrEqual(
107
+ fileCount,
108
+ );
109
+ } finally {
110
+ await result?.cleanup();
111
+ workspace.cleanup();
112
+ }
113
+
114
+ const after = openFdCount();
115
+ const delta = after - before;
116
+
117
+ // Each export reads 2×fileCount files (hash pass + tar pass). A per-file
118
+ // leak would push the delta past `fileCount`. Allow a small constant for
119
+ // incidental descriptors (temp output file already cleaned up, jitter).
120
+ expect(delta).toBeLessThan(8);
121
+ },
122
+ );
123
+ });
@@ -11,7 +11,6 @@
11
11
  import { createHash, randomUUID } from "node:crypto";
12
12
  import {
13
13
  closeSync,
14
- createReadStream,
15
14
  createWriteStream,
16
15
  existsSync,
17
16
  lstatSync,
@@ -21,7 +20,7 @@ import {
21
20
  readSync,
22
21
  realpathSync,
23
22
  } from "node:fs";
24
- import { stat, unlink } from "node:fs/promises";
23
+ import { type FileHandle, open, stat, unlink } from "node:fs/promises";
25
24
  import { tmpdir } from "node:os";
26
25
  import { dirname, join, relative, resolve, sep } from "node:path";
27
26
  import { Readable } from "node:stream";
@@ -886,9 +885,10 @@ export function walkDirectoryForMetadata(
886
885
  }
887
886
 
888
887
  /**
889
- * Compute SHA-256 hex digest of a file by streaming never buffers the
890
- * entire file in memory. When `size` is provided, only hashes the first
891
- * `size` bytes to match what will be archived in the tar entry.
888
+ * Compute SHA-256 hex digest of a file by reading it in bounded chunks —
889
+ * never buffers the entire file in memory. When `size` is provided, only
890
+ * hashes the first `size` bytes to match what will be archived in the tar
891
+ * entry.
892
892
  */
893
893
  async function computeFileSha256(
894
894
  filePath: string,
@@ -896,11 +896,28 @@ async function computeFileSha256(
896
896
  ): Promise<string> {
897
897
  const hash = createHash("sha256");
898
898
  if (size === 0) return hash.digest("hex");
899
- const streamOpts =
900
- size !== undefined ? { start: 0, end: size - 1 } : undefined;
901
- const stream = createReadStream(filePath, streamOpts);
902
- for await (const chunk of stream) {
903
- hash.update(chunk);
899
+
900
+ // Read through an explicitly-managed FileHandle so the descriptor is
901
+ // always released in `finally`. This pass opens every file in the
902
+ // workspace, so any per-file descriptor leak here would exhaust the
903
+ // daemon's file-descriptor limit (EMFILE). A bounded read loop keeps peak
904
+ // memory flat regardless of file size.
905
+ const readChunkBytes = 64 * 1024;
906
+ const handle = await open(filePath, "r");
907
+ try {
908
+ const chunk = Buffer.allocUnsafe(readChunkBytes);
909
+ let position = 0;
910
+ for (;;) {
911
+ const remaining = size !== undefined ? size - position : Infinity;
912
+ if (remaining <= 0) break;
913
+ const toRead = Math.min(chunk.length, remaining);
914
+ const { bytesRead } = await handle.read(chunk, 0, toRead, position);
915
+ if (bytesRead === 0) break;
916
+ hash.update(chunk.subarray(0, bytesRead));
917
+ position += bytesRead;
918
+ }
919
+ } finally {
920
+ await handle.close();
904
921
  }
905
922
  return hash.digest("hex");
906
923
  }
@@ -1056,21 +1073,33 @@ async function* generateTarStream(
1056
1073
  // alignment. The WAL checkpoint before export is the primary
1057
1074
  // consistency mechanism for the database.
1058
1075
  let bytesWritten = 0;
1059
- if (file.size > 0) {
1076
+ if (entrySize > 0) {
1077
+ // Read through an explicitly-managed FileHandle so the descriptor is
1078
+ // released in `finally` even when the consumer abandons the generator
1079
+ // mid-file. Each read uses a fresh buffer so a yielded chunk is never
1080
+ // overwritten by the next read before the consumer drains it.
1081
+ let handle: FileHandle | undefined;
1060
1082
  try {
1061
- const stream = createReadStream(file.diskPath, {
1062
- start: 0,
1063
- end: file.size - 1,
1064
- });
1065
- for await (const chunk of stream) {
1066
- const data =
1067
- chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
1068
- bytesWritten += data.length;
1069
- yield data;
1083
+ handle = await open(file.diskPath, "r");
1084
+ const readChunkBytes = 64 * 1024;
1085
+ while (bytesWritten < entrySize) {
1086
+ const toRead = Math.min(readChunkBytes, entrySize - bytesWritten);
1087
+ const buf = Buffer.allocUnsafe(toRead);
1088
+ const { bytesRead } = await handle.read(
1089
+ buf,
1090
+ 0,
1091
+ toRead,
1092
+ bytesWritten,
1093
+ );
1094
+ if (bytesRead === 0) break;
1095
+ bytesWritten += bytesRead;
1096
+ yield bytesRead === buf.length ? buf : buf.subarray(0, bytesRead);
1070
1097
  }
1071
1098
  } catch {
1072
1099
  // File was deleted or rotated between passes — emit zeros for
1073
1100
  // the full declared size so the tar structure stays valid
1101
+ } finally {
1102
+ if (handle) await handle.close();
1074
1103
  }
1075
1104
  }
1076
1105
 
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import type { InteractionResolutionState } from "../api/events/interaction-resolved.js";
22
+ import type { QuestionEntry } from "../api/events/question-request.js";
22
23
  import type { UserDecision } from "../permissions/types.js";
23
24
  import { getLogger } from "../util/logger.js";
24
25
  import { broadcastMessage } from "./assistant-event-hub.js";
@@ -50,6 +51,18 @@ export interface ConfirmationDetails {
50
51
  }>;
51
52
  }
52
53
 
54
+ /**
55
+ * Full batched question payload carried on a pending `question` interaction, so
56
+ * a history-load render can stamp the outstanding prompt back onto its tool
57
+ * call and rehydrate the question card on a cold reconnect. Mirrors the
58
+ * `question_request` event's `questions[]` — `metadata` only retains the
59
+ * `orderedIds`/`optionsById` the response route needs to validate submissions,
60
+ * which is insufficient to reconstruct the card.
61
+ */
62
+ export interface QuestionDetails {
63
+ entries: QuestionEntry[];
64
+ }
65
+
53
66
  export interface PendingInteraction {
54
67
  /**
55
68
  * Owning conversation, when the interaction was raised inside one. Absent
@@ -69,6 +82,8 @@ export interface PendingInteraction {
69
82
  | "host_transfer"
70
83
  | "acp_confirmation";
71
84
  confirmationDetails?: ConfirmationDetails;
85
+ /** For a pending `question`: the full batched entries, so a history-load render can rehydrate the question card. */
86
+ questionDetails?: QuestionDetails;
72
87
  /** For ACP permissions: resolves directly without a Conversation object. */
73
88
  directResolve?: (decision: UserDecision) => void;
74
89
  /** When set, the host_bash request should be routed to this specific client. */
@@ -51,6 +51,7 @@ type ListClientsResponse = {
51
51
  machineName?: string;
52
52
  connectedAt: string;
53
53
  lastActiveAt: string;
54
+ degraded?: boolean;
54
55
  }>;
55
56
  };
56
57
 
@@ -152,4 +153,16 @@ describe("list_clients route — same-user filter", () => {
152
153
  const ids = result.clients.map((c) => c.clientId).sort();
153
154
  expect(ids).toEqual(["client-A1", "client-B1", "client-noprincipal"]);
154
155
  });
156
+
157
+ test("includes a degraded flag (false for a freshly connected client)", () => {
158
+ registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
159
+
160
+ const handler = findHandler("list_clients");
161
+ const result = handler({
162
+ headers: { "x-vellum-actor-principal-id": "user-A" },
163
+ }) as ListClientsResponse;
164
+
165
+ expect(result.clients).toHaveLength(1);
166
+ expect(result.clients[0].degraded).toBe(false);
167
+ });
155
168
  });
@@ -26,6 +26,8 @@ mock.module("../../assistant-event-hub.js", () => ({
26
26
  broadcastMessage: () => {},
27
27
  }));
28
28
 
29
+ import { eq } from "drizzle-orm";
30
+
29
31
  import { getDb } from "../../../memory/db-connection.js";
30
32
  import { initializeDb } from "../../../memory/db-init.js";
31
33
  import { conversations } from "../../../memory/schema.js";
@@ -71,6 +73,10 @@ function findHandler(routes: RouteDefinition[], operationId: string) {
71
73
  return route.handler;
72
74
  }
73
75
 
76
+ const createHandler = findHandler(
77
+ CONVERSATION_MANAGEMENT_ROUTES,
78
+ "createConversation",
79
+ );
74
80
  const putHandler = findHandler(
75
81
  CONVERSATION_MANAGEMENT_ROUTES,
76
82
  "setConversationInferenceProfile",
@@ -112,6 +118,67 @@ function seedConversation(id: string): void {
112
118
  // Tests
113
119
  // ---------------------------------------------------------------------------
114
120
 
121
+ describe("POST /v1/conversations (createConversation)", () => {
122
+ beforeEach(() => {
123
+ clearConversations();
124
+ });
125
+
126
+ function readConversation(id: string) {
127
+ return getDb()
128
+ .select({
129
+ title: conversations.title,
130
+ isAutoTitle: conversations.isAutoTitle,
131
+ })
132
+ .from(conversations)
133
+ .where(eq(conversations.id, id))
134
+ .get();
135
+ }
136
+
137
+ test("with a title → persists it as a user-set title (isAutoTitle = 0)", async () => {
138
+ const result = (await createHandler({
139
+ body: { conversationType: "standard", title: "Setting up your check-in" },
140
+ })) as { id: string; created: boolean };
141
+
142
+ expect(result.created).toBe(true);
143
+ const row = readConversation(result.id);
144
+ expect(row?.title).toBe("Setting up your check-in");
145
+ // isAutoTitle = 0 keeps the async LLM titler from overwriting it.
146
+ expect(row?.isAutoTitle).toBe(0);
147
+ });
148
+
149
+ test("blank title falls back to the replaceable 'New Conversation' placeholder", async () => {
150
+ const result = (await createHandler({
151
+ body: { conversationType: "standard", title: " " },
152
+ })) as { id: string; created: boolean };
153
+
154
+ expect(result.created).toBe(true);
155
+ const row = readConversation(result.id);
156
+ expect(row?.title).toBe("New Conversation");
157
+ // Default auto-title flag (1) leaves it replaceable by the auto-titler.
158
+ expect(row?.isAutoTitle).toBe(1);
159
+ });
160
+
161
+ test("no title → 'New Conversation' placeholder", async () => {
162
+ const result = (await createHandler({
163
+ body: { conversationType: "standard" },
164
+ })) as { id: string; created: boolean };
165
+
166
+ expect(result.created).toBe(true);
167
+ const row = readConversation(result.id);
168
+ expect(row?.title).toBe("New Conversation");
169
+ expect(row?.isAutoTitle).toBe(1);
170
+ });
171
+
172
+ test("non-string title → BadRequestError (not a 500), no row created", () => {
173
+ // The shared route adapter doesn't runtime-validate the body, so the
174
+ // handler must reject a malformed title before `.trim()` throws.
175
+ expect(() =>
176
+ createHandler({ body: { conversationType: "standard", title: 123 } }),
177
+ ).toThrow(/title must be a string/);
178
+ expect(getDb().select().from(conversations).all()).toHaveLength(0);
179
+ });
180
+ });
181
+
115
182
  describe("PUT /v1/conversations/:id/inference-profile", () => {
116
183
  beforeEach(() => {
117
184
  clearConversations();
@@ -34,6 +34,12 @@
34
34
 
35
35
  import { beforeEach, describe, expect, mock, test } from "bun:test";
36
36
 
37
+ import {
38
+ type DiffPluginDeps,
39
+ type DiffPluginOptions,
40
+ type PluginDiffResult,
41
+ PluginDiffUnavailableError,
42
+ } from "../../../cli/lib/diff-plugin.js";
37
43
  import {
38
44
  type InspectPluginDeps,
39
45
  type InspectPluginOptions,
@@ -66,6 +72,7 @@ import {
66
72
  type UninstallPluginResult,
67
73
  } from "../../../cli/lib/uninstall-plugin.js";
68
74
  import {
75
+ PluginMergeBaselineError,
69
76
  PluginNotUpgradableError,
70
77
  type PluginUpgradeResult,
71
78
  type UpgradePluginDeps,
@@ -173,10 +180,28 @@ const upgradeSpy = mock(
173
180
  );
174
181
 
175
182
  mock.module("../../../cli/lib/upgrade-plugin.js", () => ({
183
+ PluginMergeBaselineError,
176
184
  PluginNotUpgradableError,
177
185
  upgradePlugin: upgradeSpy,
178
186
  }));
179
187
 
188
+ // Mock diffPlugin: the lib re-materializes the install commit and computes the
189
+ // per-file unified diff (covered by diff-plugin.test.ts); the route forwards
190
+ // the name and maps the lib's error taxonomy to HTTP status codes.
191
+ const diffSpy = mock(
192
+ async (
193
+ _opts: DiffPluginOptions,
194
+ _deps: DiffPluginDeps,
195
+ ): Promise<PluginDiffResult> => {
196
+ throw new Error("diffSpy default impl not configured");
197
+ },
198
+ );
199
+
200
+ mock.module("../../../cli/lib/diff-plugin.js", () => ({
201
+ PluginDiffUnavailableError,
202
+ diffPlugin: diffSpy,
203
+ }));
204
+
180
205
  import {
181
206
  BadRequestError,
182
207
  ConflictError,
@@ -200,6 +225,7 @@ const getHandler = findHandler("plugins_get");
200
225
  const installHandler = findHandler("plugins_install");
201
226
  const inspectHandler = findHandler("plugins_inspect");
202
227
  const upgradeHandler = findHandler("plugins_upgrade");
228
+ const diffHandler = findHandler("plugins_diff");
203
229
 
204
230
  function invoke(args: RouteHandlerArgs = {}): {
205
231
  plugins: Array<Record<string, unknown>>;
@@ -976,6 +1002,10 @@ function inspection(
976
1002
  }
977
1003
  : overrides.remote,
978
1004
  remoteError: overrides.remoteError ?? null,
1005
+ surfaces:
1006
+ overrides.surfaces === undefined
1007
+ ? { skills: [], hooks: ["post-model-call"], tools: [] }
1008
+ : overrides.surfaces,
979
1009
  };
980
1010
  }
981
1011
 
@@ -1080,6 +1110,9 @@ function upgradeResult(
1080
1110
  target: overrides.target ?? "/workspace/.vellum/plugins/level-up",
1081
1111
  fileCount: overrides.fileCount === undefined ? 12 : overrides.fileCount,
1082
1112
  dryRun: overrides.dryRun ?? false,
1113
+ strategy: overrides.strategy ?? "overwrite",
1114
+ conflicts: overrides.conflicts ?? [],
1115
+ binaryConflicts: overrides.binaryConflicts ?? [],
1083
1116
  provenanceWasUnknown: overrides.provenanceWasUnknown ?? false,
1084
1117
  };
1085
1118
  }
@@ -1094,6 +1127,9 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1094
1127
  target: string;
1095
1128
  fileCount: number | null;
1096
1129
  dryRun: boolean;
1130
+ strategy: string;
1131
+ conflicts: readonly string[];
1132
+ binaryConflicts: readonly string[];
1097
1133
  provenanceWasUnknown: boolean;
1098
1134
  }> {
1099
1135
  return (await upgradeHandler(args)) as {
@@ -1106,6 +1142,9 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1106
1142
  target: string;
1107
1143
  fileCount: number | null;
1108
1144
  dryRun: boolean;
1145
+ strategy: string;
1146
+ conflicts: readonly string[];
1147
+ binaryConflicts: readonly string[];
1109
1148
  provenanceWasUnknown: boolean;
1110
1149
  };
1111
1150
  }
@@ -1136,13 +1175,83 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1136
1175
  target: "/workspace/.vellum/plugins/level-up",
1137
1176
  fileCount: 12,
1138
1177
  dryRun: false,
1178
+ strategy: "overwrite",
1179
+ conflicts: [],
1180
+ binaryConflicts: [],
1139
1181
  provenanceWasUnknown: false,
1140
1182
  });
1141
- // AND the name + dryRun are forwarded to the lib
1183
+ // AND the name + dryRun are forwarded to the lib (strategy omitted)
1142
1184
  expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
1143
1185
  name: "level-up",
1144
1186
  dryRun: false,
1187
+ strategy: undefined,
1188
+ });
1189
+ });
1190
+
1191
+ test("forwards a requested strategy to the lib and projects it", async () => {
1192
+ // GIVEN upgradePlugin merges local edits forward via the `ours` strategy
1193
+ upgradeSpy.mockImplementation(async () =>
1194
+ upgradeResult({ strategy: "ours" }),
1195
+ );
1196
+
1197
+ // WHEN the handler runs with a strategy in the body
1198
+ const result = await invokeUpgrade({
1199
+ pathParams: { name: "level-up" },
1200
+ body: { strategy: "ours" },
1201
+ });
1202
+
1203
+ // THEN the strategy is forwarded to the lib and surfaced on the wire
1204
+ expect(result.strategy).toBe("ours");
1205
+ expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
1206
+ name: "level-up",
1207
+ dryRun: undefined,
1208
+ strategy: "ours",
1209
+ });
1210
+ });
1211
+
1212
+ test("projects conflicts + binaryConflicts from the assistant strategy onto the wire", async () => {
1213
+ // GIVEN upgradePlugin merges with conflict markers under `assistant`
1214
+ upgradeSpy.mockImplementation(async () =>
1215
+ upgradeResult({
1216
+ strategy: "assistant",
1217
+ conflicts: ["hooks/post-model-call.ts"],
1218
+ binaryConflicts: ["assets/icon.png"],
1219
+ }),
1220
+ );
1221
+
1222
+ // WHEN the handler runs with the `assistant` strategy
1223
+ const result = await invokeUpgrade({
1224
+ pathParams: { name: "level-up" },
1225
+ body: { strategy: "assistant" },
1226
+ });
1227
+
1228
+ // THEN the conflicted paths are surfaced for the assistant to resolve
1229
+ expect(result.strategy).toBe("assistant");
1230
+ expect(result.conflicts).toEqual(["hooks/post-model-call.ts"]);
1231
+ expect(result.binaryConflicts).toEqual(["assets/icon.png"]);
1232
+ expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
1233
+ name: "level-up",
1234
+ dryRun: undefined,
1235
+ strategy: "assistant",
1236
+ });
1237
+ });
1238
+
1239
+ test("PluginMergeBaselineError \u2192 ConflictError (409)", async () => {
1240
+ // A merge strategy whose install-time baseline can't be reconstructed: a
1241
+ // well-formed request that isn't actionable in the current state.
1242
+ upgradeSpy.mockImplementation(async () => {
1243
+ throw new PluginMergeBaselineError(
1244
+ "level-up",
1245
+ "the install-time baseline could not be faithfully reconstructed",
1246
+ );
1145
1247
  });
1248
+
1249
+ await expect(
1250
+ invokeUpgrade({
1251
+ pathParams: { name: "level-up" },
1252
+ body: { strategy: "ours" },
1253
+ }),
1254
+ ).rejects.toBeInstanceOf(ConflictError);
1146
1255
  });
1147
1256
 
1148
1257
  test("omits dryRun (passes undefined) when the body flag is absent", async () => {
@@ -1246,3 +1355,133 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1246
1355
  expect((caught as Error).message).toContain("ECONNRESET");
1247
1356
  });
1248
1357
  });
1358
+
1359
+ function diffResult(
1360
+ overrides: Partial<PluginDiffResult> = {},
1361
+ ): PluginDiffResult {
1362
+ return {
1363
+ name: overrides.name ?? "level-up",
1364
+ target: overrides.target ?? "/workspace/.vellum/plugins/level-up",
1365
+ commit: overrides.commit ?? "60a392b0000000000000000000000000000000aa",
1366
+ committedAt: overrides.committedAt ?? "2026-06-01T12:34:56.000Z",
1367
+ clean: overrides.clean ?? false,
1368
+ files: overrides.files ?? [
1369
+ {
1370
+ path: "src/skill.ts",
1371
+ status: "modified",
1372
+ diff: "--- a/src/skill.ts\n+++ b/src/skill.ts\n@@ -1 +1 @@\n-old\n+new\n",
1373
+ binary: false,
1374
+ reconstructed: true,
1375
+ },
1376
+ ],
1377
+ };
1378
+ }
1379
+
1380
+ async function invokeDiff(
1381
+ args: RouteHandlerArgs = {},
1382
+ ): Promise<PluginDiffResult> {
1383
+ return (await diffHandler(args)) as PluginDiffResult;
1384
+ }
1385
+
1386
+ describe("POST /v1/plugins/:name/diff", () => {
1387
+ beforeEach(() => {
1388
+ diffSpy.mockReset();
1389
+ });
1390
+
1391
+ test("forwards the name to diffPlugin and returns the diff verbatim", async () => {
1392
+ // GIVEN diffPlugin reports a single modified file against the install commit
1393
+ const view = diffResult();
1394
+ diffSpy.mockImplementation(async () => view);
1395
+
1396
+ // WHEN the route handler is invoked with the path name
1397
+ const result = await invokeDiff({ pathParams: { name: "level-up" } });
1398
+
1399
+ // THEN the diff is returned unchanged
1400
+ expect(result).toEqual(view);
1401
+ // AND only the name is forwarded to the lib (ref is never caller-supplied)
1402
+ expect(diffSpy.mock.calls[0]?.[0]).toEqual({ name: "level-up" });
1403
+ });
1404
+
1405
+ test("InvalidPluginNameError → BadRequestError (400)", async () => {
1406
+ diffSpy.mockImplementation(async () => {
1407
+ throw new InvalidPluginNameError("../escape");
1408
+ });
1409
+
1410
+ await expect(
1411
+ invokeDiff({ pathParams: { name: "../escape" } }),
1412
+ ).rejects.toBeInstanceOf(BadRequestError);
1413
+ });
1414
+
1415
+ test("PluginNotInstalledError → NotFoundError (404)", async () => {
1416
+ diffSpy.mockImplementation(async () => {
1417
+ throw new PluginNotInstalledError(
1418
+ "ghost",
1419
+ "/workspace/.vellum/plugins/ghost",
1420
+ );
1421
+ });
1422
+
1423
+ await expect(
1424
+ invokeDiff({ pathParams: { name: "ghost" } }),
1425
+ ).rejects.toBeInstanceOf(NotFoundError);
1426
+ });
1427
+
1428
+ test("PluginNotFoundError → NotFoundError (404)", async () => {
1429
+ // The recorded commit no longer resolves to a tree (source/commit gone).
1430
+ diffSpy.mockImplementation(async () => {
1431
+ throw new PluginNotFoundError(
1432
+ "level-up",
1433
+ "deadbeef",
1434
+ "vellum-ai/level-up",
1435
+ );
1436
+ });
1437
+
1438
+ await expect(
1439
+ invokeDiff({ pathParams: { name: "level-up" } }),
1440
+ ).rejects.toBeInstanceOf(NotFoundError);
1441
+ });
1442
+
1443
+ test("PluginDiffUnavailableError → ConflictError (409)", async () => {
1444
+ // The install recorded no commit, so there is no baseline to diff against:
1445
+ // a well-formed request that is not actionable in the current state.
1446
+ diffSpy.mockImplementation(async () => {
1447
+ throw new PluginDiffUnavailableError(
1448
+ "level-up",
1449
+ "no install commit was recorded",
1450
+ );
1451
+ });
1452
+
1453
+ await expect(
1454
+ invokeDiff({ pathParams: { name: "level-up" } }),
1455
+ ).rejects.toBeInstanceOf(ConflictError);
1456
+ });
1457
+
1458
+ test("PluginSourceUnavailableError → ServiceUnavailableError (503)", async () => {
1459
+ // Re-materializing the baseline can hit a rate-limited/down GitHub; that is
1460
+ // retryable, so surface 503 rather than a misleading 500.
1461
+ diffSpy.mockImplementation(async () => {
1462
+ throw new PluginSourceUnavailableError(
1463
+ "git clone failed for vellum-ai/level-up: HTTP 403",
1464
+ 503,
1465
+ );
1466
+ });
1467
+
1468
+ await expect(
1469
+ invokeDiff({ pathParams: { name: "level-up" } }),
1470
+ ).rejects.toBeInstanceOf(ServiceUnavailableError);
1471
+ });
1472
+
1473
+ test("unknown errors → InternalError with original message preserved", async () => {
1474
+ diffSpy.mockImplementation(async () => {
1475
+ throw new Error("ECONNRESET");
1476
+ });
1477
+
1478
+ let caught: unknown;
1479
+ try {
1480
+ await invokeDiff({ pathParams: { name: "level-up" } });
1481
+ } catch (err) {
1482
+ caught = err;
1483
+ }
1484
+ expect(caught).toBeInstanceOf(InternalError);
1485
+ expect((caught as Error).message).toContain("ECONNRESET");
1486
+ });
1487
+ });