@vellumai/assistant 0.6.2 → 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 (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -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,6 +221,12 @@ 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",
@@ -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
  }
@@ -4,8 +4,7 @@
4
4
  // Runs daily (or on demand). Processes nodes in partitions:
5
5
  // 1. Recency: nodes from last 7 days — merge duplicates, initial narrative
6
6
  // 2. Significance: top N by significance — update narrative arcs
7
- // 3. Decay: nodes at faded/gist fidelity — candidates for merging or marking gone
8
- // 4. Random sample: cross-pollination and pattern detection
7
+ // 3. Random sample: cross-pollination and pattern detection
9
8
  //
10
9
  // Each partition is a separate LLM call. The LLM produces a MemoryDiff
11
10
  // (same format as extraction) that is applied to the graph.
@@ -91,25 +90,25 @@ Today is ${today}.
91
90
 
92
91
  ## Your Tasks
93
92
 
94
- 1. **Merge duplicates**: If two or more nodes describe the same event or fact, merge them into one by:
93
+ 1. **Merge duplicates**: If two or more nodes describe the exact same specific event or fact with substantially the same details, merge them into one by:
95
94
  - Keeping the richer/more complete version (UPDATE it to incorporate details from duplicates)
96
95
  - DELETE the duplicates
97
96
  - Preserve the highest significance, reinforcement count, and stability from the merged nodes
98
- - Create a "supersedes" edge from the surviving node to each deleted node (the store handles deletion, but log the relationship)
97
+ - Create a "supersedes" edge from the surviving node to each deleted node
98
+ - Two nodes about the same person or topic but with DIFFERENT details, timestamps, or context are NOT duplicates — leave them both intact.
99
99
 
100
- 2. **Downgrade faded memories**: For nodes at "faded" or "gist" fidelity, rewrite their content to be shorter and more abstract — like how a real memory fades. A "faded" memory should be 1-2 sentences. A "gist" memory should be one sentence capturing only the essence.
100
+ 2. **Rewrite faded content**: For nodes at "faded" or "gist" fidelity, rewrite their content to be shorter and more abstract — like how a real memory fades. A "faded" memory should be 1-2 sentences. A "gist" memory should be one sentence capturing only the essence.
101
101
 
102
102
  3. **Update narrative roles**: If a node is clearly a turning point, inciting incident, or thesis in a larger story arc, set its narrativeRole and partOfStory.
103
103
 
104
- 4. **Mark gone**: If a node is at "gist" fidelity and has very low significance (< 0.2), mark it for deletion it's faded beyond usefulness.
105
-
106
- 5. **Resolve stale prospective nodes**: If a prospective node (type=prospective) is older than 7 days and has no "resolved-by" edge, it's likely a stale commitment. Downgrade its fidelity to "gist" and rewrite it as a past observation (e.g. "Had planned to X" instead of "Need to X"). If it's already at gist with significance < 0.2, mark it for deletion. If the node has an event_date in the past, clear it by setting event_date to null.
104
+ 4. **Resolve stale prospective nodes**: If a prospective node (type=prospective) is older than 7 days and has no "resolved-by" edge, downgrade its fidelity to "gist" and rewrite it as a past observation (e.g. "Had planned to X" instead of "Need to X"). If the node has an event_date in the past, clear it by setting event_date to null.
107
105
 
108
106
  ## Constraints
109
107
 
110
- - Do NOT create new nodes — consolidation only merges, updates, and prunes
108
+ - Do NOT create new nodes — consolidation only merges, updates, and rewrites
111
109
  - Do NOT change a node's type
112
110
  - Do NOT increase fidelity (memories only fade, never sharpen)
111
+ - Do NOT delete non-duplicate nodes — only delete the non-survivor in a merge. Fading and eventual cleanup are handled by the decay engine, not consolidation.
113
112
  - Preserve first-person prose style in content rewrites
114
113
  - When merging, keep the node with higher reinforcementCount as the survivor
115
114
 
@@ -159,7 +158,7 @@ const CONSOLIDATE_TOOL_SCHEMA = {
159
158
  },
160
159
  delete_ids: {
161
160
  type: "array" as const,
162
- description: "Node IDs to delete (merged duplicates, gone memories)",
161
+ description: "Node IDs to delete (merged duplicates only)",
163
162
  items: { type: "string" as const },
164
163
  },
165
164
  merge_edges: {
@@ -206,17 +205,6 @@ function getTopSignificanceNodes(
206
205
  .slice(0, n);
207
206
  }
208
207
 
209
- function getDecayedNodes(scopeId: string): MemoryNode[] {
210
- const all = queryNodes({
211
- scopeId,
212
- limit: 10000,
213
- });
214
- return all.filter(
215
- (n) =>
216
- (n.fidelity === "faded" || n.fidelity === "gist") && !isCapabilityNode(n),
217
- );
218
- }
219
-
220
208
  function getRandomSample(scopeId: string, n: number = 30): MemoryNode[] {
221
209
  const all = queryNodes({
222
210
  scopeId,
@@ -286,7 +274,7 @@ async function identifyDuplicateGroups(
286
274
  })
287
275
  .join("\n");
288
276
 
289
- const systemPrompt = `You are scanning a list of memory nodes for DUPLICATES — nodes that describe the same event, fact, or topic. Group duplicates together. Two nodes are duplicates if they describe the same underlying thing, even if worded differently. Be aggressiveif in doubt, group them. Only include nodes that have at least one duplicate.`;
277
+ const systemPrompt = `You are scanning a list of memory nodes for DUPLICATES — nodes that describe the exact same specific event or fact. Group duplicates together. Two nodes are duplicates ONLY if they describe the same underlying thing with substantially the same details. Be conservativenodes about the same person or topic but with different details, timestamps, or context are NOT duplicates. Only include nodes that have at least one true duplicate.`;
290
278
 
291
279
  const response = await provider.sendMessage(
292
280
  [userMessage(listing)],
@@ -657,7 +645,6 @@ export async function runConsolidation(
657
645
  const partitions: Array<{ name: string; nodes: MemoryNode[] }> = [
658
646
  { name: "recent", nodes: getRecentNodes(scopeId) },
659
647
  { name: "significant", nodes: getTopSignificanceNodes(scopeId) },
660
- { name: "decayed", nodes: getDecayedNodes(scopeId) },
661
648
  { name: "random", nodes: getRandomSample(scopeId) },
662
649
  ];
663
650
 
@@ -9,6 +9,7 @@
9
9
  import type { AssistantConfig } from "../../config/types.js";
10
10
  import { getLogger } from "../../util/logger.js";
11
11
  import { getMemoryCheckpoint, setMemoryCheckpoint } from "../checkpoints.js";
12
+ import { maybeEnqueueConversationStartersJob } from "../conversation-starters-cadence.js";
12
13
  import { asString } from "../job-utils.js";
13
14
  import type { MemoryJob } from "../jobs-store.js";
14
15
  import { runGraphExtraction } from "./extraction.js";
@@ -64,6 +65,20 @@ export async function graphExtractJob(
64
65
  },
65
66
  "Graph extraction job complete",
66
67
  );
68
+
69
+ try {
70
+ maybeEnqueueConversationStartersJob(scopeId);
71
+ } catch (cadenceErr) {
72
+ log.warn(
73
+ {
74
+ err:
75
+ cadenceErr instanceof Error
76
+ ? cadenceErr.message
77
+ : String(cadenceErr),
78
+ },
79
+ "Conversation starters cadence check failed (non-fatal)",
80
+ );
81
+ }
67
82
  } catch (err) {
68
83
  log.error(
69
84
  { conversationId, err: err instanceof Error ? err.message : String(err) },
@@ -371,8 +371,9 @@ export async function loadContextMemory(
371
371
  opts: ContextLoadOpts,
372
372
  ): Promise<ContextLoadResult> {
373
373
  const start = Date.now();
374
- const maxNodes = opts.maxNodes ?? 40;
375
- const serendipitySlots = opts.serendipitySlots ?? 10;
374
+ const ctxLoadCfg = opts.config.memory.retrieval.injection.contextLoad;
375
+ const maxNodes = opts.maxNodes ?? ctxLoadCfg.maxNodes;
376
+ const serendipitySlots = opts.serendipitySlots ?? ctxLoadCfg.serendipitySlots;
376
377
  const now = new Date();
377
378
  const nowMs = now.getTime();
378
379
 
@@ -547,13 +548,16 @@ export async function loadContextMemory(
547
548
  // 5b. Reserve slots for skill/CLI capabilities. Queried directly from
548
549
  // SQLite — no Qdrant vectors needed — so capabilities surface even on
549
550
  // fresh assistants whose embedding jobs haven't completed yet.
550
- const CAPABILITY_RESERVE = 5;
551
- const rawCapabilityNodes = queryNodes({
552
- scopeId: opts.scopeId,
553
- types: ["procedural"],
554
- fidelityNot: ["gone"],
555
- limit: CAPABILITY_RESERVE * 4,
556
- });
551
+ const capabilityReserve = ctxLoadCfg.capabilityReserve;
552
+ const rawCapabilityNodes =
553
+ capabilityReserve > 0
554
+ ? queryNodes({
555
+ scopeId: opts.scopeId,
556
+ types: ["procedural"],
557
+ fidelityNot: ["gone"],
558
+ limit: capabilityReserve * 4,
559
+ })
560
+ : [];
557
561
 
558
562
  // Dedup: both seeding systems may create nodes for the same capability.
559
563
  // Extract capability ID from content and keep only the first node per ID.
@@ -574,14 +578,14 @@ export async function loadContextMemory(
574
578
 
575
579
  // Rank by semantic similarity when a query vector exists
576
580
  let selectedCapabilities: MemoryNode[];
577
- if (queryVector && capabilityNodes.length > CAPABILITY_RESERVE) {
581
+ if (queryVector && capabilityNodes.length > capabilityReserve) {
578
582
  selectedCapabilities = capabilityNodes
579
583
  .map((node) => ({ node, sim: semanticCandidateIds.get(node.id) ?? 0 }))
580
584
  .sort((a, b) => b.sim - a.sim)
581
- .slice(0, CAPABILITY_RESERVE)
585
+ .slice(0, capabilityReserve)
582
586
  .map((e) => e.node);
583
587
  } else {
584
- selectedCapabilities = capabilityNodes.slice(0, CAPABILITY_RESERVE);
588
+ selectedCapabilities = capabilityNodes.slice(0, capabilityReserve);
585
589
  }
586
590
 
587
591
  const reservedCapabilities: ScoredNode[] = selectedCapabilities.map(
@@ -987,7 +991,8 @@ export async function retrieveForTurn(
987
991
  // 5b. Reserve slots for capability nodes (skills/CLI).
988
992
  // Sourced from vector search candidates — only semantically relevant
989
993
  // capabilities compete for reserved slots.
990
- const PROCEDURAL_RESERVE = 3;
994
+ const perTurnCfg = opts.config.memory.retrieval.injection.perTurn;
995
+ const capabilityReserve = perTurnCfg.capabilityReserve;
991
996
 
992
997
  const proceduralCandidates = capabilityCandidates
993
998
  .filter(({ node }) => !opts.tracker.isInContext(node.id))
@@ -1006,7 +1011,7 @@ export async function retrieveForTurn(
1006
1011
  }
1007
1012
  return true;
1008
1013
  })
1009
- .slice(0, PROCEDURAL_RESERVE);
1014
+ .slice(0, capabilityReserve);
1010
1015
 
1011
1016
  const proceduralScored: ScoredNode[] = rankedProcedural.map(({ node, sim }) =>
1012
1017
  scoreCandidate(
@@ -1033,8 +1038,14 @@ export async function retrieveForTurn(
1033
1038
  // Sort and apply threshold — pull a wider pool for dedup, then trim
1034
1039
  scored.sort((a, b) => b.score - a.score);
1035
1040
  const INJECTION_THRESHOLD = 0.3;
1041
+ // Hard cap on candidates fed to the dedup LLM — effectively caps maxNodes
1036
1042
  const PRE_DEDUP_POOL = 20;
1037
- const MAX_INJECTED = 4;
1043
+ const maxGeneralNodes = Math.max(
1044
+ 0,
1045
+ perTurnCfg.maxNodes -
1046
+ perTurnCfg.serendipitySlots -
1047
+ proceduralInjected.length,
1048
+ );
1038
1049
  const pool = scored
1039
1050
  .filter((s) => s.score >= INJECTION_THRESHOLD)
1040
1051
  .slice(0, PRE_DEDUP_POOL);
@@ -1042,8 +1053,12 @@ export async function retrieveForTurn(
1042
1053
  // Dedup + rerank with a fast model when the pool is large enough to warrant it
1043
1054
  let injected: ScoredNode[];
1044
1055
  let llmDedupApplied = false;
1045
- if (pool.length > MAX_INJECTED) {
1046
- const result = await dedupForTurn(pool, MAX_INJECTED, opts.userLastMessage);
1056
+ if (pool.length > maxGeneralNodes) {
1057
+ const result = await dedupForTurn(
1058
+ pool,
1059
+ maxGeneralNodes,
1060
+ opts.userLastMessage,
1061
+ );
1047
1062
  injected = result.nodes;
1048
1063
  llmDedupApplied = result.llmApplied;
1049
1064
  } else {
@@ -1054,17 +1069,17 @@ export async function retrieveForTurn(
1054
1069
  const generalInjected = injected.filter((s) => !proceduralIds.has(s.node.id));
1055
1070
 
1056
1071
  // Backfill vacated general slots from the remaining pool so we always
1057
- // return up to MAX_INJECTED general memories when eligible candidates exist.
1072
+ // return up to maxGeneralNodes when eligible candidates exist.
1058
1073
  // Only skip backfill when LLM dedup genuinely ran — it intentionally rejected
1059
1074
  // items as duplicates/irrelevant. When dedupForTurn fell back to a plain
1060
1075
  // top-N slice (no provider, tool call failure), backfill is still appropriate.
1061
- if (generalInjected.length < MAX_INJECTED && !llmDedupApplied) {
1076
+ if (generalInjected.length < maxGeneralNodes && !llmDedupApplied) {
1062
1077
  const usedIds = new Set([
1063
1078
  ...generalInjected.map((s) => s.node.id),
1064
1079
  ...proceduralIds,
1065
1080
  ]);
1066
1081
  const backfillCandidates = pool.filter((s) => !usedIds.has(s.node.id));
1067
- const needed = MAX_INJECTED - generalInjected.length;
1082
+ const needed = maxGeneralNodes - generalInjected.length;
1068
1083
  for (let i = 0; i < Math.min(needed, backfillCandidates.length); i++) {
1069
1084
  generalInjected.push(backfillCandidates[i]);
1070
1085
  }
@@ -1073,11 +1088,14 @@ export async function retrieveForTurn(
1073
1088
  const allDeterministic = [...generalInjected, ...proceduralInjected];
1074
1089
  const deterministicIds = new Set(allDeterministic.map((n) => n.node.id));
1075
1090
 
1076
- // Reserve 1 serendipity slot from scored candidates not in the deterministic set
1091
+ // Reserve serendipity slots from scored candidates not in the deterministic set
1077
1092
  const serendipityPool = scored.filter(
1078
1093
  (s) => s.score >= INJECTION_THRESHOLD && !deterministicIds.has(s.node.id),
1079
1094
  );
1080
- const serendipityPicks = sampleSerendipity(serendipityPool, 1);
1095
+ const serendipityPicks = sampleSerendipity(
1096
+ serendipityPool,
1097
+ perTurnCfg.serendipitySlots,
1098
+ );
1081
1099
  const allInjected = [...allDeterministic, ...serendipityPicks];
1082
1100
 
1083
1101
  const TOP_N = 20;
@@ -179,10 +179,12 @@ describe("node CRUD", () => {
179
179
  expect(updated!.eventDate).toBeNull();
180
180
  });
181
181
 
182
- test("deleteNode removes the node", () => {
182
+ test("deleteNode soft-deletes the node by setting fidelity to gone", () => {
183
183
  const node = createNode(makeNewNode());
184
184
  deleteNode(node.id);
185
- expect(getNode(node.id)).toBeNull();
185
+ const deleted = getNode(node.id);
186
+ expect(deleted).not.toBeNull();
187
+ expect(deleted!.fidelity).toBe("gone");
186
188
  });
187
189
  });
188
190
 
@@ -755,7 +757,9 @@ describe("applyDiff", () => {
755
757
  reinforceNodeIds: [],
756
758
  });
757
759
  expect(result.nodesDeleted).toBe(1);
758
- expect(getNode(node.id)).toBeNull();
760
+ const deleted = getNode(node.id);
761
+ expect(deleted).not.toBeNull();
762
+ expect(deleted!.fidelity).toBe("gone");
759
763
  });
760
764
 
761
765
  test("updates nodes", () => {