@vellumai/assistant 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -17,6 +17,8 @@
17
17
 
18
18
  import { beforeEach, describe, expect, mock, test } from "bun:test";
19
19
 
20
+ import type { DiskPressureStatus } from "../../daemon/disk-pressure-guard.js";
21
+
20
22
  // Stub the DB-backed override-profile read so unit tests don't need a
21
23
  // real SQLite database. The wake helper calls this on every invocation
22
24
  // to honor the conversation's pinned inference profile.
@@ -24,6 +26,48 @@ mock.module("../../memory/conversation-crud.js", () => ({
24
26
  getConversationOverrideProfile: () => undefined,
25
27
  }));
26
28
 
29
+ mock.module("../../config/loader.js", () => ({
30
+ getConfig: () => ({ llm: {} }),
31
+ loadConfig: () => ({ llm: {} }),
32
+ loadRawConfig: () => ({}),
33
+ saveRawConfig: () => {},
34
+ getConfigReadOnly: () => ({ llm: {} }),
35
+ applyNestedDefaults: (config: unknown) => config,
36
+ deepMergeOverwrite: (base: unknown) => base,
37
+ mergeDefaultWorkspaceConfig: () => {},
38
+ getNestedValue: () => undefined,
39
+ setNestedValue: () => {},
40
+ API_KEY_PROVIDERS: [],
41
+ _appendQuarantineBulletin: () => {},
42
+ invalidateConfigCache: () => {},
43
+ }));
44
+
45
+ mock.module("../../config/llm-context-resolution.js", () => ({
46
+ resolveEffectiveContextWindow: () => ({
47
+ maxInputTokens: 200_000,
48
+ }),
49
+ }));
50
+
51
+ let mockDiskPressureStatus: DiskPressureStatus = {
52
+ enabled: false,
53
+ state: "disabled",
54
+ locked: false,
55
+ acknowledged: false,
56
+ overrideActive: false,
57
+ effectivelyLocked: false,
58
+ lockId: null,
59
+ usagePercent: null,
60
+ thresholdPercent: 95,
61
+ path: null,
62
+ lastCheckedAt: null,
63
+ blockedCapabilities: [],
64
+ error: null,
65
+ };
66
+
67
+ mock.module("../../daemon/disk-pressure-guard.js", () => ({
68
+ getDiskPressureStatus: () => mockDiskPressureStatus,
69
+ }));
70
+
27
71
  import type { AgentEvent } from "../../agent/loop.js";
28
72
  import type { Message } from "../../providers/types.js";
29
73
  import {
@@ -37,7 +81,11 @@ import {
37
81
  interface MockTarget extends WakeTarget {
38
82
  emittedEvents: AgentEvent[];
39
83
  pushedMessages: Message[];
40
- runCalls: Array<{ input: Message[]; requestId?: string }>;
84
+ runCalls: Array<{
85
+ input: Message[];
86
+ requestId?: string;
87
+ turnContext?: unknown;
88
+ }>;
41
89
  processingToggles: boolean[];
42
90
  /** Tail messages handed to `persistTailMessage`, in call order. */
43
91
  persistedTailCalls: Message[];
@@ -70,7 +118,11 @@ function makeTarget(options: {
70
118
  }): MockTarget {
71
119
  const emittedEvents: AgentEvent[] = [];
72
120
  const pushedMessages: Message[] = [];
73
- const runCalls: Array<{ input: Message[]; requestId?: string }> = [];
121
+ const runCalls: Array<{
122
+ input: Message[];
123
+ requestId?: string;
124
+ turnContext?: unknown;
125
+ }> = [];
74
126
  const processingToggles: boolean[] = [];
75
127
  const persistedTailCalls: Message[] = [];
76
128
  const callSequence: string[] = [];
@@ -97,8 +149,11 @@ function makeTarget(options: {
97
149
  onEvent: (event: AgentEvent) => void | Promise<void>,
98
150
  _signal?: AbortSignal,
99
151
  requestId?: string,
152
+ _onCheckpoint?: unknown,
153
+ _callSite?: unknown,
154
+ turnContext?: unknown,
100
155
  ) => {
101
- runCalls.push({ input: [...input], requestId });
156
+ runCalls.push({ input: [...input], requestId, turnContext });
102
157
  // Emit any scripted events the test wanted us to produce.
103
158
  for (const ev of options.scriptedEvents ?? []) {
104
159
  await onEvent(ev);
@@ -165,11 +220,163 @@ function makeTarget(options: {
165
220
 
166
221
  beforeEach(() => {
167
222
  __resetWakeChainForTests();
223
+ mockDiskPressureStatus = {
224
+ enabled: false,
225
+ state: "disabled",
226
+ locked: false,
227
+ acknowledged: false,
228
+ overrideActive: false,
229
+ effectivelyLocked: false,
230
+ lockId: null,
231
+ usagePercent: null,
232
+ thresholdPercent: 95,
233
+ path: null,
234
+ lastCheckedAt: null,
235
+ blockedCapabilities: [],
236
+ error: null,
237
+ };
168
238
  });
169
239
 
170
240
  // ── Tests ────────────────────────────────────────────────────────────
171
241
 
172
242
  describe("wakeAgentForOpportunity", () => {
243
+ test("disabled disk pressure flag allows background wakes to pass through", async () => {
244
+ const target = makeTarget({
245
+ scriptedAssistant: null,
246
+ });
247
+
248
+ const result = await wakeAgentForOpportunity(
249
+ {
250
+ conversationId: target.conversationId,
251
+ hint: "background completion",
252
+ source: "background-tool",
253
+ },
254
+ { resolveTarget: async () => target },
255
+ );
256
+
257
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
258
+ expect(target.runCalls).toHaveLength(1);
259
+ });
260
+
261
+ test("blocks background wakes during disk pressure before marking processing", async () => {
262
+ mockDiskPressureStatus = {
263
+ enabled: true,
264
+ state: "critical",
265
+ locked: true,
266
+ acknowledged: true,
267
+ overrideActive: false,
268
+ effectivelyLocked: true,
269
+ lockId: "disk-pressure-test",
270
+ usagePercent: 98,
271
+ thresholdPercent: 95,
272
+ path: "/",
273
+ lastCheckedAt: "2026-05-05T00:00:00.000Z",
274
+ blockedCapabilities: ["agent-turns", "background-work", "remote-ingress"],
275
+ error: null,
276
+ };
277
+ const target = makeTarget({
278
+ isProcessing: true,
279
+ scriptedAssistant: {
280
+ role: "assistant",
281
+ content: [{ type: "text", text: "should not run" }],
282
+ },
283
+ });
284
+
285
+ const result = await wakeAgentForOpportunity(
286
+ {
287
+ conversationId: target.conversationId,
288
+ hint: "background shell completed",
289
+ source: "background-tool",
290
+ trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
291
+ },
292
+ { resolveTarget: async () => target },
293
+ );
294
+
295
+ expect(result).toEqual({
296
+ invoked: false,
297
+ producedToolCalls: false,
298
+ reason: "disk_pressure",
299
+ });
300
+ expect(target.runCalls).toHaveLength(0);
301
+ expect(target.processingToggles).toEqual([]);
302
+ expect(target.drainQueueCalls).toBe(0);
303
+ expect(target.isProcessing()).toBe(true);
304
+ });
305
+
306
+ test("blocks trusted-contact direct wakes during disk pressure", async () => {
307
+ mockDiskPressureStatus = {
308
+ enabled: true,
309
+ state: "critical",
310
+ locked: true,
311
+ acknowledged: true,
312
+ overrideActive: false,
313
+ effectivelyLocked: true,
314
+ lockId: "disk-pressure-test",
315
+ usagePercent: 98,
316
+ thresholdPercent: 95,
317
+ path: "/",
318
+ lastCheckedAt: "2026-05-05T00:00:00.000Z",
319
+ blockedCapabilities: ["agent-turns", "background-work", "remote-ingress"],
320
+ error: null,
321
+ };
322
+ const target = makeTarget({ scriptedAssistant: null });
323
+
324
+ const result = await wakeAgentForOpportunity(
325
+ {
326
+ conversationId: target.conversationId,
327
+ hint: "notify the guardian",
328
+ source: "notification",
329
+ trustContext: {
330
+ sourceChannel: "slack",
331
+ trustClass: "trusted_contact",
332
+ },
333
+ },
334
+ { resolveTarget: async () => target },
335
+ );
336
+
337
+ expect(result.reason).toBe("disk_pressure");
338
+ expect(target.runCalls).toHaveLength(0);
339
+ });
340
+
341
+ test("threads cleanup-mode injection context for explicit local-owner wakes", async () => {
342
+ mockDiskPressureStatus = {
343
+ enabled: true,
344
+ state: "critical",
345
+ locked: true,
346
+ acknowledged: true,
347
+ overrideActive: false,
348
+ effectivelyLocked: true,
349
+ lockId: "disk-pressure-test",
350
+ usagePercent: 98,
351
+ thresholdPercent: 95,
352
+ path: "/",
353
+ lastCheckedAt: "2026-05-05T00:00:00.000Z",
354
+ blockedCapabilities: ["agent-turns", "background-work", "remote-ingress"],
355
+ error: null,
356
+ };
357
+ const target = makeTarget({ scriptedAssistant: null });
358
+
359
+ const result = await wakeAgentForOpportunity(
360
+ {
361
+ conversationId: target.conversationId,
362
+ hint: "clean storage",
363
+ source: "local-cleanup",
364
+ sourceChannel: "vellum",
365
+ sourceInterface: "macos",
366
+ },
367
+ { resolveTarget: async () => target },
368
+ );
369
+
370
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
371
+ expect(target.runCalls).toHaveLength(1);
372
+ expect(target.runCalls[0]!.turnContext).toMatchObject({
373
+ conversationId: target.conversationId,
374
+ injectionInputs: {
375
+ diskPressureContext: { cleanupModeActive: true },
376
+ },
377
+ });
378
+ });
379
+
173
380
  test("silent no-op when agent produces no tool calls and no text", async () => {
174
381
  const target = makeTarget({
175
382
  baseline: [
@@ -982,4 +1189,250 @@ describe("wakeAgentForOpportunity", () => {
982
1189
  expect(target.processingDuringDrain).toEqual([false]);
983
1190
  },
984
1191
  );
1192
+
1193
+ test(
1194
+ "checkpoint fires mid-run: events stream live and tail is persisted " +
1195
+ "incrementally so a long-running wake is observable",
1196
+ async () => {
1197
+ // Locks in the streaming-during-run fix. A long-running wake (e.g.
1198
+ // memory consolidation, often 5-30 minutes and 30+ turns) must
1199
+ // emit events and persist tail messages as each turn finalizes —
1200
+ // otherwise opening the conversation mid-flight returns 0 messages
1201
+ // from fetchHistory and the client renders the empty welcome
1202
+ // state instead of the in-progress turns.
1203
+ const turn1Assistant: Message = {
1204
+ role: "assistant",
1205
+ content: [
1206
+ { type: "tool_use", id: "tu-1", name: "file_write", input: {} },
1207
+ ],
1208
+ };
1209
+ const turn1ToolResult: Message = {
1210
+ role: "user",
1211
+ content: [{ type: "tool_result", tool_use_id: "tu-1", content: "ok" }],
1212
+ };
1213
+ const turn2Assistant: Message = {
1214
+ role: "assistant",
1215
+ content: [
1216
+ { type: "tool_use", id: "tu-2", name: "remember", input: {} },
1217
+ ],
1218
+ };
1219
+ const turn2ToolResult: Message = {
1220
+ role: "user",
1221
+ content: [{ type: "tool_result", tool_use_id: "tu-2", content: "ok" }],
1222
+ };
1223
+ const finalAssistant: Message = {
1224
+ role: "assistant",
1225
+ content: [{ type: "text", text: "All done." }],
1226
+ };
1227
+
1228
+ const emittedEvents: AgentEvent[] = [];
1229
+ const pushedMessages: Message[] = [];
1230
+ const persistedTailCalls: Message[] = [];
1231
+ // Snapshot of how many tail messages had been persisted at each
1232
+ // point a streaming event reached the target. This is the actual
1233
+ // observability invariant: when a turn-2 streaming event arrives,
1234
+ // turn-1's messages must already be persisted so a fetchHistory
1235
+ // call from a client opening the conversation mid-stream returns
1236
+ // turn-1's content.
1237
+ const persistedAtEachEmit: number[] = [];
1238
+ const baseline: Message[] = [
1239
+ { role: "user", content: [{ type: "text", text: "hi" }] },
1240
+ ];
1241
+ const history: Message[] = [...baseline];
1242
+ let processing = false;
1243
+
1244
+ const target: WakeTarget = {
1245
+ conversationId: "conv-stream",
1246
+ agentLoop: {
1247
+ run: async (_input, onEvent, _signal, _requestId, onCheckpoint) => {
1248
+ // Preamble + assistant hint + postamble (mirrors what the
1249
+ // wake injects). The agent-wake helper expects these three
1250
+ // hint messages in the input it hands to run().
1251
+ const runHistory: Message[] = [..._input];
1252
+
1253
+ // Turn 1: stream a text_delta + message_complete, then
1254
+ // fire the checkpoint after the tool_result lands.
1255
+ await onEvent({ type: "text_delta", text: "Working" });
1256
+ runHistory.push(turn1Assistant);
1257
+ await onEvent({
1258
+ type: "message_complete",
1259
+ message: turn1Assistant,
1260
+ });
1261
+ runHistory.push(turn1ToolResult);
1262
+ const dec1 = await onCheckpoint!({
1263
+ turnIndex: 0,
1264
+ toolCount: 1,
1265
+ hasToolUse: true,
1266
+ history: runHistory,
1267
+ });
1268
+ expect(dec1).toBe("continue");
1269
+
1270
+ // Turn 2: another tool turn — must already see the live
1271
+ // streaming because mode flipped after turn 1.
1272
+ await onEvent({ type: "text_delta", text: "Still going" });
1273
+ runHistory.push(turn2Assistant);
1274
+ await onEvent({
1275
+ type: "message_complete",
1276
+ message: turn2Assistant,
1277
+ });
1278
+ runHistory.push(turn2ToolResult);
1279
+ const dec2 = await onCheckpoint!({
1280
+ turnIndex: 1,
1281
+ toolCount: 1,
1282
+ hasToolUse: true,
1283
+ history: runHistory,
1284
+ });
1285
+ expect(dec2).toBe("continue");
1286
+
1287
+ // Final assistant message with no tool calls — loop would
1288
+ // exit. onCheckpoint does NOT fire for the terminal turn,
1289
+ // so the post-run flushPendingTail must catch this one.
1290
+ await onEvent({ type: "text_delta", text: "All done." });
1291
+ runHistory.push(finalAssistant);
1292
+ await onEvent({
1293
+ type: "message_complete",
1294
+ message: finalAssistant,
1295
+ });
1296
+ return runHistory;
1297
+ },
1298
+ },
1299
+ getMessages: () => history,
1300
+ pushMessage: (msg) => {
1301
+ pushedMessages.push(msg);
1302
+ history.push(msg);
1303
+ },
1304
+ emitAgentEvent: (event) => {
1305
+ emittedEvents.push(event);
1306
+ persistedAtEachEmit.push(persistedTailCalls.length);
1307
+ },
1308
+ isProcessing: () => processing,
1309
+ markProcessing: (on) => {
1310
+ processing = on;
1311
+ },
1312
+ persistTailMessage: async (msg) => {
1313
+ persistedTailCalls.push(msg);
1314
+ },
1315
+ };
1316
+
1317
+ const result = await wakeAgentForOpportunity(
1318
+ {
1319
+ conversationId: "conv-stream",
1320
+ hint: "consolidate",
1321
+ source: "memory_v2_consolidation",
1322
+ },
1323
+ { resolveTarget: async () => target },
1324
+ );
1325
+
1326
+ expect(result).toEqual({ invoked: true, producedToolCalls: true });
1327
+
1328
+ // All 5 tail messages persisted in order. The first two via
1329
+ // turn-1 checkpoint, the next two via turn-2 checkpoint, and
1330
+ // `finalAssistant` via the post-run flush.
1331
+ expect(persistedTailCalls).toHaveLength(5);
1332
+ expect(persistedTailCalls[0]).toBe(turn1Assistant);
1333
+ expect(persistedTailCalls[1]).toBe(turn1ToolResult);
1334
+ expect(persistedTailCalls[2]).toBe(turn2Assistant);
1335
+ expect(persistedTailCalls[3]).toBe(turn2ToolResult);
1336
+ expect(persistedTailCalls[4]).toBe(finalAssistant);
1337
+
1338
+ // Critical observability invariant: by the time turn-2's
1339
+ // streaming text_delta reached the client, turn-1's messages
1340
+ // were already persisted. A client opening the conversation at
1341
+ // that moment would fetchHistory and see turn-1, plus stream
1342
+ // turn-2 live — instead of seeing an empty welcome view.
1343
+ const turn2DeltaIdx = emittedEvents.findIndex(
1344
+ (e) => e.type === "text_delta" && e.text === "Still going",
1345
+ );
1346
+ expect(turn2DeltaIdx).toBeGreaterThan(-1);
1347
+ expect(persistedAtEachEmit[turn2DeltaIdx]).toBeGreaterThanOrEqual(2);
1348
+ },
1349
+ );
1350
+
1351
+ test(
1352
+ "checkpoint-driven wake injects ui_surface card into the first " +
1353
+ "assistant tail message",
1354
+ async () => {
1355
+ // The wake card ("Conversation Woke") is the visual entry point —
1356
+ // it must land in the first assistant message regardless of
1357
+ // whether the wake produced output via checkpoints or only via
1358
+ // post-run (tool-free) detection. This test covers the
1359
+ // checkpoint path; the existing post-run path is covered by the
1360
+ // tool_use tests above.
1361
+ const firstAssistant: Message = {
1362
+ role: "assistant",
1363
+ content: [
1364
+ { type: "tool_use", id: "tu-1", name: "some_tool", input: {} },
1365
+ ],
1366
+ };
1367
+ const toolResult: Message = {
1368
+ role: "user",
1369
+ content: [{ type: "tool_result", tool_use_id: "tu-1", content: "ok" }],
1370
+ };
1371
+
1372
+ const persistedTailCalls: Message[] = [];
1373
+ const baseline: Message[] = [
1374
+ { role: "user", content: [{ type: "text", text: "hi" }] },
1375
+ ];
1376
+ const history: Message[] = [...baseline];
1377
+ let processing = false;
1378
+ const wakeProducedOutputCalls: string[] = [];
1379
+
1380
+ const target: WakeTarget = {
1381
+ conversationId: "conv-card",
1382
+ agentLoop: {
1383
+ run: async (_input, _onEvent, _signal, _requestId, onCheckpoint) => {
1384
+ const runHistory: Message[] = [..._input];
1385
+ runHistory.push(firstAssistant);
1386
+ runHistory.push(toolResult);
1387
+ await onCheckpoint!({
1388
+ turnIndex: 0,
1389
+ toolCount: 1,
1390
+ hasToolUse: true,
1391
+ history: runHistory,
1392
+ });
1393
+ return runHistory;
1394
+ },
1395
+ },
1396
+ getMessages: () => history,
1397
+ pushMessage: (msg) => {
1398
+ history.push(msg);
1399
+ },
1400
+ emitAgentEvent: () => {},
1401
+ isProcessing: () => processing,
1402
+ markProcessing: (on) => {
1403
+ processing = on;
1404
+ },
1405
+ persistTailMessage: async (msg) => {
1406
+ persistedTailCalls.push(msg);
1407
+ },
1408
+ onWakeProducedOutput: (_source, _hint, surfaceId) => {
1409
+ wakeProducedOutputCalls.push(surfaceId);
1410
+ },
1411
+ };
1412
+
1413
+ await wakeAgentForOpportunity(
1414
+ {
1415
+ conversationId: "conv-card",
1416
+ hint: "do the thing",
1417
+ source: "memory_v2_consolidation",
1418
+ },
1419
+ { resolveTarget: async () => target },
1420
+ );
1421
+
1422
+ // ui_surface fired exactly once (idempotent goLive), and the
1423
+ // surfaceId matches the block prepended into the first
1424
+ // assistant message.
1425
+ expect(wakeProducedOutputCalls).toHaveLength(1);
1426
+ const persistedFirst = persistedTailCalls[0];
1427
+ expect(persistedFirst).toBeDefined();
1428
+ const blocks = Array.isArray(persistedFirst!.content)
1429
+ ? persistedFirst!.content
1430
+ : [];
1431
+ const uiBlock = blocks.find(
1432
+ (b: { type?: string }) => b.type === "ui_surface",
1433
+ ) as { surfaceId?: string } | undefined;
1434
+ expect(uiBlock).toBeDefined();
1435
+ expect(uiBlock!.surfaceId).toBe(wakeProducedOutputCalls[0]);
1436
+ },
1437
+ );
985
1438
  });