@vellumai/assistant 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (535) hide show
  1. package/ARCHITECTURE.md +32 -49
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/security.md +20 -0
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +1 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
  19. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  20. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  21. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  22. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  23. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  24. package/openapi.yaml +565 -12
  25. package/package.json +6 -3
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  27. package/src/__tests__/app-bundler.test.ts +170 -1
  28. package/src/__tests__/app-control-flow.test.ts +374 -0
  29. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  30. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  31. package/src/__tests__/app-executors.test.ts +30 -43
  32. package/src/__tests__/approval-routes-http.test.ts +23 -6
  33. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  34. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  35. package/src/__tests__/assistant-event-hub.test.ts +109 -2
  36. package/src/__tests__/assistant-event.test.ts +10 -0
  37. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
  39. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  40. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  41. package/src/__tests__/btw-routes.test.ts +13 -4
  42. package/src/__tests__/call-controller.test.ts +49 -1
  43. package/src/__tests__/call-domain.test.ts +0 -2
  44. package/src/__tests__/call-routes-http.test.ts +0 -2
  45. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  46. package/src/__tests__/checker.test.ts +3 -4
  47. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  48. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  49. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  50. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  51. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  54. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  55. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  56. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  57. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  58. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  59. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  60. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  61. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  62. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  64. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  65. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  66. package/src/__tests__/credentials-cli.test.ts +5 -12
  67. package/src/__tests__/cu-unified-flow.test.ts +185 -23
  68. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  69. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  70. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  71. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  72. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  73. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  74. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  75. package/src/__tests__/heartbeat-service.test.ts +718 -1
  76. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  77. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  78. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  79. package/src/__tests__/host-bash-proxy.test.ts +246 -47
  80. package/src/__tests__/host-bash-routes.test.ts +294 -0
  81. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  82. package/src/__tests__/host-browser-routes.test.ts +39 -13
  83. package/src/__tests__/host-cu-proxy.test.ts +41 -52
  84. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  85. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  86. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  87. package/src/__tests__/host-file-proxy.test.ts +37 -43
  88. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  89. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  90. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  91. package/src/__tests__/host-proxy-base.test.ts +312 -0
  92. package/src/__tests__/host-shell-tool.test.ts +22 -4
  93. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  94. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  95. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  96. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  97. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  98. package/src/__tests__/identity-routes.test.ts +103 -1
  99. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  100. package/src/__tests__/inline-command-runner.test.ts +0 -1
  101. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  102. package/src/__tests__/integration-status.test.ts +85 -5
  103. package/src/__tests__/intent-routing.test.ts +0 -1
  104. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  105. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  106. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  107. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  108. package/src/__tests__/mcp-cli.test.ts +338 -2
  109. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  110. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  111. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  112. package/src/__tests__/oauth-cli.test.ts +0 -2
  113. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  114. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  115. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  116. package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
  117. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  118. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  119. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  120. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  121. package/src/__tests__/retry-backoff.test.ts +87 -0
  122. package/src/__tests__/runtime-events-sse.test.ts +10 -6
  123. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  124. package/src/__tests__/schedule-retry.test.ts +715 -0
  125. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  126. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  127. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  128. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  129. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  130. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  131. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  132. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  133. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  134. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  135. package/src/__tests__/slack-channel-config.test.ts +9 -14
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  137. package/src/__tests__/system-prompt.test.ts +0 -1
  138. package/src/__tests__/telegram-config.test.ts +0 -1
  139. package/src/__tests__/test-preload.ts +8 -0
  140. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  141. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  142. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  143. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  145. package/src/__tests__/tool-executor.test.ts +0 -1
  146. package/src/__tests__/twilio-config.test.ts +3 -16
  147. package/src/__tests__/twilio-routes.test.ts +3 -5
  148. package/src/__tests__/twilio-validation.test.ts +93 -0
  149. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  150. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  151. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  152. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  153. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  154. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  155. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  156. package/src/backup/__tests__/paths.test.ts +0 -22
  157. package/src/backup/__tests__/restore.test.ts +51 -151
  158. package/src/backup/paths.ts +2 -18
  159. package/src/backup/restore.ts +107 -231
  160. package/src/bundler/app-bundler.ts +51 -3
  161. package/src/calls/relay-server.ts +4 -44
  162. package/src/calls/twilio-config.ts +2 -17
  163. package/src/calls/twilio-rest.ts +33 -105
  164. package/src/calls/twilio-routes.ts +11 -12
  165. package/src/channels/types.ts +8 -7
  166. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  167. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  168. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  169. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  170. package/src/cli/commands/backup.ts +6 -331
  171. package/src/cli/commands/clients.ts +36 -37
  172. package/src/cli/commands/contacts.ts +73 -0
  173. package/src/cli/commands/conversations.ts +2 -5
  174. package/src/cli/commands/credentials.ts +15 -7
  175. package/src/cli/commands/domain.ts +66 -15
  176. package/src/cli/commands/gateway.ts +183 -0
  177. package/src/cli/commands/keys.ts +9 -6
  178. package/src/cli/commands/mcp.ts +116 -156
  179. package/src/cli/commands/memory-v2.ts +296 -1
  180. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  181. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  182. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  183. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  184. package/src/cli/commands/platform/disconnect.ts +5 -4
  185. package/src/cli/commands/platform/index.ts +0 -18
  186. package/src/cli/lib/daemon-credential-client.ts +110 -28
  187. package/src/cli/program.ts +2 -0
  188. package/src/config/assistant-feature-flags.ts +67 -10
  189. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  190. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  191. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  192. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  193. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  194. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  195. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  196. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  197. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  198. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  199. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  200. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  201. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  202. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  203. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  204. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  205. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  206. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  207. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  208. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  209. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  210. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  211. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  212. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  213. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  214. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  215. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  216. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  217. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  218. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  219. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  220. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  221. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  222. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  223. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  224. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  225. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  226. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  227. package/src/config/bundled-tool-registry.ts +21 -0
  228. package/src/config/env-registry.ts +0 -2
  229. package/src/config/env.ts +19 -12
  230. package/src/config/feature-flag-registry.json +21 -133
  231. package/src/config/loader.ts +73 -99
  232. package/src/config/sanitize-for-transfer.ts +2 -0
  233. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  234. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
  235. package/src/config/schemas/calls.ts +0 -9
  236. package/src/config/schemas/heartbeat.ts +63 -0
  237. package/src/config/schemas/ingress.ts +10 -6
  238. package/src/config/schemas/llm.ts +5 -10
  239. package/src/config/schemas/memory-lifecycle.ts +77 -24
  240. package/src/config/schemas/memory-v2.ts +48 -4
  241. package/src/config/schemas/platform.ts +6 -0
  242. package/src/config/schemas/services.ts +1 -15
  243. package/src/config/schemas/skills.ts +0 -6
  244. package/src/config/seed-inference-profiles.ts +1 -1
  245. package/src/contacts/contact-store.ts +0 -30
  246. package/src/contacts/contacts-write.ts +0 -27
  247. package/src/context/window-manager.ts +1 -2
  248. package/src/credential-execution/feature-gates.ts +10 -10
  249. package/src/credential-execution/process-manager.ts +12 -41
  250. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  251. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  252. package/src/daemon/config-watcher.ts +4 -3
  253. package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
  254. package/src/daemon/conversation-agent-loop.ts +32 -28
  255. package/src/daemon/conversation-lifecycle.ts +8 -1
  256. package/src/daemon/conversation-process.ts +16 -11
  257. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  258. package/src/daemon/conversation-surfaces.ts +125 -4
  259. package/src/daemon/conversation-tool-setup.ts +16 -55
  260. package/src/daemon/conversation.ts +21 -2
  261. package/src/daemon/doordash-steps.ts +1 -1
  262. package/src/daemon/handlers/shared.ts +4 -1
  263. package/src/daemon/host-app-control-proxy.ts +293 -0
  264. package/src/daemon/host-bash-proxy.ts +84 -74
  265. package/src/daemon/host-browser-proxy.ts +67 -82
  266. package/src/daemon/host-cu-proxy.ts +81 -86
  267. package/src/daemon/host-file-proxy.ts +93 -69
  268. package/src/daemon/host-proxy-base.ts +294 -0
  269. package/src/daemon/host-proxy-preactivation.ts +82 -0
  270. package/src/daemon/host-transfer-proxy.ts +247 -129
  271. package/src/daemon/lifecycle.ts +115 -117
  272. package/src/daemon/message-protocol.ts +3 -8
  273. package/src/daemon/message-types/contacts.ts +23 -1
  274. package/src/daemon/message-types/conversations.ts +11 -8
  275. package/src/daemon/message-types/host-app-control.ts +150 -0
  276. package/src/daemon/message-types/host-bash.ts +4 -0
  277. package/src/daemon/message-types/host-cu.ts +2 -0
  278. package/src/daemon/message-types/host-file.ts +4 -0
  279. package/src/daemon/message-types/host-transfer.ts +3 -0
  280. package/src/daemon/message-types/schedules.ts +8 -3
  281. package/src/daemon/message-types/skills.ts +2 -2
  282. package/src/daemon/process-message.ts +18 -1
  283. package/src/daemon/shutdown-handlers.ts +0 -3
  284. package/src/daemon/tool-setup-types.ts +51 -0
  285. package/src/daemon/tool-side-effects.ts +1 -1
  286. package/src/events/tool-audit-listener.ts +2 -1
  287. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
  288. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  289. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  290. package/src/heartbeat/heartbeat-service.ts +280 -49
  291. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  292. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  293. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  294. package/src/home/post-connect-feed.ts +68 -0
  295. package/src/home/relationship-state-writer.ts +17 -92
  296. package/src/home/suggested-prompts.ts +46 -10
  297. package/src/inbound/public-ingress-urls.ts +32 -34
  298. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  299. package/src/ipc/assistant-server.ts +14 -1
  300. package/src/ipc/cli-client.ts +32 -1
  301. package/src/live-voice/live-voice-metrics.ts +10 -10
  302. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  303. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  304. package/src/mcp/mcp-auth-state.ts +133 -0
  305. package/src/mcp/mcp-oauth-provider.ts +19 -0
  306. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  307. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  308. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  309. package/src/memory/anisotropy.test.ts +247 -0
  310. package/src/memory/anisotropy.ts +443 -0
  311. package/src/memory/auto-analysis-constants.ts +17 -0
  312. package/src/memory/auto-analysis-guard.ts +5 -15
  313. package/src/memory/canonical-guardian-store.ts +7 -7
  314. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  315. package/src/memory/context-search/agent-protocol.ts +6 -6
  316. package/src/memory/context-search/agent-runner.ts +32 -7
  317. package/src/memory/context-search/sources/memory-v2.ts +17 -5
  318. package/src/memory/conversation-crud.ts +1 -1
  319. package/src/memory/conversation-key-store.ts +2 -15
  320. package/src/memory/db-init.ts +4 -0
  321. package/src/memory/embedding-backend.ts +9 -21
  322. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
  323. package/src/memory/graph/conversation-graph-memory.ts +1 -24
  324. package/src/memory/graph/graph-search.ts +8 -0
  325. package/src/memory/graph/retriever.ts +28 -0
  326. package/src/memory/graph/tools.ts +1 -1
  327. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  328. package/src/memory/jobs/embed-concept-page.ts +28 -2
  329. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  330. package/src/memory/jobs-store.ts +66 -22
  331. package/src/memory/jobs-worker.ts +112 -63
  332. package/src/memory/memory-v2-activation-log-store.ts +1 -1
  333. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  334. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  335. package/src/memory/migrations/index.ts +5 -0
  336. package/src/memory/migrations/registry.ts +8 -0
  337. package/src/memory/pkb/pkb-search.ts +7 -0
  338. package/src/memory/qdrant-client.ts +50 -20
  339. package/src/memory/schema/infrastructure.ts +15 -0
  340. package/src/memory/search/semantic.ts +7 -0
  341. package/src/memory/sparse-tokenize.ts +49 -0
  342. package/src/memory/v2/__tests__/activation.test.ts +77 -95
  343. package/src/memory/v2/__tests__/injection.test.ts +43 -21
  344. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  345. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  346. package/src/memory/v2/__tests__/static-context.test.ts +0 -1
  347. package/src/memory/v2/activation.ts +69 -88
  348. package/src/memory/v2/consolidation-job.ts +3 -5
  349. package/src/memory/v2/constants.ts +7 -0
  350. package/src/memory/v2/injection.ts +86 -53
  351. package/src/memory/v2/prompts/consolidation.ts +312 -91
  352. package/src/memory/v2/qdrant.ts +99 -1
  353. package/src/memory/v2/sim.ts +126 -16
  354. package/src/memory/v2/skill-qdrant.ts +12 -3
  355. package/src/memory/v2/skill-store.ts +16 -1
  356. package/src/memory/v2/sparse-bm25.ts +245 -0
  357. package/src/memory/v2/static-context.ts +6 -5
  358. package/src/messaging/providers/gmail/types.ts +0 -49
  359. package/src/messaging/providers/slack/adapter.ts +1 -31
  360. package/src/messaging/providers/slack/types.ts +0 -32
  361. package/src/notifications/README.md +10 -10
  362. package/src/notifications/broadcaster.ts +1 -1
  363. package/src/notifications/guardian-question-mode.ts +5 -5
  364. package/src/oauth/connect-orchestrator.ts +4 -0
  365. package/src/oauth/credential-token-resolver.ts +1 -3
  366. package/src/oauth/manual-token-connection.ts +0 -4
  367. package/src/outbound-proxy/index.ts +1 -37
  368. package/src/outbound-proxy/logging.ts +1 -1
  369. package/src/outbound-proxy/policy.ts +6 -5
  370. package/src/outbound-proxy/router.ts +2 -1
  371. package/src/permissions/approval-policy.test.ts +6 -275
  372. package/src/permissions/approval-policy.ts +0 -51
  373. package/src/permissions/checker.test.ts +0 -1
  374. package/src/permissions/checker.ts +3 -17
  375. package/src/permissions/gateway-threshold-reader.ts +2 -0
  376. package/src/permissions/prompter.ts +34 -1
  377. package/src/permissions/secret-prompter.ts +6 -2
  378. package/src/prompts/bootstrap-cleanup.ts +27 -0
  379. package/src/prompts/system-prompt.ts +3 -18
  380. package/src/prompts/templates/SOUL.md +13 -1
  381. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  382. package/src/runtime/assistant-event-hub.ts +118 -96
  383. package/src/runtime/assistant-event.ts +1 -0
  384. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  385. package/src/runtime/auth/middleware.ts +0 -96
  386. package/src/runtime/auth/route-policy.ts +19 -0
  387. package/src/runtime/btw-sidechain.ts +2 -3
  388. package/src/runtime/channel-invite-transport.ts +2 -48
  389. package/src/runtime/channel-invite-transports/email.ts +1 -1
  390. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  391. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  392. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  393. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  394. package/src/runtime/channel-invite-types.ts +54 -0
  395. package/src/runtime/channel-readiness-service.ts +32 -13
  396. package/src/runtime/http-server.ts +3 -329
  397. package/src/runtime/http-types.ts +0 -5
  398. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  399. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  400. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  401. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  402. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  403. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  404. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  405. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  406. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  407. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  408. package/src/runtime/migrations/migration-transport.ts +7 -7
  409. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  410. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  411. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  412. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  413. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  414. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  415. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  416. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  417. package/src/runtime/pending-interactions.ts +35 -9
  418. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  419. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  420. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  421. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  422. package/src/runtime/routes/approval-interception-types.ts +13 -0
  423. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  424. package/src/runtime/routes/backup-routes.ts +15 -38
  425. package/src/runtime/routes/btw-routes.ts +14 -37
  426. package/src/runtime/routes/client-routes.ts +1 -0
  427. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  428. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  429. package/src/runtime/routes/conversation-routes.ts +30 -13
  430. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  431. package/src/runtime/routes/documents-routes.ts +30 -0
  432. package/src/runtime/routes/errors.ts +19 -4
  433. package/src/runtime/routes/events-routes.ts +12 -6
  434. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  435. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  436. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  437. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  438. package/src/runtime/routes/host-bash-routes.ts +36 -6
  439. package/src/runtime/routes/host-browser-routes.ts +108 -13
  440. package/src/runtime/routes/host-cu-routes.ts +44 -14
  441. package/src/runtime/routes/host-file-routes.ts +33 -10
  442. package/src/runtime/routes/host-transfer-routes.ts +64 -24
  443. package/src/runtime/routes/http-adapter.ts +1 -0
  444. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  445. package/src/runtime/routes/identity-routes.ts +15 -43
  446. package/src/runtime/routes/inbound-message-handler.ts +1 -9
  447. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  448. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  449. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  450. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  451. package/src/runtime/routes/index.ts +8 -0
  452. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  453. package/src/runtime/routes/memory-item-routes.ts +10 -12
  454. package/src/runtime/routes/memory-v2-routes.ts +441 -1
  455. package/src/runtime/routes/migration-routes.ts +96 -0
  456. package/src/runtime/routes/schedule-routes.ts +7 -0
  457. package/src/runtime/verification-templates.ts +4 -7
  458. package/src/schedule/integration-status.ts +66 -2
  459. package/src/schedule/recurrence-engine.ts +4 -1
  460. package/src/schedule/retry-backoff.ts +18 -0
  461. package/src/schedule/retry-policy.ts +82 -0
  462. package/src/schedule/schedule-recovery.ts +64 -0
  463. package/src/schedule/schedule-store.ts +106 -2
  464. package/src/schedule/scheduler-types.ts +25 -0
  465. package/src/schedule/scheduler.ts +63 -38
  466. package/src/security/oauth-callback-registry.ts +8 -0
  467. package/src/sequence/analytics.ts +5 -5
  468. package/src/sequence/engine.ts +1 -1
  469. package/src/skills/catalog-files.ts +2 -8
  470. package/src/skills/include-graph.ts +5 -5
  471. package/src/skills/remote-skill-policy.ts +5 -5
  472. package/src/skills/skill-file-provider.ts +1 -1
  473. package/src/skills/skill-file-types.ts +13 -0
  474. package/src/skills/skillssh-audit-types.ts +28 -0
  475. package/src/skills/skillssh-registry.ts +8 -21
  476. package/src/telemetry/types.ts +2 -0
  477. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  478. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  479. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  480. package/src/tools/apps/executors.ts +56 -69
  481. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  482. package/src/tools/browser/browser-execution.ts +2 -2
  483. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  484. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  485. package/src/tools/browser/cdp-client/factory.ts +23 -24
  486. package/src/tools/browser/cdp-client/index.ts +1 -14
  487. package/src/tools/computer-use/definitions.ts +42 -20
  488. package/src/tools/executor.ts +2 -0
  489. package/src/tools/host-filesystem/edit.ts +26 -0
  490. package/src/tools/host-filesystem/read.ts +26 -0
  491. package/src/tools/host-filesystem/transfer.ts +31 -1
  492. package/src/tools/host-filesystem/write.ts +26 -0
  493. package/src/tools/host-terminal/host-shell.ts +58 -0
  494. package/src/tools/schedule/create.ts +6 -0
  495. package/src/tools/schedule/list.ts +2 -0
  496. package/src/tools/schedule/update.ts +10 -0
  497. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  498. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  499. package/src/tools/skills/load.ts +0 -32
  500. package/src/tools/tool-approval-handler.ts +1 -5
  501. package/src/tools/types.ts +4 -0
  502. package/src/usage/pricing.ts +1 -1
  503. package/src/workspace/hatched-date.ts +86 -0
  504. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  505. package/src/workspace/migrations/006-services-config.ts +8 -5
  506. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  507. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  508. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  509. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  510. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  511. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  512. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  513. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  514. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  515. package/src/workspace/migrations/AGENTS.md +1 -1
  516. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  517. package/src/workspace/migrations/utils.ts +21 -0
  518. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  519. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  520. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  521. package/src/__tests__/twilio-rest.test.ts +0 -34
  522. package/src/backup/__tests__/backup-key.test.ts +0 -152
  523. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  524. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  525. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  526. package/src/backup/backup-key.ts +0 -137
  527. package/src/backup/backup-worker.ts +0 -472
  528. package/src/backup/offsite-writer.ts +0 -222
  529. package/src/backup/stream-crypt.ts +0 -263
  530. package/src/daemon/message-types/pairing.ts +0 -58
  531. package/src/outbound-proxy/config.ts +0 -20
  532. package/src/outbound-proxy/health.ts +0 -18
  533. package/src/outbound-proxy/types.ts +0 -150
  534. package/src/runtime/capability-tokens.ts +0 -190
  535. package/src/signals/mcp-reload.ts +0 -18
@@ -1,782 +0,0 @@
1
- /**
2
- * Tests for the periodic backup worker. Drives `runBackupTick` and
3
- * `createSnapshotNow` directly with fake dependencies so the whole pipeline
4
- * runs against a temp directory with an in-memory checkpoint store and
5
- * no real database.
6
- *
7
- * `streamExportVBundle` is stubbed to write a tiny byte blob to a temp
8
- * file — the worker never validates bundle contents, it just hands the
9
- * path to `writeLocalSnapshot` which renames it into place.
10
- */
11
-
12
- import {
13
- existsSync,
14
- mkdirSync,
15
- mkdtempSync,
16
- readdirSync,
17
- rmSync,
18
- writeFileSync,
19
- } from "node:fs";
20
- import { unlink, writeFile } from "node:fs/promises";
21
- import { tmpdir } from "node:os";
22
- import { join } from "node:path";
23
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
24
-
25
- import type { BackupConfig, BackupDestination } from "../../config/schema.js";
26
- import { BackupConfigSchema } from "../../config/schema.js";
27
- import type { StreamExportVBundleResult } from "../../runtime/migrations/vbundle-builder.js";
28
- import type { BackupDeps } from "../backup-worker.js";
29
- import { createSnapshotNow, runBackupTick } from "../backup-worker.js";
30
-
31
- // ---------------------------------------------------------------------------
32
- // Test fixtures
33
- // ---------------------------------------------------------------------------
34
-
35
- let ROOT: string;
36
-
37
- beforeEach(() => {
38
- ROOT = mkdtempSync(join(tmpdir(), "vellum-backup-worker-"));
39
- });
40
-
41
- afterEach(() => {
42
- try {
43
- rmSync(ROOT, { recursive: true, force: true });
44
- } catch {
45
- // best-effort
46
- }
47
- });
48
-
49
- /** Build a valid BackupConfig with overrides. Starts from schema defaults. */
50
- function makeConfig(overrides?: {
51
- enabled?: boolean;
52
- intervalHours?: number;
53
- retention?: number;
54
- localDirectory?: string | null;
55
- offsite?: {
56
- enabled?: boolean;
57
- destinations?: BackupDestination[] | null;
58
- };
59
- }): BackupConfig {
60
- const base = BackupConfigSchema.parse({});
61
- return {
62
- ...base,
63
- enabled: overrides?.enabled ?? base.enabled,
64
- intervalHours: overrides?.intervalHours ?? base.intervalHours,
65
- retention: overrides?.retention ?? base.retention,
66
- localDirectory: overrides?.localDirectory ?? base.localDirectory,
67
- offsite: {
68
- enabled: overrides?.offsite?.enabled ?? base.offsite.enabled,
69
- destinations:
70
- overrides?.offsite?.destinations === undefined
71
- ? base.offsite.destinations
72
- : overrides.offsite.destinations,
73
- },
74
- };
75
- }
76
-
77
- /**
78
- * Build an in-memory checkpoint store. Returns fake getters/setters and a
79
- * plain object so tests can inspect or preload entries.
80
- */
81
- function makeCheckpointStore(initial: Record<string, string> = {}) {
82
- const store: Record<string, string> = { ...initial };
83
- return {
84
- store,
85
- get: (key: string): string | null => store[key] ?? null,
86
- set: (key: string, value: string): void => {
87
- store[key] = value;
88
- },
89
- };
90
- }
91
-
92
- /**
93
- * Build a stub `streamExportVBundle` that writes a tiny payload to a fresh
94
- * temp file on every call. The worker only cares that `tempPath` exists and
95
- * can be renamed — the bundle content is never introspected.
96
- */
97
- function makeStreamExportStub(): {
98
- fn: BackupDeps["streamExportVBundle"];
99
- calls: Array<Parameters<NonNullable<BackupDeps["streamExportVBundle"]>>[0]>;
100
- } {
101
- const calls: Array<
102
- Parameters<NonNullable<BackupDeps["streamExportVBundle"]>>[0]
103
- > = [];
104
- let counter = 0;
105
- const fn: BackupDeps["streamExportVBundle"] = async (opts) => {
106
- calls.push(opts);
107
- // Deliberately do NOT fire opts.checkpoint?.() here — the real checkpoint
108
- // callback opens a fresh DB handle at `getDbPath()` which does not exist
109
- // in the test environment. Tests don't care about the WAL side-effect;
110
- // they just need the stub to return a valid temp bundle path.
111
- counter += 1;
112
- const tempPath = join(ROOT, `stub-bundle-${counter}.tmp`);
113
- await writeFile(tempPath, `fake bundle ${counter}`);
114
- const result: StreamExportVBundleResult = {
115
- tempPath,
116
- size: 16,
117
- manifest: {
118
- schema_version: 1,
119
- bundle_id: "00000000-0000-4000-8000-000000000000",
120
- created_at: new Date().toISOString(),
121
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
122
- origin: { mode: "self-hosted-local" },
123
- compatibility: {
124
- min_runtime_version: "0.0.0-test",
125
- max_runtime_version: null,
126
- },
127
- contents: [],
128
- checksum: "0".repeat(64),
129
- secrets_redacted: false,
130
- export_options: {
131
- include_logs: false,
132
- include_browser_state: false,
133
- include_memory_vectors: false,
134
- },
135
- },
136
- cleanup: async () => {
137
- try {
138
- await unlink(tempPath);
139
- } catch {
140
- // best-effort
141
- }
142
- },
143
- };
144
- return result;
145
- };
146
- return { fn, calls };
147
- }
148
-
149
- // ---------------------------------------------------------------------------
150
- // runBackupTick — gating
151
- // ---------------------------------------------------------------------------
152
-
153
- describe("runBackupTick — gating", () => {
154
- test("returns null when config.enabled is false", async () => {
155
- const checkpoints = makeCheckpointStore();
156
- const streamStub = makeStreamExportStub();
157
- const config = makeConfig({ enabled: false });
158
- const localDir = join(ROOT, "local");
159
-
160
- const result = await runBackupTick(config, new Date(), {
161
- streamExportVBundle: streamStub.fn,
162
- getMemoryCheckpoint: checkpoints.get,
163
- setMemoryCheckpoint: checkpoints.set,
164
- workspaceDir: ROOT,
165
- localDir,
166
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
167
- });
168
-
169
- expect(result).toBeNull();
170
- expect(streamStub.calls).toHaveLength(0);
171
- expect(Object.keys(checkpoints.store)).toHaveLength(0);
172
- expect(existsSync(localDir)).toBe(false);
173
- });
174
-
175
- test("returns null when last_run_at is within the interval window", async () => {
176
- const now = new Date("2026-04-11T10:00:00Z");
177
- const oneHourAgoMs = now.getTime() - 1 * 3600 * 1000;
178
- const checkpoints = makeCheckpointStore({
179
- "backup:last_run_at": String(oneHourAgoMs),
180
- });
181
- const streamStub = makeStreamExportStub();
182
- const config = makeConfig({ enabled: true, intervalHours: 6 });
183
- const localDir = join(ROOT, "local");
184
-
185
- const result = await runBackupTick(config, now, {
186
- streamExportVBundle: streamStub.fn,
187
- getMemoryCheckpoint: checkpoints.get,
188
- setMemoryCheckpoint: checkpoints.set,
189
- workspaceDir: ROOT,
190
- localDir,
191
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
192
- });
193
-
194
- expect(result).toBeNull();
195
- expect(streamStub.calls).toHaveLength(0);
196
- // Checkpoint unchanged
197
- expect(checkpoints.store["backup:last_run_at"]).toBe(String(oneHourAgoMs));
198
- });
199
-
200
- test("runs when last_run_at is older than the interval", async () => {
201
- const now = new Date("2026-04-11T10:00:00Z");
202
- const sevenHoursAgoMs = now.getTime() - 7 * 3600 * 1000;
203
- const checkpoints = makeCheckpointStore({
204
- "backup:last_run_at": String(sevenHoursAgoMs),
205
- });
206
- const streamStub = makeStreamExportStub();
207
- const config = makeConfig({
208
- enabled: true,
209
- intervalHours: 6,
210
- offsite: { enabled: false, destinations: null },
211
- });
212
- const localDir = join(ROOT, "local");
213
-
214
- const result = await runBackupTick(config, now, {
215
- streamExportVBundle: streamStub.fn,
216
- getMemoryCheckpoint: checkpoints.get,
217
- setMemoryCheckpoint: checkpoints.set,
218
- workspaceDir: ROOT,
219
- localDir,
220
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
221
- });
222
-
223
- expect(result).not.toBeNull();
224
- expect(streamStub.calls).toHaveLength(1);
225
- expect(checkpoints.store["backup:last_run_at"]).toBe(String(now.getTime()));
226
- // Local snapshot file was created
227
- expect(result!.local.path).toContain("backup-20260411-100000-000.vbundle");
228
- expect(existsSync(result!.local.path)).toBe(true);
229
- expect(result!.offsite).toEqual([]);
230
- });
231
-
232
- test("runs when last_run_at checkpoint is missing (first-ever run)", async () => {
233
- const now = new Date("2026-04-11T10:00:00Z");
234
- const checkpoints = makeCheckpointStore();
235
- const streamStub = makeStreamExportStub();
236
- const config = makeConfig({
237
- enabled: true,
238
- offsite: { enabled: false, destinations: null },
239
- });
240
- const localDir = join(ROOT, "local");
241
-
242
- const result = await runBackupTick(config, now, {
243
- streamExportVBundle: streamStub.fn,
244
- getMemoryCheckpoint: checkpoints.get,
245
- setMemoryCheckpoint: checkpoints.set,
246
- workspaceDir: ROOT,
247
- localDir,
248
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
249
- });
250
-
251
- expect(result).not.toBeNull();
252
- expect(checkpoints.store["backup:last_run_at"]).toBe(String(now.getTime()));
253
- });
254
- });
255
-
256
- // ---------------------------------------------------------------------------
257
- // runBackupTick — offsite destinations
258
- // ---------------------------------------------------------------------------
259
-
260
- describe("runBackupTick — offsite destinations", () => {
261
- test("config.offsite.enabled === false: offsite is empty and key is not loaded", async () => {
262
- const checkpoints = makeCheckpointStore();
263
- const streamStub = makeStreamExportStub();
264
- const ensureKey = mock(async () => Buffer.alloc(32, 1));
265
- const config = makeConfig({
266
- enabled: true,
267
- offsite: { enabled: false, destinations: null },
268
- });
269
- const localDir = join(ROOT, "local");
270
-
271
- const result = await runBackupTick(config, new Date(), {
272
- streamExportVBundle: streamStub.fn,
273
- getMemoryCheckpoint: checkpoints.get,
274
- setMemoryCheckpoint: checkpoints.set,
275
- ensureBackupKey: ensureKey,
276
- workspaceDir: ROOT,
277
- localDir,
278
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
279
- });
280
-
281
- expect(result).not.toBeNull();
282
- expect(result!.offsite).toEqual([]);
283
- expect(ensureKey).not.toHaveBeenCalled();
284
- });
285
-
286
- test("single plaintext destination: key is not loaded, file has .vbundle extension", async () => {
287
- const checkpoints = makeCheckpointStore();
288
- const streamStub = makeStreamExportStub();
289
- const ensureKey = mock(async () => Buffer.alloc(32, 1));
290
- const offsiteDir = join(ROOT, "offsite", "plain");
291
- // Parent must exist — writer probes for parent before mkdir of dest.
292
- mkdirSync(join(ROOT, "offsite"), { recursive: true });
293
- const config = makeConfig({
294
- enabled: true,
295
- offsite: {
296
- enabled: true,
297
- destinations: [{ path: offsiteDir, encrypt: false }],
298
- },
299
- });
300
- const localDir = join(ROOT, "local");
301
- const now = new Date("2026-04-11T12:00:00Z");
302
-
303
- const result = await runBackupTick(config, now, {
304
- streamExportVBundle: streamStub.fn,
305
- getMemoryCheckpoint: checkpoints.get,
306
- setMemoryCheckpoint: checkpoints.set,
307
- ensureBackupKey: ensureKey,
308
- workspaceDir: ROOT,
309
- localDir,
310
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
311
- });
312
-
313
- expect(result).not.toBeNull();
314
- expect(ensureKey).not.toHaveBeenCalled();
315
- expect(result!.offsite).toHaveLength(1);
316
- expect(result!.offsite[0].entry).not.toBeNull();
317
- expect(result!.offsite[0].entry!.filename).toBe(
318
- "backup-20260411-120000-000.vbundle",
319
- );
320
- expect(result!.offsite[0].entry!.encrypted).toBe(false);
321
- expect(existsSync(result!.offsite[0].entry!.path)).toBe(true);
322
- });
323
-
324
- test("single encrypted destination: key is loaded, file has .vbundle.enc extension", async () => {
325
- const checkpoints = makeCheckpointStore();
326
- const streamStub = makeStreamExportStub();
327
- const ensureKey = mock(async () => Buffer.alloc(32, 0xab));
328
- const offsiteDir = join(ROOT, "offsite", "enc");
329
- mkdirSync(join(ROOT, "offsite"), { recursive: true });
330
- const keyPath = join(ROOT, "backup.key");
331
- const config = makeConfig({
332
- enabled: true,
333
- offsite: {
334
- enabled: true,
335
- destinations: [{ path: offsiteDir, encrypt: true }],
336
- },
337
- });
338
- const localDir = join(ROOT, "local");
339
- const now = new Date("2026-04-11T13:00:00Z");
340
-
341
- const result = await runBackupTick(config, now, {
342
- streamExportVBundle: streamStub.fn,
343
- getMemoryCheckpoint: checkpoints.get,
344
- setMemoryCheckpoint: checkpoints.set,
345
- ensureBackupKey: ensureKey,
346
- backupKeyPath: keyPath,
347
- workspaceDir: ROOT,
348
- localDir,
349
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
350
- });
351
-
352
- expect(result).not.toBeNull();
353
- expect(ensureKey).toHaveBeenCalledTimes(1);
354
- expect(ensureKey).toHaveBeenCalledWith(keyPath);
355
- expect(result!.offsite).toHaveLength(1);
356
- expect(result!.offsite[0].entry).not.toBeNull();
357
- expect(result!.offsite[0].entry!.filename).toBe(
358
- "backup-20260411-130000-000.vbundle.enc",
359
- );
360
- expect(result!.offsite[0].entry!.encrypted).toBe(true);
361
- expect(existsSync(result!.offsite[0].entry!.path)).toBe(true);
362
- });
363
-
364
- test("mixed destinations: key is loaded once (because A needs it), both files written", async () => {
365
- const checkpoints = makeCheckpointStore();
366
- const streamStub = makeStreamExportStub();
367
- const ensureKey = mock(async () => Buffer.alloc(32, 0xcd));
368
- const encDir = join(ROOT, "offsite", "enc");
369
- const plainDir = join(ROOT, "offsite", "plain");
370
- mkdirSync(join(ROOT, "offsite"), { recursive: true });
371
- const config = makeConfig({
372
- enabled: true,
373
- offsite: {
374
- enabled: true,
375
- destinations: [
376
- { path: encDir, encrypt: true },
377
- { path: plainDir, encrypt: false },
378
- ],
379
- },
380
- });
381
- const localDir = join(ROOT, "local");
382
-
383
- const result = await runBackupTick(config, new Date(), {
384
- streamExportVBundle: streamStub.fn,
385
- getMemoryCheckpoint: checkpoints.get,
386
- setMemoryCheckpoint: checkpoints.set,
387
- ensureBackupKey: ensureKey,
388
- workspaceDir: ROOT,
389
- localDir,
390
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
391
- });
392
-
393
- expect(result).not.toBeNull();
394
- expect(ensureKey).toHaveBeenCalledTimes(1);
395
- expect(result!.offsite).toHaveLength(2);
396
- expect(result!.offsite[0].entry).not.toBeNull();
397
- expect(result!.offsite[0].entry!.encrypted).toBe(true);
398
- expect(result!.offsite[1].entry).not.toBeNull();
399
- expect(result!.offsite[1].entry!.encrypted).toBe(false);
400
- });
401
-
402
- test("mixed reachability: one ok + one parent-missing skip, local succeeds, checkpoint updated", async () => {
403
- const checkpoints = makeCheckpointStore();
404
- const streamStub = makeStreamExportStub();
405
- const reachableDir = join(ROOT, "offsite", "reachable");
406
- // Nested parent-missing: the parent directory is /nope/deeper which is
407
- // unreachable because /nope itself does not exist.
408
- const unreachableDir = join(ROOT, "nope", "deeper", "backups");
409
- mkdirSync(join(ROOT, "offsite"), { recursive: true });
410
- const config = makeConfig({
411
- enabled: true,
412
- offsite: {
413
- enabled: true,
414
- destinations: [
415
- { path: reachableDir, encrypt: false },
416
- { path: unreachableDir, encrypt: false },
417
- ],
418
- },
419
- });
420
- const localDir = join(ROOT, "local");
421
- const now = new Date("2026-04-11T14:00:00Z");
422
-
423
- const result = await runBackupTick(config, now, {
424
- streamExportVBundle: streamStub.fn,
425
- getMemoryCheckpoint: checkpoints.get,
426
- setMemoryCheckpoint: checkpoints.set,
427
- workspaceDir: ROOT,
428
- localDir,
429
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
430
- });
431
-
432
- expect(result).not.toBeNull();
433
- expect(result!.offsite).toHaveLength(2);
434
- expect(result!.offsite[0].entry).not.toBeNull();
435
- expect(result!.offsite[1].entry).toBeNull();
436
- expect(result!.offsite[1].skipped).toBe("parent-missing");
437
- // Local still succeeded
438
- expect(existsSync(result!.local.path)).toBe(true);
439
- // Checkpoint updated because performBackup returned successfully
440
- expect(checkpoints.store["backup:last_run_at"]).toBe(String(now.getTime()));
441
- });
442
- });
443
-
444
- // ---------------------------------------------------------------------------
445
- // runBackupTick — error propagation
446
- // ---------------------------------------------------------------------------
447
-
448
- describe("runBackupTick — error propagation", () => {
449
- test("throws when streamExportVBundle throws and leaves checkpoint untouched", async () => {
450
- const checkpoints = makeCheckpointStore();
451
- const throwingStream: BackupDeps["streamExportVBundle"] = async () => {
452
- throw new Error("boom");
453
- };
454
- const config = makeConfig({
455
- enabled: true,
456
- offsite: { enabled: false, destinations: null },
457
- });
458
- const localDir = join(ROOT, "local");
459
-
460
- await expect(
461
- runBackupTick(config, new Date(), {
462
- streamExportVBundle: throwingStream,
463
- getMemoryCheckpoint: checkpoints.get,
464
- setMemoryCheckpoint: checkpoints.set,
465
- workspaceDir: ROOT,
466
- localDir,
467
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
468
- }),
469
- ).rejects.toThrow("boom");
470
- expect(checkpoints.store["backup:last_run_at"]).toBeUndefined();
471
- });
472
- });
473
-
474
- // ---------------------------------------------------------------------------
475
- // createSnapshotNow — manual trigger
476
- // ---------------------------------------------------------------------------
477
-
478
- describe("createSnapshotNow", () => {
479
- test("bypasses enabled check (snapshot created even when enabled is false)", async () => {
480
- const checkpoints = makeCheckpointStore();
481
- const streamStub = makeStreamExportStub();
482
- const config = makeConfig({
483
- enabled: false,
484
- offsite: { enabled: false, destinations: null },
485
- });
486
- const localDir = join(ROOT, "local");
487
-
488
- const result = await createSnapshotNow(config, new Date(), {
489
- streamExportVBundle: streamStub.fn,
490
- getMemoryCheckpoint: checkpoints.get,
491
- setMemoryCheckpoint: checkpoints.set,
492
- workspaceDir: ROOT,
493
- localDir,
494
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
495
- });
496
-
497
- expect(result).not.toBeNull();
498
- expect(streamStub.calls).toHaveLength(1);
499
- // Manual runs do NOT update the automatic cadence checkpoint
500
- expect(checkpoints.store["backup:last_run_at"]).toBeUndefined();
501
- });
502
-
503
- test("bypasses interval check even when a recent run was recorded", async () => {
504
- const now = new Date("2026-04-11T10:00:00Z");
505
- const checkpoints = makeCheckpointStore({
506
- "backup:last_run_at": String(now.getTime() - 60_000),
507
- });
508
- const streamStub = makeStreamExportStub();
509
- const config = makeConfig({
510
- enabled: true,
511
- intervalHours: 6,
512
- offsite: { enabled: false, destinations: null },
513
- });
514
- const localDir = join(ROOT, "local");
515
-
516
- const result = await createSnapshotNow(config, now, {
517
- streamExportVBundle: streamStub.fn,
518
- getMemoryCheckpoint: checkpoints.get,
519
- setMemoryCheckpoint: checkpoints.set,
520
- workspaceDir: ROOT,
521
- localDir,
522
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
523
- });
524
-
525
- expect(result).not.toBeNull();
526
- expect(streamStub.calls).toHaveLength(1);
527
- // The pre-existing checkpoint is preserved — manual runs do not touch it.
528
- expect(checkpoints.store["backup:last_run_at"]).toBe(
529
- String(now.getTime() - 60_000),
530
- );
531
- });
532
-
533
- test("two concurrent calls: second throws 'snapshot in progress'", async () => {
534
- const checkpoints = makeCheckpointStore();
535
- // Stub that holds the first caller indefinitely until we release it,
536
- // giving the test a clean window to observe the mutex from a second call.
537
- let release: () => void = () => {};
538
- const holdPromise = new Promise<void>((resolve) => {
539
- release = resolve;
540
- });
541
- let callCount = 0;
542
- const holdingStream: BackupDeps["streamExportVBundle"] = async (_opts) => {
543
- callCount += 1;
544
- if (callCount === 1) {
545
- await holdPromise;
546
- }
547
- const tempPath = join(ROOT, `hold-${callCount}.tmp`);
548
- writeFileSync(tempPath, "payload");
549
- return {
550
- tempPath,
551
- size: 7,
552
- manifest: {
553
- schema_version: 1,
554
- bundle_id: "00000000-0000-4000-8000-000000000000",
555
- created_at: new Date().toISOString(),
556
- assistant: {
557
- id: "self",
558
- name: "Test",
559
- runtime_version: "0.0.0-test",
560
- },
561
- origin: { mode: "self-hosted-local" },
562
- compatibility: {
563
- min_runtime_version: "0.0.0-test",
564
- max_runtime_version: null,
565
- },
566
- contents: [],
567
- checksum: "0".repeat(64),
568
- secrets_redacted: false,
569
- export_options: {
570
- include_logs: false,
571
- include_browser_state: false,
572
- include_memory_vectors: false,
573
- },
574
- },
575
- cleanup: async () => {
576
- try {
577
- await unlink(tempPath);
578
- } catch {
579
- // best-effort
580
- }
581
- },
582
- };
583
- };
584
- const config = makeConfig({
585
- enabled: true,
586
- offsite: { enabled: false, destinations: null },
587
- });
588
- const localDir = join(ROOT, "local");
589
-
590
- // Start the first call — it will park inside `streamExportVBundle`
591
- // waiting on holdPromise.
592
- const first = createSnapshotNow(config, new Date(), {
593
- streamExportVBundle: holdingStream,
594
- getMemoryCheckpoint: checkpoints.get,
595
- setMemoryCheckpoint: checkpoints.set,
596
- workspaceDir: ROOT,
597
- localDir,
598
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
599
- });
600
-
601
- // Yield once so the first call has a chance to enter the mutex + the
602
- // stream stub before we kick off the second call.
603
- await Promise.resolve();
604
- await Promise.resolve();
605
-
606
- await expect(
607
- createSnapshotNow(config, new Date(), {
608
- streamExportVBundle: holdingStream,
609
- getMemoryCheckpoint: checkpoints.get,
610
- setMemoryCheckpoint: checkpoints.set,
611
- workspaceDir: ROOT,
612
- localDir,
613
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
614
- }),
615
- ).rejects.toThrow("snapshot in progress");
616
-
617
- release();
618
- await first;
619
- // Only the first call should have been executed by the stub.
620
- expect(callCount).toBe(1);
621
- });
622
- });
623
-
624
- // ---------------------------------------------------------------------------
625
- // Cross-process lock (simulates a second process holding the lock)
626
- // ---------------------------------------------------------------------------
627
-
628
- describe("cross-process snapshot lock", () => {
629
- test("after performBackup succeeds, the lock file no longer exists", async () => {
630
- const checkpoints = makeCheckpointStore();
631
- const streamStub = makeStreamExportStub();
632
- const config = makeConfig({
633
- enabled: true,
634
- offsite: { enabled: false, destinations: null },
635
- });
636
- const localDir = join(ROOT, "local");
637
- const lockPath = join(ROOT, ".snapshot.lock");
638
-
639
- const result = await createSnapshotNow(config, new Date(), {
640
- streamExportVBundle: streamStub.fn,
641
- getMemoryCheckpoint: checkpoints.get,
642
- setMemoryCheckpoint: checkpoints.set,
643
- workspaceDir: ROOT,
644
- localDir,
645
- snapshotLockPath: lockPath,
646
- });
647
-
648
- expect(result).not.toBeNull();
649
- // Lock file released on the finally path — must not linger on disk.
650
- expect(existsSync(lockPath)).toBe(false);
651
- });
652
-
653
- test("another process holds the lock: createSnapshotNow throws 'snapshot in progress'", async () => {
654
- const checkpoints = makeCheckpointStore();
655
- const streamStub = makeStreamExportStub();
656
- const config = makeConfig({
657
- enabled: true,
658
- offsite: { enabled: false, destinations: null },
659
- });
660
- const localDir = join(ROOT, "local");
661
- const lockPath = join(ROOT, ".snapshot.lock");
662
-
663
- // Simulate a concurrent CLI invocation by writing a lock file with the
664
- // CURRENT pid (which is definitely alive — it's us). Because the lock
665
- // file pre-exists and the PID probes as alive, the in-process flag will
666
- // pass (it's reset after the previous test) but the cross-process lock
667
- // will reject with "snapshot in progress (locked by pid N)".
668
- writeFileSync(lockPath, `${process.pid} ${Date.now()}\n`, { mode: 0o600 });
669
-
670
- await expect(
671
- createSnapshotNow(config, new Date(), {
672
- streamExportVBundle: streamStub.fn,
673
- getMemoryCheckpoint: checkpoints.get,
674
- setMemoryCheckpoint: checkpoints.set,
675
- workspaceDir: ROOT,
676
- localDir,
677
- snapshotLockPath: lockPath,
678
- }),
679
- ).rejects.toThrow(/snapshot in progress/);
680
-
681
- // The stream stub must not have been invoked because acquisition failed
682
- // before performBackup ran.
683
- expect(streamStub.calls).toHaveLength(0);
684
- // The pre-existing lock file is preserved — we did not own it, so we
685
- // must not have removed it on the failed-acquisition path.
686
- expect(existsSync(lockPath)).toBe(true);
687
- });
688
-
689
- test("runBackupTick defers silently when another process holds the lock", async () => {
690
- const now = new Date("2026-04-11T10:00:00Z");
691
- const checkpoints = makeCheckpointStore();
692
- const streamStub = makeStreamExportStub();
693
- const config = makeConfig({
694
- enabled: true,
695
- offsite: { enabled: false, destinations: null },
696
- });
697
- const localDir = join(ROOT, "local");
698
- const lockPath = join(ROOT, ".snapshot.lock");
699
-
700
- // Pre-seed the lock file with the live PID so the worker observes a
701
- // conflict on its cross-process check.
702
- writeFileSync(lockPath, `${process.pid} ${Date.now()}\n`, { mode: 0o600 });
703
-
704
- const result = await runBackupTick(config, now, {
705
- streamExportVBundle: streamStub.fn,
706
- getMemoryCheckpoint: checkpoints.get,
707
- setMemoryCheckpoint: checkpoints.set,
708
- workspaceDir: ROOT,
709
- localDir,
710
- snapshotLockPath: lockPath,
711
- });
712
-
713
- // Scheduled tick defers silently on conflict rather than throwing — the
714
- // next interval will retry.
715
- expect(result).toBeNull();
716
- expect(streamStub.calls).toHaveLength(0);
717
- // Checkpoint must not advance when the tick defers.
718
- expect(checkpoints.store["backup:last_run_at"]).toBeUndefined();
719
- // Pre-existing lock file is preserved.
720
- expect(existsSync(lockPath)).toBe(true);
721
- });
722
- });
723
-
724
- // ---------------------------------------------------------------------------
725
- // Retention — integration across multiple ticks
726
- // ---------------------------------------------------------------------------
727
-
728
- describe("retention across successive ticks", () => {
729
- test("three ticks past the interval with retention=2 leaves 2 local + 2 offsite", async () => {
730
- const checkpoints = makeCheckpointStore();
731
- const streamStub = makeStreamExportStub();
732
- const offsiteDir = join(ROOT, "offsite", "plain");
733
- mkdirSync(join(ROOT, "offsite"), { recursive: true });
734
- const config = makeConfig({
735
- enabled: true,
736
- intervalHours: 1,
737
- retention: 2,
738
- offsite: {
739
- enabled: true,
740
- destinations: [{ path: offsiteDir, encrypt: false }],
741
- },
742
- });
743
- const localDir = join(ROOT, "local");
744
-
745
- // Three successive runs, each 2 hours apart (past the 1-hour interval).
746
- const t1 = new Date("2026-04-11T10:00:00Z");
747
- const t2 = new Date("2026-04-11T12:00:00Z");
748
- const t3 = new Date("2026-04-11T14:00:00Z");
749
-
750
- for (const t of [t1, t2, t3]) {
751
- const result = await runBackupTick(config, t, {
752
- streamExportVBundle: streamStub.fn,
753
- getMemoryCheckpoint: checkpoints.get,
754
- setMemoryCheckpoint: checkpoints.set,
755
- workspaceDir: ROOT,
756
- localDir,
757
- snapshotLockPath: join(ROOT, ".snapshot.lock"),
758
- });
759
- expect(result).not.toBeNull();
760
- }
761
-
762
- // After three runs with retention=2, only the two newest survive in
763
- // both local and offsite pools.
764
- const localFiles = readdirSync(localDir)
765
- .filter((f) => f.startsWith("backup-"))
766
- .sort();
767
- expect(localFiles).toHaveLength(2);
768
- expect(localFiles).toEqual([
769
- "backup-20260411-120000-000.vbundle",
770
- "backup-20260411-140000-000.vbundle",
771
- ]);
772
-
773
- const offsiteFiles = readdirSync(offsiteDir)
774
- .filter((f) => f.startsWith("backup-"))
775
- .sort();
776
- expect(offsiteFiles).toHaveLength(2);
777
- expect(offsiteFiles).toEqual([
778
- "backup-20260411-120000-000.vbundle",
779
- "backup-20260411-140000-000.vbundle",
780
- ]);
781
- });
782
- });