@vellumai/assistant 0.6.1 → 0.6.3

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 (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,75 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { AssistantConfig } from "../config/types.js";
4
+ import {
5
+ clearEmbeddingBackendCache,
6
+ resetLocalEmbeddingFailureState,
7
+ selectEmbeddingBackend,
8
+ } from "./embedding-backend.js";
9
+
10
+ const LOCAL_CONFIG = {
11
+ memory: {
12
+ embeddings: {
13
+ provider: "local",
14
+ localModel: "BAAI/bge-small-en-v1.5",
15
+ },
16
+ },
17
+ } as unknown as AssistantConfig;
18
+
19
+ describe("embedding backend cache invalidation", () => {
20
+ afterEach(() => {
21
+ clearEmbeddingBackendCache();
22
+ });
23
+
24
+ test("clearEmbeddingBackendCache disposes cached backends before clearing", async () => {
25
+ const firstSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
26
+ expect(firstSelection.backend).not.toBeNull();
27
+
28
+ const dispose = mock();
29
+ (firstSelection.backend as { dispose?: () => void }).dispose = dispose;
30
+
31
+ clearEmbeddingBackendCache();
32
+
33
+ expect(dispose).toHaveBeenCalledTimes(1);
34
+
35
+ const secondSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
36
+ expect(secondSelection.backend).not.toBe(firstSelection.backend);
37
+ });
38
+
39
+ test("resetLocalEmbeddingFailureState preserves live cached backends", async () => {
40
+ const firstSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
41
+ expect(firstSelection.backend).not.toBeNull();
42
+
43
+ const dispose = mock();
44
+ (firstSelection.backend as { dispose?: () => void }).dispose = dispose;
45
+
46
+ resetLocalEmbeddingFailureState();
47
+
48
+ expect(dispose).not.toHaveBeenCalled();
49
+
50
+ const secondSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
51
+ expect(secondSelection.backend).toBe(firstSelection.backend);
52
+ });
53
+
54
+ test("resetLocalEmbeddingFailureState clears poisoned local backend retry state", async () => {
55
+ const firstSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
56
+ expect(firstSelection.backend).not.toBeNull();
57
+
58
+ const poisonedInitPromise = Promise.reject(new Error("poisoned"));
59
+ poisonedInitPromise.catch(() => {});
60
+
61
+ const backend = firstSelection.backend as unknown as {
62
+ delegate: unknown;
63
+ initPromise: Promise<unknown> | null;
64
+ };
65
+ backend.delegate = null;
66
+ backend.initPromise = poisonedInitPromise;
67
+
68
+ resetLocalEmbeddingFailureState();
69
+
70
+ expect(backend.initPromise).toBeNull();
71
+
72
+ const secondSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
73
+ expect(secondSelection.backend).toBe(firstSelection.backend);
74
+ });
75
+ });
@@ -1,7 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
3
4
  import { getOllamaBaseUrlEnv } from "../config/env.js";
4
5
  import type { AssistantConfig } from "../config/types.js";
6
+ import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
7
+ import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
5
8
  import { getProviderKeyAsync } from "../security/secure-keys.js";
6
9
  import { getLogger } from "../util/logger.js";
7
10
  import { GeminiEmbeddingBackend } from "./embedding-gemini.js";
@@ -67,6 +70,16 @@ class LazyLocalEmbeddingBackend implements EmbeddingBackend {
67
70
  }
68
71
  }
69
72
 
73
+ dispose(): void {
74
+ this.delegate?.dispose?.();
75
+ }
76
+
77
+ resetForRetry(): void {
78
+ if (!this.delegate) {
79
+ this.initPromise = null;
80
+ }
81
+ }
82
+
70
83
  private async getDelegate(): Promise<EmbeddingBackend> {
71
84
  if (this.delegate) return this.delegate;
72
85
  if (!this.initPromise) {
@@ -176,12 +189,32 @@ function putInVectorCache(
176
189
 
177
190
  /** Clear cached embedding backends and the in-memory vector cache. */
178
191
  export function clearEmbeddingBackendCache(): void {
192
+ for (const backend of new Set(backendCache.values())) {
193
+ try {
194
+ backend.dispose?.();
195
+ } catch (err) {
196
+ log.warn(
197
+ { err, provider: backend.provider, model: backend.model },
198
+ "Failed to dispose embedding backend during cache clear",
199
+ );
200
+ }
201
+ }
179
202
  backendCache.clear();
180
203
  vectorCache.clear();
181
204
  vectorCacheBytes = 0;
182
205
  localBackendBroken = false;
183
206
  }
184
207
 
208
+ /** Reset the sticky local-backend failure flag without evicting live backends. */
209
+ export function resetLocalEmbeddingFailureState(): void {
210
+ localBackendBroken = false;
211
+ for (const backend of new Set(backendCache.values())) {
212
+ if (backend instanceof LazyLocalEmbeddingBackend) {
213
+ backend.resetForRetry();
214
+ }
215
+ }
216
+ }
217
+
185
218
  function cacheKey(provider: string, model: string, extras?: string[]): string {
186
219
  if (extras && extras.length > 0) {
187
220
  return `${provider}:${model}:${extras.join(":")}`;
@@ -243,6 +276,7 @@ export interface EmbeddingBackend {
243
276
  inputs: EmbeddingInput[],
244
277
  options?: EmbeddingRequestOptions,
245
278
  ): Promise<number[][]>;
279
+ dispose?(): void;
246
280
  }
247
281
 
248
282
  export interface EmbeddingBackendSelection {
@@ -280,6 +314,44 @@ export async function selectEmbeddingBackend(
280
314
  };
281
315
  }
282
316
 
317
+ // When the managed-gemini-embeddings-enabled flag is on AND managed proxy
318
+ // prerequisites are satisfied, insert managed-proxy Gemini at the front of
319
+ // the auto chain so platform assistants use Vellum-managed Gemini embeddings.
320
+ if (
321
+ (requested === "auto" || requested === "gemini") &&
322
+ isAssistantFeatureFlagEnabled("managed-gemini-embeddings-enabled", config)
323
+ ) {
324
+ const proxyCtx = await resolveManagedProxyContext();
325
+ if (proxyCtx.enabled) {
326
+ const meta = MANAGED_PROVIDER_META["gemini"];
327
+ if (meta?.managed && meta.proxyPath) {
328
+ const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
329
+ const managedModel = config.memory.embeddings.geminiModel;
330
+ const managedDimensions =
331
+ config.memory.embeddings.geminiDimensions ?? 3072;
332
+ const extras = geminiCacheExtras(config);
333
+ return {
334
+ backend: getCachedOrCreate(
335
+ "gemini",
336
+ managedModel,
337
+ () =>
338
+ new GeminiEmbeddingBackend(
339
+ proxyCtx.assistantApiKey,
340
+ managedModel,
341
+ {
342
+ taskType: config.memory.embeddings.geminiTaskType,
343
+ dimensions: managedDimensions,
344
+ managedBaseUrl,
345
+ },
346
+ ),
347
+ [...extras, "managed"],
348
+ ),
349
+ reason: null,
350
+ };
351
+ }
352
+ }
353
+ }
354
+
283
355
  // Auto order: local → openai → gemini → ollama
284
356
  const order: EmbeddingProviderName[] =
285
357
  requested === "auto"
@@ -329,11 +401,18 @@ export async function selectEmbeddingBackend(
329
401
  case "gemini": {
330
402
  const geminiKey = await getProviderKeyAsync("gemini");
331
403
  if (!geminiKey) {
332
- const cached = getCached(
333
- "gemini",
334
- config.memory.embeddings.geminiModel,
335
- geminiCacheExtras(config),
336
- );
404
+ // Check managed cache variant first so a warm managed backend
405
+ // survives transient proxy-context blips, then non-managed.
406
+ const cached =
407
+ getCached("gemini", config.memory.embeddings.geminiModel, [
408
+ ...geminiCacheExtras(config),
409
+ "managed",
410
+ ]) ??
411
+ getCached(
412
+ "gemini",
413
+ config.memory.embeddings.geminiModel,
414
+ geminiCacheExtras(config),
415
+ );
337
416
  if (cached) return { backend: cached, reason: null };
338
417
  continue;
339
418
  }
@@ -614,6 +693,53 @@ async function selectFallbackBackends(
614
693
  geminiCacheExtras(config),
615
694
  ),
616
695
  );
696
+ } else if (
697
+ isAssistantFeatureFlagEnabled(
698
+ "managed-gemini-embeddings-enabled",
699
+ config,
700
+ )
701
+ ) {
702
+ // Try managed proxy Gemini as fallback when no direct key exists.
703
+ const proxyCtx = await resolveManagedProxyContext();
704
+ const meta = MANAGED_PROVIDER_META["gemini"];
705
+ if (proxyCtx.enabled && meta?.managed && meta.proxyPath) {
706
+ const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
707
+ const managedModel = config.memory.embeddings.geminiModel;
708
+ const managedDimensions =
709
+ config.memory.embeddings.geminiDimensions ?? 3072;
710
+ const extras = geminiCacheExtras(config);
711
+ backends.push(
712
+ getCachedOrCreate(
713
+ "gemini",
714
+ managedModel,
715
+ () =>
716
+ new GeminiEmbeddingBackend(
717
+ proxyCtx.assistantApiKey,
718
+ managedModel,
719
+ {
720
+ taskType: config.memory.embeddings.geminiTaskType,
721
+ dimensions: managedDimensions,
722
+ managedBaseUrl,
723
+ },
724
+ ),
725
+ [...extras, "managed"],
726
+ ),
727
+ );
728
+ } else {
729
+ // Check managed cache variant first, then non-managed, so a warm
730
+ // managed backend survives transient proxy-context blips.
731
+ const cached =
732
+ getCached("gemini", config.memory.embeddings.geminiModel, [
733
+ ...geminiCacheExtras(config),
734
+ "managed",
735
+ ]) ??
736
+ getCached(
737
+ "gemini",
738
+ config.memory.embeddings.geminiModel,
739
+ geminiCacheExtras(config),
740
+ );
741
+ if (cached) backends.push(cached);
742
+ }
617
743
  } else {
618
744
  // Preserve cached backend on transient credential-store failures.
619
745
  const cached = getCached(
@@ -253,4 +253,58 @@ describe("GeminiEmbeddingBackend", () => {
253
253
  expect(result[1]).toEqual([0.2, 0.4]);
254
254
  });
255
255
  });
256
+
257
+ describe("managed proxy transport", () => {
258
+ test("routes through managed proxy base URL when managedBaseUrl is set", async () => {
259
+ const backend = new GeminiEmbeddingBackend(
260
+ "ast-managed-key",
261
+ "gemini-embedding-2-preview",
262
+ {
263
+ managedBaseUrl:
264
+ "https://platform.example.com/v1/runtime-proxy/gemini",
265
+ },
266
+ );
267
+ await backend.embed(["hello"]);
268
+
269
+ expect(mockFetch).toHaveBeenCalledTimes(1);
270
+ const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
271
+ expect(url).toBe(
272
+ "https://platform.example.com/v1/runtime-proxy/gemini/v1beta/models/gemini-embedding-2-preview:embedContent",
273
+ );
274
+ // Should NOT have key= query param
275
+ expect(url).not.toContain("key=");
276
+ // Should have Bearer auth header
277
+ const headers = init.headers as Record<string, string>;
278
+ expect(headers["Authorization"]).toBe("Bearer ast-managed-key");
279
+ });
280
+
281
+ test("uses direct Google API URL when managedBaseUrl is not set", async () => {
282
+ const backend = new GeminiEmbeddingBackend("direct-key", "test-model");
283
+ await backend.embed(["hello"]);
284
+
285
+ const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
286
+ expect(url).toContain("generativelanguage.googleapis.com");
287
+ expect(url).toContain("key=direct-key");
288
+ // Should NOT have Authorization header
289
+ const headers = init.headers as Record<string, string>;
290
+ expect(headers["Authorization"]).toBeUndefined();
291
+ });
292
+
293
+ test("includes outputDimensionality with managed proxy", async () => {
294
+ const backend = new GeminiEmbeddingBackend(
295
+ "ast-managed-key",
296
+ "gemini-embedding-2-preview",
297
+ {
298
+ managedBaseUrl:
299
+ "https://platform.example.com/v1/runtime-proxy/gemini",
300
+ dimensions: 3072,
301
+ },
302
+ );
303
+ await backend.embed(["hello"]);
304
+
305
+ const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
306
+ const body = JSON.parse(init.body as string);
307
+ expect(body.outputDimensionality).toBe(3072);
308
+ });
309
+ });
256
310
  });
@@ -15,22 +15,27 @@ interface GeminiEmbedResponse {
15
15
  };
16
16
  }
17
17
 
18
+ export interface GeminiEmbeddingOptions {
19
+ taskType?: EmbeddingTaskType;
20
+ dimensions?: number;
21
+ /** When set, routes requests through the managed proxy at this base URL. */
22
+ managedBaseUrl?: string;
23
+ }
24
+
18
25
  export class GeminiEmbeddingBackend implements EmbeddingBackend {
19
26
  readonly provider = "gemini" as const;
20
27
  readonly model: string;
21
28
  private readonly apiKey: string;
22
29
  private readonly taskType?: EmbeddingTaskType;
23
30
  private readonly dimensions?: number;
31
+ private readonly managedBaseUrl?: string;
24
32
 
25
- constructor(
26
- apiKey: string,
27
- model: string,
28
- options?: { taskType?: EmbeddingTaskType; dimensions?: number },
29
- ) {
33
+ constructor(apiKey: string, model: string, options?: GeminiEmbeddingOptions) {
30
34
  this.apiKey = apiKey;
31
35
  this.model = model;
32
36
  this.taskType = options?.taskType;
33
37
  this.dimensions = options?.dimensions;
38
+ this.managedBaseUrl = options?.managedBaseUrl;
34
39
  }
35
40
 
36
41
  async embed(
@@ -59,12 +64,18 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
59
64
  if (this.taskType) body.taskType = this.taskType;
60
65
  if (this.dimensions) body.outputDimensionality = this.dimensions;
61
66
 
62
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(
63
- this.model,
64
- )}:embedContent?key=${encodeURIComponent(this.apiKey)}`;
67
+ const url = this.managedBaseUrl
68
+ ? `${this.managedBaseUrl}/v1beta/models/${encodeURIComponent(this.model)}:embedContent`
69
+ : `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(this.model)}:embedContent?key=${encodeURIComponent(this.apiKey)}`;
70
+ const headers: Record<string, string> = {
71
+ "Content-Type": "application/json",
72
+ };
73
+ if (this.managedBaseUrl) {
74
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
75
+ }
65
76
  const response = await fetch(url, {
66
77
  method: "POST",
67
- headers: { "Content-Type": "application/json" },
78
+ headers,
68
79
  body: JSON.stringify(body),
69
80
  signal: options?.signal,
70
81
  });
@@ -1,4 +1,10 @@
1
- import { existsSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ rmSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
9
 
4
10
  import { getIsContainerized } from "../config/env-registry.js";
@@ -83,6 +89,8 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
83
89
  }
84
90
  >();
85
91
  private stdoutReaderActive = false;
92
+ private activeEmbeds = 0;
93
+ private disposeRequested = false;
86
94
 
87
95
  private readonly initGuard = new PromiseGuard<void>();
88
96
 
@@ -94,6 +102,9 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
94
102
  inputs: EmbeddingInput[],
95
103
  options?: EmbeddingRequestOptions,
96
104
  ): Promise<number[][]> {
105
+ if (this.disposeRequested) {
106
+ throw new Error("Local embedding backend is shutting down");
107
+ }
97
108
  if (inputs.length === 0) return [];
98
109
 
99
110
  const texts = inputs.map((i) => {
@@ -106,24 +117,30 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
106
117
  if (options?.signal?.aborted)
107
118
  throw new DOMException("Aborted", "AbortError");
108
119
 
109
- await this.ensureInitialized();
110
-
111
- const results: number[][] = [];
112
- const batchSize = 32;
113
- for (let i = 0; i < texts.length; i += batchSize) {
114
- if (options?.signal?.aborted)
115
- throw new DOMException("Aborted", "AbortError");
116
- const batch = texts.slice(i, i + batchSize);
117
- const response = await this.sendRequest(batch);
118
- if (response.error) {
119
- throw new Error(`Embedding worker error: ${response.error}`);
120
- }
121
- if (!response.vectors) {
122
- throw new Error("Embedding worker returned no vectors");
120
+ this.activeEmbeds++;
121
+ try {
122
+ await this.ensureInitialized();
123
+
124
+ const results: number[][] = [];
125
+ const batchSize = 32;
126
+ for (let i = 0; i < texts.length; i += batchSize) {
127
+ if (options?.signal?.aborted)
128
+ throw new DOMException("Aborted", "AbortError");
129
+ const batch = texts.slice(i, i + batchSize);
130
+ const response = await this.sendRequest(batch);
131
+ if (response.error) {
132
+ throw new Error(`Embedding worker error: ${response.error}`);
133
+ }
134
+ if (!response.vectors) {
135
+ throw new Error("Embedding worker returned no vectors");
136
+ }
137
+ results.push(...response.vectors);
123
138
  }
124
- results.push(...response.vectors);
139
+ return results;
140
+ } finally {
141
+ this.activeEmbeds--;
142
+ this.disposeIfIdle();
125
143
  }
126
- return results;
127
144
  }
128
145
 
129
146
  private sendRequest(texts: string[]): Promise<WorkerResponse> {
@@ -148,6 +165,11 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
148
165
  await this.initGuard.run(() => this.initialize());
149
166
  }
150
167
 
168
+ dispose(): void {
169
+ this.disposeRequested = true;
170
+ this.disposeIfIdle();
171
+ }
172
+
151
173
  private async initialize(): Promise<void> {
152
174
  log.info({ model: this.model }, "Initializing local embedding backend");
153
175
 
@@ -199,13 +221,19 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
199
221
  const embeddingModelsDir = getEmbeddingModelsDir();
200
222
  const modelCacheDir = `${embeddingModelsDir}/model-cache`;
201
223
 
224
+ // Singleton guard: an orphaned embed worker from a previous daemon
225
+ // (e.g. one that crashed without cleanup) may still be running and
226
+ // holding the workspace's PID file. Detect and reclaim it before
227
+ // spawning so we never leave duplicate workers eating CPU/memory.
228
+ this.reclaimStaleWorker(workerPath);
229
+
202
230
  log.info(
203
231
  { bunPath, workerPath, model: this.model },
204
232
  "Spawning embedding worker process",
205
233
  );
206
234
 
207
235
  const proc = Bun.spawn({
208
- cmd: [bunPath, workerPath, this.model, modelCacheDir],
236
+ cmd: [bunPath, "--smol", workerPath, this.model, modelCacheDir],
209
237
  stdin: "pipe",
210
238
  stdout: "pipe",
211
239
  stderr: "pipe",
@@ -255,6 +283,8 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
255
283
  { pid: proc.pid, model: this.model },
256
284
  "Embedding worker process started",
257
285
  );
286
+
287
+ this.disposeIfIdle();
258
288
  }
259
289
 
260
290
  private drainStderr(stderr: ReadableStream<Uint8Array>): void {
@@ -355,6 +385,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
355
385
  if (pending) {
356
386
  this.pendingRequests.delete(msg.id);
357
387
  pending.resolve(msg);
388
+ this.disposeIfIdle();
358
389
  }
359
390
  }
360
391
  }
@@ -425,4 +456,132 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
425
456
  // Best-effort
426
457
  }
427
458
  }
459
+
460
+ /** Read the PID from the on-disk PID file, or null if missing/invalid. */
461
+ private readPidFile(): number | null {
462
+ const path = this.getPidFilePath();
463
+ if (!existsSync(path)) return null;
464
+ try {
465
+ const pid = parseInt(readFileSync(path, "utf-8").trim(), 10);
466
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Verify a PID belongs to this workspace's embed worker before sending
474
+ * signals — defends against PID reuse killing an unrelated process if the
475
+ * original worker exited and the OS recycled the PID.
476
+ *
477
+ * Matching `embed-worker` alone would also match a sibling assistant
478
+ * instance's worker (different VELLUM_WORKSPACE_DIR), so we match against
479
+ * the absolute worker script path, which lives under THIS workspace's
480
+ * embedding-models directory and is therefore unique per instance.
481
+ */
482
+ private isOurEmbedWorker(pid: number, workerPath: string): boolean {
483
+ try {
484
+ // `-ww` disables column-width truncation. Without it, macOS `ps` clips
485
+ // the command field to the terminal width, which can cut off the
486
+ // workerPath argument and cause this check to spuriously return false
487
+ // for genuine orphans. Same flag is used by daemon-control.ts:123 for
488
+ // exactly this reason.
489
+ const result = Bun.spawnSync({
490
+ cmd: ["ps", "-ww", "-p", String(pid), "-o", "command="],
491
+ stdout: "pipe",
492
+ stderr: "ignore",
493
+ });
494
+ if (result.exitCode !== 0) return false;
495
+ const cmd = new TextDecoder().decode(result.stdout).trim();
496
+ if (!cmd) return false;
497
+ return cmd.includes(workerPath);
498
+ } catch {
499
+ return false;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * If a previous embed worker is still running for this workspace (orphaned
505
+ * by a crashed daemon, for example), terminate it before spawning a new one
506
+ * so we never end up with duplicate workers competing for the same workspace.
507
+ *
508
+ * Stale PID files (process no longer exists) are silently cleaned up.
509
+ * PIDs that have been recycled to unrelated processes — including embed
510
+ * workers belonging to *other* assistant instances — are left untouched.
511
+ */
512
+ private reclaimStaleWorker(workerPath: string): void {
513
+ const pid = this.readPidFile();
514
+ if (pid == null) return;
515
+
516
+ // Never signal ourselves — should not happen since the worker is a child
517
+ // process, but guard against logic bugs that would deadlock the daemon.
518
+ if (pid === process.pid) {
519
+ this.removePidFile();
520
+ return;
521
+ }
522
+
523
+ let isAlive = false;
524
+ try {
525
+ // Signal 0 just probes for liveness without delivering a signal.
526
+ process.kill(pid, 0);
527
+ isAlive = true;
528
+ } catch {
529
+ // ESRCH — no such process. PID file is stale.
530
+ }
531
+
532
+ if (!isAlive) {
533
+ log.info(
534
+ { pid, model: this.model },
535
+ "Removing stale embed worker PID file (process no longer exists)",
536
+ );
537
+ this.removePidFile();
538
+ return;
539
+ }
540
+
541
+ if (!this.isOurEmbedWorker(pid, workerPath)) {
542
+ // PID points to something that isn't this workspace's embed worker —
543
+ // either an unrelated process (PID reuse after the original worker
544
+ // exited) or another assistant instance's worker. Either way, don't
545
+ // signal it; just drop the stale file so the new worker can claim it.
546
+ log.warn(
547
+ { pid, model: this.model },
548
+ "PID file points to a process that is not this workspace's embed worker; clearing without killing",
549
+ );
550
+ this.removePidFile();
551
+ return;
552
+ }
553
+
554
+ log.warn(
555
+ { pid, model: this.model },
556
+ "Found orphaned embed worker from a previous daemon, terminating it",
557
+ );
558
+ try {
559
+ process.kill(pid, "SIGTERM");
560
+ } catch {
561
+ // Race: it exited between the liveness check and the kill — fine.
562
+ }
563
+ this.removePidFile();
564
+ }
565
+
566
+ private disposeIfIdle(): void {
567
+ if (!this.disposeRequested) return;
568
+ if (this.activeEmbeds > 0) return;
569
+ if (this.pendingRequests.size > 0) return;
570
+ if (this.readyResolve || this.readyReject) return;
571
+
572
+ const proc = this.workerProc;
573
+ this.workerProc = null;
574
+ this.stdoutReaderActive = false;
575
+ this.stdoutBuffer = "";
576
+ this.initGuard.reset();
577
+ this.removePidFile();
578
+
579
+ if (!proc) return;
580
+
581
+ try {
582
+ proc.kill();
583
+ } catch {
584
+ // Worker may already be exiting
585
+ }
586
+ }
428
587
  }
@@ -127,9 +127,7 @@ export function seedSkillGraphNodes(): void {
127
127
  // skip catalog-based pruning to avoid incorrectly marking valid
128
128
  // uninstalled catalog nodes as gone. But still prune locally disabled
129
129
  // skills so stale capability nodes don't linger after cold start.
130
- log.info(
131
- "Catalog cache is cold — pruning only locally disabled skills",
132
- );
130
+ log.info("Catalog cache is cold — pruning only locally disabled skills");
133
131
  const disabled = resolved.filter((r) => r.state !== "enabled");
134
132
  for (const { summary } of disabled) {
135
133
  deleteSkillCapabilityNode(summary.id);
@@ -158,9 +156,9 @@ export function seedSkillGraphNodes(): void {
158
156
  * Seed graph nodes for all CLI commands.
159
157
  * Prunes stale nodes whose commands are no longer registered.
160
158
  */
161
- export function seedCliGraphNodes(): void {
159
+ export async function seedCliGraphNodes(): Promise<void> {
162
160
  try {
163
- const program = buildCliProgram();
161
+ const program = await buildCliProgram();
164
162
 
165
163
  const seenKeys = new Set<string>();
166
164
  for (const cmd of program.commands) {