@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
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Tests for `annotatePersistedAssistantMessage` persisting the 3 risk-option
3
+ * arrays alongside the existing `_risk*` scalars.
4
+ *
5
+ * Phase B of the conflation track. Without these annotations, the Rule Editor
6
+ * Modal's chip ladder loses its scope/allowlist/directory options on chat-
7
+ * history reload and falls back to the synthesized `*` allowlist.
8
+ *
9
+ * The test exercises the full populate → annotate → persist round-trip:
10
+ * handleToolResult(event with 3 arrays)
11
+ * → state.toolRiskOutcomes captures them
12
+ * → annotatePersistedAssistantMessage writes _risk*Options onto the row
13
+ * → updateMessageContent receives the JSON-serialized output
14
+ *
15
+ * Read-side coverage (renderHistoryContent in handlers/shared.ts) lives in
16
+ * server-history-render.test.ts.
17
+ */
18
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ // ── Mock platform (must precede imports that read it) ─────────────────────────
21
+ mock.module("../util/logger.js", () => ({
22
+ getLogger: () =>
23
+ new Proxy({} as Record<string, unknown>, {
24
+ get: () => () => {},
25
+ }),
26
+ }));
27
+
28
+ mock.module("../config/loader.js", () => ({
29
+ getConfig: () => ({
30
+ skills: {
31
+ entries: {},
32
+ load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
33
+ install: { nodeManager: "npm" },
34
+ allowBundled: null,
35
+ remoteProviders: {
36
+ skillssh: { enabled: true },
37
+ clawhub: { enabled: true },
38
+ },
39
+ remotePolicy: {
40
+ blockSuspicious: true,
41
+ blockMalware: true,
42
+ maxSkillsShRisk: "medium",
43
+ },
44
+ },
45
+ }),
46
+ loadConfig: () => ({}),
47
+ }));
48
+
49
+ let mockedRowContent = "";
50
+ const updates: Array<{ id: string; content: string }> = [];
51
+
52
+ mock.module("../memory/conversation-crud.js", () => ({
53
+ addMessage: () => ({ id: "mock-msg-id" }),
54
+ getMessageById: (id: string) =>
55
+ mockedRowContent ? { id, content: mockedRowContent } : null,
56
+ updateMessageContent: (id: string, content: string) => {
57
+ updates.push({ id, content });
58
+ },
59
+ provenanceFromTrustContext: () => ({}),
60
+ }));
61
+
62
+ mock.module("../memory/llm-request-log-store.js", () => ({
63
+ recordRequestLog: () => {},
64
+ backfillMessageIdOnLogs: () => {},
65
+ }));
66
+
67
+ // ── Imports (after mocks) ─────────────────────────────────────────────────────
68
+ import type {
69
+ EventHandlerDeps,
70
+ EventHandlerState,
71
+ } from "../daemon/conversation-agent-loop-handlers.js";
72
+ import {
73
+ createEventHandlerState,
74
+ handleToolResult,
75
+ } from "../daemon/conversation-agent-loop-handlers.js";
76
+
77
+ // ── Helpers ───────────────────────────────────────────────────────────────────
78
+
79
+ function makeDeps(): EventHandlerDeps {
80
+ return {
81
+ ctx: {
82
+ conversationId: "test-conv",
83
+ provider: { name: "anthropic" },
84
+ traceEmitter: { emit: () => {} },
85
+ streamThinking: false,
86
+ emitActivityState: () => {},
87
+ markWorkspaceTopLevelDirty: () => {},
88
+ currentTurnSurfaces: [],
89
+ } as unknown as EventHandlerDeps["ctx"],
90
+ onEvent: () => {},
91
+ reqId: "test-req",
92
+ isFirstMessage: false,
93
+ shouldGenerateTitle: false,
94
+ rlog: new Proxy({} as Record<string, unknown>, {
95
+ get: () => () => {},
96
+ }) as unknown as EventHandlerDeps["rlog"],
97
+ turnChannelContext: {
98
+ userMessageChannel: "vellum",
99
+ assistantMessageChannel: "vellum",
100
+ } as unknown as EventHandlerDeps["turnChannelContext"],
101
+ turnInterfaceContext: {
102
+ userMessageInterface: "web",
103
+ assistantMessageInterface: "web",
104
+ } as unknown as EventHandlerDeps["turnInterfaceContext"],
105
+ };
106
+ }
107
+
108
+ function setupState(toolUseId: string): EventHandlerState {
109
+ const state = createEventHandlerState();
110
+ state.lastAssistantMessageId = "msg-1";
111
+ state.toolUseIdToName.set(toolUseId, "bash");
112
+ state.toolCallTimestamps.set(toolUseId, { startedAt: Date.now() });
113
+ state.currentTurnToolUseIds.push(toolUseId);
114
+ return state;
115
+ }
116
+
117
+ function findPersistedToolUse(
118
+ rawContent: string,
119
+ toolUseId: string,
120
+ ): Record<string, unknown> {
121
+ const parsed = JSON.parse(rawContent) as Array<Record<string, unknown>>;
122
+ const block = parsed.find(
123
+ (b) => b.type === "tool_use" && b.id === toolUseId,
124
+ );
125
+ if (!block) throw new Error(`tool_use block ${toolUseId} not found`);
126
+ return block;
127
+ }
128
+
129
+ // ── Tests ─────────────────────────────────────────────────────────────────────
130
+
131
+ describe("annotatePersistedAssistantMessage — risk-option arrays (Phase B)", () => {
132
+ beforeEach(() => {
133
+ updates.length = 0;
134
+ mockedRowContent = "";
135
+ });
136
+
137
+ test("persists all 3 risk-option arrays from the live tool_result event", () => {
138
+ const toolUseId = "tu_persist_full";
139
+ const state = setupState(toolUseId);
140
+
141
+ mockedRowContent = JSON.stringify([
142
+ {
143
+ type: "tool_use",
144
+ id: toolUseId,
145
+ name: "bash",
146
+ input: { command: "rm -rf /tmp" },
147
+ },
148
+ ]);
149
+
150
+ const scopeOptions = [
151
+ { pattern: "exact", label: "exact: rm -rf /tmp" },
152
+ { pattern: "by-program", label: "All rm" },
153
+ ];
154
+ const allowlistOptions = [
155
+ { label: "exact", description: "exact match", pattern: "rm -rf /tmp" },
156
+ { label: "All rm", description: "All rm commands", pattern: "rm *" },
157
+ ];
158
+ const directoryScopeOptions = [
159
+ { scope: "/Users/me/code", label: "in code/" },
160
+ { scope: "everywhere", label: "Everywhere" },
161
+ ];
162
+
163
+ handleToolResult(state, makeDeps(), {
164
+ type: "tool_result",
165
+ toolUseId,
166
+ content: "ok",
167
+ isError: false,
168
+ riskLevel: "high",
169
+ riskReason: "Modifies state",
170
+ matchedTrustRuleId: "rule_42",
171
+ riskScopeOptions: scopeOptions,
172
+ riskAllowlistOptions: allowlistOptions,
173
+ riskDirectoryScopeOptions: directoryScopeOptions,
174
+ approvalMode: "prompted",
175
+ approvalReason: "user_approved",
176
+ riskThreshold: "relaxed",
177
+ });
178
+
179
+ expect(updates).toHaveLength(1);
180
+ const block = findPersistedToolUse(updates[0].content, toolUseId);
181
+ // Existing scalars still flow through.
182
+ expect(block._riskLevel).toBe("high");
183
+ expect(block._riskReason).toBe("Modifies state");
184
+ expect(block._matchedTrustRuleId).toBe("rule_42");
185
+ expect(block._approvalMode).toBe("prompted");
186
+ expect(block._approvalReason).toBe("user_approved");
187
+ expect(block._riskThreshold).toBe("relaxed");
188
+ // New: 3 risk-option arrays persisted verbatim.
189
+ expect(block._riskScopeOptions).toEqual(scopeOptions);
190
+ expect(block._riskAllowlistOptions).toEqual(allowlistOptions);
191
+ expect(block._riskDirectoryScopeOptions).toEqual(directoryScopeOptions);
192
+ });
193
+
194
+ test("omits empty arrays from the persisted block (saves DB space)", () => {
195
+ const toolUseId = "tu_persist_empty";
196
+ const state = setupState(toolUseId);
197
+
198
+ mockedRowContent = JSON.stringify([
199
+ {
200
+ type: "tool_use",
201
+ id: toolUseId,
202
+ name: "bash",
203
+ input: { command: "ls" },
204
+ },
205
+ ]);
206
+
207
+ handleToolResult(state, makeDeps(), {
208
+ type: "tool_result",
209
+ toolUseId,
210
+ content: "ok",
211
+ isError: false,
212
+ riskLevel: "low",
213
+ riskScopeOptions: [],
214
+ riskAllowlistOptions: [],
215
+ riskDirectoryScopeOptions: [],
216
+ });
217
+
218
+ expect(updates).toHaveLength(1);
219
+ const block = findPersistedToolUse(updates[0].content, toolUseId);
220
+ expect(block._riskLevel).toBe("low");
221
+ expect(block._riskScopeOptions).toBeUndefined();
222
+ expect(block._riskAllowlistOptions).toBeUndefined();
223
+ expect(block._riskDirectoryScopeOptions).toBeUndefined();
224
+ });
225
+
226
+ test("omits absent (undefined) arrays from the persisted block", () => {
227
+ // Mirrors classic bash/file tools that don't always emit all 3 arrays —
228
+ // e.g. recall, file_read with riskLevel=low and no allowlist coverage.
229
+ const toolUseId = "tu_persist_absent";
230
+ const state = setupState(toolUseId);
231
+
232
+ mockedRowContent = JSON.stringify([
233
+ {
234
+ type: "tool_use",
235
+ id: toolUseId,
236
+ name: "recall",
237
+ input: { query: "anything" },
238
+ },
239
+ ]);
240
+
241
+ handleToolResult(state, makeDeps(), {
242
+ type: "tool_result",
243
+ toolUseId,
244
+ content: "ok",
245
+ isError: false,
246
+ riskLevel: "low",
247
+ // No risk-option arrays passed at all.
248
+ });
249
+
250
+ expect(updates).toHaveLength(1);
251
+ const block = findPersistedToolUse(updates[0].content, toolUseId);
252
+ expect(block._riskLevel).toBe("low");
253
+ expect(block._riskScopeOptions).toBeUndefined();
254
+ expect(block._riskAllowlistOptions).toBeUndefined();
255
+ expect(block._riskDirectoryScopeOptions).toBeUndefined();
256
+ });
257
+
258
+ test("partial coverage — only allowlist options present (e.g. tools with classifier but no scope ladder)", () => {
259
+ const toolUseId = "tu_partial";
260
+ const state = setupState(toolUseId);
261
+
262
+ mockedRowContent = JSON.stringify([
263
+ {
264
+ type: "tool_use",
265
+ id: toolUseId,
266
+ name: "file_write",
267
+ input: { path: "/tmp/foo.txt" },
268
+ },
269
+ ]);
270
+
271
+ const allowlistOptions = [
272
+ { label: "exact", description: "exact match", pattern: "/tmp/foo.txt" },
273
+ ];
274
+
275
+ handleToolResult(state, makeDeps(), {
276
+ type: "tool_result",
277
+ toolUseId,
278
+ content: "ok",
279
+ isError: false,
280
+ riskLevel: "medium",
281
+ riskAllowlistOptions: allowlistOptions,
282
+ });
283
+
284
+ expect(updates).toHaveLength(1);
285
+ const block = findPersistedToolUse(updates[0].content, toolUseId);
286
+ expect(block._riskLevel).toBe("medium");
287
+ expect(block._riskAllowlistOptions).toEqual(allowlistOptions);
288
+ expect(block._riskScopeOptions).toBeUndefined();
289
+ expect(block._riskDirectoryScopeOptions).toBeUndefined();
290
+ });
291
+ });
@@ -13,8 +13,9 @@
13
13
  * differs in two notable ways:
14
14
  * 1. Result payloads carry `pngBase64` (not screenshots-as-strings) and
15
15
  * surface as image content blocks with `media_type: "image/png"`.
16
- * 2. A module-level singleton lock guards `app_control_start` — only one
17
- * conversation may hold an active session at a time.
16
+ * 2. A module-level session lock binds `(conversationId, app)` — only
17
+ * one conversation may hold an active session at a time, and non-start
18
+ * tools must target the same `app` the user approved at start time.
18
19
  */
19
20
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
20
21
 
@@ -87,8 +88,9 @@ mock.module("../runtime/pending-interactions.js", () => ({
87
88
 
88
89
  const {
89
90
  HostAppControlProxy,
90
- _getActiveAppControlConversationId,
91
- _resetActiveAppControlConversationId,
91
+ _getActiveAppControlSession,
92
+ _resetActiveAppControlSession,
93
+ _setActiveAppControlSession,
92
94
  } = await import("../daemon/host-app-control-proxy.js");
93
95
  const { ROUTES } = await import("../runtime/routes/host-app-control-routes.js");
94
96
  const { surfaceProxyResolver } =
@@ -176,11 +178,11 @@ describe("app-control end-to-end flow", () => {
176
178
  pending.clear();
177
179
  clearConversations();
178
180
  mockHasClient = true;
179
- _resetActiveAppControlConversationId();
181
+ _resetActiveAppControlSession();
180
182
  });
181
183
 
182
184
  afterEach(() => {
183
- _resetActiveAppControlConversationId();
185
+ _resetActiveAppControlSession();
184
186
  clearConversations();
185
187
  });
186
188
 
@@ -238,8 +240,10 @@ describe("app-control end-to-end flow", () => {
238
240
  },
239
241
  });
240
242
 
241
- // Singleton lock is held by this conversation now.
242
- expect(_getActiveAppControlConversationId()).toBe(conversationId);
243
+ // Session lock is held by this conversation now, bound to the started app.
244
+ const session = _getActiveAppControlSession();
245
+ expect(session?.conversationId).toBe(conversationId);
246
+ expect(session?.app).toBe("com.example.app");
243
247
 
244
248
  proxy.dispose();
245
249
  });
@@ -253,6 +257,12 @@ describe("app-control end-to-end flow", () => {
253
257
  const proxy = new HostAppControlProxy(conversationId);
254
258
  const ctx = buildContext(proxy, conversationId);
255
259
  registerConversation(conversationId, proxy);
260
+ // Prime a session so observe passes the auth gate. This test exercises
261
+ // the result-formatting path, not the start flow.
262
+ _setActiveAppControlSession({
263
+ conversationId,
264
+ app: "com.example.app",
265
+ });
256
266
 
257
267
  const resultPromise = surfaceProxyResolver(ctx, "app_control_observe", {
258
268
  tool: "observe",
@@ -306,7 +316,7 @@ describe("app-control end-to-end flow", () => {
306
316
  });
307
317
  await postResult({ state: "running", pngBase64: TINY_PNG_B64 });
308
318
  await startPromise;
309
- expect(_getActiveAppControlConversationId()).toBe(conversationId);
319
+ expect(_getActiveAppControlSession()?.conversationId).toBe(conversationId);
310
320
 
311
321
  // Wrap dispose to verify it was called by the resolver.
312
322
  let disposeCalls = 0;
@@ -328,7 +338,7 @@ describe("app-control end-to-end flow", () => {
328
338
  expect(sentMessages).toHaveLength(0);
329
339
  expect(disposeCalls).toBe(1);
330
340
  // Lock released.
331
- expect(_getActiveAppControlConversationId()).toBeUndefined();
341
+ expect(_getActiveAppControlSession()).toBeUndefined();
332
342
  });
333
343
 
334
344
  // -------------------------------------------------------------------------
@@ -347,7 +357,7 @@ describe("app-control end-to-end flow", () => {
347
357
  await postResult({ state: "running", pngBase64: TINY_PNG_B64 });
348
358
  const resultA = await startA;
349
359
  expect(resultA.isError).toBe(false);
350
- expect(_getActiveAppControlConversationId()).toBe("conv-a");
360
+ expect(_getActiveAppControlSession()?.conversationId).toBe("conv-a");
351
361
 
352
362
  // Second conversation tries to start while conv-a holds the lock.
353
363
  sentMessages.length = 0;
@@ -291,21 +291,13 @@ function seedPendingConfirmation(
291
291
  conversation: Conversation,
292
292
  requestId: string,
293
293
  ): void {
294
+ // Access private ownedIds so denyAllPending/dispose can find this request.
295
+ // promptResolve/promptReject callbacks are stored in pendingInteractions via
296
+ // registerPendingInteraction, which is called separately in each test.
294
297
  const prompter = conversation["prompter"] as unknown as {
295
- pending: Map<
296
- string,
297
- {
298
- resolve: (...args: unknown[]) => void;
299
- reject: (...args: unknown[]) => void;
300
- timer: ReturnType<typeof setTimeout>;
301
- }
302
- >;
298
+ ownedIds: Set<string>;
303
299
  };
304
- prompter.pending.set(requestId, {
305
- resolve: () => {},
306
- reject: () => {},
307
- timer: setTimeout(() => {}, 60_000),
308
- });
300
+ prompter.ownedIds.add(requestId);
309
301
  }
310
302
 
311
303
  /**
@@ -439,12 +431,12 @@ describe("approval cascading", () => {
439
431
  makeConfirmationDetails(["bash:echo stale"]),
440
432
  );
441
433
 
442
- // Remove req-stale from the prompter's pending map (simulating it was
434
+ // Remove req-stale from the prompter's ownedIds (simulating it was
443
435
  // already resolved by another path before cascade reaches it)
444
436
  const prompter = conversationObj["prompter"] as unknown as {
445
- pending: Map<string, unknown>;
437
+ ownedIds: Set<string>;
446
438
  };
447
- prompter.pending.delete("req-stale");
439
+ prompter.ownedIds.delete("req-stale");
448
440
 
449
441
  // This should not throw — cascade should skip req-stale gracefully
450
442
  expect(() => {
@@ -192,6 +192,8 @@ function makeIdleSession(opts?: {
192
192
  processing = false;
193
193
  },
194
194
  handleConfirmationResponse: (requestId: string, decision: string) => {
195
+ // Simulate PermissionPrompter.resolveConfirmation(): prompter owns deregistration.
196
+ pendingInteractions.resolve(requestId);
195
197
  opts?.onConfirmation?.(requestId, decision);
196
198
  },
197
199
  handleSecretResponse: (
@@ -199,6 +201,8 @@ function makeIdleSession(opts?: {
199
201
  value?: string,
200
202
  delivery?: string,
201
203
  ) => {
204
+ // Simulate SecretPrompter.resolveSecret(): prompter owns deregistration.
205
+ pendingInteractions.resolve(requestId);
202
206
  opts?.onSecret?.(requestId, value, delivery);
203
207
  },
204
208
  } as unknown as Conversation;
@@ -285,6 +289,8 @@ function makeConfirmationEmittingSession(opts?: {
285
289
  await new Promise<void>(() => {});
286
290
  },
287
291
  handleConfirmationResponse: (requestId: string, decision: string) => {
292
+ // Simulate PermissionPrompter.resolveConfirmation(): prompter owns deregistration.
293
+ pendingInteractions.resolve(requestId);
288
294
  opts?.onConfirmation?.(requestId, decision);
289
295
  },
290
296
  handleSecretResponse: () => {},
@@ -356,6 +356,54 @@ describe("AssistantEventHub — re-entrancy / snapshot isolation", () => {
356
356
  });
357
357
  });
358
358
 
359
+ // ── ClientEntry actorPrincipalId capture ────────────────────────────────────
360
+
361
+ describe("AssistantEventHub — actorPrincipalId on ClientEntry", () => {
362
+ test("stores actorPrincipalId provided at subscribe time", () => {
363
+ const hub = new AssistantEventHub();
364
+
365
+ hub.subscribe({
366
+ type: "client" as const,
367
+ clientId: "client-with-principal",
368
+ interfaceId: "macos",
369
+ capabilities: ["host_bash"],
370
+ actorPrincipalId: "user-A",
371
+ callback: () => {},
372
+ });
373
+
374
+ expect(hub.getClientById("client-with-principal")?.actorPrincipalId).toBe(
375
+ "user-A",
376
+ );
377
+ expect(hub.getActorPrincipalIdForClient("client-with-principal")).toBe(
378
+ "user-A",
379
+ );
380
+ });
381
+
382
+ test("actorPrincipalId is undefined when omitted at subscribe time", () => {
383
+ const hub = new AssistantEventHub();
384
+
385
+ hub.subscribe({
386
+ type: "client" as const,
387
+ clientId: "client-no-principal",
388
+ interfaceId: "macos",
389
+ capabilities: ["host_bash"],
390
+ callback: () => {},
391
+ });
392
+
393
+ expect(
394
+ hub.getClientById("client-no-principal")?.actorPrincipalId,
395
+ ).toBeUndefined();
396
+ expect(
397
+ hub.getActorPrincipalIdForClient("client-no-principal"),
398
+ ).toBeUndefined();
399
+ });
400
+
401
+ test("getActorPrincipalIdForClient returns undefined for unknown clientId", () => {
402
+ const hub = new AssistantEventHub();
403
+ expect(hub.getActorPrincipalIdForClient("does-not-exist")).toBeUndefined();
404
+ });
405
+ });
406
+
359
407
  // ── capabilityForMessageType — host-prefix routing ───────────────────────────
360
408
 
361
409
  describe("capabilityForMessageType — host-prefix routing", () => {
@@ -4,7 +4,6 @@ import type { AssistantEvent } from "../runtime/assistant-event.js";
4
4
  import {
5
5
  formatSseFrame,
6
6
  formatSseHeartbeat,
7
- formatSseHeartbeatWithData,
8
7
  } from "../runtime/assistant-event.js";
9
8
 
10
9
  // ── Type / shape tests ────────────────────────────────────────────────────────
@@ -126,12 +125,3 @@ describe("formatSseHeartbeat", () => {
126
125
  expect(formatSseHeartbeat().startsWith(":")).toBe(true);
127
126
  });
128
127
  });
129
-
130
- describe("formatSseHeartbeatWithData", () => {
131
- test("includes both a comment and a data-bearing event", () => {
132
- const hb = formatSseHeartbeatWithData();
133
- expect(hb).toContain(": heartbeat\n\n");
134
- expect(hb).toContain("event: assistant_event\n");
135
- expect(hb).toContain('data: {"type":"heartbeat"}\n');
136
- });
137
- });
@@ -261,8 +261,7 @@ describe("SSE route — heartbeat", () => {
261
261
  reader.cancel();
262
262
 
263
263
  const text = new TextDecoder().decode(value);
264
- expect(text).toContain(": heartbeat");
265
- expect(text).toContain('{"type":"heartbeat"}');
264
+ expect(text).toBe(": heartbeat\n\n");
266
265
  });
267
266
 
268
267
  test("emits multiple heartbeats over time", async () => {
@@ -298,11 +297,7 @@ describe("SSE route — heartbeat", () => {
298
297
  reader.cancel();
299
298
 
300
299
  expect(chunks.length).toBeGreaterThan(0);
301
- expect(
302
- chunks.every(
303
- (c) => c.includes(": heartbeat") && c.includes('{"type":"heartbeat"}'),
304
- ),
305
- ).toBe(true);
300
+ expect(chunks.every((c) => c === ": heartbeat\n\n")).toBe(true);
306
301
  });
307
302
  });
308
303
 
@@ -14,6 +14,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
14
 
15
15
  const DECLARED_FLAG_ID = "email-channel";
16
16
  const DECLARED_FLAG_KEY = DECLARED_FLAG_ID;
17
+ const SAFE_STORAGE_LIMITS_FLAG = "safe-storage-limits";
17
18
 
18
19
  const { isAssistantFeatureFlagEnabled, _setOverridesForTesting } =
19
20
  await import("../config/assistant-feature-flags.js");
@@ -62,6 +63,23 @@ describe("isAssistantFeatureFlagEnabled", () => {
62
63
  );
63
64
  });
64
65
 
66
+ test("safe-storage-limits defaults to disabled", () => {
67
+ const config = {} as any;
68
+
69
+ expect(
70
+ isAssistantFeatureFlagEnabled(SAFE_STORAGE_LIMITS_FLAG, config),
71
+ ).toBe(false);
72
+ });
73
+
74
+ test("safe-storage-limits respects explicit override", () => {
75
+ _setOverridesForTesting({ [SAFE_STORAGE_LIMITS_FLAG]: true });
76
+ const config = {} as any;
77
+
78
+ expect(
79
+ isAssistantFeatureFlagEnabled(SAFE_STORAGE_LIMITS_FLAG, config),
80
+ ).toBe(true);
81
+ });
82
+
65
83
  test("unknown flag defaults to true when no persisted override", () => {
66
84
  const config = {} as any;
67
85
 
@@ -389,13 +389,19 @@ describe("auto-analysis batch trigger uses analysis.batchSize cadence", () => {
389
389
  const originalExtractionBatch = TEST_CONFIG.memory.extraction.batchSize;
390
390
  const originalAnalysisBatch = TEST_CONFIG.analysis.batchSize;
391
391
 
392
+ const originalV2Enabled = TEST_CONFIG.memory.v2.enabled;
393
+
392
394
  beforeEach(() => {
393
395
  _setOverridesForTesting({ "auto-analyze": true });
396
+ // memory.v2.enabled gates v1 graph_extract enqueue; force off so
397
+ // these cadence tests can observe the v1 path.
398
+ TEST_CONFIG.memory.v2.enabled = false;
394
399
  TEST_CONFIG.memory.extraction.batchSize = 2;
395
400
  TEST_CONFIG.analysis.batchSize = 5;
396
401
  });
397
402
 
398
403
  afterEach(() => {
404
+ TEST_CONFIG.memory.v2.enabled = originalV2Enabled;
399
405
  TEST_CONFIG.memory.extraction.batchSize = originalExtractionBatch;
400
406
  TEST_CONFIG.analysis.batchSize = originalAnalysisBatch;
401
407
  });
@@ -537,3 +543,45 @@ describe("auto-analysis batch trigger uses analysis.batchSize cadence", () => {
537
543
  expect(row.runAfter).toBeLessThanOrEqual(after + 1_000);
538
544
  });
539
545
  });
546
+
547
+ // ─────────────────────────────────────────────────────────────────
548
+ // Indexer v1/v2 mutual exclusion: when memory.v2.enabled is on, the
549
+ // v1 graph_extract enqueue is suppressed (v2 reads from buffer.md,
550
+ // so v1 graph data is unread). When v2 is disabled, v1 graph_extract
551
+ // fires.
552
+ // ─────────────────────────────────────────────────────────────────
553
+
554
+ describe("indexer v1/v2 mutual exclusion for graph_extract", () => {
555
+ // Force the v1 batch trigger so any enqueued row is observable.
556
+ const originalExtractionBatch = TEST_CONFIG.memory.extraction.batchSize;
557
+ const originalV2Enabled = TEST_CONFIG.memory.v2.enabled;
558
+
559
+ beforeEach(() => {
560
+ TEST_CONFIG.memory.extraction.batchSize = 1;
561
+ });
562
+
563
+ afterEach(() => {
564
+ TEST_CONFIG.memory.extraction.batchSize = originalExtractionBatch;
565
+ TEST_CONFIG.memory.v2.enabled = originalV2Enabled;
566
+ });
567
+
568
+ test("v2 active (config on) → graph_extract not enqueued", async () => {
569
+ TEST_CONFIG.memory.v2.enabled = true;
570
+
571
+ const source = createConversation("v2-active");
572
+ await indexMessages(source.id, 2);
573
+
574
+ expect(countJobsOfType("graph_extract", source.id)).toBe(0);
575
+ });
576
+
577
+ test("config gate off → graph_extract enqueued", async () => {
578
+ TEST_CONFIG.memory.v2.enabled = false;
579
+
580
+ const source = createConversation("v2-config-off");
581
+ await indexMessages(source.id, 2);
582
+
583
+ expect(countJobsOfType("graph_extract", source.id)).toBeGreaterThanOrEqual(
584
+ 1,
585
+ );
586
+ });
587
+ });