@vellumai/assistant 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -67,7 +67,8 @@ function upsertMemoryItem(opts: {
67
67
  created: now,
68
68
  lastAccessed: now,
69
69
  lastConsolidated: now,
70
- emotionalCharge: '{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
70
+ emotionalCharge:
71
+ '{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
71
72
  fidelity: "vivid",
72
73
  confidence: 0.8,
73
74
  significance: clampUnitInterval(opts.importance),
@@ -87,7 +88,7 @@ function upsertMemoryItem(opts: {
87
88
 
88
89
  export async function run(
89
90
  input: Record<string, unknown>,
90
- context: ToolContext,
91
+ _context: ToolContext,
91
92
  ): Promise<ToolExecutionResult> {
92
93
  const platform = input.platform as string | undefined;
93
94
  const maxMessages = Math.min(
@@ -119,7 +120,7 @@ export async function run(
119
120
  return err("No style patterns were extracted. Try with more messages.");
120
121
  }
121
122
 
122
- const scopeId = context.memoryScopeId ?? "default";
123
+ const scopeId = "default";
123
124
  let savedCount = 0;
124
125
 
125
126
  for (const pattern of result.stylePatterns) {
@@ -10,18 +10,24 @@ Run `assistant config set calls.enabled true`.
10
10
 
11
11
  ## "No public base URL configured"
12
12
 
13
- Run the **public-ingress** skill to set up ngrok or another custom tunnel and configure `ingress.publicBaseUrl`. Ngrok remains supported. Velay can also publish `ingress.twilioPublicBaseUrl` for Twilio calls when `VELAY_BASE_URL` is set on the gateway, but it does not replace `ingress.publicBaseUrl` for Telegram, OAuth, email, or other non-Twilio webhooks.
13
+ First check whether this is a managed/platform assistant:
14
+
15
+ ```bash
16
+ assistant platform status --json
17
+ ```
18
+
19
+ If it reports an available platform assistant, do not install or start ngrok for Twilio. The gateway should use Velay, and `velayTunnel.connected` should become `true` after registration. If this is a local/self-hosted assistant without Velay, run the **public-ingress** skill to set up ngrok or another custom tunnel and configure `ingress.publicBaseUrl`.
14
20
 
15
21
  ## Call fails immediately after initiating
16
22
 
17
23
  - Check that the phone number is in E.164 format
18
24
  - Verify Twilio credentials are correct (wrong auth token causes API errors)
19
25
  - On trial accounts, ensure the destination number is verified
20
- - Check that the configured tunnel is still running. For ngrok, use `curl -s http://127.0.0.1:4040/api/tunnels`. For Velay, confirm the gateway logs show `Velay tunnel registered` and `ingress.twilioPublicBaseUrl` is populated.
26
+ - Check that the configured tunnel is still running. For ngrok, use `curl -s http://127.0.0.1:4040/api/tunnels`. For Velay, run `assistant platform status --json` and confirm `velayTunnel.connected` is `true`, or check gateway logs for `Velay tunnel registered`.
21
27
 
22
28
  ## Call connects but no audio / one-way audio
23
29
 
24
- - The ConversationRelay WebSocket may not be connecting. If you are using ngrok or a custom tunnel, check that `ingress.publicBaseUrl` is correct and the tunnel is active. If you are using Velay, check that `ingress.twilioPublicBaseUrl` is present and points to the registered Velay URL.
30
+ - The ConversationRelay WebSocket may not be connecting. If you are using ngrok or a custom tunnel, check that `ingress.publicBaseUrl` is correct and the tunnel is active. If you are using Velay, check `assistant platform status --json`; a 503 from `https://velay.../<assistant-id>/...` usually means the assistant tunnel is not connected or the gateway did not complete the WebSocket open, not that ngrok is required.
25
31
  - Verify the assistant is running
26
32
 
27
33
  ## "Number not eligible for caller identity"
@@ -50,20 +56,20 @@ assistant config set ingress.publicBaseUrl "<new-url>"
50
56
 
51
57
  Or re-run the public-ingress skill to auto-detect and save the new URL.
52
58
 
53
- Velay does not change this setting. When `VELAY_BASE_URL` is set on the gateway, Velay registration writes only `ingress.twilioPublicBaseUrl`; Twilio uses that URL first, and non-Twilio webhook ingress keeps using `ingress.publicBaseUrl`.
59
+ Do not rotate ngrok to work around a managed Velay WebSocket failure. Fix the Velay tunnel state instead, or restart the assistant/gateway so it re-registers.
54
60
 
55
61
  ## Velay tunnel is not registering
56
62
 
57
- - Confirm vembda passes `VELAY_BASE_URL=http://host.docker.internal:8501` to the gateway container.
63
+ - Confirm vembda passes the environment-appropriate `VELAY_BASE_URL` to the gateway container.
58
64
  - Re-hatch or restart the assistant after changing the environment.
59
65
  - Check gateway logs for `Velay tunnel connected` followed by `Velay tunnel registered`.
60
- - If `VELAY_BASE_URL` is not set, the gateway does not start the Velay client. Use ngrok or another custom tunnel in `ingress.publicBaseUrl`.
66
+ - If `VELAY_BASE_URL` is not set on a local/self-hosted assistant, the gateway does not start the Velay client. Use ngrok or another custom tunnel in `ingress.publicBaseUrl`.
61
67
 
62
68
  ## Local Twilio Velay smoke tests
63
69
 
64
70
  - HTTP bridge: request `${VELAY_PUBLIC_BASE_URL}/<assistant-id>/healthz` and `${VELAY_PUBLIC_BASE_URL}/<assistant-id>/schema`. When testing a JSON webhook route under active development, POST a small JSON body through the same Velay public URL and confirm the gateway receives it.
65
71
  - Synthetic WebSocket: connect a local WebSocket client to `${VELAY_PUBLIC_BASE_URL}/<assistant-id>/webhooks/twilio/relay?callSessionId=session-123&token=<edge-token>` and confirm the upgrade reaches the gateway.
66
- - Real Twilio call: back `VELAY_PUBLIC_BASE_URL` with a public HTTPS/WSS tunnel, wait for the gateway to re-register, then place a call and confirm Twilio fetches `/webhooks/twilio/voice` and opens the relay or media-stream WebSocket through the Velay URL.
72
+ - Real Twilio call: wait for the gateway to register with Velay, then place a call and confirm Twilio fetches `/webhooks/twilio/voice` and opens the relay or media-stream WebSocket through the Velay URL.
67
73
 
68
74
  ## Call drops after 30 seconds of silence
69
75
 
@@ -18,7 +18,7 @@ const VALID_AUTONOMY_LEVELS = new Set<string>(["auto", "draft", "notify"]);
18
18
 
19
19
  export async function executePlaybookCreate(
20
20
  input: Record<string, unknown>,
21
- context: ToolContext,
21
+ _context: ToolContext,
22
22
  ): Promise<ToolExecutionResult> {
23
23
  const trigger = input.trigger as string;
24
24
  const action = input.action as string;
@@ -58,7 +58,7 @@ export async function executePlaybookCreate(
58
58
  const sanitizedTrigger = trigger.replace(/[\r\n]+/g, " ");
59
59
  const subject = `Playbook: ${sanitizedTrigger}`.slice(0, 80);
60
60
  const content = `${subject}\n${statement}`;
61
- const scopeId = context.memoryScopeId ?? "default";
61
+ const scopeId = "default";
62
62
 
63
63
  try {
64
64
  const db = getDb();
@@ -7,7 +7,7 @@ import type {
7
7
 
8
8
  export async function executePlaybookDelete(
9
9
  input: Record<string, unknown>,
10
- context: ToolContext,
10
+ _context: ToolContext,
11
11
  ): Promise<ToolExecutionResult> {
12
12
  const playbookId = input.playbook_id as string;
13
13
  if (!playbookId || typeof playbookId !== "string") {
@@ -17,7 +17,7 @@ export async function executePlaybookDelete(
17
17
  };
18
18
  }
19
19
 
20
- const scopeId = context.memoryScopeId ?? "default";
20
+ const scopeId = "default";
21
21
 
22
22
  try {
23
23
  const existing = getNode(playbookId);
@@ -10,9 +10,9 @@ import type {
10
10
 
11
11
  export async function executePlaybookList(
12
12
  input: Record<string, unknown>,
13
- context: ToolContext,
13
+ _context: ToolContext,
14
14
  ): Promise<ToolExecutionResult> {
15
- const scopeId = context.memoryScopeId ?? "default";
15
+ const scopeId = "default";
16
16
  const channelFilter =
17
17
  typeof input.channel === "string" ? input.channel : null;
18
18
  const categoryFilter =
@@ -18,7 +18,7 @@ const VALID_AUTONOMY_LEVELS = new Set<string>(["auto", "draft", "notify"]);
18
18
 
19
19
  export async function executePlaybookUpdate(
20
20
  input: Record<string, unknown>,
21
- context: ToolContext,
21
+ _context: ToolContext,
22
22
  ): Promise<ToolExecutionResult> {
23
23
  const playbookId = input.playbook_id as string;
24
24
  if (!playbookId || typeof playbookId !== "string") {
@@ -28,7 +28,7 @@ export async function executePlaybookUpdate(
28
28
  };
29
29
  }
30
30
 
31
- const scopeId = context.memoryScopeId ?? "default";
31
+ const scopeId = "default";
32
32
 
33
33
  try {
34
34
  const existing = getNode(playbookId);
package/src/config/env.ts CHANGED
@@ -253,14 +253,6 @@ export function getPlatformUserId(): string {
253
253
  return str("PLATFORM_USER_ID") ?? _platformUserIdOverride ?? "";
254
254
  }
255
255
 
256
- /**
257
- * PLATFORM_INTERNAL_API_KEY — static internal gateway key for authenticating
258
- * with the platform's internal gateway callback route registration endpoint.
259
- */
260
- export function getPlatformInternalApiKey(): string {
261
- return str("PLATFORM_INTERNAL_API_KEY") ?? "";
262
- }
263
-
264
256
  // ── Startup validation ──────────────────────────────────────────────────────
265
257
 
266
258
  /**
@@ -242,11 +242,11 @@
242
242
  "defaultEnabled": false
243
243
  },
244
244
  {
245
- "id": "memory-v2-enabled",
245
+ "id": "safe-storage-limits",
246
246
  "scope": "assistant",
247
- "key": "memory-v2-enabled",
248
- "label": "Memory v2 (concept-page activation model)",
249
- "description": "Enables the v2 memory subsystem: prose concept pages with bidirectional edges, activation-based retrieval, and hourly LLM-driven consolidation. v1 graph + PKB stays write-active until cutover.",
247
+ "key": "safe-storage-limits",
248
+ "label": "Safe Storage Limits",
249
+ "description": "Enable disk pressure protection flows that block background work and remote actors while storage is critically low.",
250
250
  "defaultEnabled": false
251
251
  },
252
252
  {
@@ -255,7 +255,7 @@
255
255
  "key": "account-deletion",
256
256
  "label": "Account Deletion",
257
257
  "description": "Surfaces the user-initiated account deletion flow in client settings.",
258
- "defaultEnabled": false
258
+ "defaultEnabled": true
259
259
  },
260
260
  {
261
261
  "id": "app-control",
@@ -272,6 +272,14 @@
272
272
  "label": "Analyze Conversation",
273
273
  "description": "Show the 'Analyze' / 'Analyze conversation' option in conversation context menus and the conversation title actions dropdown.",
274
274
  "defaultEnabled": false
275
+ },
276
+ {
277
+ "id": "pro-plan-adjust",
278
+ "scope": "assistant",
279
+ "key": "pro-plan-adjust",
280
+ "label": "Pro Plan Adjust",
281
+ "description": "Show the rich Plan card (current plan, features, Manage/Upgrade CTA) at the top of the macOS Settings → Billing tab.",
282
+ "defaultEnabled": false
275
283
  }
276
284
  ]
277
285
  }
@@ -7,6 +7,7 @@ import {
7
7
  } from "node:fs";
8
8
  import { basename, dirname, join } from "node:path";
9
9
 
10
+ import { safeStatSync } from "../util/fs.js";
10
11
  import { getLogger } from "../util/logger.js";
11
12
  import {
12
13
  ensureDataDir,
@@ -22,8 +23,22 @@ export { API_KEY_PROVIDERS } from "../providers/provider-secret-catalog.js";
22
23
  const log = getLogger("config");
23
24
 
24
25
  let cached: AssistantConfig | null = null;
26
+ let cachedFileSignature: ConfigFileSignature | null = null;
25
27
  let loading = false;
26
28
 
29
+ type ConfigFileSignature =
30
+ | {
31
+ path: string;
32
+ exists: true;
33
+ size: number;
34
+ mtimeMs: number;
35
+ ctimeMs: number;
36
+ }
37
+ | {
38
+ path: string;
39
+ exists: false;
40
+ };
41
+
27
42
  function getConfigPath(): string {
28
43
  return getWorkspaceConfigPath();
29
44
  }
@@ -32,6 +47,48 @@ function ensureMigratedDataDir(): void {
32
47
  ensureDataDir();
33
48
  }
34
49
 
50
+ function readConfigFileSignature(configPath: string): ConfigFileSignature {
51
+ const stats = safeStatSync(configPath);
52
+ if (!stats) {
53
+ return {
54
+ path: configPath,
55
+ exists: false,
56
+ };
57
+ }
58
+
59
+ return {
60
+ path: configPath,
61
+ exists: true,
62
+ size: stats.size,
63
+ mtimeMs: stats.mtimeMs,
64
+ ctimeMs: stats.ctimeMs,
65
+ };
66
+ }
67
+
68
+ function configFileSignaturesEqual(
69
+ a: ConfigFileSignature,
70
+ b: ConfigFileSignature,
71
+ ): boolean {
72
+ if (a.path !== b.path || a.exists !== b.exists) return false;
73
+ if (!a.exists || !b.exists) return true;
74
+ return (
75
+ a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs
76
+ );
77
+ }
78
+
79
+ function getCachedConfigIfFresh(): AssistantConfig | null {
80
+ if (!cached || !cachedFileSignature) return null;
81
+
82
+ const currentSignature = readConfigFileSignature(getConfigPath());
83
+ if (configFileSignaturesEqual(cachedFileSignature, currentSignature)) {
84
+ return cached;
85
+ }
86
+
87
+ cached = null;
88
+ cachedFileSignature = null;
89
+ return null;
90
+ }
91
+
35
92
  /**
36
93
  * Parse a raw config through the Zod schema, applying all nested defaults.
37
94
  *
@@ -51,14 +108,16 @@ function cloneDefaultConfig(): AssistantConfig {
51
108
 
52
109
  /**
53
110
  * Returns deployment-context-aware config defaults that override schema
54
- * defaults for platform-managed assistants. Only applied when initializing
55
- * a fresh config (config.json does not yet exist).
111
+ * defaults for platform-managed assistants. Applied to every `loadConfig()`
112
+ * call as a fill-only pass they only fill keys that are absent from the
113
+ * raw config on disk, so an explicit user choice (e.g. saving "your-own"
114
+ * via the macOS Models & Services UI) always wins.
56
115
  *
57
116
  * IS_PLATFORM is set by the Vellum platform launcher for all hosted
58
117
  * assistant deployments. Local, Docker, and bare-metal assistants are
59
118
  * unaffected.
60
119
  */
61
- function getDeploymentContextDefaults(): Record<string, unknown> {
120
+ export function getDeploymentContextDefaults(): Record<string, unknown> {
62
121
  if (process.env.IS_PLATFORM !== "true" && process.env.IS_PLATFORM !== "1") {
63
122
  return {};
64
123
  }
@@ -73,10 +132,57 @@ function getDeploymentContextDefaults(): Record<string, unknown> {
73
132
  "linear-oauth": managed,
74
133
  "github-oauth": managed,
75
134
  "notion-oauth": managed,
135
+ "asana-oauth": managed,
136
+ "todoist-oauth": managed,
137
+ "discord-oauth": managed,
138
+ "hubspot-oauth": managed,
76
139
  },
77
140
  };
78
141
  }
79
142
 
143
+ /**
144
+ * Apply `contextDefaults` to `target` for any leaf keys that are absent from
145
+ * `fileConfig` (the raw config-on-disk payload). Mutates `target` in place.
146
+ *
147
+ * "Absent" is checked at the leaf level by walking the `contextDefaults`
148
+ * shape: nested objects recurse so a partial override on disk (e.g.
149
+ * `{services: {inference: {model: "x"}}}` with no explicit `mode`) lets the
150
+ * context default for `mode` win while leaving the user's `model` untouched.
151
+ *
152
+ * Pre-condition: `target` has already been passed through `validateWithSchema`
153
+ * so every nested object in `contextDefaults` has a corresponding object in
154
+ * `target`. The defensive whole-subtree assignment in the `!targetChild`
155
+ * branch only fires for malformed inputs.
156
+ */
157
+ export function fillContextDefaultsForMissingKeys(
158
+ target: Record<string, unknown>,
159
+ fileConfig: Record<string, unknown>,
160
+ contextDefaults: Record<string, unknown>,
161
+ ): void {
162
+ for (const [key, value] of Object.entries(contextDefaults)) {
163
+ const fileVal = fileConfig[key];
164
+ if (
165
+ value !== null &&
166
+ typeof value === "object" &&
167
+ !Array.isArray(value)
168
+ ) {
169
+ const targetChild = readPlainObject(target[key]);
170
+ const fileChild = readPlainObject(fileVal);
171
+ if (targetChild) {
172
+ fillContextDefaultsForMissingKeys(
173
+ targetChild,
174
+ fileChild ?? {},
175
+ value as Record<string, unknown>,
176
+ );
177
+ } else {
178
+ target[key] = structuredClone(value);
179
+ }
180
+ } else if (fileVal === undefined) {
181
+ target[key] = value;
182
+ }
183
+ }
184
+ }
185
+
80
186
  /**
81
187
  * Build a filesystem-safe ISO-8601 timestamp for use in quarantine filenames.
82
188
  * Replaces `:` (invalid on Windows, confusing on macOS Finder) with `-` so the
@@ -343,6 +449,13 @@ function stripNullLeaves(value: unknown): unknown {
343
449
  return out;
344
450
  }
345
451
 
452
+ function readPlainObject(value: unknown): Record<string, unknown> | null {
453
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
454
+ return null;
455
+ }
456
+ return value as Record<string, unknown>;
457
+ }
458
+
346
459
  /**
347
460
  * Deep-merge `overrides` into `target`, overwriting leaf values.
348
461
  * Recursively merges nested objects; scalars and arrays from `overrides`
@@ -406,6 +519,18 @@ export function deepMergeOverwrite(
406
519
  }
407
520
  }
408
521
 
522
+ export type DefaultWorkspaceConfigMergeResult = {
523
+ providedLlmProfileNames: Set<string>;
524
+ providedLlmActiveProfile: boolean;
525
+ };
526
+
527
+ function emptyDefaultWorkspaceConfigMergeResult(): DefaultWorkspaceConfigMergeResult {
528
+ return {
529
+ providedLlmProfileNames: new Set(),
530
+ providedLlmActiveProfile: false,
531
+ };
532
+ }
533
+
409
534
  /**
410
535
  * Merge default workspace config from the file referenced by
411
536
  * VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH into the workspace config on disk.
@@ -415,9 +540,11 @@ export function deepMergeOverwrite(
415
540
  * Schema defaults are no longer materialized into the file on load — the
416
541
  * in-memory `loadConfig()` cache applies them at access time instead.
417
542
  */
418
- export function mergeDefaultWorkspaceConfig(): void {
543
+ export function mergeDefaultWorkspaceConfig(): DefaultWorkspaceConfigMergeResult {
419
544
  const defaultConfigPath = process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
420
- if (!defaultConfigPath || !existsSync(defaultConfigPath)) return;
545
+ if (!defaultConfigPath || !existsSync(defaultConfigPath)) {
546
+ return emptyDefaultWorkspaceConfigMergeResult();
547
+ }
421
548
 
422
549
  let defaults: unknown;
423
550
  try {
@@ -428,7 +555,7 @@ export function mergeDefaultWorkspaceConfig(): void {
428
555
  "Failed to read default workspace config from %s",
429
556
  defaultConfigPath,
430
557
  );
431
- return;
558
+ return emptyDefaultWorkspaceConfigMergeResult();
432
559
  }
433
560
 
434
561
  if (
@@ -436,16 +563,44 @@ export function mergeDefaultWorkspaceConfig(): void {
436
563
  typeof defaults !== "object" ||
437
564
  Array.isArray(defaults)
438
565
  ) {
439
- return;
566
+ return emptyDefaultWorkspaceConfigMergeResult();
440
567
  }
441
568
 
569
+ const llmDefaults = readPlainObject(
570
+ (defaults as Record<string, unknown>).llm,
571
+ );
572
+ const providedProfiles = readPlainObject(llmDefaults?.profiles);
573
+ const mergeResult: DefaultWorkspaceConfigMergeResult = {
574
+ providedLlmProfileNames: new Set(
575
+ providedProfiles ? Object.keys(providedProfiles) : [],
576
+ ),
577
+ providedLlmActiveProfile:
578
+ llmDefaults != null &&
579
+ Object.prototype.hasOwnProperty.call(llmDefaults, "activeProfile"),
580
+ };
581
+
442
582
  const configPath = getConfigPath();
443
583
  let existing: Record<string, unknown> = {};
444
584
  if (existsSync(configPath)) {
445
585
  try {
446
586
  existing = JSON.parse(readFileSync(configPath, "utf-8"));
447
- } catch {
448
- // If existing config is corrupt, start fresh
587
+ } catch (err) {
588
+ quarantineCorruptConfig(configPath, err);
589
+ // After preserving the corrupt file, start fresh so the default overlay
590
+ // can still initialize a valid config for this startup.
591
+ }
592
+ }
593
+
594
+ if (mergeResult.providedLlmProfileNames.size > 0) {
595
+ // Default-config profile entries are authoritative fragments. Remove any
596
+ // old same-name profile first so recursive merge does not leave stale
597
+ // provider-specific leaves behind.
598
+ const existingLlm = readPlainObject(existing.llm);
599
+ const existingProfiles = readPlainObject(existingLlm?.profiles);
600
+ if (existingProfiles) {
601
+ for (const name of mergeResult.providedLlmProfileNames) {
602
+ delete existingProfiles[name];
603
+ }
449
604
  }
450
605
  }
451
606
 
@@ -456,6 +611,7 @@ export function mergeDefaultWorkspaceConfig(): void {
456
611
  mkdirSync(dir, { recursive: true });
457
612
  }
458
613
  writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
614
+ invalidateConfigCache();
459
615
 
460
616
  // Move the temp file into the workspace directory as a permanent record.
461
617
  // This prevents re-application on daemon restart (the env var still points
@@ -471,10 +627,13 @@ export function mergeDefaultWorkspaceConfig(): void {
471
627
  } catch {
472
628
  log.info("Merged default workspace config from %s", defaultConfigPath);
473
629
  }
630
+
631
+ return mergeResult;
474
632
  }
475
633
 
476
634
  export function loadConfig(): AssistantConfig {
477
- if (cached) return cached;
635
+ const freshCached = getCachedConfigIfFresh();
636
+ if (freshCached) return freshCached;
478
637
 
479
638
  // Re-entrancy guard: log calls during loading (e.g. file-mode warning)
480
639
  // can trigger loadConfig again. Return defaults to break the cycle
@@ -551,11 +710,31 @@ export function loadConfig(): AssistantConfig {
551
710
  }
552
711
  }
553
712
 
713
+ // Layer deployment-context defaults (e.g. IS_PLATFORM=true → all service
714
+ // modes = "managed") onto the in-memory config for any leaves that aren't
715
+ // explicitly set in `fileConfig`. This runs on every load — not just the
716
+ // first — because the workspace config file is written by upstream
717
+ // lifecycle steps (`mergeDefaultWorkspaceConfig`, `seedInferenceProfiles`)
718
+ // before `loadConfig()` is reached. Gating on `!configFileExisted` would
719
+ // make the context defaults dead code on platform-managed daemons whose
720
+ // config.json was created by those earlier steps without service-mode
721
+ // entries. Explicit user choices on disk are preserved because the helper
722
+ // only fills missing keys.
723
+ const contextDefaults = getDeploymentContextDefaults();
724
+ if (Object.keys(contextDefaults).length > 0) {
725
+ fillContextDefaultsForMissingKeys(
726
+ config as unknown as Record<string, unknown>,
727
+ fileConfig,
728
+ contextDefaults,
729
+ );
730
+ }
731
+
554
732
  // First-launch seed only: when config.json does not exist, write the full
555
- // schema defaults to disk so users can discover and edit all available
556
- // options. When the file already exists, leave it alone disk represents
557
- // user intent, while the in-memory `cached: AssistantConfig` (above) has
558
- // all schema defaults applied via `applyNestedDefaults`/`validateWithSchema`,
733
+ // schema defaults (with any deployment-context overrides already applied
734
+ // above) to disk so users can discover and edit all available options.
735
+ // When the file already exists, leave it alone disk represents user
736
+ // intent, while the in-memory `cached: AssistantConfig` (above) has all
737
+ // schema defaults applied via `applyNestedDefaults`/`validateWithSchema`,
559
738
  // so consumers calling `getConfig().memory.v2.bm25_b` continue to receive
560
739
  // the schema default whenever the field is absent on disk.
561
740
  //
@@ -573,18 +752,6 @@ export function loadConfig(): AssistantConfig {
573
752
  }
574
753
  // Strip dataDir (runtime-derived) from the persisted config
575
754
  const { dataDir: _, ...persistable } = config;
576
-
577
- // Layer deployment context defaults on top of schema defaults.
578
- // These are overrides the daemon derives from its environment (e.g.
579
- // IS_PLATFORM → all service modes = "managed"). Schema defaults
580
- // remain the fallback for non-platform deployments.
581
- const contextDefaults = getDeploymentContextDefaults();
582
- if (Object.keys(contextDefaults).length > 0) {
583
- deepMergeOverwrite(
584
- persistable as Record<string, unknown>,
585
- contextDefaults,
586
- );
587
- }
588
755
  writeFileSync(configPath, JSON.stringify(persistable, null, 2) + "\n");
589
756
  log.info("Wrote default config to %s", configPath);
590
757
  } catch (err) {
@@ -593,12 +760,14 @@ export function loadConfig(): AssistantConfig {
593
760
  }
594
761
 
595
762
  cached = config;
763
+ cachedFileSignature = readConfigFileSignature(configPath);
596
764
 
597
765
  loading = false;
598
766
  return config;
599
767
  } catch (err) {
600
768
  // Loading failed — clear cached so the next call retries
601
769
  cached = null;
770
+ cachedFileSignature = null;
602
771
  loading = false;
603
772
  throw err;
604
773
  }
@@ -632,7 +801,8 @@ export function getConfig(): AssistantConfig {
632
801
  * workspace-existence check runs.
633
802
  */
634
803
  export function getConfigReadOnly(): AssistantConfig {
635
- if (cached) return cached;
804
+ const freshCached = getCachedConfigIfFresh();
805
+ if (freshCached) return freshCached;
636
806
 
637
807
  const configPath = getConfigPath();
638
808
  let fileConfig: Record<string, unknown> = {};
@@ -649,6 +819,7 @@ export function getConfigReadOnly(): AssistantConfig {
649
819
 
650
820
  export function invalidateConfigCache(): void {
651
821
  cached = null;
822
+ cachedFileSignature = null;
652
823
  loading = false;
653
824
  }
654
825
 
@@ -686,6 +857,7 @@ export function saveRawConfig(config: Record<string, unknown>): void {
686
857
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
687
858
 
688
859
  cached = null; // invalidate cache
860
+ cachedFileSignature = null;
689
861
  }
690
862
 
691
863
  export function getNestedValue(
@@ -7,7 +7,7 @@ describe("MemoryV2ConfigSchema", () => {
7
7
  test("parses an empty object to documented defaults", () => {
8
8
  const parsed = MemoryV2ConfigSchema.parse({});
9
9
  expect(parsed).toEqual({
10
- enabled: false,
10
+ enabled: true,
11
11
  sweep_enabled: false,
12
12
  d: 0.3,
13
13
  c_user: 0.3,
@@ -15,9 +15,8 @@ describe("MemoryV2ConfigSchema", () => {
15
15
  c_now: 0.2,
16
16
  k: 0.5,
17
17
  hops: 2,
18
- top_k: 20,
18
+ top_k: 25,
19
19
  ann_candidate_limit: null,
20
- top_k_skills: 5,
21
20
  epsilon: 0.01,
22
21
  dense_weight: 0.85,
23
22
  sparse_weight: 0.15,
@@ -26,6 +25,13 @@ describe("MemoryV2ConfigSchema", () => {
26
25
  consolidation_interval_hours: 4,
27
26
  max_page_chars: 5000,
28
27
  consolidation_prompt_path: null,
28
+ rerank: {
29
+ enabled: false,
30
+ top_k: 50,
31
+ alpha: 0.3,
32
+ model: "Alibaba-NLP/gte-reranker-modernbert-base",
33
+ dtype: "q8",
34
+ },
29
35
  });
30
36
  });
31
37
 
@@ -155,12 +161,11 @@ describe("MemoryConfigSchema integration with v2 block", () => {
155
161
  test("parses an empty memory config and includes a v2 block with defaults", () => {
156
162
  const parsed = MemoryConfigSchema.parse({});
157
163
  expect(parsed.v2).toBeDefined();
158
- expect(parsed.v2.enabled).toBe(false);
164
+ expect(parsed.v2.enabled).toBe(true);
159
165
  expect(parsed.v2.sweep_enabled).toBe(false);
160
166
  expect(parsed.v2.d).toBe(0.3);
161
167
  expect(parsed.v2.dense_weight).toBe(0.85);
162
168
  expect(parsed.v2.sparse_weight).toBe(0.15);
163
- expect(parsed.v2.top_k_skills).toBe(5);
164
169
  expect(parsed.v2.consolidation_interval_hours).toBe(4);
165
170
  expect(parsed.v2.max_page_chars).toBe(5000);
166
171
  });
@@ -264,6 +264,20 @@ const CATALOG_RECORD: CatalogRecord = {
264
264
  description: "General-purpose LLM inference call site for skill use.",
265
265
  domain: "skills",
266
266
  },
267
+ proactiveArtifactDecision: {
268
+ id: "proactiveArtifactDecision",
269
+ displayName: "Proactive Artifact Decision",
270
+ description:
271
+ "Decides what personalized artifact to build for new users based on conversation context.",
272
+ domain: "agentLoop",
273
+ },
274
+ proactiveArtifactBuild: {
275
+ id: "proactiveArtifactBuild",
276
+ displayName: "Proactive Artifact Build",
277
+ description:
278
+ "Builds the personalized artifact in a background conversation with tool access.",
279
+ domain: "agentLoop",
280
+ },
267
281
  };
268
282
 
269
283
  // Source of truth for call-site display metadata. API responses and usage
@@ -121,8 +121,3 @@ export const SlackConfigSchema = z
121
121
  .describe("Slack bot display name"),
122
122
  })
123
123
  .describe("Slack channel configuration");
124
-
125
- export type TwilioConfig = z.infer<typeof TwilioConfigSchema>;
126
- export type WhatsAppConfig = z.infer<typeof WhatsAppConfigSchema>;
127
- export type TelegramConfig = z.infer<typeof TelegramConfigSchema>;
128
- export type SlackConfig = z.infer<typeof SlackConfigSchema>;
@@ -11,7 +11,7 @@ export const HeartbeatConfigSchema = z
11
11
  .number({ error: "heartbeat.intervalMs must be a number" })
12
12
  .int("heartbeat.intervalMs must be an integer")
13
13
  .positive("heartbeat.intervalMs must be a positive integer")
14
- .default(6 * 3_600_000)
14
+ .default(30 * 60_000)
15
15
  .describe("Time between heartbeat checks in milliseconds"),
16
16
  cronExpression: z
17
17
  .string()