@vellumai/assistant 0.7.2 → 0.7.3

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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -0,0 +1,867 @@
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 { buildMessageCopyPrompt, parseMessageCopy } =
235
+ await import("./message-copy.js");
236
+
237
+ // ── Test helpers ────────────────────────────────────────────────────────
238
+
239
+ let broadcastCalls: Array<Record<string, unknown>> = [];
240
+ const mockBroadcast: any = (msg: Record<string, unknown>) => {
241
+ broadcastCalls.push(msg);
242
+ };
243
+
244
+ const defaultTranscript = [
245
+ { role: "user", content: "Hello there" },
246
+ { role: "assistant", content: "Hi! How can I help?" },
247
+ { role: "user", content: "I need a budget tracker" },
248
+ { role: "assistant", content: "I can help with that." },
249
+ { role: "user", content: "I spend about $3000 per month" },
250
+ { role: "assistant", content: "Got it, that is useful context." },
251
+ { role: "user", content: "Yes, let us track groceries and rent" },
252
+ {
253
+ role: "assistant",
254
+ content: "Great, I will remember those categories.",
255
+ },
256
+ ];
257
+
258
+ const decisionYesApp = `SHOULD_BUILD: yes
259
+ ARTIFACT_TYPE: app
260
+ ARTIFACT_TITLE: Budget Tracker
261
+ ARTIFACT_DESCRIPTION: A budget tracking app for monthly expenses around $3000, focusing on groceries and rent.`;
262
+
263
+ const decisionYesDocument = `SHOULD_BUILD: yes
264
+ ARTIFACT_TYPE: document
265
+ ARTIFACT_TITLE: Monthly Budget Guide
266
+ ARTIFACT_DESCRIPTION: A structured guide for tracking monthly expenses with categories for groceries and rent.`;
267
+
268
+ const decisionNo = `SHOULD_BUILD: no
269
+ SKIP_REASON: Not enough context to build something specific.`;
270
+
271
+ function resetState() {
272
+ decisionProviderAvailable = true;
273
+ buildProviderAvailable = true;
274
+ decisionResponse = "";
275
+ buildResponse = "";
276
+ copyResponse = "";
277
+ providerSendCalls = [];
278
+ rawAllRows = [];
279
+ bootstrapCalls = [];
280
+ processMessageCalls = [];
281
+ processMessageShouldThrow = false;
282
+ mockApps = [];
283
+ addAppConvCalls = [];
284
+ saveDocumentCalls = [];
285
+ saveDocumentResult = { success: true, surfaceId: "doc-123" };
286
+ addDocConvCalls = [];
287
+ releaseClaimCalls = 0;
288
+ addMessageCalls = [];
289
+ emitSignalCalls = [];
290
+ broadcastCalls = [];
291
+ mockConversations = new Map();
292
+ logWarnCalls = [];
293
+ uuidCounter = 0;
294
+ }
295
+
296
+ // ── Tests ───────────────────────────────────────────────────────────────
297
+
298
+ beforeEach(() => {
299
+ resetState();
300
+ });
301
+
302
+ afterEach(() => {
303
+ resetState();
304
+ });
305
+
306
+ describe("runProactiveArtifactJob", () => {
307
+ describe("Phase 1 — Decision", () => {
308
+ test("shouldBuild:false → releases claim, no Phase 2", async () => {
309
+ rawAllRows = defaultTranscript;
310
+ decisionResponse = decisionNo;
311
+
312
+ await runProactiveArtifactJob({
313
+ conversationId: "conv-1",
314
+ userMessageCutoff: 1000,
315
+ assistantMessageId: "msg-4",
316
+ broadcastMessage: mockBroadcast,
317
+ });
318
+
319
+ expect(releaseClaimCalls).toBe(1);
320
+ expect(bootstrapCalls).toHaveLength(0);
321
+ expect(processMessageCalls).toHaveLength(0);
322
+ expect(addMessageCalls).toHaveLength(0);
323
+ expect(emitSignalCalls).toHaveLength(0);
324
+ expect(broadcastCalls).toHaveLength(0);
325
+ });
326
+
327
+ test("null (malformed) → releases claim, silent exit", async () => {
328
+ rawAllRows = defaultTranscript;
329
+ decisionResponse = "THIS IS GARBAGE OUTPUT";
330
+
331
+ await runProactiveArtifactJob({
332
+ conversationId: "conv-1",
333
+ userMessageCutoff: 1000,
334
+ assistantMessageId: "msg-4",
335
+ broadcastMessage: mockBroadcast,
336
+ });
337
+
338
+ expect(releaseClaimCalls).toBe(1);
339
+ expect(bootstrapCalls).toHaveLength(0);
340
+ expect(processMessageCalls).toHaveLength(0);
341
+ expect(addMessageCalls).toHaveLength(0);
342
+ expect(emitSignalCalls).toHaveLength(0);
343
+ });
344
+
345
+ test("provider unavailable → releases claim, silent return", async () => {
346
+ rawAllRows = defaultTranscript;
347
+ decisionProviderAvailable = false;
348
+
349
+ await runProactiveArtifactJob({
350
+ conversationId: "conv-1",
351
+ userMessageCutoff: 1000,
352
+ assistantMessageId: "msg-4",
353
+ broadcastMessage: mockBroadcast,
354
+ });
355
+
356
+ expect(releaseClaimCalls).toBe(1);
357
+ expect(providerSendCalls).toHaveLength(0);
358
+ expect(bootstrapCalls).toHaveLength(0);
359
+ expect(addMessageCalls).toHaveLength(0);
360
+ expect(emitSignalCalls).toHaveLength(0);
361
+ });
362
+ });
363
+
364
+ describe("Phase 2 — Build", () => {
365
+ test("Phase 2 failure → releases claim, no message, no notification", async () => {
366
+ rawAllRows = defaultTranscript;
367
+ decisionResponse = decisionYesApp;
368
+ processMessageShouldThrow = true;
369
+
370
+ await runProactiveArtifactJob({
371
+ conversationId: "conv-1",
372
+ userMessageCutoff: 1000,
373
+ assistantMessageId: "msg-4",
374
+ broadcastMessage: mockBroadcast,
375
+ });
376
+
377
+ // processMessage was called (the build attempt happened)
378
+ expect(processMessageCalls).toHaveLength(1);
379
+ // But no message injection or notification
380
+ expect(addMessageCalls).toHaveLength(0);
381
+ expect(emitSignalCalls).toHaveLength(0);
382
+ expect(broadcastCalls).toHaveLength(0);
383
+ // Claim released so next turn can retry
384
+ expect(releaseClaimCalls).toBe(1);
385
+ });
386
+
387
+ test("successful app: Phase 1 → Phase 2 → app store query → message copy → inject → notify", async () => {
388
+ rawAllRows = defaultTranscript;
389
+ decisionResponse = decisionYesApp;
390
+ copyResponse = "MESSAGE: I built a budget tracker for you!";
391
+
392
+ const buildStartedAt = Date.now();
393
+ mockApps = [
394
+ {
395
+ id: "app-123",
396
+ name: "Budget Tracker",
397
+ createdAt: buildStartedAt + 100,
398
+ updatedAt: buildStartedAt + 100,
399
+ },
400
+ ];
401
+
402
+ // Set up an idle conversation so injection works fully
403
+ const convMessages: unknown[] = [];
404
+ mockConversations.set("conv-1", {
405
+ processing: false,
406
+ messages: convMessages,
407
+ getMessages: () => convMessages,
408
+ });
409
+
410
+ await runProactiveArtifactJob({
411
+ conversationId: "conv-1",
412
+ userMessageCutoff: 1000,
413
+ assistantMessageId: "msg-4",
414
+ broadcastMessage: mockBroadcast,
415
+ });
416
+
417
+ // Phase 1: decision provider called
418
+ expect(
419
+ providerSendCalls.some(
420
+ (c) => c.callSite === "proactiveArtifactDecision",
421
+ ),
422
+ ).toBe(true);
423
+
424
+ // Phase 2: processMessage called with correct options
425
+ expect(processMessageCalls).toHaveLength(1);
426
+ expect(processMessageCalls[0].prompt).toContain("Budget Tracker");
427
+ expect(processMessageCalls[0].prompt).toContain("auto_open: false");
428
+ const pmOpts = processMessageCalls[0].options as Record<string, unknown>;
429
+ expect(pmOpts.callSite).toBe("proactiveArtifactBuild");
430
+ expect(pmOpts.trustContext).toEqual({
431
+ sourceChannel: "vellum",
432
+ trustClass: "guardian",
433
+ });
434
+
435
+ // Bootstrap conversation created for app build
436
+ expect(bootstrapCalls).toHaveLength(1);
437
+ expect(bootstrapCalls[0].conversationType).toBe("background");
438
+ expect(bootstrapCalls[0].source).toBe("proactive_artifact");
439
+
440
+ // App associated with user's conversation for Assets chip
441
+ expect(addAppConvCalls).toHaveLength(1);
442
+ expect(addAppConvCalls[0].appId).toBe("app-123");
443
+ expect(addAppConvCalls[0].conversationId).toBe("conv-1");
444
+
445
+ // Message injection: addMessage called with skipIndexing
446
+ expect(addMessageCalls).toHaveLength(1);
447
+ expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
448
+ expect(addMessageCalls[0].conversationId).toBe("conv-1");
449
+
450
+ // Notification emitted
451
+ expect(emitSignalCalls).toHaveLength(1);
452
+ expect(emitSignalCalls[0].sourceEventName).toBe("activity.complete");
453
+ expect(emitSignalCalls[0].sourceChannel).toBe("vellum");
454
+ expect(emitSignalCalls[0].dedupeKey).toBe("proactive-artifact");
455
+ const hints = emitSignalCalls[0].attentionHints as Record<
456
+ string,
457
+ unknown
458
+ >;
459
+ expect(hints.visibleInSourceNow).toBe(false);
460
+ expect(hints.isAsyncBackground).toBe(true);
461
+ expect(hints.requiresAction).toBe(false);
462
+ // No conversationAffinityHint
463
+ expect(emitSignalCalls[0].conversationAffinityHint).toBeUndefined();
464
+
465
+ // Claim NOT released on success — guard stays permanent
466
+ expect(releaseClaimCalls).toBe(0);
467
+ });
468
+
469
+ test("successful document: Phase 1 → content gen → saveDocument → message copy → inject → notify", async () => {
470
+ rawAllRows = defaultTranscript;
471
+ decisionResponse = decisionYesDocument;
472
+ buildResponse =
473
+ "# Monthly Budget Guide\n\nTrack groceries and rent expenses.";
474
+ copyResponse =
475
+ "MESSAGE: I created a monthly budget guide tailored to your needs.";
476
+
477
+ mockConversations.set("conv-1", {
478
+ processing: false,
479
+ messages: [],
480
+ getMessages: () => [],
481
+ });
482
+
483
+ await runProactiveArtifactJob({
484
+ conversationId: "conv-1",
485
+ userMessageCutoff: 1000,
486
+ assistantMessageId: "msg-4",
487
+ broadcastMessage: mockBroadcast,
488
+ });
489
+
490
+ // Document saved via saveDocument (not raw file writes)
491
+ expect(saveDocumentCalls).toHaveLength(1);
492
+ expect(saveDocumentCalls[0].title).toBe("Monthly Budget Guide");
493
+ expect(saveDocumentCalls[0].conversationId).toBe("conv-1");
494
+ expect(saveDocumentCalls[0].content).toContain("Monthly Budget Guide");
495
+ expect(
496
+ (saveDocumentCalls[0].surfaceId as string).startsWith("doc-"),
497
+ ).toBe(true);
498
+ expect((saveDocumentCalls[0].wordCount as number) > 0).toBe(true);
499
+
500
+ // No bootstrapConversation or processMessage for document path
501
+ expect(bootstrapCalls).toHaveLength(0);
502
+ expect(processMessageCalls).toHaveLength(0);
503
+
504
+ // Message injection and notification
505
+ expect(addMessageCalls).toHaveLength(1);
506
+ expect(emitSignalCalls).toHaveLength(1);
507
+
508
+ // Claim NOT released on success
509
+ expect(releaseClaimCalls).toBe(0);
510
+ });
511
+
512
+ test("app build - no matching app found → releases claim for retry", async () => {
513
+ rawAllRows = defaultTranscript;
514
+ decisionResponse = decisionYesApp;
515
+ mockApps = []; // no apps in store
516
+
517
+ await runProactiveArtifactJob({
518
+ conversationId: "conv-1",
519
+ userMessageCutoff: 1000,
520
+ assistantMessageId: "msg-4",
521
+ broadcastMessage: mockBroadcast,
522
+ });
523
+
524
+ expect(processMessageCalls).toHaveLength(1);
525
+ expect(addMessageCalls).toHaveLength(0);
526
+ expect(emitSignalCalls).toHaveLength(0);
527
+ expect(releaseClaimCalls).toBe(1);
528
+ });
529
+
530
+ test("app decision with foreground app tool suppresses background build permanently", async () => {
531
+ rawAllRows = defaultTranscript;
532
+ decisionResponse = decisionYesApp;
533
+
534
+ await runProactiveArtifactJob({
535
+ conversationId: "conv-1",
536
+ userMessageCutoff: 1000,
537
+ assistantMessageId: "msg-4",
538
+ suppressAppBuild: true,
539
+ broadcastMessage: mockBroadcast,
540
+ });
541
+
542
+ expect(bootstrapCalls).toHaveLength(0);
543
+ expect(processMessageCalls).toHaveLength(0);
544
+ expect(addMessageCalls).toHaveLength(0);
545
+ expect(emitSignalCalls).toHaveLength(0);
546
+ expect(releaseClaimCalls).toBe(0);
547
+ });
548
+
549
+ test("app decision with recent app activity in source conversation suppresses background build permanently", async () => {
550
+ rawAllRows = defaultTranscript;
551
+ decisionResponse = decisionYesApp;
552
+ mockApps = [
553
+ {
554
+ id: "app-main",
555
+ name: "Budget Tracker",
556
+ createdAt: 1200,
557
+ updatedAt: 1300,
558
+ conversationIds: ["conv-1"],
559
+ },
560
+ ];
561
+
562
+ await runProactiveArtifactJob({
563
+ conversationId: "conv-1",
564
+ userMessageCutoff: 1000,
565
+ assistantMessageId: "msg-4",
566
+ broadcastMessage: mockBroadcast,
567
+ });
568
+
569
+ expect(bootstrapCalls).toHaveLength(0);
570
+ expect(processMessageCalls).toHaveLength(0);
571
+ expect(addMessageCalls).toHaveLength(0);
572
+ expect(emitSignalCalls).toHaveLength(0);
573
+ expect(releaseClaimCalls).toBe(0);
574
+ });
575
+
576
+ test("document build - build provider unavailable → releases claim for retry", async () => {
577
+ rawAllRows = defaultTranscript;
578
+ decisionResponse = decisionYesDocument;
579
+ buildProviderAvailable = false;
580
+
581
+ await runProactiveArtifactJob({
582
+ conversationId: "conv-1",
583
+ userMessageCutoff: 1000,
584
+ assistantMessageId: "msg-4",
585
+ broadcastMessage: mockBroadcast,
586
+ });
587
+
588
+ // No message, no notification
589
+ expect(addMessageCalls).toHaveLength(0);
590
+ expect(emitSignalCalls).toHaveLength(0);
591
+ expect(releaseClaimCalls).toBe(1);
592
+ });
593
+ });
594
+
595
+ describe("Message copy", () => {
596
+ test("uses fallback message when copy provider unavailable", async () => {
597
+ rawAllRows = defaultTranscript;
598
+ decisionResponse = decisionYesApp;
599
+ // Build provider is unavailable for copy step, but we need it for
600
+ // the copy call. Since the same callSite is used, we'll test the
601
+ // fallback by making the copy return unparseable output.
602
+ buildProviderAvailable = true;
603
+ copyResponse = "INVALID OUTPUT WITHOUT MESSAGE PREFIX";
604
+
605
+ const buildTime = Date.now();
606
+ mockApps = [
607
+ { id: "app-456", name: "Budget Tracker", createdAt: buildTime + 50 },
608
+ ];
609
+
610
+ mockConversations.set("conv-1", {
611
+ processing: false,
612
+ messages: [],
613
+ getMessages: () => [],
614
+ });
615
+
616
+ await runProactiveArtifactJob({
617
+ conversationId: "conv-1",
618
+ userMessageCutoff: 1000,
619
+ assistantMessageId: "msg-4",
620
+ broadcastMessage: mockBroadcast,
621
+ });
622
+
623
+ // Verify fallback message was used
624
+ expect(addMessageCalls).toHaveLength(1);
625
+ const content = JSON.parse(addMessageCalls[0].content);
626
+ expect(content[0].text).toContain("I made something for you");
627
+ expect(content[0].text).toContain("Budget Tracker");
628
+ });
629
+ });
630
+
631
+ describe("Transcript collection", () => {
632
+ test("dual-condition transcript query uses userMessageCutoff and assistantMessageId", async () => {
633
+ rawAllRows = defaultTranscript;
634
+ decisionResponse = decisionNo;
635
+
636
+ await runProactiveArtifactJob({
637
+ conversationId: "conv-1",
638
+ userMessageCutoff: 5000,
639
+ assistantMessageId: "asst-msg-99",
640
+ broadcastMessage: mockBroadcast,
641
+ });
642
+
643
+ // The rawAll mock captures all calls; verify the decision was called
644
+ // (proving transcript was collected and passed to decision)
645
+ expect(
646
+ providerSendCalls.some(
647
+ (c) => c.callSite === "proactiveArtifactDecision",
648
+ ),
649
+ ).toBe(true);
650
+ });
651
+
652
+ test("empty transcript → early return", async () => {
653
+ rawAllRows = [];
654
+
655
+ await runProactiveArtifactJob({
656
+ conversationId: "conv-1",
657
+ userMessageCutoff: 1000,
658
+ assistantMessageId: "msg-4",
659
+ broadcastMessage: mockBroadcast,
660
+ });
661
+
662
+ expect(releaseClaimCalls).toBe(1);
663
+ expect(providerSendCalls).toHaveLength(0);
664
+ expect(addMessageCalls).toHaveLength(0);
665
+ });
666
+ });
667
+ });
668
+
669
+ describe("injectAuxAssistantMessage", () => {
670
+ test("idle conversation: persists with skipIndexing, pushes to getMessages(), broadcasts delta + complete(aux) + list_invalidated", async () => {
671
+ const messages: unknown[] = [];
672
+ mockConversations.set("conv-inject-1", {
673
+ processing: false,
674
+ messages,
675
+ getMessages: () => messages,
676
+ });
677
+
678
+ await injectAuxAssistantMessage({
679
+ conversationId: "conv-inject-1",
680
+ text: "Here is your artifact!",
681
+ broadcastMessage: mockBroadcast,
682
+ });
683
+
684
+ // Persisted with skipIndexing
685
+ expect(addMessageCalls).toHaveLength(1);
686
+ expect(addMessageCalls[0].conversationId).toBe("conv-inject-1");
687
+ expect(addMessageCalls[0].role).toBe("assistant");
688
+ expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
689
+
690
+ // Pushed to in-memory messages
691
+ expect(messages).toHaveLength(1);
692
+
693
+ // Broadcasts: delta, complete(aux), list_invalidated
694
+ const deltaMsg = broadcastCalls.find(
695
+ (c) => c.type === "assistant_text_delta",
696
+ );
697
+ expect(deltaMsg).toBeDefined();
698
+ expect(deltaMsg!.text).toBe("Here is your artifact!");
699
+ expect(deltaMsg!.conversationId).toBe("conv-inject-1");
700
+
701
+ const completeMsg = broadcastCalls.find(
702
+ (c) => c.type === "message_complete",
703
+ );
704
+ expect(completeMsg).toBeDefined();
705
+ expect(completeMsg!.source).toBe("aux");
706
+ expect(completeMsg!.messageId).toBeDefined();
707
+
708
+ const listMsg = broadcastCalls.find(
709
+ (c) => c.type === "conversation_list_invalidated",
710
+ );
711
+ expect(listMsg).toBeDefined();
712
+ expect(listMsg!.reason).toBe("reordered");
713
+ });
714
+
715
+ test("processing → idle: waits for processing to become false before persisting", async () => {
716
+ const messages: unknown[] = [];
717
+ let processingFlag = true;
718
+ const conv: MockConversation = {
719
+ get processing() {
720
+ return processingFlag;
721
+ },
722
+ set processing(v: boolean) {
723
+ processingFlag = v;
724
+ },
725
+ messages,
726
+ getMessages: () => messages,
727
+ };
728
+ mockConversations.set("conv-inject-2", conv);
729
+
730
+ // Simulate processing becoming idle after a short delay
731
+ setTimeout(() => {
732
+ processingFlag = false;
733
+ }, 100);
734
+
735
+ await injectAuxAssistantMessage({
736
+ conversationId: "conv-inject-2",
737
+ text: "Deferred message",
738
+ broadcastMessage: mockBroadcast,
739
+ });
740
+
741
+ // Should have waited and then injected
742
+ expect(addMessageCalls).toHaveLength(1);
743
+ expect(messages).toHaveLength(1);
744
+ expect(broadcastCalls.some((c) => c.type === "assistant_text_delta")).toBe(
745
+ true,
746
+ );
747
+ });
748
+
749
+ test("processing → timeout: injects anyway with warning after poll timeout", async () => {
750
+ const messages: unknown[] = [];
751
+ // Conversation stays processing permanently — never becomes idle
752
+ const conv: MockConversation = {
753
+ processing: true,
754
+ messages,
755
+ getMessages: () => messages,
756
+ };
757
+ mockConversations.set("conv-inject-3", conv);
758
+
759
+ // Mock Date.now() to simulate time past the 60s timeout.
760
+ // First call sets `start`, second call must exceed IDLE_TIMEOUT_MS (60_000).
761
+ const realDateNow = Date.now;
762
+ let dateNowCallCount = 0;
763
+ const baseTime = 1_000_000;
764
+ Date.now = () => {
765
+ dateNowCallCount++;
766
+ // First call: start = baseTime
767
+ // Second call onward: past the timeout
768
+ if (dateNowCallCount <= 1) return baseTime;
769
+ return baseTime + 60_001;
770
+ };
771
+
772
+ try {
773
+ await injectAuxAssistantMessage({
774
+ conversationId: "conv-inject-3",
775
+ text: "Timeout message",
776
+ broadcastMessage: mockBroadcast,
777
+ });
778
+ } finally {
779
+ Date.now = realDateNow;
780
+ }
781
+
782
+ // Message was still persisted despite timeout
783
+ expect(addMessageCalls).toHaveLength(1);
784
+ expect(addMessageCalls[0].conversationId).toBe("conv-inject-3");
785
+
786
+ // Warning log was emitted about the timeout
787
+ expect(logWarnCalls.length).toBeGreaterThanOrEqual(1);
788
+ const warnMsg = logWarnCalls.find((c) =>
789
+ c.args.some(
790
+ (arg) => typeof arg === "string" && arg.includes("Timed out"),
791
+ ),
792
+ );
793
+ expect(warnMsg).toBeDefined();
794
+
795
+ // Since conversation is still processing, no delta/complete broadcasts
796
+ expect(
797
+ broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
798
+ ).toHaveLength(0);
799
+ expect(
800
+ broadcastCalls.filter((c) => c.type === "message_complete"),
801
+ ).toHaveLength(0);
802
+
803
+ // But list_invalidated IS sent (always sent regardless of processing state)
804
+ expect(
805
+ broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
806
+ ).toHaveLength(1);
807
+ });
808
+
809
+ test("inactive/unloaded conversation: persists + list_invalidated only", async () => {
810
+ // No conversation in the store
811
+ await injectAuxAssistantMessage({
812
+ conversationId: "conv-inject-4",
813
+ text: "Offline message",
814
+ broadcastMessage: mockBroadcast,
815
+ });
816
+
817
+ // Message persisted
818
+ expect(addMessageCalls).toHaveLength(1);
819
+ expect(addMessageCalls[0].conversationId).toBe("conv-inject-4");
820
+
821
+ // No delta or complete (conversation not loaded)
822
+ expect(
823
+ broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
824
+ ).toHaveLength(0);
825
+ expect(
826
+ broadcastCalls.filter((c) => c.type === "message_complete"),
827
+ ).toHaveLength(0);
828
+
829
+ // But list_invalidated IS sent
830
+ expect(
831
+ broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
832
+ ).toHaveLength(1);
833
+ });
834
+ });
835
+
836
+ describe("message-copy", () => {
837
+ test("buildMessageCopyPrompt includes all parameters", () => {
838
+ const prompt = buildMessageCopyPrompt({
839
+ artifactType: "app",
840
+ artifactTitle: "Budget Tracker",
841
+ artifactId: "app-123",
842
+ transcript: "[User]: I need a budget tool",
843
+ });
844
+
845
+ expect(prompt).toContain("app");
846
+ expect(prompt).toContain("Budget Tracker");
847
+ expect(prompt).toContain("app-123");
848
+ expect(prompt).toContain("I need a budget tool");
849
+ expect(prompt).toContain("MESSAGE:");
850
+ });
851
+
852
+ test("parseMessageCopy extracts MESSAGE value", () => {
853
+ expect(parseMessageCopy("MESSAGE: Hello there!")).toBe("Hello there!");
854
+ expect(
855
+ parseMessageCopy("MESSAGE: I built something special for you."),
856
+ ).toBe("I built something special for you.");
857
+ });
858
+
859
+ test("parseMessageCopy returns null for missing MESSAGE", () => {
860
+ expect(parseMessageCopy("Some random text")).toBeNull();
861
+ expect(parseMessageCopy("")).toBeNull();
862
+ });
863
+
864
+ test("parseMessageCopy returns null for empty MESSAGE", () => {
865
+ expect(parseMessageCopy("MESSAGE: ")).toBeNull();
866
+ });
867
+ });