@vellumai/assistant 0.5.16 → 0.6.0

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 (407) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +0 -3
  3. package/knip.json +2 -1
  4. package/openapi.yaml +660 -80
  5. package/package.json +1 -1
  6. package/src/__tests__/actor-token-service.test.ts +68 -0
  7. package/src/__tests__/agent-loop.test.ts +0 -32
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  9. package/src/__tests__/anthropic-provider.test.ts +57 -3
  10. package/src/__tests__/app-compiler.test.ts +120 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  12. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  13. package/src/__tests__/call-domain.test.ts +2 -6
  14. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  15. package/src/__tests__/call-recovery.test.ts +2 -6
  16. package/src/__tests__/call-routes-http.test.ts +2 -6
  17. package/src/__tests__/call-store.test.ts +2 -6
  18. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  19. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  20. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  21. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  22. package/src/__tests__/checker.test.ts +25 -3
  23. package/src/__tests__/clawhub.test.ts +54 -24
  24. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  25. package/src/__tests__/cli-memory.test.ts +74 -69
  26. package/src/__tests__/config-schema.test.ts +1 -1
  27. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  28. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  29. package/src/__tests__/contacts-tools.test.ts +31 -0
  30. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  31. package/src/__tests__/context-token-estimator.test.ts +175 -10
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  34. package/src/__tests__/conversation-attachments.test.ts +2 -6
  35. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  36. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  37. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  38. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  39. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  40. package/src/__tests__/conversation-error.test.ts +33 -2
  41. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  42. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  43. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  44. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  45. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  46. package/src/__tests__/conversation-store.test.ts +2 -6
  47. package/src/__tests__/conversation-usage.test.ts +2 -6
  48. package/src/__tests__/conversation-wipe.test.ts +11 -408
  49. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  50. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  51. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  52. package/src/__tests__/followup-tools.test.ts +2 -6
  53. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  54. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  55. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  56. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  57. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  58. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  59. package/src/__tests__/guardian-action-store.test.ts +2 -6
  60. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  61. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  62. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  63. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  64. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  65. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  66. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  67. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  68. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  69. package/src/__tests__/injection-block.test.ts +154 -0
  70. package/src/__tests__/install-meta.test.ts +506 -0
  71. package/src/__tests__/install-skill-routing.test.ts +292 -0
  72. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  73. package/src/__tests__/invite-routes-http.test.ts +2 -6
  74. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  75. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  76. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  77. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  78. package/src/__tests__/llm-usage-store.test.ts +2 -6
  79. package/src/__tests__/log-export-workspace.test.ts +2 -6
  80. package/src/__tests__/managed-store.test.ts +38 -11
  81. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  82. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  83. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  84. package/src/__tests__/non-member-access-request.test.ts +2 -6
  85. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  86. package/src/__tests__/oauth-cli.test.ts +364 -2
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  88. package/src/__tests__/outlook-attachments.test.ts +301 -0
  89. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  90. package/src/__tests__/outlook-categories.test.ts +212 -0
  91. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  92. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  93. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  94. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  95. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  96. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  97. package/src/__tests__/outlook-trash.test.ts +77 -0
  98. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  99. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  100. package/src/__tests__/playbook-execution.test.ts +76 -80
  101. package/src/__tests__/playbook-tools.test.ts +5 -7
  102. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  103. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  104. package/src/__tests__/registry.test.ts +2 -2
  105. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  106. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  107. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  108. package/src/__tests__/schedule-store.test.ts +2 -6
  109. package/src/__tests__/schedule-tools.test.ts +2 -6
  110. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  111. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  112. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  113. package/src/__tests__/search-skills-unified.test.ts +421 -0
  114. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  115. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  116. package/src/__tests__/sequence-store.test.ts +2 -6
  117. package/src/__tests__/server-history-render.test.ts +2 -6
  118. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  119. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  120. package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
  121. package/src/__tests__/skill-memory.test.ts +140 -98
  122. package/src/__tests__/skills-uninstall.test.ts +2 -2
  123. package/src/__tests__/skills.test.ts +1 -1
  124. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  125. package/src/__tests__/task-compiler.test.ts +2 -6
  126. package/src/__tests__/task-management-tools.test.ts +2 -6
  127. package/src/__tests__/task-memory-cleanup.test.ts +173 -229
  128. package/src/__tests__/task-runner.test.ts +2 -6
  129. package/src/__tests__/task-scheduler.test.ts +2 -6
  130. package/src/__tests__/test-preload.ts +3 -0
  131. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  132. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  133. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  134. package/src/__tests__/trust-store.test.ts +1 -1
  135. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  138. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  139. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  140. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  141. package/src/__tests__/usage-routes.test.ts +2 -6
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  143. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  145. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  146. package/src/__tests__/volume-security-guard.test.ts +2 -0
  147. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  148. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  149. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  150. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  151. package/src/__tests__/workspace-policy.test.ts +1 -1
  152. package/src/agent/attachments.ts +7 -2
  153. package/src/agent/image-optimize.ts +165 -0
  154. package/src/agent/loop.ts +1 -15
  155. package/src/bundler/app-compiler.ts +179 -2
  156. package/src/bundler/package-resolver.ts +3 -5
  157. package/src/cli/__tests__/notifications.test.ts +1 -2
  158. package/src/cli/cli-memory.ts +67 -64
  159. package/src/cli/commands/avatar.ts +3 -3
  160. package/src/cli/commands/config.ts +26 -13
  161. package/src/cli/commands/doctor.ts +2 -2
  162. package/src/cli/commands/memory.ts +41 -55
  163. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  164. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  165. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  166. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  167. package/src/cli/commands/oauth/connect.ts +11 -6
  168. package/src/cli/commands/oauth/mode.ts +7 -0
  169. package/src/cli/commands/oauth/shared.ts +39 -3
  170. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  171. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  172. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  173. package/src/cli/commands/platform/index.ts +16 -16
  174. package/src/cli/commands/skills.ts +88 -16
  175. package/src/cli/commands/trust.ts +2 -2
  176. package/src/cli/lib/daemon-credential-client.ts +2 -3
  177. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  178. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  179. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  180. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  181. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  182. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  183. package/src/config/bundled-skills/messaging/SKILL.md +10 -18
  184. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  185. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  186. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  187. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  188. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  189. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  190. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  191. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  192. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  193. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  194. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  195. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  196. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  197. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  198. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  199. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  200. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  201. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  202. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  203. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  204. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  205. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  206. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  207. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  208. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  209. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  210. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  211. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  212. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  213. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  214. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  215. package/src/config/bundled-tool-registry.ts +56 -4
  216. package/src/config/env-registry.ts +15 -8
  217. package/src/config/feature-flag-registry.json +21 -124
  218. package/src/config/schemas/platform.ts +8 -0
  219. package/src/config/schemas/timeouts.ts +1 -1
  220. package/src/config/skills.ts +18 -7
  221. package/src/context/token-estimator.ts +25 -18
  222. package/src/context/window-manager.ts +6 -2
  223. package/src/credential-execution/process-manager.ts +3 -1
  224. package/src/daemon/context-overflow-reducer.ts +46 -2
  225. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  226. package/src/daemon/conversation-agent-loop.ts +96 -61
  227. package/src/daemon/conversation-error.ts +31 -8
  228. package/src/daemon/conversation-lifecycle.ts +33 -0
  229. package/src/daemon/conversation-media-retry.ts +85 -7
  230. package/src/daemon/conversation-notifiers.ts +4 -1
  231. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  232. package/src/daemon/conversation.ts +41 -2
  233. package/src/daemon/daemon-control.ts +8 -2
  234. package/src/daemon/handlers/shared.ts +22 -12
  235. package/src/daemon/handlers/skills.ts +416 -202
  236. package/src/daemon/lifecycle.ts +40 -1
  237. package/src/daemon/main.ts +5 -1
  238. package/src/daemon/message-types/conversations.ts +4 -1
  239. package/src/daemon/message-types/messages.ts +3 -1
  240. package/src/daemon/message-types/skills.ts +97 -36
  241. package/src/daemon/providers-setup.ts +5 -0
  242. package/src/daemon/server.ts +11 -2
  243. package/src/daemon/tool-side-effects.ts +27 -5
  244. package/src/heartbeat/heartbeat-service.ts +1 -0
  245. package/src/hooks/cli.ts +2 -2
  246. package/src/hooks/runner.ts +15 -38
  247. package/src/inbound/platform-callback-registration.ts +14 -14
  248. package/src/memory/admin.ts +11 -45
  249. package/src/memory/conversation-bootstrap.ts +2 -0
  250. package/src/memory/conversation-crud.ts +242 -348
  251. package/src/memory/conversation-group-migration.ts +157 -0
  252. package/src/memory/conversation-queries.ts +4 -2
  253. package/src/memory/db-init.ts +30 -3
  254. package/src/memory/embed.ts +73 -0
  255. package/src/memory/embedding-backend.ts +8 -14
  256. package/src/memory/embedding-runtime-manager.ts +12 -114
  257. package/src/memory/fingerprint.ts +2 -2
  258. package/src/memory/graph/bootstrap.ts +512 -0
  259. package/src/memory/graph/capability-seed.ts +297 -0
  260. package/src/memory/graph/consolidation.ts +691 -0
  261. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  262. package/src/memory/graph/decay.test.ts +208 -0
  263. package/src/memory/graph/decay.ts +195 -0
  264. package/src/memory/graph/extraction-job.ts +69 -0
  265. package/src/memory/graph/extraction.test.ts +936 -0
  266. package/src/memory/graph/extraction.ts +1254 -0
  267. package/src/memory/graph/graph-search.ts +266 -0
  268. package/src/memory/graph/image-ref-utils.ts +29 -0
  269. package/src/memory/graph/injection.test.ts +513 -0
  270. package/src/memory/graph/injection.ts +439 -0
  271. package/src/memory/graph/inspect.ts +534 -0
  272. package/src/memory/graph/narrative.ts +267 -0
  273. package/src/memory/graph/pattern-scan.ts +269 -0
  274. package/src/memory/graph/retriever.ts +1008 -0
  275. package/src/memory/graph/scoring.test.ts +548 -0
  276. package/src/memory/graph/scoring.ts +232 -0
  277. package/src/memory/graph/serendipity.ts +65 -0
  278. package/src/memory/graph/store.test.ts +1050 -0
  279. package/src/memory/graph/store.ts +699 -0
  280. package/src/memory/graph/tool-handlers.ts +426 -0
  281. package/src/memory/graph/tools.ts +141 -0
  282. package/src/memory/graph/triggers.test.ts +487 -0
  283. package/src/memory/graph/triggers.ts +223 -0
  284. package/src/memory/graph/types.ts +271 -0
  285. package/src/memory/group-crud.ts +191 -0
  286. package/src/memory/indexer.ts +37 -19
  287. package/src/memory/job-handlers/cleanup.ts +0 -53
  288. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  289. package/src/memory/job-handlers/embedding.ts +5 -31
  290. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  291. package/src/memory/job-handlers/summarization.ts +32 -17
  292. package/src/memory/job-utils.ts +1 -1
  293. package/src/memory/jobs-store.ts +50 -70
  294. package/src/memory/jobs-worker.ts +147 -112
  295. package/src/memory/message-content.ts +1 -0
  296. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  297. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  298. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  299. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  300. package/src/memory/migrations/index.ts +4 -0
  301. package/src/memory/migrations/registry.ts +8 -0
  302. package/src/memory/qdrant-client.ts +44 -17
  303. package/src/memory/schema/index.ts +1 -0
  304. package/src/memory/schema/memory-graph.ts +139 -0
  305. package/src/memory/search/semantic.ts +47 -91
  306. package/src/memory/task-memory-cleanup.ts +28 -50
  307. package/src/messaging/providers/outlook/adapter.ts +8 -1
  308. package/src/messaging/providers/outlook/client.ts +299 -0
  309. package/src/messaging/providers/outlook/types.ts +118 -0
  310. package/src/notifications/adapters/macos.ts +1 -0
  311. package/src/notifications/copy-composer.ts +9 -0
  312. package/src/notifications/signal.ts +16 -0
  313. package/src/oauth/seed-providers.ts +2 -1
  314. package/src/permissions/checker.ts +24 -3
  315. package/src/permissions/defaults.ts +4 -4
  316. package/src/permissions/workspace-policy.ts +1 -1
  317. package/src/playbooks/playbook-compiler.ts +19 -18
  318. package/src/playbooks/types.ts +4 -3
  319. package/src/prompts/system-prompt.ts +3 -29
  320. package/src/providers/anthropic/client.ts +47 -19
  321. package/src/providers/gemini/client.ts +1 -1
  322. package/src/providers/openai/client.ts +1 -1
  323. package/src/providers/registry.ts +1 -1
  324. package/src/providers/retry.ts +19 -3
  325. package/src/runtime/actor-trust-resolver.ts +5 -1
  326. package/src/runtime/auth/route-policy.ts +7 -0
  327. package/src/runtime/guardian-reply-router.ts +5 -1
  328. package/src/runtime/http-server.ts +23 -3
  329. package/src/runtime/middleware/auth.ts +20 -0
  330. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  331. package/src/runtime/routes/attachment-routes.ts +106 -16
  332. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  333. package/src/runtime/routes/btw-routes.ts +8 -0
  334. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  335. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  336. package/src/runtime/routes/debug-routes.ts +1 -1
  337. package/src/runtime/routes/global-search-routes.ts +21 -19
  338. package/src/runtime/routes/group-routes.ts +207 -0
  339. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  340. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  341. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  342. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  343. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +2 -14
  345. package/src/runtime/routes/memory-item-routes.ts +341 -388
  346. package/src/runtime/routes/schedule-routes.ts +2 -0
  347. package/src/runtime/routes/skills-routes.ts +103 -37
  348. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  349. package/src/schedule/scheduler.ts +8 -1
  350. package/src/security/oauth2.ts +1 -1
  351. package/src/security/secure-keys.ts +4 -8
  352. package/src/shared/provider-env-vars.ts +19 -0
  353. package/src/skills/catalog-cache.ts +5 -0
  354. package/src/skills/catalog-install.ts +15 -14
  355. package/src/skills/clawhub.ts +134 -154
  356. package/src/skills/install-meta.ts +208 -0
  357. package/src/skills/managed-store.ts +27 -16
  358. package/src/skills/skill-memory.ts +152 -77
  359. package/src/skills/skillssh-registry.ts +19 -17
  360. package/src/tasks/task-runner.ts +3 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  362. package/src/tools/browser/runtime-check.ts +3 -1
  363. package/src/tools/memory/register.ts +63 -46
  364. package/src/tools/permission-checker.ts +7 -1
  365. package/src/tools/shared/filesystem/image-read.ts +22 -85
  366. package/src/tools/terminal/safe-env.ts +1 -0
  367. package/src/tools/tool-manifest.ts +3 -3
  368. package/src/util/browser.ts +25 -10
  369. package/src/util/bun-runtime.ts +172 -0
  370. package/src/watcher/providers/outlook-calendar.ts +343 -0
  371. package/src/watcher/providers/outlook.ts +198 -0
  372. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  373. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  374. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  375. package/src/workspace/migrations/registry.ts +6 -0
  376. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  377. package/src/__tests__/journal-context.test.ts +0 -268
  378. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  379. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  380. package/src/__tests__/memory-query-builder.test.ts +0 -59
  381. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  382. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  383. package/src/__tests__/memory-regressions.test.ts +0 -3696
  384. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  385. package/src/daemon/conversation-memory.ts +0 -207
  386. package/src/memory/conversation-starters-cadence.ts +0 -74
  387. package/src/memory/items-extractor.ts +0 -860
  388. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  389. package/src/memory/job-handlers/extraction.ts +0 -40
  390. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  391. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  392. package/src/memory/journal-memory.ts +0 -224
  393. package/src/memory/query-builder.ts +0 -47
  394. package/src/memory/query-expansion.ts +0 -83
  395. package/src/memory/retriever.test.ts +0 -1592
  396. package/src/memory/retriever.ts +0 -1331
  397. package/src/memory/search/formatting.test.ts +0 -140
  398. package/src/memory/search/formatting.ts +0 -262
  399. package/src/memory/search/mmr.ts +0 -139
  400. package/src/memory/search/ranking.ts +0 -15
  401. package/src/memory/search/staleness.ts +0 -40
  402. package/src/memory/search/tier-classifier.ts +0 -18
  403. package/src/memory/search/types.ts +0 -121
  404. package/src/prompts/journal-context.ts +0 -154
  405. package/src/tools/memory/definitions.ts +0 -69
  406. package/src/tools/memory/handlers.test.ts +0 -562
  407. package/src/tools/memory/handlers.ts +0 -434
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Runtime migration for the conversation_groups table and group_id column
3
+ * on the conversations table. Follows the same lazy-initialization pattern
4
+ * as conversation-display-order-migration.ts.
5
+ *
6
+ * group_id is display/organizational metadata, consistent with how
7
+ * display_order and is_pinned are handled via runtime migration rather
8
+ * than the formal migrations/ pipeline.
9
+ */
10
+
11
+ import { getLogger } from "../util/logger.js";
12
+ import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
13
+ import { rawExec, rawGet, rawRun } from "./db.js";
14
+
15
+ const log = getLogger("conversation-store");
16
+
17
+ function isDuplicateColumnError(err: unknown): boolean {
18
+ return err instanceof Error && /duplicate column name:/i.test(err.message);
19
+ }
20
+
21
+ let migrated = false;
22
+
23
+ /**
24
+ * Uses raw BEGIN/COMMIT for the one-time backfill. Must NOT be called
25
+ * for the first time inside a Drizzle db.transaction() block — SQLite
26
+ * does not support nested transactions. In practice this is safe because
27
+ * the migration is triggered by early startup queries (listConversations,
28
+ * batchSetDisplayOrders) before any transaction-wrapped paths run, and
29
+ * the `migrated` flag makes subsequent calls no-ops.
30
+ */
31
+ export function ensureGroupMigration(): void {
32
+ if (migrated) return;
33
+
34
+ // 1. Create groups table if not exists
35
+ rawExec(`
36
+ CREATE TABLE IF NOT EXISTS conversation_groups (
37
+ id TEXT PRIMARY KEY,
38
+ name TEXT NOT NULL,
39
+ sort_position REAL NOT NULL DEFAULT 0,
40
+ is_system_group BOOLEAN NOT NULL DEFAULT FALSE,
41
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
42
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
43
+ )
44
+ `);
45
+
46
+ // 2. Add group_id column if not exists
47
+ // Match existing error-handling pattern from conversation-display-order-migration.ts:
48
+ // only swallow duplicate-column errors, log+throw everything else.
49
+ try {
50
+ rawRun(
51
+ "ALTER TABLE conversations ADD COLUMN group_id TEXT REFERENCES conversation_groups(id) ON DELETE SET NULL",
52
+ );
53
+ } catch (err) {
54
+ if (!isDuplicateColumnError(err)) {
55
+ log.error({ err }, "Failed to add group_id column");
56
+ throw err;
57
+ }
58
+ }
59
+
60
+ // 3. Seed system groups (three: pinned, scheduled, background)
61
+ rawExec(`
62
+ INSERT OR IGNORE INTO conversation_groups (id, name, sort_position, is_system_group)
63
+ VALUES
64
+ ('system:pinned', 'Pinned', 0, TRUE),
65
+ ('system:scheduled', 'Scheduled', 1, TRUE),
66
+ ('system:background', 'Background', 2, TRUE)
67
+ `);
68
+
69
+ // 4. One-time backfill (guard: persistent marker prevents re-running on restart)
70
+ //
71
+ // The backfill sets group_id on existing conversations based on their attributes.
72
+ // It must only run once — re-running would overwrite user-explicit ungrouping
73
+ // (e.g., user removes a conversation from a group → group_id = NULL → next restart
74
+ // would backfill it back into system:background). A persistent marker row in
75
+ // conversation_groups tracks whether backfill has already completed.
76
+ const backfillDone = rawGet<{ id: string }>(
77
+ "SELECT id FROM conversation_groups WHERE id = '_backfill_complete'",
78
+ );
79
+
80
+ if (!backfillDone) {
81
+ ensureDisplayOrderMigration(); // ensure is_pinned column exists first
82
+
83
+ // Canonical classification rules (full decision tree with precedence):
84
+ //
85
+ // 1. Pinned: is_pinned = TRUE
86
+ // → system:pinned (always wins, checked first)
87
+ //
88
+ // 2. Scheduled: source IN ('schedule', 'reminder') OR schedule_job_id IS NOT NULL
89
+ // → system:scheduled (checked second)
90
+ //
91
+ // 3. Background: (conversation_type = 'background' AND COALESCE(source, '') != 'notification')
92
+ // OR source IN ('heartbeat', 'task')
93
+ // AND COALESCE(source, '') NOT IN ('schedule', 'reminder') ← safety + NULL handling
94
+ // AND schedule_job_id IS NULL ← safety: prevents overlap with step 2
95
+ // → system:background
96
+ //
97
+ // 4. Else: ungrouped (group_id = NULL)
98
+ //
99
+ // Each backfill step uses AND group_id IS NULL to avoid reassignment.
100
+ // The exclusion guards in step 3 are belt-and-suspenders — step 2's
101
+ // group_id IS NULL guard already prevents overlap, but the explicit
102
+ // exclusions make the SQL self-documenting and migration-order-independent.
103
+ // COALESCE handles NULL source values (legacy rows).
104
+
105
+ // Wrap all backfill steps in a transaction so a partial failure
106
+ // (e.g. crash mid-backfill) doesn't leave conversations half-classified
107
+ // with the marker row missing, which would cause a re-run to skip
108
+ // already-classified rows (group_id IS NULL guard).
109
+ try {
110
+ rawExec("BEGIN");
111
+
112
+ // Step A: Pinned -> system:pinned (runs first, always wins)
113
+ rawExec(`
114
+ UPDATE conversations SET group_id = 'system:pinned'
115
+ WHERE is_pinned = 1 AND group_id IS NULL
116
+ `);
117
+
118
+ // Step B: Scheduled -> system:scheduled (schedule/reminder source or has schedule_job_id)
119
+ rawExec(`
120
+ UPDATE conversations SET group_id = 'system:scheduled'
121
+ WHERE (source IN ('schedule', 'reminder') OR schedule_job_id IS NOT NULL)
122
+ AND group_id IS NULL
123
+ `);
124
+
125
+ // Step C: Background -> system:background (background type, heartbeat, or task — excluding notifications)
126
+ // Explicit exclusion of schedule sources + schedule_job_id as a safety net.
127
+ // Step B already catches those via group_id IS NULL guard, but the explicit exclusion
128
+ // makes the SQL self-documenting and protects against future migration ordering changes.
129
+ // Note: source can be NULL for legacy rows. NULL != 'notification' evaluates to NULL (not TRUE)
130
+ // in SQL, so use COALESCE to treat NULL source as empty string for the exclusion checks.
131
+ rawExec(`
132
+ UPDATE conversations SET group_id = 'system:background'
133
+ WHERE (
134
+ (conversation_type = 'background' AND COALESCE(source, '') != 'notification')
135
+ OR source IN ('heartbeat', 'task')
136
+ )
137
+ AND COALESCE(source, '') NOT IN ('schedule', 'reminder')
138
+ AND schedule_job_id IS NULL
139
+ AND group_id IS NULL
140
+ `);
141
+
142
+ // Mark backfill as complete so it won't re-run on future process restarts
143
+ rawExec(`
144
+ INSERT OR IGNORE INTO conversation_groups (id, name, sort_position, is_system_group)
145
+ VALUES ('_backfill_complete', '_backfill_complete', -1, TRUE)
146
+ `);
147
+
148
+ rawExec("COMMIT");
149
+ } catch (err) {
150
+ rawExec("ROLLBACK");
151
+ log.error({ err }, "Group backfill transaction failed, rolled back");
152
+ throw err;
153
+ }
154
+ }
155
+
156
+ migrated = true;
157
+ }
@@ -4,6 +4,7 @@ import { getLogger } from "../util/logger.js";
4
4
  import type { ConversationRow } from "./conversation-crud.js";
5
5
  import { parseConversation } from "./conversation-crud.js";
6
6
  import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
7
+ import { ensureGroupMigration } from "./conversation-group-migration.js";
7
8
  import { getDb, rawAll } from "./db.js";
8
9
  import { conversations, messages } from "./schema.js";
9
10
 
@@ -13,7 +14,7 @@ const log = getLogger("conversation-store");
13
14
  * Build an FTS5 MATCH query string from natural text by extracting tokens.
14
15
  * Used for messages_fts full-text search over conversation content.
15
16
  */
16
- function buildFtsMatchQuery(text: string): string | null {
17
+ export function buildFtsMatchQuery(text: string): string | null {
17
18
  const tokens = text
18
19
  .toLowerCase()
19
20
  .split(/[^a-z0-9_]+/g)
@@ -30,6 +31,7 @@ export function listConversations(
30
31
  offset = 0,
31
32
  ): ConversationRow[] {
32
33
  ensureDisplayOrderMigration();
34
+ ensureGroupMigration();
33
35
  const db = getDb();
34
36
  const where = backgroundOnly
35
37
  ? sql`${conversations.conversationType} = 'background'`
@@ -309,7 +311,7 @@ export function searchConversations(
309
311
  * occurrence of `query`. The content may be JSON (content blocks) or plain
310
312
  * text; we extract a readable snippet in either case.
311
313
  */
312
- function buildExcerpt(rawContent: string, query: string): string {
314
+ export function buildExcerpt(rawContent: string, query: string): string {
313
315
  // Try to extract plain text from JSON content blocks first.
314
316
  let text = rawContent;
315
317
  try {
@@ -11,6 +11,7 @@ import { dirname, join } from "node:path";
11
11
 
12
12
  import { ensureDataDir, getDbPath } from "../util/platform.js";
13
13
  import { getDb, getSqlite } from "./db-connection.js";
14
+ import { migrateToolCreatedItems } from "./graph/bootstrap.js";
14
15
  import {
15
16
  addCoreColumns,
16
17
  createActorRefreshTokenRecordsTable,
@@ -57,6 +58,7 @@ import {
57
58
  migrateContactsUserFileColumn,
58
59
  migrateConversationForkLineage,
59
60
  migrateConversationsThreadTypeIndex,
61
+ migrateCreateMemoryGraphTables,
60
62
  migrateCreateMemoryRecallLogs,
61
63
  migrateCreateThreadStartersTable,
62
64
  migrateCreateTraceEventsTable,
@@ -69,6 +71,7 @@ import {
69
71
  migrateDropEntityTables,
70
72
  migrateDropLegacyMemberGuardianTables,
71
73
  migrateDropLoopbackPortColumn,
74
+ migrateDropMemoryItemsTables,
72
75
  migrateDropMemorySegmentFts,
73
76
  migrateDropOrphanedMediaTables,
74
77
  migrateDropRemindersTable,
@@ -91,6 +94,7 @@ import {
91
94
  migrateInviteContactId,
92
95
  migrateLlmRequestLogMessageId,
93
96
  migrateLlmRequestLogProvider,
97
+ migrateMemoryGraphImageRefs,
94
98
  migrateMemoryItemSupersession,
95
99
  migrateMessagesConversationCreatedAtIndex,
96
100
  migrateMessagesFtsBackfill,
@@ -111,6 +115,7 @@ import {
111
115
  migrateRenameGmailProviderKeyToGoogle,
112
116
  migrateRenameGuardianVerificationValues,
113
117
  migrateRenameInboxThreadStateTable,
118
+ migrateRenameMemoryGraphTypeValues,
114
119
  migrateRenameNotificationThreadColumns,
115
120
  migrateRenameSequenceEnrollmentsThreadIdColumn,
116
121
  migrateRenameSequenceStepsReplyKey,
@@ -139,8 +144,8 @@ import {
139
144
  // ---------------------------------------------------------------------------
140
145
 
141
146
  function getTemplateDbPath(): string {
142
- // Hash this file + all migration files so the template auto-invalidates
143
- // when any migration changes.
147
+ // Hash this file + all migration files + bootstrap migration so the template
148
+ // auto-invalidates when any migration changes.
144
149
  const thisFile = new URL(import.meta.url).pathname;
145
150
  const hash = createHash("md5");
146
151
  hash.update(readFileSync(thisFile, "utf-8"));
@@ -150,9 +155,15 @@ function getTemplateDbPath(): string {
150
155
  hash.update(readFileSync(join(migrationsDir, name), "utf-8"));
151
156
  }
152
157
  }
158
+ // Include the bootstrap migration (migrateToolCreatedItems) which also runs
159
+ // during initializeDb but lives outside the migrations/ directory.
160
+ const bootstrapFile = join(dirname(thisFile), "graph", "bootstrap.ts");
161
+ if (existsSync(bootstrapFile)) {
162
+ hash.update(readFileSync(bootstrapFile, "utf-8"));
163
+ }
153
164
  return join(
154
165
  tmpdir(),
155
- `vellum-test-db-template-${hash.digest("hex").slice(0, 12)}.db`,
166
+ `vellum-test-db-template-${hash.digest("hex").slice(0, 12)}.db`
156
167
  );
157
168
  }
158
169
 
@@ -549,6 +560,22 @@ export function initializeDb(): void {
549
560
  // (transport is now chosen per-flow via the callbackTransport option, not per-provider)
550
561
  migrateDropCallbackTransportColumn(database);
551
562
 
563
+ // 101. Memory graph tables (nodes, edges, triggers)
564
+ migrateCreateMemoryGraphTables(database);
565
+
566
+ // 101b. Migrate tool-created items from legacy memory_items → graph nodes
567
+ // MUST run before migration 102 drops memory_items so data is preserved.
568
+ migrateToolCreatedItems();
569
+
570
+ // 102. Drop legacy memory_items and memory_item_sources tables (migrated to memory_graph_nodes)
571
+ migrateDropMemoryItemsTables(database);
572
+
573
+ // 103. Rename legacy memory graph node type values: style → behavioral, relationship → semantic
574
+ migrateRenameMemoryGraphTypeValues(database);
575
+
576
+ // 104. Add nullable image_refs TEXT column to memory_graph_nodes
577
+ migrateMemoryGraphImageRefs(database);
578
+
552
579
  validateMigrationState(database);
553
580
 
554
581
  if (process.env.BUN_TEST === "1") {
@@ -0,0 +1,73 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared embedding utility with retry + exponential backoff
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { AssistantConfig } from "../config/types.js";
6
+ import { getLogger } from "../util/logger.js";
7
+ import {
8
+ abortableSleep,
9
+ computeRetryDelay,
10
+ isRetryableNetworkError,
11
+ } from "../util/retry.js";
12
+ import { type EmbeddingInput, embedWithBackend } from "./embedding-backend.js";
13
+
14
+ const log = getLogger("memory-embed");
15
+
16
+ const EMBED_MAX_RETRIES = 3;
17
+ const EMBED_BASE_DELAY_MS = 500;
18
+
19
+ /**
20
+ * Wrap embedWithBackend with retry + exponential backoff for transient failures
21
+ * (network errors, 429s, 5xx). Aborts immediately if the caller's signal fires.
22
+ */
23
+ export async function embedWithRetry(
24
+ config: AssistantConfig,
25
+ texts: EmbeddingInput[],
26
+ opts?: { signal?: AbortSignal },
27
+ ): ReturnType<typeof embedWithBackend> {
28
+ let lastError: unknown;
29
+ for (let attempt = 0; attempt <= EMBED_MAX_RETRIES; attempt++) {
30
+ try {
31
+ return await embedWithBackend(config, texts, opts);
32
+ } catch (err) {
33
+ lastError = err;
34
+ if (opts?.signal?.aborted || isAbortError(err)) throw err;
35
+ const isTransient =
36
+ isRetryableNetworkError(err) || isHttpStatusError(err);
37
+ if (!isTransient || attempt === EMBED_MAX_RETRIES) throw err;
38
+ const delay = computeRetryDelay(attempt, EMBED_BASE_DELAY_MS);
39
+ log.warn(
40
+ { err, attempt: attempt + 1, delayMs: Math.round(delay) },
41
+ "Transient embedding failure, retrying",
42
+ );
43
+ await abortableSleep(delay, opts?.signal);
44
+ if (opts?.signal?.aborted) throw err;
45
+ }
46
+ }
47
+ throw lastError;
48
+ }
49
+
50
+ function isAbortError(err: unknown): boolean {
51
+ if (!(err instanceof Error)) return false;
52
+ return err.name === "AbortError" || err.name === "APIUserAbortError";
53
+ }
54
+
55
+ function getErrorStatusCode(err: Error): unknown {
56
+ if ("status" in err) {
57
+ const status = (err as { status: unknown }).status;
58
+ if (status != null) return status;
59
+ }
60
+ if ("statusCode" in err) return (err as { statusCode: unknown }).statusCode;
61
+ return undefined;
62
+ }
63
+
64
+ function isHttpStatusError(err: unknown): boolean {
65
+ if (!(err instanceof Error)) return false;
66
+ const status = getErrorStatusCode(err);
67
+ if (typeof status === "number") {
68
+ return status === 429 || (status >= 500 && status < 600);
69
+ }
70
+ return /\b429\b|(?:failed|error)\s*\((?:429|5\d{2})\)|(?:status|http)\s*(?:code\s*)?:?\s*5\d{2}\b/i.test(
71
+ err.message,
72
+ );
73
+ }
@@ -650,27 +650,21 @@ async function selectFallbackBackends(
650
650
  }
651
651
 
652
652
  /**
653
- * Returns true when the embedding pipeline can handle multimodal inputs
654
- * (images, audio, video). Today only Gemini supports multimodal.
653
+ * Returns true when the active (primary) embedding backend can handle
654
+ * multimodal inputs (images, audio, video). Today only Gemini supports
655
+ * multimodal.
655
656
  *
656
- * In auto mode, the primary backend is usually local or OpenAI, but
657
- * embedWithBackend builds a fallback chain that includes Gemini when
658
- * available. We check both the primary and fallback backends so that
659
- * multimodal jobs are still enqueued when Gemini is reachable via fallback.
657
+ * Only returns true when Gemini is the primary selected backend not when
658
+ * it's merely available as a fallback. Writing multimodal embeddings via a
659
+ * fallback provider while queries go through the primary text backend would
660
+ * mix incompatible vector spaces, making retrieval unreliable.
660
661
  */
661
662
  export async function selectedBackendSupportsMultimodal(
662
663
  config: AssistantConfig,
663
664
  ): Promise<boolean> {
664
665
  const { backend } = await selectEmbeddingBackend(config);
665
666
  if (!backend) return false;
666
- if (backend.provider === "gemini") return true;
667
-
668
- // In auto mode, check if Gemini is available as a fallback backend.
669
- if (config.memory.embeddings.provider === "auto") {
670
- const fallbacks = await selectFallbackBackends(config, backend.provider);
671
- return fallbacks.some((fb) => fb.provider === "gemini");
672
- }
673
- return false;
667
+ return backend.provider === "gemini";
674
668
  }
675
669
 
676
670
  async function isOllamaConfigured(config: AssistantConfig): Promise<boolean> {
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  import {
17
- chmodSync,
18
17
  existsSync,
19
18
  mkdirSync,
20
19
  readdirSync,
@@ -26,6 +25,7 @@ import {
26
25
  import { arch, platform } from "node:os";
27
26
  import { join } from "node:path";
28
27
 
28
+ import { ensureBun, findBun } from "../util/bun-runtime.js";
29
29
  import { getLogger } from "../util/logger.js";
30
30
  import { getEmbeddingModelsDir } from "../util/platform.js";
31
31
  import { PromiseGuard } from "../util/promise-guard.js";
@@ -38,9 +38,6 @@ const ONNXRUNTIME_COMMON_VERSION = "1.21.0";
38
38
  const TRANSFORMERS_VERSION = "3.8.1";
39
39
  const JINJA_VERSION = "0.5.5";
40
40
 
41
- /** Bun version to download when system bun is not available. */
42
- const BUN_VERSION = "1.2.0";
43
-
44
41
  /** Composite version string for cache invalidation. */
45
42
  const RUNTIME_VERSION = `ort-${ONNXRUNTIME_NODE_VERSION}_hf-${TRANSFORMERS_VERSION}_jinja-${JINJA_VERSION}`;
46
43
 
@@ -200,25 +197,15 @@ export class EmbeddingRuntimeManager {
200
197
 
201
198
  /**
202
199
  * Find a usable bun binary.
203
- * Checks: downloaded copy common install locations.
200
+ * Delegates to the shared bun-runtime helper, also checking
201
+ * the legacy per-runtime bin/ directory for backwards compat.
204
202
  */
205
203
  getBunPath(): string | undefined {
206
- // 1. Downloaded bun
207
- const downloadedBun = join(this.baseDir, "bin", "bun");
208
- if (existsSync(downloadedBun)) return downloadedBun;
209
-
210
- // 2. Common installation paths — the compiled daemon inherits a
211
- // restricted PATH, so we check well-known prefixes directly.
212
- const home = process.env.HOME ?? "";
213
- for (const p of [
214
- join(home, ".bun", "bin", "bun"),
215
- "/opt/homebrew/bin/bun",
216
- "/usr/local/bin/bun",
217
- ]) {
218
- if (existsSync(p)) return p;
219
- }
204
+ // Check per-runtime bin/ directory for backwards compat
205
+ const legacyBun = join(this.baseDir, "bin", "bun");
206
+ if (existsSync(legacyBun)) return legacyBun;
220
207
 
221
- return undefined;
208
+ return findBun();
222
209
  }
223
210
 
224
211
  /**
@@ -301,12 +288,10 @@ export class EmbeddingRuntimeManager {
301
288
 
302
289
  // Declared outside try so catch/finally can reference them for cleanup
303
290
  const modelCacheDir = join(this.baseDir, "model-cache");
304
- const existingBinDir = join(this.baseDir, "bin");
305
291
  let tmpModelCache: string | null = null;
306
- let tmpBinDir: string | null = null;
307
292
 
308
293
  try {
309
- // Step 1: Download npm packages (and bun if needed) in parallel
294
+ // Step 1: Download npm packages in parallel
310
295
  const nodeModules = join(tmpDir, "node_modules");
311
296
  const downloads: Promise<void>[] = [
312
297
  downloadAndExtract(
@@ -331,13 +316,11 @@ export class EmbeddingRuntimeManager {
331
316
  ),
332
317
  ];
333
318
 
334
- // Download bun binary if not already available on the system
335
- if (!this.getBunPath()) {
336
- downloads.push(this.downloadBunBinary(tmpDir, signal));
337
- }
338
-
339
319
  await Promise.all(downloads);
340
320
 
321
+ // Ensure bun is available (downloads to shared location if needed)
322
+ await ensureBun();
323
+
341
324
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
342
325
 
343
326
  log.info("npm packages downloaded, stripping non-platform binaries");
@@ -411,7 +394,7 @@ export class EmbeddingRuntimeManager {
411
394
  );
412
395
 
413
396
  // Step 6: Atomic swap — remove old install and rename temp to final
414
- // Preserve model-cache/, bin/ (downloaded bun), and .gitignore
397
+ // Preserve model-cache/ and .gitignore
415
398
  const hadModelCache = existsSync(modelCacheDir);
416
399
  if (hadModelCache) {
417
400
  tmpModelCache = join(
@@ -421,14 +404,6 @@ export class EmbeddingRuntimeManager {
421
404
  renameSync(modelCacheDir, tmpModelCache);
422
405
  }
423
406
 
424
- // Preserve downloaded bun binary if it exists and we didn't just download a new one
425
- const newBinDir = join(tmpDir, "bin");
426
- const hadBinDir = existsSync(existingBinDir) && !existsSync(newBinDir);
427
- if (hadBinDir) {
428
- tmpBinDir = join(this.baseDir, `.bin-preserve-${Date.now()}`);
429
- renameSync(existingBinDir, tmpBinDir);
430
- }
431
-
432
407
  // Remove old install (preserving dotfiles like .gitignore, .downloading, temp dirs)
433
408
  for (const entry of readdirSync(this.baseDir)) {
434
409
  if (entry.startsWith(".") || entry === tmpDir.split("/").pop())
@@ -446,11 +421,6 @@ export class EmbeddingRuntimeManager {
446
421
  renameSync(tmpModelCache, modelCacheDir);
447
422
  }
448
423
 
449
- // Restore bun binary
450
- if (tmpBinDir && existsSync(tmpBinDir)) {
451
- renameSync(tmpBinDir, existingBinDir);
452
- }
453
-
454
424
  log.info(
455
425
  { runtimeVersion: RUNTIME_VERSION },
456
426
  "Embedding runtime installed successfully",
@@ -468,13 +438,6 @@ export class EmbeddingRuntimeManager {
468
438
  /* best effort */
469
439
  }
470
440
  }
471
- if (tmpBinDir && existsSync(tmpBinDir) && !existsSync(existingBinDir)) {
472
- try {
473
- renameSync(tmpBinDir, existingBinDir);
474
- } catch {
475
- /* best effort */
476
- }
477
- }
478
441
  log.error({ err }, "Failed to install embedding runtime");
479
442
  throw err;
480
443
  } finally {
@@ -482,72 +445,7 @@ export class EmbeddingRuntimeManager {
482
445
  rmSync(tmpDir, { recursive: true, force: true });
483
446
  if (tmpModelCache)
484
447
  rmSync(tmpModelCache, { recursive: true, force: true });
485
- if (tmpBinDir) rmSync(tmpBinDir, { recursive: true, force: true });
486
- }
487
- }
488
-
489
- private async downloadBunBinary(
490
- installDir: string,
491
- signal?: AbortSignal,
492
- ): Promise<void> {
493
- const os = platform();
494
- const cpu = arch() === "arm64" ? "aarch64" : arch();
495
- const target = `${os}-${cpu}`;
496
- const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-${target}.zip`;
497
-
498
- log.info(
499
- { url, target, bunVersion: BUN_VERSION },
500
- "Downloading bun binary",
501
- );
502
-
503
- const response = await fetch(url, {
504
- signal,
505
- redirect: "follow",
506
- });
507
- if (!response.ok) {
508
- throw new Error(
509
- `Failed to download bun: ${response.status} ${response.statusText}`,
510
- );
511
- }
512
-
513
- const zipData = await response.arrayBuffer();
514
- const binDir = join(installDir, "bin");
515
- mkdirSync(binDir, { recursive: true });
516
-
517
- const tmpZip = join(binDir, `bun-download-${Date.now()}.zip`);
518
- writeFileSync(tmpZip, Buffer.from(zipData));
519
-
520
- try {
521
- // Extract zip
522
- const proc = Bun.spawn({
523
- cmd: ["unzip", "-o", tmpZip, "-d", binDir],
524
- stdout: "ignore",
525
- stderr: "pipe",
526
- });
527
- await proc.exited;
528
- if (proc.exitCode !== 0) {
529
- const stderr = await new Response(proc.stderr).text();
530
- throw new Error(`Failed to extract bun zip: ${stderr}`);
531
- }
532
-
533
- // Move binary from bun-{target}/bun to bin/bun
534
- const extractedBun = join(binDir, `bun-${target}`, "bun");
535
- if (existsSync(extractedBun)) {
536
- renameSync(extractedBun, join(binDir, "bun"));
537
- rmSync(join(binDir, `bun-${target}`), { recursive: true, force: true });
538
- }
539
-
540
- // Make executable
541
- chmodSync(join(binDir, "bun"), 0o755);
542
- } finally {
543
- try {
544
- rmSync(tmpZip);
545
- } catch {
546
- /* ignore */
547
- }
548
448
  }
549
-
550
- log.info("Bun binary downloaded successfully");
551
449
  }
552
450
 
553
451
  private readManifest(): VersionManifest | null {
@@ -5,8 +5,8 @@ import { createHash } from "node:crypto";
5
5
  *
6
6
  * Format: sha256(`${scopeId}|${kind}|${subject}|${statement}`)
7
7
  *
8
- * All writers (memory_manage save/update ops, items-extractor, playbook-create,
9
- * playbook-update, gmail-analyze-style) MUST use this function so the
8
+ * All writers (memory_manage save/update ops, items-extractor,
9
+ * gmail-analyze-style) MUST use this function so the
10
10
  * fingerprint scheme stays consistent and deduplication works correctly.
11
11
  */
12
12
  export function computeMemoryFingerprint(