@vellumai/assistant 0.8.8 → 0.8.9-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -1,962 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- import {
4
- conversationMessagesSyncTag,
5
- SYNC_TAGS,
6
- } from "../daemon/message-types/sync.js";
7
-
8
- // ── Mock state ──────────────────────────────────────────────────────────
9
-
10
- // Provider mock
11
- let decisionProviderAvailable = true;
12
- let buildProviderAvailable = true;
13
- let decisionResponse = "";
14
- let buildResponse = "";
15
- let copyResponse = "";
16
- let providerSendCalls: Array<{ callSite: string; messages: unknown[] }> = [];
17
-
18
- const mockProvider = (callSite: string) => ({
19
- name: "mock-provider",
20
- sendMessage: async (messages: unknown[]) => {
21
- providerSendCalls.push({ callSite, messages });
22
- if (callSite === "proactiveArtifactDecision") {
23
- return { content: [{ type: "text", text: decisionResponse }] };
24
- }
25
- if (callSite === "proactiveArtifactBuild") {
26
- // copyResponse is used when it's for message copy (second call)
27
- const isCopyCall = providerSendCalls.filter(
28
- (c) => c.callSite === "proactiveArtifactBuild",
29
- ).length;
30
- if (isCopyCall > 1) {
31
- return { content: [{ type: "text", text: copyResponse }] };
32
- }
33
- return { content: [{ type: "text", text: buildResponse }] };
34
- }
35
- return { content: [{ type: "text", text: "" }] };
36
- },
37
- });
38
-
39
- mock.module("../providers/provider-send-message.js", () => ({
40
- getConfiguredProvider: async (callSite: string) => {
41
- if (
42
- callSite === "proactiveArtifactDecision" &&
43
- !decisionProviderAvailable
44
- ) {
45
- return null;
46
- }
47
- if (callSite === "proactiveArtifactBuild" && !buildProviderAvailable) {
48
- return null;
49
- }
50
- return mockProvider(callSite);
51
- },
52
- extractText: (response: { content: Array<{ type: string; text: string }> }) =>
53
- response.content.find((b: { type: string }) => b.type === "text")?.text ??
54
- "",
55
- }));
56
-
57
- // rawAll mock
58
- let rawAllRows: Array<{ role: string; content: string }> = [];
59
- let rawAllLastSql = "";
60
- let rawAllLastArgs: unknown[] = [];
61
-
62
- mock.module("../memory/raw-query.js", () => ({
63
- rawAll: (sql: string, ...args: unknown[]) => {
64
- rawAllLastSql = sql;
65
- rawAllLastArgs = args;
66
- return rawAllRows;
67
- },
68
- rawRun: () => 0,
69
- }));
70
-
71
- // bootstrapConversation mock
72
- let bootstrapCalls: Array<Record<string, unknown>> = [];
73
-
74
- mock.module("../memory/conversation-bootstrap.js", () => ({
75
- bootstrapConversation: (opts: Record<string, unknown>) => {
76
- bootstrapCalls.push(opts);
77
- return { id: `bg-conv-${bootstrapCalls.length}` };
78
- },
79
- }));
80
-
81
- // processMessage mock
82
- let processMessageCalls: Array<{
83
- conversationId: string;
84
- prompt: string;
85
- options: unknown;
86
- }> = [];
87
- let processMessageShouldThrow = false;
88
-
89
- mock.module("../daemon/process-message.js", () => ({
90
- processMessage: async (
91
- conversationId: string,
92
- prompt: string,
93
- options: unknown,
94
- ) => {
95
- processMessageCalls.push({ conversationId, prompt, options });
96
- if (processMessageShouldThrow) {
97
- throw new Error("processMessage failed");
98
- }
99
- return { messageId: "pm-msg-1" };
100
- },
101
- }));
102
-
103
- // App store mock
104
- let mockApps: Array<{
105
- id: string;
106
- name: string;
107
- createdAt: number;
108
- updatedAt?: number;
109
- conversationIds?: string[];
110
- }> = [];
111
- let addAppConvCalls: Array<{ appId: string; conversationId: string }> = [];
112
-
113
- mock.module("../memory/app-store.js", () => ({
114
- listApps: () => mockApps,
115
- listAppsByConversation: (conversationId: string) =>
116
- mockApps.filter((app) => app.conversationIds?.includes(conversationId)),
117
- addAppConversationId: (appId: string, conversationId: string) => {
118
- addAppConvCalls.push({ appId, conversationId });
119
- return true;
120
- },
121
- }));
122
-
123
- // Document store mock
124
- let saveDocumentCalls: Array<Record<string, unknown>> = [];
125
- let saveDocumentResult: {
126
- success: boolean;
127
- surfaceId?: string;
128
- error?: string;
129
- } = {
130
- success: true,
131
- surfaceId: "doc-123",
132
- };
133
- let addDocConvCalls: Array<{ surfaceId: string; conversationId: string }> = [];
134
-
135
- mock.module("../documents/document-store.js", () => ({
136
- saveDocument: (params: Record<string, unknown>) => {
137
- saveDocumentCalls.push(params);
138
- return saveDocumentResult;
139
- },
140
- addDocumentConversation: (surfaceId: string, conversationId: string) => {
141
- addDocConvCalls.push({ surfaceId, conversationId });
142
- },
143
- }));
144
-
145
- // addMessage mock
146
- let addMessageCalls: Array<{
147
- conversationId: string;
148
- role: string;
149
- content: string;
150
- options: unknown;
151
- }> = [];
152
-
153
- mock.module("../memory/conversation-crud.js", () => ({
154
- addMessage: async (
155
- conversationId: string,
156
- role: string,
157
- content: string,
158
- options: unknown,
159
- ) => {
160
- addMessageCalls.push({ conversationId, role, content, options });
161
- return { id: `msg-${addMessageCalls.length}` };
162
- },
163
- reserveMessage: mock(async () => ({ id: "msg-reserve" })),
164
- }));
165
-
166
- // emitNotificationSignal mock
167
- let emitSignalCalls: Array<Record<string, unknown>> = [];
168
-
169
- mock.module("../notifications/emit-signal.js", () => ({
170
- emitNotificationSignal: async (params: Record<string, unknown>) => {
171
- emitSignalCalls.push(params);
172
- return {
173
- signalId: "signal-1",
174
- deduplicated: false,
175
- dispatched: true,
176
- reason: "ok",
177
- deliveryResults: [],
178
- };
179
- },
180
- }));
181
-
182
- // app sync invalidation mock
183
- let publishAppsChangedCalls: Array<string | undefined> = [];
184
-
185
- mock.module("../runtime/sync/resource-sync-events.js", () => ({
186
- publishAppsChanged: (originClientId?: string) => {
187
- publishAppsChangedCalls.push(originClientId);
188
- },
189
- }));
190
-
191
- // findConversation mock
192
- type MockConversation = {
193
- isProcessing(): boolean;
194
- messages: unknown[];
195
- getMessages: () => unknown[];
196
- };
197
- let mockConversations: Map<string, MockConversation> = new Map();
198
-
199
- mock.module("../daemon/conversation-store.js", () => ({
200
- findConversation: (id: string) => mockConversations.get(id),
201
- }));
202
-
203
- // createAssistantMessage mock
204
- mock.module("../agent/message-types.js", () => ({
205
- createAssistantMessage: (text: string) => ({
206
- role: "assistant",
207
- content: [{ type: "text", text }],
208
- }),
209
- }));
210
-
211
- // Trigger state mock
212
- let releaseClaimCalls = 0;
213
-
214
- mock.module("./trigger-state.js", () => ({
215
- releaseProactiveArtifactClaim: () => {
216
- releaseClaimCalls++;
217
- },
218
- }));
219
-
220
- // Trust context mock
221
- mock.module("../daemon/trust-context.js", () => ({
222
- INTERNAL_GUARDIAN_TRUST_CONTEXT: {
223
- sourceChannel: "vellum",
224
- trustClass: "guardian",
225
- },
226
- }));
227
-
228
- // Logger mock
229
- let logWarnCalls: Array<{ args: unknown[] }> = [];
230
-
231
- mock.module("../util/logger.js", () => ({
232
- getLogger: () => ({
233
- info: () => {},
234
- warn: (...args: unknown[]) => {
235
- logWarnCalls.push({ args });
236
- },
237
- error: () => {},
238
- debug: () => {},
239
- }),
240
- }));
241
-
242
- // uuid mock — deterministic IDs for testing
243
- let uuidCounter = 0;
244
- mock.module("uuid", () => ({
245
- v4: () => `test-uuid-${++uuidCounter}`,
246
- }));
247
-
248
- // ── Import SUT after mocks ─────────────────────────────────────────────
249
-
250
- const { runProactiveArtifactJob } = await import("./job.js");
251
- const { injectAuxAssistantMessage } = await import("./aux-message-injector.js");
252
- const {
253
- buildMessageCopyPrompt,
254
- ensureMessageMentionsLibraryLocation,
255
- parseMessageCopy,
256
- } = await import("./message-copy.js");
257
-
258
- // ── Test helpers ────────────────────────────────────────────────────────
259
-
260
- let broadcastCalls: Array<Record<string, unknown>> = [];
261
- const mockBroadcast: any = (msg: Record<string, unknown>) => {
262
- broadcastCalls.push(msg);
263
- };
264
-
265
- const defaultTranscript = [
266
- { role: "user", content: "Hello there" },
267
- { role: "assistant", content: "Hi! How can I help?" },
268
- { role: "user", content: "I need a budget tracker" },
269
- { role: "assistant", content: "I can help with that." },
270
- { role: "user", content: "I spend about $3000 per month" },
271
- { role: "assistant", content: "Got it, that is useful context." },
272
- { role: "user", content: "Yes, let us track groceries and rent" },
273
- {
274
- role: "assistant",
275
- content: "Great, I will remember those categories.",
276
- },
277
- ];
278
-
279
- const decisionYesApp = `SHOULD_BUILD: yes
280
- ARTIFACT_TYPE: app
281
- ARTIFACT_TITLE: Budget Tracker
282
- ARTIFACT_DESCRIPTION: A budget tracking app for monthly expenses around $3000, focusing on groceries and rent.`;
283
-
284
- const decisionYesDocument = `SHOULD_BUILD: yes
285
- ARTIFACT_TYPE: document
286
- ARTIFACT_TITLE: Monthly Budget Guide
287
- ARTIFACT_DESCRIPTION: A structured guide for tracking monthly expenses with categories for groceries and rent.`;
288
-
289
- const decisionNo = `SHOULD_BUILD: no
290
- SKIP_REASON: Not enough context to build something specific.`;
291
-
292
- function resetState() {
293
- decisionProviderAvailable = true;
294
- buildProviderAvailable = true;
295
- decisionResponse = "";
296
- buildResponse = "";
297
- copyResponse = "";
298
- providerSendCalls = [];
299
- rawAllRows = [];
300
- rawAllLastSql = "";
301
- rawAllLastArgs = [];
302
- bootstrapCalls = [];
303
- processMessageCalls = [];
304
- processMessageShouldThrow = false;
305
- mockApps = [];
306
- addAppConvCalls = [];
307
- saveDocumentCalls = [];
308
- saveDocumentResult = { success: true, surfaceId: "doc-123" };
309
- addDocConvCalls = [];
310
- releaseClaimCalls = 0;
311
- addMessageCalls = [];
312
- emitSignalCalls = [];
313
- publishAppsChangedCalls = [];
314
- broadcastCalls = [];
315
- mockConversations = new Map();
316
- logWarnCalls = [];
317
- uuidCounter = 0;
318
- }
319
-
320
- // ── Tests ───────────────────────────────────────────────────────────────
321
-
322
- beforeEach(() => {
323
- resetState();
324
- });
325
-
326
- afterEach(() => {
327
- resetState();
328
- });
329
-
330
- describe("runProactiveArtifactJob", () => {
331
- describe("Phase 1 — Decision", () => {
332
- test("shouldBuild:false → releases claim, no Phase 2", async () => {
333
- rawAllRows = defaultTranscript;
334
- decisionResponse = decisionNo;
335
-
336
- await runProactiveArtifactJob({
337
- conversationId: "conv-1",
338
- userMessageCutoff: 1000,
339
- assistantMessageId: "msg-4",
340
- broadcastMessage: mockBroadcast,
341
- });
342
-
343
- expect(releaseClaimCalls).toBe(1);
344
- expect(bootstrapCalls).toHaveLength(0);
345
- expect(processMessageCalls).toHaveLength(0);
346
- expect(addMessageCalls).toHaveLength(0);
347
- expect(emitSignalCalls).toHaveLength(0);
348
- expect(broadcastCalls).toHaveLength(0);
349
- });
350
-
351
- test("null (malformed) → releases claim, silent exit", async () => {
352
- rawAllRows = defaultTranscript;
353
- decisionResponse = "THIS IS GARBAGE OUTPUT";
354
-
355
- await runProactiveArtifactJob({
356
- conversationId: "conv-1",
357
- userMessageCutoff: 1000,
358
- assistantMessageId: "msg-4",
359
- broadcastMessage: mockBroadcast,
360
- });
361
-
362
- expect(releaseClaimCalls).toBe(1);
363
- expect(bootstrapCalls).toHaveLength(0);
364
- expect(processMessageCalls).toHaveLength(0);
365
- expect(addMessageCalls).toHaveLength(0);
366
- expect(emitSignalCalls).toHaveLength(0);
367
- });
368
-
369
- test("provider unavailable → releases claim, silent return", async () => {
370
- rawAllRows = defaultTranscript;
371
- decisionProviderAvailable = false;
372
-
373
- await runProactiveArtifactJob({
374
- conversationId: "conv-1",
375
- userMessageCutoff: 1000,
376
- assistantMessageId: "msg-4",
377
- broadcastMessage: mockBroadcast,
378
- });
379
-
380
- expect(releaseClaimCalls).toBe(1);
381
- expect(providerSendCalls).toHaveLength(0);
382
- expect(bootstrapCalls).toHaveLength(0);
383
- expect(addMessageCalls).toHaveLength(0);
384
- expect(emitSignalCalls).toHaveLength(0);
385
- });
386
- });
387
-
388
- describe("Phase 2 — Build", () => {
389
- test("Phase 2 failure → releases claim, no message, no notification", async () => {
390
- rawAllRows = defaultTranscript;
391
- decisionResponse = decisionYesApp;
392
- processMessageShouldThrow = true;
393
-
394
- await runProactiveArtifactJob({
395
- conversationId: "conv-1",
396
- userMessageCutoff: 1000,
397
- assistantMessageId: "msg-4",
398
- broadcastMessage: mockBroadcast,
399
- });
400
-
401
- // processMessage was called (the build attempt happened)
402
- expect(processMessageCalls).toHaveLength(1);
403
- // But no message injection or notification
404
- expect(addMessageCalls).toHaveLength(0);
405
- expect(emitSignalCalls).toHaveLength(0);
406
- expect(broadcastCalls).toHaveLength(0);
407
- // Claim released so next turn can retry
408
- expect(releaseClaimCalls).toBe(1);
409
- });
410
-
411
- test("successful app: Phase 1 → Phase 2 → app store query → message copy → inject → notify", async () => {
412
- rawAllRows = defaultTranscript;
413
- decisionResponse = decisionYesApp;
414
- copyResponse = "MESSAGE: I built a budget tracker for you!";
415
-
416
- const buildStartedAt = Date.now();
417
- mockApps = [
418
- {
419
- id: "app-123",
420
- name: "Budget Tracker",
421
- createdAt: buildStartedAt + 100,
422
- updatedAt: buildStartedAt + 100,
423
- },
424
- ];
425
-
426
- // Set up an idle conversation so injection works fully
427
- const convMessages: unknown[] = [];
428
- mockConversations.set("conv-1", {
429
- isProcessing: () => false,
430
- messages: convMessages,
431
- getMessages: () => convMessages,
432
- });
433
-
434
- await runProactiveArtifactJob({
435
- conversationId: "conv-1",
436
- userMessageCutoff: 1000,
437
- assistantMessageId: "msg-4",
438
- broadcastMessage: mockBroadcast,
439
- });
440
-
441
- // Phase 1: decision provider called
442
- expect(
443
- providerSendCalls.some(
444
- (c) => c.callSite === "proactiveArtifactDecision",
445
- ),
446
- ).toBe(true);
447
-
448
- // Phase 2: processMessage called with correct options
449
- expect(processMessageCalls).toHaveLength(1);
450
- expect(processMessageCalls[0].prompt).toContain("Budget Tracker");
451
- expect(processMessageCalls[0].prompt).toContain("auto_open: false");
452
- const pmOpts = processMessageCalls[0].options as Record<string, unknown>;
453
- expect(pmOpts.callSite).toBe("proactiveArtifactBuild");
454
- expect(pmOpts.trustContext).toEqual({
455
- sourceChannel: "vellum",
456
- trustClass: "guardian",
457
- });
458
-
459
- // Bootstrap conversation created for app build
460
- expect(bootstrapCalls).toHaveLength(1);
461
- expect(bootstrapCalls[0].conversationType).toBe("background");
462
- expect(bootstrapCalls[0].source).toBe("proactive_artifact");
463
-
464
- // App associated with user's conversation for existing artifact linkage
465
- expect(addAppConvCalls).toHaveLength(1);
466
- expect(addAppConvCalls[0].appId).toBe("app-123");
467
- expect(addAppConvCalls[0].conversationId).toBe("conv-1");
468
-
469
- expect(broadcastCalls).toContainEqual({
470
- type: "app_files_changed",
471
- appId: "app-123",
472
- });
473
- expect(publishAppsChangedCalls).toEqual([undefined]);
474
-
475
- // Message injection: addMessage called with skipIndexing
476
- expect(addMessageCalls).toHaveLength(1);
477
- expect(
478
- (addMessageCalls[0].options as Record<string, unknown>).skipIndexing,
479
- ).toBe(true);
480
- expect(addMessageCalls[0].conversationId).toBe("conv-1");
481
- const injectedAppContent = JSON.parse(addMessageCalls[0].content);
482
- expect(injectedAppContent[0].text).toContain("Library");
483
- expect(injectedAppContent[0].text).not.toContain("Assets");
484
-
485
- // Notification emitted
486
- expect(emitSignalCalls).toHaveLength(1);
487
- expect(emitSignalCalls[0].sourceEventName).toBe("activity.complete");
488
- expect(emitSignalCalls[0].sourceChannel).toBe("vellum");
489
- expect(emitSignalCalls[0].dedupeKey).toBe("proactive-artifact");
490
- const hints = emitSignalCalls[0].attentionHints as Record<
491
- string,
492
- unknown
493
- >;
494
- expect(hints.visibleInSourceNow).toBe(false);
495
- expect(hints.isAsyncBackground).toBe(true);
496
- expect(hints.requiresAction).toBe(false);
497
- // No conversationAffinityHint
498
- expect(emitSignalCalls[0].conversationAffinityHint).toBeUndefined();
499
-
500
- // Claim NOT released on success — guard stays permanent
501
- expect(releaseClaimCalls).toBe(0);
502
- });
503
-
504
- test("successful document: Phase 1 → content gen → saveDocument → message copy → inject → notify", async () => {
505
- rawAllRows = defaultTranscript;
506
- decisionResponse = decisionYesDocument;
507
- buildResponse =
508
- "# Monthly Budget Guide\n\nTrack groceries and rent expenses.";
509
- copyResponse =
510
- "MESSAGE: I created a monthly budget guide tailored to your needs.";
511
-
512
- mockConversations.set("conv-1", {
513
- isProcessing: () => false,
514
- messages: [],
515
- getMessages: () => [],
516
- });
517
-
518
- await runProactiveArtifactJob({
519
- conversationId: "conv-1",
520
- userMessageCutoff: 1000,
521
- assistantMessageId: "msg-4",
522
- broadcastMessage: mockBroadcast,
523
- });
524
-
525
- // Document saved via saveDocument (not raw file writes)
526
- expect(saveDocumentCalls).toHaveLength(1);
527
- expect(saveDocumentCalls[0].title).toBe("Monthly Budget Guide");
528
- expect(saveDocumentCalls[0].conversationId).toBe("conv-1");
529
- expect(saveDocumentCalls[0].content).toContain("Monthly Budget Guide");
530
- expect(
531
- (saveDocumentCalls[0].surfaceId as string).startsWith("doc-"),
532
- ).toBe(true);
533
- expect((saveDocumentCalls[0].wordCount as number) > 0).toBe(true);
534
-
535
- // No bootstrapConversation or processMessage for document path
536
- expect(bootstrapCalls).toHaveLength(0);
537
- expect(processMessageCalls).toHaveLength(0);
538
-
539
- // Message injection and notification
540
- expect(addMessageCalls).toHaveLength(1);
541
- const injectedDocumentContent = JSON.parse(addMessageCalls[0].content);
542
- expect(injectedDocumentContent[0].text).toContain("Library");
543
- expect(injectedDocumentContent[0].text).not.toContain("Assets");
544
- expect(emitSignalCalls).toHaveLength(1);
545
-
546
- // Claim NOT released on success
547
- expect(releaseClaimCalls).toBe(0);
548
- });
549
-
550
- test("app build - no matching app found → releases claim for retry", async () => {
551
- rawAllRows = defaultTranscript;
552
- decisionResponse = decisionYesApp;
553
- mockApps = []; // no apps in store
554
-
555
- await runProactiveArtifactJob({
556
- conversationId: "conv-1",
557
- userMessageCutoff: 1000,
558
- assistantMessageId: "msg-4",
559
- broadcastMessage: mockBroadcast,
560
- });
561
-
562
- expect(processMessageCalls).toHaveLength(1);
563
- expect(addMessageCalls).toHaveLength(0);
564
- expect(emitSignalCalls).toHaveLength(0);
565
- expect(releaseClaimCalls).toBe(1);
566
- });
567
-
568
- test("app decision with foreground app tool suppresses background build permanently", async () => {
569
- rawAllRows = defaultTranscript;
570
- decisionResponse = decisionYesApp;
571
-
572
- await runProactiveArtifactJob({
573
- conversationId: "conv-1",
574
- userMessageCutoff: 1000,
575
- assistantMessageId: "msg-4",
576
- suppressAppBuild: true,
577
- broadcastMessage: mockBroadcast,
578
- });
579
-
580
- expect(bootstrapCalls).toHaveLength(0);
581
- expect(processMessageCalls).toHaveLength(0);
582
- expect(addMessageCalls).toHaveLength(0);
583
- expect(emitSignalCalls).toHaveLength(0);
584
- expect(releaseClaimCalls).toBe(0);
585
- });
586
-
587
- test("app decision with recent app activity in source conversation suppresses background build permanently", async () => {
588
- rawAllRows = defaultTranscript;
589
- decisionResponse = decisionYesApp;
590
- mockApps = [
591
- {
592
- id: "app-main",
593
- name: "Budget Tracker",
594
- createdAt: 1200,
595
- updatedAt: 1300,
596
- conversationIds: ["conv-1"],
597
- },
598
- ];
599
-
600
- await runProactiveArtifactJob({
601
- conversationId: "conv-1",
602
- userMessageCutoff: 1000,
603
- assistantMessageId: "msg-4",
604
- broadcastMessage: mockBroadcast,
605
- });
606
-
607
- expect(bootstrapCalls).toHaveLength(0);
608
- expect(processMessageCalls).toHaveLength(0);
609
- expect(addMessageCalls).toHaveLength(0);
610
- expect(emitSignalCalls).toHaveLength(0);
611
- expect(releaseClaimCalls).toBe(0);
612
- });
613
-
614
- test("document build - build provider unavailable → releases claim for retry", async () => {
615
- rawAllRows = defaultTranscript;
616
- decisionResponse = decisionYesDocument;
617
- buildProviderAvailable = false;
618
-
619
- await runProactiveArtifactJob({
620
- conversationId: "conv-1",
621
- userMessageCutoff: 1000,
622
- assistantMessageId: "msg-4",
623
- broadcastMessage: mockBroadcast,
624
- });
625
-
626
- // No message, no notification
627
- expect(addMessageCalls).toHaveLength(0);
628
- expect(emitSignalCalls).toHaveLength(0);
629
- expect(releaseClaimCalls).toBe(1);
630
- });
631
- });
632
-
633
- describe("Message copy", () => {
634
- test("uses fallback message when copy provider unavailable", async () => {
635
- rawAllRows = defaultTranscript;
636
- decisionResponse = decisionYesApp;
637
- // Build provider is unavailable for copy step, but we need it for
638
- // the copy call. Since the same callSite is used, we'll test the
639
- // fallback by making the copy return unparseable output.
640
- buildProviderAvailable = true;
641
- copyResponse = "INVALID OUTPUT WITHOUT MESSAGE PREFIX";
642
-
643
- const buildTime = Date.now();
644
- mockApps = [
645
- { id: "app-456", name: "Budget Tracker", createdAt: buildTime + 50 },
646
- ];
647
-
648
- mockConversations.set("conv-1", {
649
- isProcessing: () => false,
650
- messages: [],
651
- getMessages: () => [],
652
- });
653
-
654
- await runProactiveArtifactJob({
655
- conversationId: "conv-1",
656
- userMessageCutoff: 1000,
657
- assistantMessageId: "msg-4",
658
- broadcastMessage: mockBroadcast,
659
- });
660
-
661
- // Verify fallback message was used
662
- expect(addMessageCalls).toHaveLength(1);
663
- const content = JSON.parse(addMessageCalls[0].content);
664
- expect(content[0].text).toContain("I made an app for you");
665
- expect(content[0].text).toContain("Budget Tracker");
666
- expect(content[0].text).toContain("Library");
667
- expect(content[0].text).not.toContain("Assets");
668
- });
669
- });
670
-
671
- describe("Transcript collection", () => {
672
- test("transcript query is scoped to the triggering conversation", async () => {
673
- rawAllRows = defaultTranscript;
674
- decisionResponse = decisionNo;
675
-
676
- await runProactiveArtifactJob({
677
- conversationId: "conv-1",
678
- userMessageCutoff: 5000,
679
- assistantMessageId: "asst-msg-99",
680
- broadcastMessage: mockBroadcast,
681
- });
682
-
683
- expect(rawAllLastSql).toContain("AND m.conversation_id = ?");
684
- expect(rawAllLastArgs).toEqual(["conv-1", 5000, "asst-msg-99"]);
685
- expect(
686
- providerSendCalls.some(
687
- (c) => c.callSite === "proactiveArtifactDecision",
688
- ),
689
- ).toBe(true);
690
- });
691
-
692
- test("empty transcript → early return", async () => {
693
- rawAllRows = [];
694
-
695
- await runProactiveArtifactJob({
696
- conversationId: "conv-1",
697
- userMessageCutoff: 1000,
698
- assistantMessageId: "msg-4",
699
- broadcastMessage: mockBroadcast,
700
- });
701
-
702
- expect(releaseClaimCalls).toBe(1);
703
- expect(providerSendCalls).toHaveLength(0);
704
- expect(addMessageCalls).toHaveLength(0);
705
- });
706
- });
707
- });
708
-
709
- describe("injectAuxAssistantMessage", () => {
710
- test("idle conversation: persists with skipIndexing, pushes to getMessages(), broadcasts delta + complete(aux) + list sync", async () => {
711
- const messages: unknown[] = [];
712
- mockConversations.set("conv-inject-1", {
713
- isProcessing: () => false,
714
- messages,
715
- getMessages: () => messages,
716
- });
717
-
718
- await injectAuxAssistantMessage({
719
- conversationId: "conv-inject-1",
720
- text: "Here is your artifact!",
721
- broadcastMessage: mockBroadcast,
722
- });
723
-
724
- // Persisted with skipIndexing
725
- expect(addMessageCalls).toHaveLength(1);
726
- expect(addMessageCalls[0].conversationId).toBe("conv-inject-1");
727
- expect(addMessageCalls[0].role).toBe("assistant");
728
- expect(
729
- (addMessageCalls[0].options as Record<string, unknown>).skipIndexing,
730
- ).toBe(true);
731
-
732
- // Pushed to in-memory messages
733
- expect(messages).toHaveLength(1);
734
-
735
- // Broadcasts: delta, complete(aux), list invalidation + sync tag
736
- const deltaMsg = broadcastCalls.find(
737
- (c) => c.type === "assistant_text_delta",
738
- );
739
- expect(deltaMsg).toBeDefined();
740
- expect(deltaMsg!.text).toBe("Here is your artifact!");
741
- expect(deltaMsg!.conversationId).toBe("conv-inject-1");
742
-
743
- const completeMsg = broadcastCalls.find(
744
- (c) => c.type === "message_complete",
745
- );
746
- expect(completeMsg).toBeDefined();
747
- expect(completeMsg!.source).toBe("aux");
748
- expect(completeMsg!.messageId).toBeDefined();
749
-
750
- const listMsg = broadcastCalls.find(
751
- (c) => c.type === "conversation_list_invalidated",
752
- );
753
- expect(listMsg).toBeDefined();
754
- expect(listMsg!.reason).toBe("reordered");
755
-
756
- const syncMsg = broadcastCalls.find((c) => c.type === "sync_changed");
757
- expect(syncMsg).toEqual({
758
- type: "sync_changed",
759
- tags: [
760
- SYNC_TAGS.conversationsList,
761
- conversationMessagesSyncTag("conv-inject-1"),
762
- ],
763
- });
764
- });
765
-
766
- test("processing → idle: waits for processing to become false before persisting", async () => {
767
- const messages: unknown[] = [];
768
- let processingFlag = true;
769
- const conv: MockConversation = {
770
- isProcessing: () => processingFlag,
771
- messages,
772
- getMessages: () => messages,
773
- };
774
- mockConversations.set("conv-inject-2", conv);
775
-
776
- // Simulate processing becoming idle after a short delay
777
- setTimeout(() => {
778
- processingFlag = false;
779
- }, 100);
780
-
781
- await injectAuxAssistantMessage({
782
- conversationId: "conv-inject-2",
783
- text: "Deferred message",
784
- broadcastMessage: mockBroadcast,
785
- });
786
-
787
- // Should have waited and then injected
788
- expect(addMessageCalls).toHaveLength(1);
789
- expect(messages).toHaveLength(1);
790
- expect(broadcastCalls.some((c) => c.type === "assistant_text_delta")).toBe(
791
- true,
792
- );
793
- });
794
-
795
- test("processing → timeout: injects anyway with warning after poll timeout", async () => {
796
- const messages: unknown[] = [];
797
- // Conversation stays processing permanently — never becomes idle
798
- const conv: MockConversation = {
799
- isProcessing: () => true,
800
- messages,
801
- getMessages: () => messages,
802
- };
803
- mockConversations.set("conv-inject-3", conv);
804
-
805
- // Mock Date.now() to simulate time past the 60s timeout.
806
- // First call sets `start`, second call must exceed IDLE_TIMEOUT_MS (60_000).
807
- const realDateNow = Date.now;
808
- let dateNowCallCount = 0;
809
- const baseTime = 1_000_000;
810
- Date.now = () => {
811
- dateNowCallCount++;
812
- // First call: start = baseTime
813
- // Second call onward: past the timeout
814
- if (dateNowCallCount <= 1) return baseTime;
815
- return baseTime + 60_001;
816
- };
817
-
818
- try {
819
- await injectAuxAssistantMessage({
820
- conversationId: "conv-inject-3",
821
- text: "Timeout message",
822
- broadcastMessage: mockBroadcast,
823
- });
824
- } finally {
825
- Date.now = realDateNow;
826
- }
827
-
828
- // Message was still persisted despite timeout
829
- expect(addMessageCalls).toHaveLength(1);
830
- expect(addMessageCalls[0].conversationId).toBe("conv-inject-3");
831
-
832
- // Warning log was emitted about the timeout
833
- expect(logWarnCalls.length).toBeGreaterThanOrEqual(1);
834
- const warnMsg = logWarnCalls.find((c) =>
835
- c.args.some(
836
- (arg) => typeof arg === "string" && arg.includes("Timed out"),
837
- ),
838
- );
839
- expect(warnMsg).toBeDefined();
840
-
841
- // Since conversation is still processing, no delta/complete broadcasts
842
- expect(
843
- broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
844
- ).toHaveLength(0);
845
- expect(
846
- broadcastCalls.filter((c) => c.type === "message_complete"),
847
- ).toHaveLength(0);
848
-
849
- // But list invalidation + sync tag ARE sent regardless of processing state.
850
- expect(
851
- broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
852
- ).toHaveLength(1);
853
- expect(broadcastCalls.filter((c) => c.type === "sync_changed")).toEqual([
854
- {
855
- type: "sync_changed",
856
- tags: [
857
- SYNC_TAGS.conversationsList,
858
- conversationMessagesSyncTag("conv-inject-3"),
859
- ],
860
- },
861
- ]);
862
- });
863
-
864
- test("inactive/unloaded conversation: persists + list sync only", async () => {
865
- // No conversation in the store
866
- await injectAuxAssistantMessage({
867
- conversationId: "conv-inject-4",
868
- text: "Offline message",
869
- broadcastMessage: mockBroadcast,
870
- });
871
-
872
- // Message persisted
873
- expect(addMessageCalls).toHaveLength(1);
874
- expect(addMessageCalls[0].conversationId).toBe("conv-inject-4");
875
-
876
- // No delta or complete (conversation not loaded)
877
- expect(
878
- broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
879
- ).toHaveLength(0);
880
- expect(
881
- broadcastCalls.filter((c) => c.type === "message_complete"),
882
- ).toHaveLength(0);
883
-
884
- // But list invalidation + sync tag ARE sent
885
- expect(
886
- broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
887
- ).toHaveLength(1);
888
- expect(broadcastCalls.filter((c) => c.type === "sync_changed")).toEqual([
889
- {
890
- type: "sync_changed",
891
- tags: [
892
- SYNC_TAGS.conversationsList,
893
- conversationMessagesSyncTag("conv-inject-4"),
894
- ],
895
- },
896
- ]);
897
- });
898
- });
899
-
900
- describe("message-copy", () => {
901
- test("buildMessageCopyPrompt includes all parameters", () => {
902
- const prompt = buildMessageCopyPrompt({
903
- artifactType: "app",
904
- artifactTitle: "Budget Tracker",
905
- artifactId: "app-123",
906
- transcript: "[User]: I need a budget tool",
907
- });
908
-
909
- expect(prompt).toContain("app");
910
- expect(prompt).toContain("Budget Tracker");
911
- expect(prompt).toContain("app-123");
912
- expect(prompt).toContain("I need a budget tool");
913
- expect(prompt).toContain("Library");
914
- expect(prompt).not.toContain("Assets pill");
915
- expect(prompt).toContain("MESSAGE:");
916
- });
917
-
918
- test("parseMessageCopy extracts MESSAGE value", () => {
919
- expect(parseMessageCopy("MESSAGE: Hello there!")).toBe("Hello there!");
920
- expect(
921
- parseMessageCopy("MESSAGE: I built something special for you."),
922
- ).toBe("I built something special for you.");
923
- });
924
-
925
- test("parseMessageCopy returns null for missing MESSAGE", () => {
926
- expect(parseMessageCopy("Some random text")).toBeNull();
927
- expect(parseMessageCopy("")).toBeNull();
928
- });
929
-
930
- test("parseMessageCopy returns null for empty MESSAGE", () => {
931
- expect(parseMessageCopy("MESSAGE: ")).toBeNull();
932
- });
933
-
934
- test("ensureMessageMentionsLibraryLocation appends missing location", () => {
935
- const message = ensureMessageMentionsLibraryLocation(
936
- "I built a budget tracker for your rent and groceries.",
937
- "app",
938
- );
939
- expect(message).toContain("Library");
940
- expect(message).not.toContain("Assets");
941
- });
942
-
943
- test("ensureMessageMentionsLibraryLocation normalizes terminal punctuation once", () => {
944
- const message = ensureMessageMentionsLibraryLocation(
945
- "I built a budget tracker for you!",
946
- "app",
947
- );
948
- expect(message).toBe(
949
- "I built a budget tracker for you. You can find the app in Library.",
950
- );
951
- });
952
-
953
- test("ensureMessageMentionsLibraryLocation replaces artifact panel wording", () => {
954
- const message = ensureMessageMentionsLibraryLocation(
955
- "You'll find it in the artifact panel.",
956
- "document",
957
- );
958
- expect(message).toContain("Library");
959
- expect(message).not.toContain("Assets");
960
- expect(message).not.toContain("artifact panel");
961
- });
962
- });