@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,914 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ── Mock state ──────────────────────────────────────────────────────────
4
+
5
+ // Provider mock
6
+ let decisionProviderAvailable = true;
7
+ let buildProviderAvailable = true;
8
+ let decisionResponse = "";
9
+ let buildResponse = "";
10
+ let copyResponse = "";
11
+ let providerSendCalls: Array<{ callSite: string; messages: unknown[] }> = [];
12
+
13
+ const mockProvider = (callSite: string) => ({
14
+ name: "mock-provider",
15
+ sendMessage: async (messages: unknown[]) => {
16
+ providerSendCalls.push({ callSite, messages });
17
+ if (callSite === "proactiveArtifactDecision") {
18
+ return { content: [{ type: "text", text: decisionResponse }] };
19
+ }
20
+ if (callSite === "proactiveArtifactBuild") {
21
+ // copyResponse is used when it's for message copy (second call)
22
+ const isCopyCall = providerSendCalls.filter(
23
+ (c) => c.callSite === "proactiveArtifactBuild",
24
+ ).length;
25
+ if (isCopyCall > 1) {
26
+ return { content: [{ type: "text", text: copyResponse }] };
27
+ }
28
+ return { content: [{ type: "text", text: buildResponse }] };
29
+ }
30
+ return { content: [{ type: "text", text: "" }] };
31
+ },
32
+ });
33
+
34
+ mock.module("../providers/provider-send-message.js", () => ({
35
+ getConfiguredProvider: async (callSite: string) => {
36
+ if (
37
+ callSite === "proactiveArtifactDecision" &&
38
+ !decisionProviderAvailable
39
+ ) {
40
+ return null;
41
+ }
42
+ if (callSite === "proactiveArtifactBuild" && !buildProviderAvailable) {
43
+ return null;
44
+ }
45
+ return mockProvider(callSite);
46
+ },
47
+ extractText: (response: { content: Array<{ type: string; text: string }> }) =>
48
+ response.content.find((b: { type: string }) => b.type === "text")?.text ??
49
+ "",
50
+ }));
51
+
52
+ // rawAll mock
53
+ let rawAllRows: Array<{ role: string; content: string }> = [];
54
+
55
+ mock.module("../memory/raw-query.js", () => ({
56
+ rawAll: () => rawAllRows,
57
+ rawRun: () => 0,
58
+ }));
59
+
60
+ // bootstrapConversation mock
61
+ let bootstrapCalls: Array<Record<string, unknown>> = [];
62
+
63
+ mock.module("../memory/conversation-bootstrap.js", () => ({
64
+ bootstrapConversation: (opts: Record<string, unknown>) => {
65
+ bootstrapCalls.push(opts);
66
+ return { id: `bg-conv-${bootstrapCalls.length}` };
67
+ },
68
+ }));
69
+
70
+ // processMessage mock
71
+ let processMessageCalls: Array<{
72
+ conversationId: string;
73
+ prompt: string;
74
+ options: unknown;
75
+ }> = [];
76
+ let processMessageShouldThrow = false;
77
+
78
+ mock.module("../daemon/process-message.js", () => ({
79
+ processMessage: async (
80
+ conversationId: string,
81
+ prompt: string,
82
+ _attachmentIds: unknown,
83
+ options: unknown,
84
+ ) => {
85
+ processMessageCalls.push({ conversationId, prompt, options });
86
+ if (processMessageShouldThrow) {
87
+ throw new Error("processMessage failed");
88
+ }
89
+ return { messageId: "pm-msg-1" };
90
+ },
91
+ }));
92
+
93
+ // App store mock
94
+ let mockApps: Array<{
95
+ id: string;
96
+ name: string;
97
+ createdAt: number;
98
+ updatedAt?: number;
99
+ conversationIds?: string[];
100
+ }> = [];
101
+ let addAppConvCalls: Array<{ appId: string; conversationId: string }> = [];
102
+
103
+ mock.module("../memory/app-store.js", () => ({
104
+ listApps: () => mockApps,
105
+ listAppsByConversation: (conversationId: string) =>
106
+ mockApps.filter((app) => app.conversationIds?.includes(conversationId)),
107
+ addAppConversationId: (appId: string, conversationId: string) => {
108
+ addAppConvCalls.push({ appId, conversationId });
109
+ return true;
110
+ },
111
+ }));
112
+
113
+ // Document store mock
114
+ let saveDocumentCalls: Array<Record<string, unknown>> = [];
115
+ let saveDocumentResult: {
116
+ success: boolean;
117
+ surfaceId?: string;
118
+ error?: string;
119
+ } = {
120
+ success: true,
121
+ surfaceId: "doc-123",
122
+ };
123
+ let addDocConvCalls: Array<{ surfaceId: string; conversationId: string }> = [];
124
+
125
+ mock.module("../documents/document-store.js", () => ({
126
+ saveDocument: (params: Record<string, unknown>) => {
127
+ saveDocumentCalls.push(params);
128
+ return saveDocumentResult;
129
+ },
130
+ addDocumentConversation: (surfaceId: string, conversationId: string) => {
131
+ addDocConvCalls.push({ surfaceId, conversationId });
132
+ },
133
+ }));
134
+
135
+ // addMessage mock
136
+ let addMessageCalls: Array<{
137
+ conversationId: string;
138
+ role: string;
139
+ content: string;
140
+ metadata: unknown;
141
+ opts: unknown;
142
+ }> = [];
143
+
144
+ mock.module("../memory/conversation-crud.js", () => ({
145
+ addMessage: async (
146
+ conversationId: string,
147
+ role: string,
148
+ content: string,
149
+ metadata: unknown,
150
+ opts: unknown,
151
+ ) => {
152
+ addMessageCalls.push({ conversationId, role, content, metadata, opts });
153
+ return { id: `msg-${addMessageCalls.length}` };
154
+ },
155
+ }));
156
+
157
+ // emitNotificationSignal mock
158
+ let emitSignalCalls: Array<Record<string, unknown>> = [];
159
+
160
+ mock.module("../notifications/emit-signal.js", () => ({
161
+ emitNotificationSignal: async (params: Record<string, unknown>) => {
162
+ emitSignalCalls.push(params);
163
+ return {
164
+ signalId: "signal-1",
165
+ deduplicated: false,
166
+ dispatched: true,
167
+ reason: "ok",
168
+ deliveryResults: [],
169
+ };
170
+ },
171
+ }));
172
+
173
+ // findConversation mock
174
+ type MockConversation = {
175
+ processing: boolean;
176
+ messages: unknown[];
177
+ getMessages: () => unknown[];
178
+ };
179
+ let mockConversations: Map<string, MockConversation> = new Map();
180
+
181
+ mock.module("../daemon/conversation-store.js", () => ({
182
+ findConversation: (id: string) => mockConversations.get(id),
183
+ }));
184
+
185
+ // createAssistantMessage mock
186
+ mock.module("../agent/message-types.js", () => ({
187
+ createAssistantMessage: (text: string) => ({
188
+ role: "assistant",
189
+ content: [{ type: "text", text }],
190
+ }),
191
+ }));
192
+
193
+ // Trigger state mock
194
+ let releaseClaimCalls = 0;
195
+
196
+ mock.module("./trigger-state.js", () => ({
197
+ releaseProactiveArtifactClaim: () => {
198
+ releaseClaimCalls++;
199
+ },
200
+ }));
201
+
202
+ // Trust context mock
203
+ mock.module("../daemon/trust-context.js", () => ({
204
+ INTERNAL_GUARDIAN_TRUST_CONTEXT: {
205
+ sourceChannel: "vellum",
206
+ trustClass: "guardian",
207
+ },
208
+ }));
209
+
210
+ // Logger mock
211
+ let logWarnCalls: Array<{ args: unknown[] }> = [];
212
+
213
+ mock.module("../util/logger.js", () => ({
214
+ getLogger: () => ({
215
+ info: () => {},
216
+ warn: (...args: unknown[]) => {
217
+ logWarnCalls.push({ args });
218
+ },
219
+ error: () => {},
220
+ debug: () => {},
221
+ }),
222
+ }));
223
+
224
+ // uuid mock — deterministic IDs for testing
225
+ let uuidCounter = 0;
226
+ mock.module("uuid", () => ({
227
+ v4: () => `test-uuid-${++uuidCounter}`,
228
+ }));
229
+
230
+ // ── Import SUT after mocks ─────────────────────────────────────────────
231
+
232
+ const { runProactiveArtifactJob } = await import("./job.js");
233
+ const { injectAuxAssistantMessage } = await import("./aux-message-injector.js");
234
+ const {
235
+ buildMessageCopyPrompt,
236
+ ensureMessageMentionsLibraryLocation,
237
+ parseMessageCopy,
238
+ } = await import("./message-copy.js");
239
+
240
+ // ── Test helpers ────────────────────────────────────────────────────────
241
+
242
+ let broadcastCalls: Array<Record<string, unknown>> = [];
243
+ const mockBroadcast: any = (msg: Record<string, unknown>) => {
244
+ broadcastCalls.push(msg);
245
+ };
246
+
247
+ const defaultTranscript = [
248
+ { role: "user", content: "Hello there" },
249
+ { role: "assistant", content: "Hi! How can I help?" },
250
+ { role: "user", content: "I need a budget tracker" },
251
+ { role: "assistant", content: "I can help with that." },
252
+ { role: "user", content: "I spend about $3000 per month" },
253
+ { role: "assistant", content: "Got it, that is useful context." },
254
+ { role: "user", content: "Yes, let us track groceries and rent" },
255
+ {
256
+ role: "assistant",
257
+ content: "Great, I will remember those categories.",
258
+ },
259
+ ];
260
+
261
+ const decisionYesApp = `SHOULD_BUILD: yes
262
+ ARTIFACT_TYPE: app
263
+ ARTIFACT_TITLE: Budget Tracker
264
+ ARTIFACT_DESCRIPTION: A budget tracking app for monthly expenses around $3000, focusing on groceries and rent.`;
265
+
266
+ const decisionYesDocument = `SHOULD_BUILD: yes
267
+ ARTIFACT_TYPE: document
268
+ ARTIFACT_TITLE: Monthly Budget Guide
269
+ ARTIFACT_DESCRIPTION: A structured guide for tracking monthly expenses with categories for groceries and rent.`;
270
+
271
+ const decisionNo = `SHOULD_BUILD: no
272
+ SKIP_REASON: Not enough context to build something specific.`;
273
+
274
+ function resetState() {
275
+ decisionProviderAvailable = true;
276
+ buildProviderAvailable = true;
277
+ decisionResponse = "";
278
+ buildResponse = "";
279
+ copyResponse = "";
280
+ providerSendCalls = [];
281
+ rawAllRows = [];
282
+ bootstrapCalls = [];
283
+ processMessageCalls = [];
284
+ processMessageShouldThrow = false;
285
+ mockApps = [];
286
+ addAppConvCalls = [];
287
+ saveDocumentCalls = [];
288
+ saveDocumentResult = { success: true, surfaceId: "doc-123" };
289
+ addDocConvCalls = [];
290
+ releaseClaimCalls = 0;
291
+ addMessageCalls = [];
292
+ emitSignalCalls = [];
293
+ broadcastCalls = [];
294
+ mockConversations = new Map();
295
+ logWarnCalls = [];
296
+ uuidCounter = 0;
297
+ }
298
+
299
+ // ── Tests ───────────────────────────────────────────────────────────────
300
+
301
+ beforeEach(() => {
302
+ resetState();
303
+ });
304
+
305
+ afterEach(() => {
306
+ resetState();
307
+ });
308
+
309
+ describe("runProactiveArtifactJob", () => {
310
+ describe("Phase 1 — Decision", () => {
311
+ test("shouldBuild:false → releases claim, no Phase 2", async () => {
312
+ rawAllRows = defaultTranscript;
313
+ decisionResponse = decisionNo;
314
+
315
+ await runProactiveArtifactJob({
316
+ conversationId: "conv-1",
317
+ userMessageCutoff: 1000,
318
+ assistantMessageId: "msg-4",
319
+ broadcastMessage: mockBroadcast,
320
+ });
321
+
322
+ expect(releaseClaimCalls).toBe(1);
323
+ expect(bootstrapCalls).toHaveLength(0);
324
+ expect(processMessageCalls).toHaveLength(0);
325
+ expect(addMessageCalls).toHaveLength(0);
326
+ expect(emitSignalCalls).toHaveLength(0);
327
+ expect(broadcastCalls).toHaveLength(0);
328
+ });
329
+
330
+ test("null (malformed) → releases claim, silent exit", async () => {
331
+ rawAllRows = defaultTranscript;
332
+ decisionResponse = "THIS IS GARBAGE OUTPUT";
333
+
334
+ await runProactiveArtifactJob({
335
+ conversationId: "conv-1",
336
+ userMessageCutoff: 1000,
337
+ assistantMessageId: "msg-4",
338
+ broadcastMessage: mockBroadcast,
339
+ });
340
+
341
+ expect(releaseClaimCalls).toBe(1);
342
+ expect(bootstrapCalls).toHaveLength(0);
343
+ expect(processMessageCalls).toHaveLength(0);
344
+ expect(addMessageCalls).toHaveLength(0);
345
+ expect(emitSignalCalls).toHaveLength(0);
346
+ });
347
+
348
+ test("provider unavailable → releases claim, silent return", async () => {
349
+ rawAllRows = defaultTranscript;
350
+ decisionProviderAvailable = false;
351
+
352
+ await runProactiveArtifactJob({
353
+ conversationId: "conv-1",
354
+ userMessageCutoff: 1000,
355
+ assistantMessageId: "msg-4",
356
+ broadcastMessage: mockBroadcast,
357
+ });
358
+
359
+ expect(releaseClaimCalls).toBe(1);
360
+ expect(providerSendCalls).toHaveLength(0);
361
+ expect(bootstrapCalls).toHaveLength(0);
362
+ expect(addMessageCalls).toHaveLength(0);
363
+ expect(emitSignalCalls).toHaveLength(0);
364
+ });
365
+ });
366
+
367
+ describe("Phase 2 — Build", () => {
368
+ test("Phase 2 failure → releases claim, no message, no notification", async () => {
369
+ rawAllRows = defaultTranscript;
370
+ decisionResponse = decisionYesApp;
371
+ processMessageShouldThrow = true;
372
+
373
+ await runProactiveArtifactJob({
374
+ conversationId: "conv-1",
375
+ userMessageCutoff: 1000,
376
+ assistantMessageId: "msg-4",
377
+ broadcastMessage: mockBroadcast,
378
+ });
379
+
380
+ // processMessage was called (the build attempt happened)
381
+ expect(processMessageCalls).toHaveLength(1);
382
+ // But no message injection or notification
383
+ expect(addMessageCalls).toHaveLength(0);
384
+ expect(emitSignalCalls).toHaveLength(0);
385
+ expect(broadcastCalls).toHaveLength(0);
386
+ // Claim released so next turn can retry
387
+ expect(releaseClaimCalls).toBe(1);
388
+ });
389
+
390
+ test("successful app: Phase 1 → Phase 2 → app store query → message copy → inject → notify", async () => {
391
+ rawAllRows = defaultTranscript;
392
+ decisionResponse = decisionYesApp;
393
+ copyResponse = "MESSAGE: I built a budget tracker for you!";
394
+
395
+ const buildStartedAt = Date.now();
396
+ mockApps = [
397
+ {
398
+ id: "app-123",
399
+ name: "Budget Tracker",
400
+ createdAt: buildStartedAt + 100,
401
+ updatedAt: buildStartedAt + 100,
402
+ },
403
+ ];
404
+
405
+ // Set up an idle conversation so injection works fully
406
+ const convMessages: unknown[] = [];
407
+ mockConversations.set("conv-1", {
408
+ processing: false,
409
+ messages: convMessages,
410
+ getMessages: () => convMessages,
411
+ });
412
+
413
+ await runProactiveArtifactJob({
414
+ conversationId: "conv-1",
415
+ userMessageCutoff: 1000,
416
+ assistantMessageId: "msg-4",
417
+ broadcastMessage: mockBroadcast,
418
+ });
419
+
420
+ // Phase 1: decision provider called
421
+ expect(
422
+ providerSendCalls.some(
423
+ (c) => c.callSite === "proactiveArtifactDecision",
424
+ ),
425
+ ).toBe(true);
426
+
427
+ // Phase 2: processMessage called with correct options
428
+ expect(processMessageCalls).toHaveLength(1);
429
+ expect(processMessageCalls[0].prompt).toContain("Budget Tracker");
430
+ expect(processMessageCalls[0].prompt).toContain("auto_open: false");
431
+ const pmOpts = processMessageCalls[0].options as Record<string, unknown>;
432
+ expect(pmOpts.callSite).toBe("proactiveArtifactBuild");
433
+ expect(pmOpts.trustContext).toEqual({
434
+ sourceChannel: "vellum",
435
+ trustClass: "guardian",
436
+ });
437
+
438
+ // Bootstrap conversation created for app build
439
+ expect(bootstrapCalls).toHaveLength(1);
440
+ expect(bootstrapCalls[0].conversationType).toBe("background");
441
+ expect(bootstrapCalls[0].source).toBe("proactive_artifact");
442
+
443
+ // App associated with user's conversation for existing artifact linkage
444
+ expect(addAppConvCalls).toHaveLength(1);
445
+ expect(addAppConvCalls[0].appId).toBe("app-123");
446
+ expect(addAppConvCalls[0].conversationId).toBe("conv-1");
447
+
448
+ expect(broadcastCalls).toContainEqual({
449
+ type: "app_files_changed",
450
+ appId: "app-123",
451
+ });
452
+
453
+ // Message injection: addMessage called with skipIndexing
454
+ expect(addMessageCalls).toHaveLength(1);
455
+ expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
456
+ expect(addMessageCalls[0].conversationId).toBe("conv-1");
457
+ const injectedAppContent = JSON.parse(addMessageCalls[0].content);
458
+ expect(injectedAppContent[0].text).toContain("Library");
459
+ expect(injectedAppContent[0].text).not.toContain("Assets");
460
+
461
+ // Notification emitted
462
+ expect(emitSignalCalls).toHaveLength(1);
463
+ expect(emitSignalCalls[0].sourceEventName).toBe("activity.complete");
464
+ expect(emitSignalCalls[0].sourceChannel).toBe("vellum");
465
+ expect(emitSignalCalls[0].dedupeKey).toBe("proactive-artifact");
466
+ const hints = emitSignalCalls[0].attentionHints as Record<
467
+ string,
468
+ unknown
469
+ >;
470
+ expect(hints.visibleInSourceNow).toBe(false);
471
+ expect(hints.isAsyncBackground).toBe(true);
472
+ expect(hints.requiresAction).toBe(false);
473
+ // No conversationAffinityHint
474
+ expect(emitSignalCalls[0].conversationAffinityHint).toBeUndefined();
475
+
476
+ // Claim NOT released on success — guard stays permanent
477
+ expect(releaseClaimCalls).toBe(0);
478
+ });
479
+
480
+ test("successful document: Phase 1 → content gen → saveDocument → message copy → inject → notify", async () => {
481
+ rawAllRows = defaultTranscript;
482
+ decisionResponse = decisionYesDocument;
483
+ buildResponse =
484
+ "# Monthly Budget Guide\n\nTrack groceries and rent expenses.";
485
+ copyResponse =
486
+ "MESSAGE: I created a monthly budget guide tailored to your needs.";
487
+
488
+ mockConversations.set("conv-1", {
489
+ processing: false,
490
+ messages: [],
491
+ getMessages: () => [],
492
+ });
493
+
494
+ await runProactiveArtifactJob({
495
+ conversationId: "conv-1",
496
+ userMessageCutoff: 1000,
497
+ assistantMessageId: "msg-4",
498
+ broadcastMessage: mockBroadcast,
499
+ });
500
+
501
+ // Document saved via saveDocument (not raw file writes)
502
+ expect(saveDocumentCalls).toHaveLength(1);
503
+ expect(saveDocumentCalls[0].title).toBe("Monthly Budget Guide");
504
+ expect(saveDocumentCalls[0].conversationId).toBe("conv-1");
505
+ expect(saveDocumentCalls[0].content).toContain("Monthly Budget Guide");
506
+ expect(
507
+ (saveDocumentCalls[0].surfaceId as string).startsWith("doc-"),
508
+ ).toBe(true);
509
+ expect((saveDocumentCalls[0].wordCount as number) > 0).toBe(true);
510
+
511
+ // No bootstrapConversation or processMessage for document path
512
+ expect(bootstrapCalls).toHaveLength(0);
513
+ expect(processMessageCalls).toHaveLength(0);
514
+
515
+ // Message injection and notification
516
+ expect(addMessageCalls).toHaveLength(1);
517
+ const injectedDocumentContent = JSON.parse(addMessageCalls[0].content);
518
+ expect(injectedDocumentContent[0].text).toContain("Library");
519
+ expect(injectedDocumentContent[0].text).not.toContain("Assets");
520
+ expect(emitSignalCalls).toHaveLength(1);
521
+
522
+ // Claim NOT released on success
523
+ expect(releaseClaimCalls).toBe(0);
524
+ });
525
+
526
+ test("app build - no matching app found → releases claim for retry", async () => {
527
+ rawAllRows = defaultTranscript;
528
+ decisionResponse = decisionYesApp;
529
+ mockApps = []; // no apps in store
530
+
531
+ await runProactiveArtifactJob({
532
+ conversationId: "conv-1",
533
+ userMessageCutoff: 1000,
534
+ assistantMessageId: "msg-4",
535
+ broadcastMessage: mockBroadcast,
536
+ });
537
+
538
+ expect(processMessageCalls).toHaveLength(1);
539
+ expect(addMessageCalls).toHaveLength(0);
540
+ expect(emitSignalCalls).toHaveLength(0);
541
+ expect(releaseClaimCalls).toBe(1);
542
+ });
543
+
544
+ test("app decision with foreground app tool suppresses background build permanently", async () => {
545
+ rawAllRows = defaultTranscript;
546
+ decisionResponse = decisionYesApp;
547
+
548
+ await runProactiveArtifactJob({
549
+ conversationId: "conv-1",
550
+ userMessageCutoff: 1000,
551
+ assistantMessageId: "msg-4",
552
+ suppressAppBuild: true,
553
+ broadcastMessage: mockBroadcast,
554
+ });
555
+
556
+ expect(bootstrapCalls).toHaveLength(0);
557
+ expect(processMessageCalls).toHaveLength(0);
558
+ expect(addMessageCalls).toHaveLength(0);
559
+ expect(emitSignalCalls).toHaveLength(0);
560
+ expect(releaseClaimCalls).toBe(0);
561
+ });
562
+
563
+ test("app decision with recent app activity in source conversation suppresses background build permanently", async () => {
564
+ rawAllRows = defaultTranscript;
565
+ decisionResponse = decisionYesApp;
566
+ mockApps = [
567
+ {
568
+ id: "app-main",
569
+ name: "Budget Tracker",
570
+ createdAt: 1200,
571
+ updatedAt: 1300,
572
+ conversationIds: ["conv-1"],
573
+ },
574
+ ];
575
+
576
+ await runProactiveArtifactJob({
577
+ conversationId: "conv-1",
578
+ userMessageCutoff: 1000,
579
+ assistantMessageId: "msg-4",
580
+ broadcastMessage: mockBroadcast,
581
+ });
582
+
583
+ expect(bootstrapCalls).toHaveLength(0);
584
+ expect(processMessageCalls).toHaveLength(0);
585
+ expect(addMessageCalls).toHaveLength(0);
586
+ expect(emitSignalCalls).toHaveLength(0);
587
+ expect(releaseClaimCalls).toBe(0);
588
+ });
589
+
590
+ test("document build - build provider unavailable → releases claim for retry", async () => {
591
+ rawAllRows = defaultTranscript;
592
+ decisionResponse = decisionYesDocument;
593
+ buildProviderAvailable = false;
594
+
595
+ await runProactiveArtifactJob({
596
+ conversationId: "conv-1",
597
+ userMessageCutoff: 1000,
598
+ assistantMessageId: "msg-4",
599
+ broadcastMessage: mockBroadcast,
600
+ });
601
+
602
+ // No message, no notification
603
+ expect(addMessageCalls).toHaveLength(0);
604
+ expect(emitSignalCalls).toHaveLength(0);
605
+ expect(releaseClaimCalls).toBe(1);
606
+ });
607
+ });
608
+
609
+ describe("Message copy", () => {
610
+ test("uses fallback message when copy provider unavailable", async () => {
611
+ rawAllRows = defaultTranscript;
612
+ decisionResponse = decisionYesApp;
613
+ // Build provider is unavailable for copy step, but we need it for
614
+ // the copy call. Since the same callSite is used, we'll test the
615
+ // fallback by making the copy return unparseable output.
616
+ buildProviderAvailable = true;
617
+ copyResponse = "INVALID OUTPUT WITHOUT MESSAGE PREFIX";
618
+
619
+ const buildTime = Date.now();
620
+ mockApps = [
621
+ { id: "app-456", name: "Budget Tracker", createdAt: buildTime + 50 },
622
+ ];
623
+
624
+ mockConversations.set("conv-1", {
625
+ processing: false,
626
+ messages: [],
627
+ getMessages: () => [],
628
+ });
629
+
630
+ await runProactiveArtifactJob({
631
+ conversationId: "conv-1",
632
+ userMessageCutoff: 1000,
633
+ assistantMessageId: "msg-4",
634
+ broadcastMessage: mockBroadcast,
635
+ });
636
+
637
+ // Verify fallback message was used
638
+ expect(addMessageCalls).toHaveLength(1);
639
+ const content = JSON.parse(addMessageCalls[0].content);
640
+ expect(content[0].text).toContain("I made an app for you");
641
+ expect(content[0].text).toContain("Budget Tracker");
642
+ expect(content[0].text).toContain("Library");
643
+ expect(content[0].text).not.toContain("Assets");
644
+ });
645
+ });
646
+
647
+ describe("Transcript collection", () => {
648
+ test("dual-condition transcript query uses userMessageCutoff and assistantMessageId", async () => {
649
+ rawAllRows = defaultTranscript;
650
+ decisionResponse = decisionNo;
651
+
652
+ await runProactiveArtifactJob({
653
+ conversationId: "conv-1",
654
+ userMessageCutoff: 5000,
655
+ assistantMessageId: "asst-msg-99",
656
+ broadcastMessage: mockBroadcast,
657
+ });
658
+
659
+ // The rawAll mock captures all calls; verify the decision was called
660
+ // (proving transcript was collected and passed to decision)
661
+ expect(
662
+ providerSendCalls.some(
663
+ (c) => c.callSite === "proactiveArtifactDecision",
664
+ ),
665
+ ).toBe(true);
666
+ });
667
+
668
+ test("empty transcript → early return", async () => {
669
+ rawAllRows = [];
670
+
671
+ await runProactiveArtifactJob({
672
+ conversationId: "conv-1",
673
+ userMessageCutoff: 1000,
674
+ assistantMessageId: "msg-4",
675
+ broadcastMessage: mockBroadcast,
676
+ });
677
+
678
+ expect(releaseClaimCalls).toBe(1);
679
+ expect(providerSendCalls).toHaveLength(0);
680
+ expect(addMessageCalls).toHaveLength(0);
681
+ });
682
+ });
683
+ });
684
+
685
+ describe("injectAuxAssistantMessage", () => {
686
+ test("idle conversation: persists with skipIndexing, pushes to getMessages(), broadcasts delta + complete(aux) + list_invalidated", async () => {
687
+ const messages: unknown[] = [];
688
+ mockConversations.set("conv-inject-1", {
689
+ processing: false,
690
+ messages,
691
+ getMessages: () => messages,
692
+ });
693
+
694
+ await injectAuxAssistantMessage({
695
+ conversationId: "conv-inject-1",
696
+ text: "Here is your artifact!",
697
+ broadcastMessage: mockBroadcast,
698
+ });
699
+
700
+ // Persisted with skipIndexing
701
+ expect(addMessageCalls).toHaveLength(1);
702
+ expect(addMessageCalls[0].conversationId).toBe("conv-inject-1");
703
+ expect(addMessageCalls[0].role).toBe("assistant");
704
+ expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
705
+
706
+ // Pushed to in-memory messages
707
+ expect(messages).toHaveLength(1);
708
+
709
+ // Broadcasts: delta, complete(aux), list_invalidated
710
+ const deltaMsg = broadcastCalls.find(
711
+ (c) => c.type === "assistant_text_delta",
712
+ );
713
+ expect(deltaMsg).toBeDefined();
714
+ expect(deltaMsg!.text).toBe("Here is your artifact!");
715
+ expect(deltaMsg!.conversationId).toBe("conv-inject-1");
716
+
717
+ const completeMsg = broadcastCalls.find(
718
+ (c) => c.type === "message_complete",
719
+ );
720
+ expect(completeMsg).toBeDefined();
721
+ expect(completeMsg!.source).toBe("aux");
722
+ expect(completeMsg!.messageId).toBeDefined();
723
+
724
+ const listMsg = broadcastCalls.find(
725
+ (c) => c.type === "conversation_list_invalidated",
726
+ );
727
+ expect(listMsg).toBeDefined();
728
+ expect(listMsg!.reason).toBe("reordered");
729
+ });
730
+
731
+ test("processing → idle: waits for processing to become false before persisting", async () => {
732
+ const messages: unknown[] = [];
733
+ let processingFlag = true;
734
+ const conv: MockConversation = {
735
+ get processing() {
736
+ return processingFlag;
737
+ },
738
+ set processing(v: boolean) {
739
+ processingFlag = v;
740
+ },
741
+ messages,
742
+ getMessages: () => messages,
743
+ };
744
+ mockConversations.set("conv-inject-2", conv);
745
+
746
+ // Simulate processing becoming idle after a short delay
747
+ setTimeout(() => {
748
+ processingFlag = false;
749
+ }, 100);
750
+
751
+ await injectAuxAssistantMessage({
752
+ conversationId: "conv-inject-2",
753
+ text: "Deferred message",
754
+ broadcastMessage: mockBroadcast,
755
+ });
756
+
757
+ // Should have waited and then injected
758
+ expect(addMessageCalls).toHaveLength(1);
759
+ expect(messages).toHaveLength(1);
760
+ expect(broadcastCalls.some((c) => c.type === "assistant_text_delta")).toBe(
761
+ true,
762
+ );
763
+ });
764
+
765
+ test("processing → timeout: injects anyway with warning after poll timeout", async () => {
766
+ const messages: unknown[] = [];
767
+ // Conversation stays processing permanently — never becomes idle
768
+ const conv: MockConversation = {
769
+ processing: true,
770
+ messages,
771
+ getMessages: () => messages,
772
+ };
773
+ mockConversations.set("conv-inject-3", conv);
774
+
775
+ // Mock Date.now() to simulate time past the 60s timeout.
776
+ // First call sets `start`, second call must exceed IDLE_TIMEOUT_MS (60_000).
777
+ const realDateNow = Date.now;
778
+ let dateNowCallCount = 0;
779
+ const baseTime = 1_000_000;
780
+ Date.now = () => {
781
+ dateNowCallCount++;
782
+ // First call: start = baseTime
783
+ // Second call onward: past the timeout
784
+ if (dateNowCallCount <= 1) return baseTime;
785
+ return baseTime + 60_001;
786
+ };
787
+
788
+ try {
789
+ await injectAuxAssistantMessage({
790
+ conversationId: "conv-inject-3",
791
+ text: "Timeout message",
792
+ broadcastMessage: mockBroadcast,
793
+ });
794
+ } finally {
795
+ Date.now = realDateNow;
796
+ }
797
+
798
+ // Message was still persisted despite timeout
799
+ expect(addMessageCalls).toHaveLength(1);
800
+ expect(addMessageCalls[0].conversationId).toBe("conv-inject-3");
801
+
802
+ // Warning log was emitted about the timeout
803
+ expect(logWarnCalls.length).toBeGreaterThanOrEqual(1);
804
+ const warnMsg = logWarnCalls.find((c) =>
805
+ c.args.some(
806
+ (arg) => typeof arg === "string" && arg.includes("Timed out"),
807
+ ),
808
+ );
809
+ expect(warnMsg).toBeDefined();
810
+
811
+ // Since conversation is still processing, no delta/complete broadcasts
812
+ expect(
813
+ broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
814
+ ).toHaveLength(0);
815
+ expect(
816
+ broadcastCalls.filter((c) => c.type === "message_complete"),
817
+ ).toHaveLength(0);
818
+
819
+ // But list_invalidated IS sent (always sent regardless of processing state)
820
+ expect(
821
+ broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
822
+ ).toHaveLength(1);
823
+ });
824
+
825
+ test("inactive/unloaded conversation: persists + list_invalidated only", async () => {
826
+ // No conversation in the store
827
+ await injectAuxAssistantMessage({
828
+ conversationId: "conv-inject-4",
829
+ text: "Offline message",
830
+ broadcastMessage: mockBroadcast,
831
+ });
832
+
833
+ // Message persisted
834
+ expect(addMessageCalls).toHaveLength(1);
835
+ expect(addMessageCalls[0].conversationId).toBe("conv-inject-4");
836
+
837
+ // No delta or complete (conversation not loaded)
838
+ expect(
839
+ broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
840
+ ).toHaveLength(0);
841
+ expect(
842
+ broadcastCalls.filter((c) => c.type === "message_complete"),
843
+ ).toHaveLength(0);
844
+
845
+ // But list_invalidated IS sent
846
+ expect(
847
+ broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
848
+ ).toHaveLength(1);
849
+ });
850
+ });
851
+
852
+ describe("message-copy", () => {
853
+ test("buildMessageCopyPrompt includes all parameters", () => {
854
+ const prompt = buildMessageCopyPrompt({
855
+ artifactType: "app",
856
+ artifactTitle: "Budget Tracker",
857
+ artifactId: "app-123",
858
+ transcript: "[User]: I need a budget tool",
859
+ });
860
+
861
+ expect(prompt).toContain("app");
862
+ expect(prompt).toContain("Budget Tracker");
863
+ expect(prompt).toContain("app-123");
864
+ expect(prompt).toContain("I need a budget tool");
865
+ expect(prompt).toContain("Library");
866
+ expect(prompt).not.toContain("Assets pill");
867
+ expect(prompt).toContain("MESSAGE:");
868
+ });
869
+
870
+ test("parseMessageCopy extracts MESSAGE value", () => {
871
+ expect(parseMessageCopy("MESSAGE: Hello there!")).toBe("Hello there!");
872
+ expect(
873
+ parseMessageCopy("MESSAGE: I built something special for you."),
874
+ ).toBe("I built something special for you.");
875
+ });
876
+
877
+ test("parseMessageCopy returns null for missing MESSAGE", () => {
878
+ expect(parseMessageCopy("Some random text")).toBeNull();
879
+ expect(parseMessageCopy("")).toBeNull();
880
+ });
881
+
882
+ test("parseMessageCopy returns null for empty MESSAGE", () => {
883
+ expect(parseMessageCopy("MESSAGE: ")).toBeNull();
884
+ });
885
+
886
+ test("ensureMessageMentionsLibraryLocation appends missing location", () => {
887
+ const message = ensureMessageMentionsLibraryLocation(
888
+ "I built a budget tracker for your rent and groceries.",
889
+ "app",
890
+ );
891
+ expect(message).toContain("Library");
892
+ expect(message).not.toContain("Assets");
893
+ });
894
+
895
+ test("ensureMessageMentionsLibraryLocation normalizes terminal punctuation once", () => {
896
+ const message = ensureMessageMentionsLibraryLocation(
897
+ "I built a budget tracker for you!",
898
+ "app",
899
+ );
900
+ expect(message).toBe(
901
+ "I built a budget tracker for you. You can find the app in Library.",
902
+ );
903
+ });
904
+
905
+ test("ensureMessageMentionsLibraryLocation replaces artifact panel wording", () => {
906
+ const message = ensureMessageMentionsLibraryLocation(
907
+ "You'll find it in the artifact panel.",
908
+ "document",
909
+ );
910
+ expect(message).toContain("Library");
911
+ expect(message).not.toContain("Assets");
912
+ expect(message).not.toContain("artifact panel");
913
+ });
914
+ });