@vellumai/assistant 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -11,31 +11,37 @@ const PROVIDER_DEFAULT_MODELS: Record<string, string> = Object.fromEntries(
11
11
 
12
12
  const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
13
13
  anthropic: {
14
+ balanced: "claude-sonnet-4-6",
14
15
  "latency-optimized": "claude-haiku-4-5-20251001",
15
16
  "quality-optimized": "claude-opus-4-7",
16
17
  "vision-optimized": "claude-opus-4-6",
17
18
  },
18
19
  openai: {
20
+ balanced: "gpt-5.4-mini",
19
21
  "latency-optimized": "gpt-5.4-nano",
20
22
  "quality-optimized": "gpt-5.4",
21
23
  "vision-optimized": "gpt-5.4",
22
24
  },
23
25
  gemini: {
26
+ balanced: "gemini-3-flash-preview",
24
27
  "latency-optimized": "gemini-3.1-flash-lite-preview",
25
28
  "quality-optimized": "gemini-3.1-pro-preview",
26
29
  "vision-optimized": "gemini-3-flash-preview",
27
30
  },
28
31
  ollama: {
32
+ balanced: "llama3.2",
29
33
  "latency-optimized": "llama3.2",
30
34
  "quality-optimized": "llama3.2",
31
35
  "vision-optimized": "llama3.2",
32
36
  },
33
37
  fireworks: {
38
+ balanced: "accounts/fireworks/models/kimi-k2p5",
34
39
  "latency-optimized": "accounts/fireworks/models/kimi-k2p5",
35
40
  "quality-optimized": "accounts/fireworks/models/kimi-k2p5",
36
41
  "vision-optimized": "accounts/fireworks/models/kimi-k2p5",
37
42
  },
38
43
  openrouter: {
44
+ balanced: "anthropic/claude-sonnet-4.6",
39
45
  "latency-optimized": "anthropic/claude-haiku-4.5",
40
46
  "quality-optimized": "anthropic/claude-opus-4.7",
41
47
  "vision-optimized": "anthropic/claude-opus-4.6",
@@ -45,6 +51,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
45
51
  const FALLBACK_DEFAULT_MODEL = "claude-opus-4-7";
46
52
 
47
53
  const MODEL_INTENTS = new Set<ModelIntent>([
54
+ "balanced",
48
55
  "latency-optimized",
49
56
  "quality-optimized",
50
57
  "vision-optimized",
@@ -1,3 +1,4 @@
1
+ import { ProviderError } from "../../util/errors.js";
1
2
  import { AnthropicProvider } from "../anthropic/client.js";
2
3
  import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
3
4
  import { isThinkingConfigEnabled } from "../thinking-config.js";
@@ -139,6 +140,13 @@ export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
139
140
  cause: error,
140
141
  });
141
142
  }
143
+ if (error instanceof ProviderError && error.provider !== this.name) {
144
+ throw new ProviderError(error.message, this.name, error.statusCode, {
145
+ cause: error.cause ?? error,
146
+ retryAfterMs: error.retryAfterMs,
147
+ abortReason: error.abortReason,
148
+ });
149
+ }
142
150
  throw error;
143
151
  }
144
152
  }
@@ -273,6 +273,56 @@ function normalizeSendMessageOptions(
273
273
  delete nextConfig.thinking;
274
274
  }
275
275
 
276
+ // Anthropic (and OpenRouter fronting Anthropic) rejects requests that
277
+ // combine extended thinking with `temperature` ≠ 1. From the API:
278
+ // "`temperature` may only be set to 1 when thinking is enabled or in
279
+ // adaptive mode."
280
+ //
281
+ // Defense-in-depth: callers that hardcode a non-default temperature in
282
+ // their per-call config are easy to miss when reviewing — we already had
283
+ // this bug ship in three places (reply suggestions, recall agent
284
+ // round, recall fallback finalize). Drop the offending temperature with
285
+ // a warn log so the request goes through with Anthropic's default
286
+ // (which is 1 in thinking mode anyway). We keep `thinking` rather than
287
+ // `temperature` because thinking is the more deliberate, profile-level
288
+ // choice — silently downgrading reasoning capacity for an unrelated
289
+ // per-call hint would be the worse failure mode.
290
+ //
291
+ // Scope:
292
+ // - Anthropic: always.
293
+ // - OpenRouter fronting `anthropic/*`: same wire constraint applies.
294
+ // - Other providers: not our problem here (e.g. OpenAI reasoning models
295
+ // strip `temperature` upstream; non-Anthropic OpenRouter reasoning
296
+ // models don't have this exact constraint).
297
+ const isThinkingTemperatureConflict = (() => {
298
+ if (nextConfig.thinking == null) return false;
299
+ if (isThinkingConfigDisabled(nextConfig.thinking)) return false;
300
+ const temp = nextConfig.temperature;
301
+ if (typeof temp !== "number") return false;
302
+ if (temp === 1) return false;
303
+ if (providerName === "anthropic") return true;
304
+ if (providerName === "openrouter") {
305
+ const model =
306
+ typeof nextConfig.model === "string" ? nextConfig.model : "";
307
+ return model.startsWith("anthropic/");
308
+ }
309
+ return false;
310
+ })();
311
+ if (isThinkingTemperatureConflict) {
312
+ log.warn(
313
+ {
314
+ providerName,
315
+ callSite: config.callSite,
316
+ droppedTemperature: nextConfig.temperature,
317
+ },
318
+ "Dropping `temperature` because thinking is enabled — Anthropic only " +
319
+ "accepts `temperature: 1` (or unset) when thinking/adaptive mode is " +
320
+ "on. Set `thinking: { type: 'disabled' }` on the call site if you " +
321
+ "need a specific temperature.",
322
+ );
323
+ delete nextConfig.temperature;
324
+ }
325
+
276
326
  // effort is supported by Anthropic, OpenAI, and OpenAI-compatible providers; strip for others
277
327
  if (
278
328
  !EFFORT_SUPPORTED_PROVIDERS.has(providerName) &&
@@ -91,6 +91,7 @@ export interface Message {
91
91
  }
92
92
 
93
93
  export type ModelIntent =
94
+ | "balanced"
94
95
  | "latency-optimized"
95
96
  | "quality-optimized"
96
97
  | "vision-optimized";
@@ -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
  });