@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
@@ -153,25 +153,18 @@ describe("pre-chat onboarding contract", () => {
153
153
  const result = buildSystemPrompt({ onboardingContext: context });
154
154
  const dynamic = dynamicBlock(result);
155
155
 
156
- expect(dynamic).toContain("## Pre-chat Onboarding Context");
156
+ expect(dynamic).toContain("## First-Run User Context");
157
157
  expect(dynamic).toContain(
158
- "The user completed the native pre-chat onboarding.",
159
- );
160
- expect(dynamic).toContain('"tools"');
161
- expect(dynamic).toContain('"slack"');
162
- expect(dynamic).toContain('"linear"');
163
- expect(dynamic).toContain('"tasks"');
164
- expect(dynamic).toContain('"code-building"');
165
- expect(dynamic).toContain('"tone": "warm"');
166
- expect(dynamic).toContain('"userName": "Alex"');
167
- expect(dynamic).toContain('"assistantName": "Nova"');
168
- expect(dynamic).toContain("```json");
169
- expect(dynamic).toContain(
170
- "Use this to personalize your opener and skip redundant discovery.",
171
- );
172
- expect(dynamic).toContain(
173
- "If `assistantName` is present, it is the name the user chose for you; preserve it in IDENTITY.md.",
158
+ "The user completed setup before this conversation.",
174
159
  );
160
+ expect(dynamic).toContain("- Daily tools: Slack, Linear");
161
+ expect(dynamic).toContain("- Common work: builds code, apps, or tools");
162
+ expect(dynamic).toContain("- Name: Alex");
163
+ expect(dynamic).toContain("- Chosen assistant name: Nova");
164
+ expect(dynamic).toContain("Apply this context quietly.");
165
+
166
+ // Raw JSON must NOT be present
167
+ expect(dynamic).not.toContain("```json");
175
168
  });
176
169
 
177
170
  test("does NOT inject onboarding context when BOOTSTRAP.md does not exist", () => {
@@ -186,9 +179,9 @@ describe("pre-chat onboarding contract", () => {
186
179
  const result = buildSystemPrompt({ onboardingContext: context });
187
180
  const dynamic = dynamicBlock(result);
188
181
 
189
- expect(dynamic).not.toContain("## Pre-chat Onboarding Context");
190
- expect(dynamic).not.toContain("pre-chat onboarding");
191
- expect(dynamic).not.toContain('"tools"');
182
+ expect(dynamic).not.toContain("## First-Run User Context");
183
+ expect(dynamic).not.toContain("First-Run User Context");
184
+ expect(dynamic).not.toContain("- Daily tools:");
192
185
  });
193
186
 
194
187
  test("does NOT inject onboarding context when excludeBootstrap is true", () => {
@@ -209,7 +202,7 @@ describe("pre-chat onboarding contract", () => {
209
202
  });
210
203
  const dynamic = dynamicBlock(result);
211
204
 
212
- expect(dynamic).not.toContain("## Pre-chat Onboarding Context");
205
+ expect(dynamic).not.toContain("## First-Run User Context");
213
206
  expect(dynamic).not.toContain("First-Run Ritual");
214
207
  });
215
208
 
@@ -225,7 +218,7 @@ describe("pre-chat onboarding contract", () => {
225
218
  // Bootstrap should still be present
226
219
  expect(dynamic).toContain("First-Run Ritual");
227
220
  // But no onboarding context section
228
- expect(dynamic).not.toContain("## Pre-chat Onboarding Context");
221
+ expect(dynamic).not.toContain("## First-Run User Context");
229
222
  });
230
223
 
231
224
  test("accepts all four personality tones", () => {
@@ -247,12 +240,12 @@ describe("pre-chat onboarding contract", () => {
247
240
  const result = buildSystemPrompt({ onboardingContext: context });
248
241
  const dynamic = dynamicBlock(result);
249
242
 
250
- expect(dynamic).toContain("## Pre-chat Onboarding Context");
251
- expect(dynamic).toContain(`"tone": "${tone}"`);
243
+ expect(dynamic).toContain("## First-Run User Context");
244
+ expect(dynamic).toContain(`- Preferred initial voice: ${tone}`);
252
245
  }
253
246
  });
254
247
 
255
- test("serializes onboarding context as pretty-printed JSON", () => {
248
+ test("renders compact markdown, not JSON", () => {
256
249
  writeFileSync(
257
250
  join(TEST_DIR, "BOOTSTRAP.md"),
258
251
  "# Bootstrap\n\nOnboarding.",
@@ -269,9 +262,65 @@ describe("pre-chat onboarding contract", () => {
269
262
  const result = buildSystemPrompt({ onboardingContext: context });
270
263
  const dynamic = dynamicBlock(result);
271
264
 
272
- // Verify it contains the pretty-printed JSON (indented with 2 spaces)
265
+ // Should contain compact markdown lines
266
+ expect(dynamic).toContain("## First-Run User Context");
267
+ expect(dynamic).toContain("- Name: Jane");
268
+ expect(dynamic).toContain("- Common work: plans and coordinates work");
269
+ expect(dynamic).toContain("- Daily tools: Notion");
270
+ expect(dynamic).toContain("- Chosen assistant name: Kit");
271
+ expect(dynamic).toContain("- Preferred initial voice: warm");
272
+
273
+ // Must NOT contain JSON output
274
+ expect(dynamic).not.toContain("```json");
273
275
  const expectedJson = JSON.stringify(context, null, 2);
274
- expect(dynamic).toContain(expectedJson);
276
+ expect(dynamic).not.toContain(expectedJson);
277
+ });
278
+
279
+ test("empty tools/tasks arrays result in no Daily tools / Common work lines", () => {
280
+ writeFileSync(
281
+ join(TEST_DIR, "BOOTSTRAP.md"),
282
+ "# Bootstrap\n\nOnboarding.",
283
+ );
284
+
285
+ const context: OnboardingContext = {
286
+ tools: [],
287
+ tasks: [],
288
+ tone: "warm",
289
+ userName: "Alex",
290
+ };
291
+
292
+ const result = buildSystemPrompt({ onboardingContext: context });
293
+ const dynamic = dynamicBlock(result);
294
+
295
+ expect(dynamic).toContain("## First-Run User Context");
296
+ expect(dynamic).toContain("- Name: Alex");
297
+ expect(dynamic).not.toContain("- Daily tools:");
298
+ expect(dynamic).not.toContain("- Common work:");
299
+ });
300
+
301
+ test("absent userName results in no Name line", () => {
302
+ writeFileSync(
303
+ join(TEST_DIR, "BOOTSTRAP.md"),
304
+ "# Bootstrap\n\nOnboarding.",
305
+ );
306
+
307
+ const context: OnboardingContext = {
308
+ tools: ["slack"],
309
+ tasks: ["writing"],
310
+ tone: "warm",
311
+ };
312
+
313
+ const result = buildSystemPrompt({ onboardingContext: context });
314
+ const dynamic = dynamicBlock(result);
315
+
316
+ expect(dynamic).toContain("## First-Run User Context");
317
+ expect(dynamic).not.toContain("- Name:");
318
+ // Other fields should still be present
319
+ expect(dynamic).toContain("- Daily tools: Slack");
320
+ expect(dynamic).toContain(
321
+ "- Common work: writes docs, emails, or content",
322
+ );
323
+ expect(dynamic).toContain("- Preferred initial voice: warm");
275
324
  });
276
325
  });
277
326
 
@@ -290,4 +339,142 @@ describe("pre-chat onboarding contract", () => {
290
339
  expect(true).toBe(true); // structural acknowledgment
291
340
  });
292
341
  });
342
+
343
+ describe("end-to-end onboarding integration", () => {
344
+ test("with BOOTSTRAP.md present, onboarding context produces compact markdown with normalized labels", () => {
345
+ writeFileSync(
346
+ join(TEST_DIR, "BOOTSTRAP.md"),
347
+ "# Bootstrap\n\nWelcome to your first conversation.",
348
+ );
349
+
350
+ const context: OnboardingContext = {
351
+ tools: ["slack", "notion", "linear"],
352
+ tasks: ["code-building", "writing", "project-management"],
353
+ tone: "grounded",
354
+ userName: "Alice",
355
+ assistantName: "Pax",
356
+ };
357
+
358
+ const result = buildSystemPrompt({ onboardingContext: context });
359
+ const dynamic = dynamicBlock(result);
360
+
361
+ // Heading is present
362
+ expect(dynamic).toContain("## First-Run User Context");
363
+
364
+ // Normalized labels appear (capitalised tool names, human-readable task descriptions)
365
+ expect(dynamic).toContain("- Daily tools: Slack, Notion, Linear");
366
+ expect(dynamic).toContain("- Name: Alice");
367
+ expect(dynamic).toContain("- Chosen assistant name: Pax");
368
+ expect(dynamic).toContain("- Preferred initial voice: grounded");
369
+ // Common work descriptions are normalised from task IDs
370
+ expect(dynamic).toContain("- Common work:");
371
+
372
+ // No raw JSON anywhere in the dynamic block
373
+ expect(dynamic).not.toContain("```json");
374
+ expect(dynamic).not.toContain('"tools"');
375
+ expect(dynamic).not.toContain('"tasks"');
376
+ expect(dynamic).not.toContain('"tone"');
377
+ expect(dynamic).not.toContain('"userName"');
378
+ expect(dynamic).not.toContain('"assistantName"');
379
+ });
380
+
381
+ test("without BOOTSTRAP.md, onboarding context does NOT appear in system prompt", () => {
382
+ // No BOOTSTRAP.md created — simulates a returning user session
383
+ const context: OnboardingContext = {
384
+ tools: ["slack", "figma"],
385
+ tasks: ["design", "writing"],
386
+ tone: "warm",
387
+ userName: "Bob",
388
+ assistantName: "Kit",
389
+ };
390
+
391
+ const result = buildSystemPrompt({ onboardingContext: context });
392
+ const dynamic = dynamicBlock(result);
393
+
394
+ // Onboarding section must be absent
395
+ expect(dynamic).not.toContain("## First-Run User Context");
396
+ expect(dynamic).not.toContain("First-Run Ritual");
397
+ expect(dynamic).not.toContain("- Daily tools:");
398
+ expect(dynamic).not.toContain("- Name: Bob");
399
+ expect(dynamic).not.toContain("- Chosen assistant name:");
400
+ expect(dynamic).not.toContain("Apply this context quietly.");
401
+ });
402
+
403
+ test("excludeBootstrap suppresses both bootstrap and onboarding sections", () => {
404
+ writeFileSync(
405
+ join(TEST_DIR, "BOOTSTRAP.md"),
406
+ "# Bootstrap\n\nFirst run instructions.",
407
+ );
408
+
409
+ const context: OnboardingContext = {
410
+ tools: ["linear"],
411
+ tasks: ["code-building"],
412
+ tone: "energetic",
413
+ userName: "Charlie",
414
+ assistantName: "Nova",
415
+ };
416
+
417
+ const result = buildSystemPrompt({
418
+ onboardingContext: context,
419
+ excludeBootstrap: true,
420
+ });
421
+ const dynamic = dynamicBlock(result);
422
+
423
+ // Both bootstrap and onboarding must be suppressed
424
+ expect(dynamic).not.toContain("First-Run Ritual");
425
+ expect(dynamic).not.toContain("## First-Run User Context");
426
+ expect(dynamic).not.toContain("- Daily tools:");
427
+ expect(dynamic).not.toContain("- Name: Charlie");
428
+ expect(dynamic).not.toContain("Apply this context quietly.");
429
+ });
430
+
431
+ test("userPersona is included independently of onboarding context", () => {
432
+ // No BOOTSTRAP.md — the durable persona path after bootstrap is deleted
433
+ const personaContent =
434
+ "# User Persona\n\nPrefers concise answers. Works in fintech.";
435
+
436
+ const result = buildSystemPrompt({
437
+ userPersona: personaContent,
438
+ // No onboardingContext — simulates post-onboarding conversation
439
+ });
440
+ const dynamic = dynamicBlock(result);
441
+
442
+ // Persona content appears in prompt even without bootstrap or onboarding
443
+ expect(dynamic).toContain("# User Persona");
444
+ expect(dynamic).toContain("Prefers concise answers. Works in fintech.");
445
+
446
+ // No onboarding section should be present
447
+ expect(dynamic).not.toContain("## First-Run User Context");
448
+ expect(dynamic).not.toContain("First-Run Ritual");
449
+ });
450
+
451
+ test("userPersona appears alongside onboarding context during first run", () => {
452
+ writeFileSync(
453
+ join(TEST_DIR, "BOOTSTRAP.md"),
454
+ "# Bootstrap\n\nOnboarding flow.",
455
+ );
456
+
457
+ const personaContent =
458
+ "# User Persona\n\nEarly-stage startup founder. Likes bullet points.";
459
+ const context: OnboardingContext = {
460
+ tools: ["slack"],
461
+ tasks: ["writing"],
462
+ tone: "warm",
463
+ userName: "Dana",
464
+ };
465
+
466
+ const result = buildSystemPrompt({
467
+ userPersona: personaContent,
468
+ onboardingContext: context,
469
+ });
470
+ const dynamic = dynamicBlock(result);
471
+
472
+ // Both persona and onboarding context appear
473
+ expect(dynamic).toContain("# User Persona");
474
+ expect(dynamic).toContain("Likes bullet points.");
475
+ expect(dynamic).toContain("## First-Run User Context");
476
+ expect(dynamic).toContain("- Name: Dana");
477
+ expect(dynamic).toContain("- Daily tools: Slack");
478
+ });
479
+ });
293
480
  });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ isProviderSafeToolName,
5
+ toProviderSafeToolName,
6
+ } from "../tools/provider-tool-name.js";
7
+
8
+ describe("provider tool names", () => {
9
+ test("leaves already-safe names unchanged", () => {
10
+ expect(toProviderSafeToolName("deploy")).toBe("deploy");
11
+ expect(isProviderSafeToolName("deploy")).toBe(true);
12
+ });
13
+
14
+ test("preserves raw-name identity for names that differ by edge whitespace", () => {
15
+ const plain = toProviderSafeToolName("deploy");
16
+ const padded = toProviderSafeToolName(" deploy ");
17
+
18
+ expect(plain).toBe("deploy");
19
+ expect(padded).toMatch(/^deploy__[a-f0-9]{12}$/);
20
+ expect(padded).not.toBe(plain);
21
+ expect(isProviderSafeToolName(padded)).toBe(true);
22
+ });
23
+ });
@@ -362,11 +362,24 @@ function getLatestAssistantText(conversationId: string): string | null {
362
362
  if (Array.isArray(parsed)) {
363
363
  return parsed
364
364
  .filter(
365
- (block): block is { type: string; text?: string } =>
366
- typeof block === "object" && block != null,
365
+ (
366
+ block,
367
+ ): block is {
368
+ type: string;
369
+ text?: string;
370
+ surfaceType?: string;
371
+ data?: { summaryText?: string };
372
+ } => typeof block === "object" && block != null,
367
373
  )
368
- .filter((block) => block.type === "text")
369
- .map((block) => block.text ?? "")
374
+ .map((block) => {
375
+ if (block.type === "text") return block.text ?? "";
376
+ if (
377
+ block.type === "ui_surface" &&
378
+ block.surfaceType === "call_summary"
379
+ )
380
+ return block.data?.summaryText ?? "";
381
+ return "";
382
+ })
370
383
  .join("");
371
384
  }
372
385
  if (typeof parsed === "string") return parsed;
@@ -2321,7 +2334,7 @@ describe("relay-server", () => {
2321
2334
 
2322
2335
  expect(relay.getConnectionState()).toBe("awaiting_name");
2323
2336
 
2324
- // Fallback prompt should NOT include assistant name but should include guardian label
2337
+ // Fallback prompt should use the existing guardian-label wording.
2325
2338
  const textMessages = ws.sentMessages
2326
2339
  .map((raw) => JSON.parse(raw) as { type: string; token?: string })
2327
2340
  .filter((m) => m.type === "text");
@@ -2337,6 +2350,48 @@ describe("relay-server", () => {
2337
2350
  }
2338
2351
  });
2339
2352
 
2353
+ test("inbound voice: unknown caller name capture does not speak a UUID assistant name", async () => {
2354
+ const prevName = mockAssistantName;
2355
+ mockAssistantName = "11111111-2222-4333-8444-555555555555";
2356
+ const db = getDb();
2357
+ db.run("DELETE FROM contact_channels");
2358
+ db.run("DELETE FROM contacts");
2359
+ try {
2360
+ ensureConversation("conv-invite-uuid-name");
2361
+ const session = createCallSession({
2362
+ conversationId: "conv-invite-uuid-name",
2363
+ provider: "twilio",
2364
+ fromNumber: "+12125550157",
2365
+ toNumber: "+12125550111",
2366
+ });
2367
+
2368
+ const { ws, relay } = createMockWs(session.id);
2369
+
2370
+ await relay.handleMessage(
2371
+ JSON.stringify({
2372
+ type: "setup",
2373
+ callSid: "CA_invite_uuid_name",
2374
+ from: "+12125550157",
2375
+ to: "+12125550111",
2376
+ }),
2377
+ );
2378
+
2379
+ expect(relay.getConnectionState()).toBe("awaiting_name");
2380
+
2381
+ const promptText = ws.sentMessages
2382
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
2383
+ .filter((m) => m.type === "text")
2384
+ .map((m) => m.token ?? "")
2385
+ .join("");
2386
+ expect(promptText).toContain("Hi, this is my human's assistant.");
2387
+ expect(promptText).not.toContain("11111111-2222-4333-8444-555555555555");
2388
+
2389
+ relay.destroy();
2390
+ } finally {
2391
+ mockAssistantName = prevName;
2392
+ }
2393
+ });
2394
+
2340
2395
  // ── Friend-initiated in-call guardian approval flow ────────────────────
2341
2396
 
2342
2397
  test("name capture flow: caller provides name and enters guardian decision wait", async () => {
@@ -135,12 +135,10 @@ describe("SSE assistant-events endpoint", () => {
135
135
  // Read the first frame directly from the stream.
136
136
  const reader = stream.getReader();
137
137
 
138
- // The first chunk is the immediate heartbeat (comment + data event) enqueued in start().
138
+ // The first chunk is the immediate heartbeat comment enqueued in start().
139
139
  const initial = await reader.read();
140
140
  expect(initial.done).toBe(false);
141
- const initialText = new TextDecoder().decode(initial.value);
142
- expect(initialText).toContain(": heartbeat");
143
- expect(initialText).toContain('{"type":"heartbeat"}');
141
+ expect(new TextDecoder().decode(initial.value)).toBe(": heartbeat\n\n");
144
142
 
145
143
  // The second chunk is the actual assistant event.
146
144
  const { value, done } = await reader.read();
@@ -172,12 +170,10 @@ describe("SSE assistant-events endpoint", () => {
172
170
 
173
171
  const reader = stream.getReader();
174
172
 
175
- // Consume the initial heartbeat (comment + data event).
173
+ // Consume the initial heartbeat.
176
174
  const heartbeat = await reader.read();
177
175
  expect(heartbeat.done).toBe(false);
178
- const heartbeatText = new TextDecoder().decode(heartbeat.value);
179
- expect(heartbeatText).toContain(": heartbeat");
180
- expect(heartbeatText).toContain('{"type":"heartbeat"}');
176
+ expect(new TextDecoder().decode(heartbeat.value)).toBe(": heartbeat\n\n");
181
177
 
182
178
  // Publish events with two different conversationIds.
183
179
  const eventA = buildAssistantEvent({ type: "pong" }, "conversation-aaa");
@@ -0,0 +1,148 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../util/logger.js", () => ({
4
+ getLogger: () => ({
5
+ info: () => {},
6
+ debug: () => {},
7
+ warn: () => {},
8
+ error: () => {},
9
+ }),
10
+ truncateForLog: (value: string) => value,
11
+ }));
12
+
13
+ mock.module("../runtime/agent-wake.js", () => ({
14
+ wakeAgentForOpportunity: mock(() =>
15
+ Promise.resolve({ invoked: true, producedToolCalls: false }),
16
+ ),
17
+ }));
18
+
19
+ mock.module("../home/emit-feed-event.js", () => ({
20
+ emitFeedEvent: mock(() => Promise.resolve()),
21
+ }));
22
+
23
+ mock.module("../config/loader.js", () => ({
24
+ getConfig: () => ({}),
25
+ loadConfig: () => ({}),
26
+ loadRawConfig: () => ({}),
27
+ saveRawConfig: () => {},
28
+ getConfigReadOnly: () => ({}),
29
+ applyNestedDefaults: (config: unknown) => config,
30
+ deepMergeOverwrite: (base: unknown) => base,
31
+ mergeDefaultWorkspaceConfig: () => {},
32
+ getNestedValue: () => undefined,
33
+ setNestedValue: () => {},
34
+ API_KEY_PROVIDERS: [],
35
+ _appendQuarantineBulletin: () => {},
36
+ invalidateConfigCache: () => {},
37
+ }));
38
+
39
+ let locked = true;
40
+ mock.module("../daemon/disk-pressure-background-gate.js", () => ({
41
+ checkDiskPressureBackgroundGate: () =>
42
+ locked
43
+ ? {
44
+ action: "skip",
45
+ reason: "disk_pressure",
46
+ blockedCapability: "background-work",
47
+ status: {
48
+ enabled: true,
49
+ state: "critical",
50
+ locked: true,
51
+ acknowledged: true,
52
+ overrideActive: false,
53
+ effectivelyLocked: true,
54
+ lockId: "disk-pressure-test",
55
+ usagePercent: 98,
56
+ thresholdPercent: 95,
57
+ path: "/",
58
+ lastCheckedAt: "2026-05-05T00:00:00.000Z",
59
+ blockedCapabilities: [
60
+ "agent-turns",
61
+ "background-work",
62
+ "remote-ingress",
63
+ ],
64
+ error: null,
65
+ },
66
+ }
67
+ : {
68
+ action: "allow",
69
+ status: {
70
+ enabled: false,
71
+ state: "disabled",
72
+ locked: false,
73
+ acknowledged: false,
74
+ overrideActive: false,
75
+ effectivelyLocked: false,
76
+ lockId: null,
77
+ usagePercent: null,
78
+ thresholdPercent: 95,
79
+ path: null,
80
+ lastCheckedAt: null,
81
+ blockedCapabilities: [],
82
+ error: null,
83
+ },
84
+ },
85
+ diskPressureBackgroundSkipLogFields: () => ({
86
+ reason: "disk_pressure",
87
+ thresholdPercent: 95,
88
+ usagePercent: 98,
89
+ blockedCapability: "background-work",
90
+ lockId: "disk-pressure-test",
91
+ path: "/",
92
+ }),
93
+ shouldLogDiskPressureBackgroundSkip: () => true,
94
+ }));
95
+
96
+ import { getDb } from "../memory/db-connection.js";
97
+ import { initializeDb } from "../memory/db-init.js";
98
+ import { createSchedule } from "../schedule/schedule-store.js";
99
+ import { runScheduleOnce } from "../schedule/scheduler.js";
100
+
101
+ initializeDb();
102
+
103
+ function rawDb(): import("bun:sqlite").Database {
104
+ return (getDb() as unknown as { $client: import("bun:sqlite").Database })
105
+ .$client;
106
+ }
107
+
108
+ describe("scheduler disk pressure gate", () => {
109
+ beforeEach(() => {
110
+ locked = true;
111
+ const db = getDb();
112
+ db.run("DELETE FROM cron_runs");
113
+ db.run("DELETE FROM cron_jobs");
114
+ db.run("DELETE FROM task_runs");
115
+ db.run("DELETE FROM tasks");
116
+ db.run("DELETE FROM messages");
117
+ db.run("DELETE FROM conversations");
118
+ });
119
+
120
+ test("skips before claiming due schedules while disk pressure is locked", async () => {
121
+ const dueAt = Date.now() - 10_000;
122
+ const schedule = createSchedule({
123
+ name: "Due reminder",
124
+ message: "Do not fire while locked",
125
+ mode: "notify",
126
+ nextRunAt: dueAt,
127
+ });
128
+
129
+ const processMessage = mock(() => Promise.resolve());
130
+ const notify = mock(() => Promise.resolve());
131
+
132
+ const processed = await runScheduleOnce(processMessage, notify);
133
+
134
+ expect(processed).toBe(0);
135
+ expect(processMessage).not.toHaveBeenCalled();
136
+ expect(notify).not.toHaveBeenCalled();
137
+
138
+ const row = rawDb()
139
+ .query("SELECT status, next_run_at FROM cron_jobs WHERE id = ?")
140
+ .get(schedule.id) as { status: string; next_run_at: number } | null;
141
+ expect(row).toEqual({ status: "active", next_run_at: dueAt });
142
+
143
+ const runCount = rawDb()
144
+ .query("SELECT COUNT(*) AS count FROM cron_runs")
145
+ .get() as { count: number };
146
+ expect(runCount.count).toBe(0);
147
+ });
148
+ });
@@ -106,7 +106,6 @@ mock.module("../memory/conversation-crud.js", () => ({
106
106
  provenanceFromTrustContext: () => undefined,
107
107
  setConversationOriginChannelIfUnset: () => {},
108
108
  setConversationOriginInterfaceIfUnset: () => {},
109
- getConversationMemoryScopeId: () => undefined,
110
109
  }));
111
110
 
112
111
  mock.module("../runtime/local-actor-identity.js", () => ({
@@ -39,15 +39,17 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
39
39
  }));
40
40
 
41
41
  // Stub pendingInteractions — SecretPrompter registers/resolves there now
42
+ // Use a real Map so SecretPrompter can store and retrieve promptResolve/promptReject callbacks.
43
+ const _piStore = new Map<string, object>();
42
44
  mock.module("../runtime/pending-interactions.js", () => ({
43
- register: () => {},
44
- resolve: () => undefined,
45
- get: () => undefined,
46
- getAll: () => [],
45
+ register: (id: string, entry: object) => _piStore.set(id, entry),
46
+ resolve: (id: string) => { const e = _piStore.get(id); _piStore.delete(id); return e; },
47
+ get: (id: string) => _piStore.get(id),
48
+ getAll: () => [..._piStore.values()],
47
49
  getByConversation: () => [],
48
50
  getByKind: () => [],
49
51
  removeByConversation: () => {},
50
- clear: () => {},
52
+ clear: () => _piStore.clear(),
51
53
  }));
52
54
 
53
55
  // Use a tiny timeout so the setTimeout branch fires quickly in tests
@@ -38,15 +38,17 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
38
38
  broadcastMessage: (msg: ServerMessage) => broadcastMessages.push(msg),
39
39
  }));
40
40
 
41
+ // Use a real Map so SecretPrompter can store and retrieve promptResolve/promptReject callbacks.
42
+ const _piStore = new Map<string, object>();
41
43
  mock.module("../runtime/pending-interactions.js", () => ({
42
- register: () => {},
43
- resolve: () => undefined,
44
- get: () => undefined,
45
- getAll: () => [],
44
+ register: (id: string, entry: object) => _piStore.set(id, entry),
45
+ resolve: (id: string) => { const e = _piStore.get(id); _piStore.delete(id); return e; },
46
+ get: (id: string) => _piStore.get(id),
47
+ getAll: () => [..._piStore.values()],
46
48
  getByConversation: () => [],
47
49
  getByKind: () => [],
48
50
  removeByConversation: () => {},
49
- clear: () => {},
51
+ clear: () => _piStore.clear(),
50
52
  }));
51
53
 
52
54
  const { SecretPrompter } = await import("../permissions/secret-prompter.js");
@@ -11,15 +11,17 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
11
11
  broadcastMessage: (msg: ServerMessage) => broadcastedMessages.push(msg),
12
12
  }));
13
13
 
14
+ // Use a real Map so SecretPrompter can store and retrieve promptResolve/promptReject callbacks.
15
+ const _piStore = new Map<string, object>();
14
16
  mock.module("../runtime/pending-interactions.js", () => ({
15
- register: () => {},
16
- resolve: () => undefined,
17
- get: () => undefined,
18
- getAll: () => [],
17
+ register: (id: string, entry: object) => _piStore.set(id, entry),
18
+ resolve: (id: string) => { const e = _piStore.get(id); _piStore.delete(id); return e; },
19
+ get: (id: string) => _piStore.get(id),
20
+ getAll: () => [..._piStore.values()],
19
21
  getByConversation: () => [],
20
22
  getByKind: () => [],
21
23
  removeByConversation: () => {},
22
- clear: () => {},
24
+ clear: () => _piStore.clear(),
23
25
  }));
24
26
 
25
27
  const { SecretPrompter } = await import("../permissions/secret-prompter.js");