@vellumai/assistant 0.12.3-staging.1 → 0.12.3-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 (560) hide show
  1. package/ARCHITECTURE.md +43 -29
  2. package/Dockerfile +63 -0
  3. package/docs/architecture/security.md +10 -5
  4. package/docs/architecture/turn-actor.md +2 -0
  5. package/docs/desktop-browser-cli.md +45 -4
  6. package/docs/live-voice-task-outcomes.md +100 -0
  7. package/docs/mode-sessions.md +183 -0
  8. package/docs/voice-input-diagnostics.md +58 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
  17. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
  18. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
  19. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  20. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  22. package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
  23. package/openapi.yaml +665 -14
  24. package/package.json +1 -1
  25. package/scripts/smoke-desktop-browser-cli.ts +10 -26
  26. package/scripts/smoke-desktop-window-theme.ts +2 -3
  27. package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
  28. package/src/__tests__/agent-loop-provider-error-recording.test.ts +87 -19
  29. package/src/__tests__/annotate-activity-metadata.test.ts +3 -1
  30. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  31. package/src/__tests__/answered-question-persistence.test.ts +2 -0
  32. package/src/__tests__/anthropic-gateway-shared.test.ts +22 -0
  33. package/src/__tests__/approval-message-composer.test.ts +1 -118
  34. package/src/__tests__/approval-routes-http.test.ts +3 -0
  35. package/src/__tests__/assistant-event-hub-connection-history.test.ts +111 -0
  36. package/src/__tests__/assistant-event-hub-targeted.test.ts +50 -0
  37. package/src/__tests__/assistant-event-targeting.test.ts +186 -0
  38. package/src/__tests__/assistant-stream-state.test.ts +29 -9
  39. package/src/__tests__/browser-fill-credential.test.ts +11 -22
  40. package/src/__tests__/btw-routes.test.ts +4 -1
  41. package/src/__tests__/build-persisted-content.test.ts +1 -1
  42. package/src/__tests__/call-conversation-messages.test.ts +5 -0
  43. package/src/__tests__/call-recovery.test.ts +7 -2
  44. package/src/__tests__/call-routes-http.test.ts +12 -0
  45. package/src/__tests__/call-store.test.ts +7 -2
  46. package/src/__tests__/channel-retry-sweep.test.ts +9 -0
  47. package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
  48. package/src/__tests__/checker.test.ts +11 -3
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
  50. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +0 -25
  51. package/src/__tests__/computer-use-tools.test.ts +1 -233
  52. package/src/__tests__/config-model-image-provider.test.ts +39 -0
  53. package/src/__tests__/config-schema.test.ts +16 -0
  54. package/src/__tests__/config-watcher.test.ts +24 -0
  55. package/src/__tests__/context-search-agent-runner.test.ts +260 -1
  56. package/src/__tests__/context-search-fanout.test.ts +1 -1
  57. package/src/__tests__/context-search-types.test.ts +7 -7
  58. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
  59. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
  60. package/src/__tests__/conversation-agent-loop.test.ts +546 -16
  61. package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
  62. package/src/__tests__/conversation-evictor.test.ts +62 -37
  63. package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
  64. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
  65. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  66. package/src/__tests__/conversation-retry-route.test.ts +22 -0
  67. package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
  68. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
  69. package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
  70. package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +29 -0
  73. package/src/__tests__/conversation-surface-snapshots.test.ts +543 -0
  74. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
  75. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
  76. package/src/__tests__/conversation-tool-setup-attribution.test.ts +23 -0
  77. package/src/__tests__/conversation-undo.test.ts +61 -0
  78. package/src/__tests__/credential-execution-client.test.ts +6 -6
  79. package/src/__tests__/credential-prompt-route.test.ts +35 -3
  80. package/src/__tests__/cu-unified-flow.test.ts +82 -1
  81. package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
  82. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
  83. package/src/__tests__/dm-backfill.test.ts +91 -1
  84. package/src/__tests__/document-create-route.test.ts +88 -0
  85. package/src/__tests__/document-sync-tags.test.ts +27 -0
  86. package/src/__tests__/emit-signal-routing-intent.test.ts +142 -2
  87. package/src/__tests__/events-tail-route.test.ts +25 -0
  88. package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
  89. package/src/__tests__/guardian-action-sweep.test.ts +16 -0
  90. package/src/__tests__/guardian-dispatch.test.ts +13 -0
  91. package/src/__tests__/headless-browser-interactions.test.ts +64 -20
  92. package/src/__tests__/heartbeat-service.test.ts +2 -59
  93. package/src/__tests__/helpers/mock-conversation.ts +21 -0
  94. package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
  95. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +5 -0
  96. package/src/__tests__/host-cu-proxy.test.ts +46 -2
  97. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  98. package/src/__tests__/image-credentials.test.ts +49 -0
  99. package/src/__tests__/image-service-dispatcher.test.ts +45 -0
  100. package/src/__tests__/inference-profile-session-handler.test.ts +1 -1
  101. package/src/__tests__/interrupt-on-send.test.ts +57 -43
  102. package/src/__tests__/interrupt-turn-note.test.ts +5 -6
  103. package/src/__tests__/llm-catalog-parity.test.ts +2 -2
  104. package/src/__tests__/media-generate-image.test.ts +49 -1
  105. package/src/__tests__/message-provenance-contact-id.test.ts +69 -0
  106. package/src/__tests__/migration-export-to-gcs.test.ts +142 -0
  107. package/src/__tests__/mode-session-history.test.ts +245 -0
  108. package/src/__tests__/mode-session-recovery.test.ts +222 -0
  109. package/src/__tests__/mode-session-wire.test.ts +141 -0
  110. package/src/__tests__/notification-guardian-path.test.ts +7 -0
  111. package/src/__tests__/notification-telegram-adapter.test.ts +2 -2
  112. package/src/__tests__/notification-vellum-adapter.test.ts +34 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +12 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  115. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  116. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  117. package/src/__tests__/plugin-api-provider.test.ts +5 -0
  118. package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
  119. package/src/__tests__/plugin-import-boundary-guard.test.ts +9 -0
  120. package/src/__tests__/process-message-display-content.test.ts +56 -0
  121. package/src/__tests__/reaction-persistence.test.ts +5 -0
  122. package/src/__tests__/request-secret-standalone-channel-gate.test.ts +25 -9
  123. package/src/__tests__/runtime-events-sse-reconnect.test.ts +1 -1
  124. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +73 -0
  125. package/src/__tests__/secret-ingress-http.test.ts +2 -0
  126. package/src/__tests__/secure-keys.test.ts +43 -1
  127. package/src/__tests__/send-endpoint-busy.test.ts +163 -1
  128. package/src/__tests__/server-history-render.test.ts +91 -1
  129. package/src/__tests__/settings-routes.test.ts +50 -1
  130. package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -2
  132. package/src/__tests__/subagent-manager-notify.test.ts +1 -2
  133. package/src/__tests__/subagent-notify-parent.test.ts +146 -39
  134. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +2 -4
  135. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
  136. package/src/__tests__/thread-backfill.test.ts +53 -0
  137. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  138. package/src/__tests__/tool-result-metadata-plumbing.test.ts +3 -1
  139. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  140. package/src/__tests__/twilio-routes.test.ts +1 -0
  141. package/src/__tests__/web-search-backend-failure.test.ts +1 -1
  142. package/src/activation/progress-store.test.ts +12 -7
  143. package/src/agent/loop.ts +99 -41
  144. package/src/api/events/assistant-turn-start.ts +3 -0
  145. package/src/api/events/generation-handoff.ts +2 -0
  146. package/src/api/events/message-complete.ts +2 -0
  147. package/src/api/events/notification-conversation-created.ts +14 -16
  148. package/src/api/events/notification-intent.ts +4 -2
  149. package/src/api/events/tool-result.ts +85 -0
  150. package/src/api/events/tool-use-start.ts +3 -0
  151. package/src/api/events/user-message-echo.ts +3 -0
  152. package/src/api/index.ts +31 -6
  153. package/src/api/mode-session.test.ts +131 -0
  154. package/src/api/mode-session.ts +148 -0
  155. package/src/api/remember-tool.ts +15 -0
  156. package/src/api/responses/conversation-message.ts +6 -0
  157. package/src/approvals/AGENTS.md +4 -2
  158. package/src/browser/__tests__/operations.test.ts +29 -0
  159. package/src/browser/operation-meta.ts +2 -1
  160. package/src/browser/operations.ts +29 -0
  161. package/src/browser/virtual-desktop-target.ts +2 -8
  162. package/src/bundler/bundle-signer.ts +1 -3
  163. package/src/calls/__tests__/barge-in-guard.test.ts +3 -0
  164. package/src/calls/__tests__/call-controller.test.ts +94 -0
  165. package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
  166. package/src/calls/__tests__/voice-control-protocol.test.ts +15 -0
  167. package/src/calls/__tests__/voice-escalation-judge.test.ts +209 -0
  168. package/src/calls/__tests__/voice-leg-coordinator.test.ts +168 -0
  169. package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
  170. package/src/calls/__tests__/voice-session-bridge.test.ts +576 -122
  171. package/src/calls/barge-in-guard.ts +5 -0
  172. package/src/calls/call-controller.ts +196 -1
  173. package/src/calls/call-domain.ts +4 -0
  174. package/src/calls/call-store.ts +87 -2
  175. package/src/calls/guardian-action-sweep.ts +22 -8
  176. package/src/calls/media-stream-audio-transcode.ts +1 -3
  177. package/src/calls/typesafe-noul.ts +156 -0
  178. package/src/calls/voice-control-protocol.ts +6 -0
  179. package/src/calls/voice-escalation-judge.ts +145 -0
  180. package/src/calls/voice-escalation-target.ts +10 -0
  181. package/src/calls/voice-leg-coordinator.ts +121 -3
  182. package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
  183. package/src/calls/voice-session-bridge.ts +256 -112
  184. package/src/calls/voice-triage-escalate.ts +7 -0
  185. package/src/cli/__tests__/roadmap-cli.test.ts +4 -41
  186. package/src/cli/commands/__tests__/notifications.test.ts +35 -10
  187. package/src/cli/commands/browser.help.ts +4 -5
  188. package/src/cli/commands/browser.ts +0 -1
  189. package/src/cli/commands/clients.help.ts +46 -0
  190. package/src/cli/commands/clients.ts +116 -0
  191. package/src/cli/commands/credentials.help.ts +6 -1
  192. package/src/cli/commands/notifications.help.ts +16 -2
  193. package/src/cli/commands/notifications.ts +30 -8
  194. package/src/cli/commands/roadmap.help.ts +18 -21
  195. package/src/cli/commands/roadmap.ts +4 -21
  196. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +5 -2
  197. package/src/cli/lib/bundled-marketplace.json +84 -34
  198. package/src/cli/lib/plugin-marketplace.ts +8 -0
  199. package/src/cli/lib/publish-plugin.ts +1 -2
  200. package/src/config/__tests__/default-provider.test.ts +3 -1
  201. package/src/config/__tests__/memory-retrospective-schema.test.ts +14 -1
  202. package/src/config/bundled-skills/computer-use/SKILL.md +1 -1
  203. package/src/config/bundled-skills/computer-use/TOOLS.json +89 -1
  204. package/src/config/bundled-skills/image-studio/SKILL.md +17 -2
  205. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +7 -11
  206. package/src/config/bundled-skills/sequences/TOOLS.json +1 -5
  207. package/src/config/call-site-defaults.ts +9 -0
  208. package/src/config/feature-flag-registry.json +16 -0
  209. package/src/config/profile-text-generation.test.ts +2 -2
  210. package/src/config/schemas/__tests__/tools.test.ts +31 -0
  211. package/src/config/schemas/call-site-catalog.ts +14 -0
  212. package/src/config/schemas/channels.ts +0 -18
  213. package/src/config/schemas/llm.ts +3 -1
  214. package/src/config/schemas/mcp.ts +2 -1
  215. package/src/config/schemas/memory-retrospective.ts +11 -2
  216. package/src/config/schemas/services.ts +15 -6
  217. package/src/config/schemas/tools.ts +10 -0
  218. package/src/config/session-groups-gate.ts +21 -0
  219. package/src/config/skills.ts +2 -6
  220. package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
  221. package/src/conversations/message-consolidation.ts +102 -5
  222. package/src/credential-execution/ces-connect.test.ts +69 -0
  223. package/src/credential-execution/ces-connect.ts +115 -0
  224. package/src/credential-execution/ces-runtime.ts +55 -93
  225. package/src/credential-execution/executable-discovery.ts +1 -11
  226. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
  227. package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
  228. package/src/daemon/browser-mode-session.test.ts +402 -0
  229. package/src/daemon/browser-mode-session.ts +186 -0
  230. package/src/daemon/computer-use-mode-session.test.ts +210 -0
  231. package/src/daemon/computer-use-mode-session.ts +105 -0
  232. package/src/daemon/config-watcher.ts +10 -0
  233. package/src/daemon/conversation-agent-loop-handlers.ts +58 -10
  234. package/src/daemon/conversation-agent-loop.ts +219 -25
  235. package/src/daemon/conversation-evictor.ts +5 -24
  236. package/src/daemon/conversation-history.ts +10 -0
  237. package/src/daemon/conversation-interrupt-repair.ts +4 -43
  238. package/src/daemon/conversation-interrupt.ts +7 -11
  239. package/src/daemon/conversation-messaging.ts +58 -5
  240. package/src/daemon/conversation-mode-session.test.ts +1667 -0
  241. package/src/daemon/conversation-mode-session.ts +1008 -0
  242. package/src/daemon/conversation-process.ts +5 -0
  243. package/src/daemon/conversation-runtime-assembly.ts +10 -20
  244. package/src/daemon/conversation-skill-tools.ts +1 -0
  245. package/src/daemon/conversation-store.ts +4 -4
  246. package/src/daemon/conversation-surface-snapshots.ts +135 -0
  247. package/src/daemon/conversation-surfaces.ts +113 -5
  248. package/src/daemon/conversation-tool-setup.ts +8 -3
  249. package/src/daemon/conversation.ts +143 -41
  250. package/src/daemon/handlers/config-model.ts +7 -5
  251. package/src/daemon/handlers/shared.ts +55 -16
  252. package/src/daemon/host-cu-proxy.ts +51 -22
  253. package/src/daemon/lifecycle.ts +30 -13
  254. package/src/daemon/message-protocol.ts +0 -1
  255. package/src/daemon/message-provenance.ts +28 -4
  256. package/src/daemon/mode-session-startup-recovery.ts +23 -0
  257. package/src/daemon/mode-session-tracking.ts +54 -0
  258. package/src/daemon/process-message.ts +13 -4
  259. package/src/daemon/virtual-desktop-context.ts +21 -0
  260. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  261. package/src/desktop/desktop-accessibility-script.ts +88 -0
  262. package/src/desktop/desktop-accessibility.test.ts +160 -0
  263. package/src/desktop/desktop-accessibility.ts +169 -0
  264. package/src/desktop/desktop-automation-lease.test.ts +76 -68
  265. package/src/desktop/desktop-automation-lease.ts +41 -50
  266. package/src/desktop/desktop-browser-endpoint.ts +4 -0
  267. package/src/desktop/desktop-computer-use-routing.test.ts +330 -0
  268. package/src/desktop/desktop-computer-use.test.ts +449 -0
  269. package/src/desktop/desktop-computer-use.ts +502 -0
  270. package/src/desktop/desktop-dependencies.ts +47 -293
  271. package/src/desktop/desktop-display.ts +2 -2
  272. package/src/desktop/desktop-panel-config.test.ts +13 -3
  273. package/src/desktop/desktop-panel-config.ts +109 -15
  274. package/src/desktop/desktop-session-manager.test.ts +69 -21
  275. package/src/desktop/desktop-session-manager.ts +76 -22
  276. package/src/desktop/desktop-stream-bridge.test.ts +11 -122
  277. package/src/desktop/desktop-stream-bridge.ts +6 -8
  278. package/src/desktop/desktop-window-theme.ts +5 -25
  279. package/src/desktop/virtual-desktop-feature.ts +52 -2
  280. package/src/desktop/virtual-desktop-platform.test.ts +5 -9
  281. package/src/documents/document-store.ts +23 -0
  282. package/src/heartbeat/__tests__/heartbeat-service.test.ts +22 -26
  283. package/src/heartbeat/heartbeat-service.ts +5 -33
  284. package/src/ipc/__tests__/browser-ipc.test.ts +215 -6
  285. package/src/ipc/assistant-server.ts +2 -0
  286. package/src/ipc/routes/__tests__/channel-reply-ipc-routes.test.ts +111 -0
  287. package/src/ipc/routes/channel-reply-ipc-routes.ts +73 -0
  288. package/src/live-voice/__tests__/continuation-judge.test.ts +151 -0
  289. package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
  290. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +743 -31
  291. package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +191 -10
  292. package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
  293. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +5 -1
  294. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
  295. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
  296. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +224 -1
  297. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +101 -0
  298. package/src/live-voice/__tests__/live-voice-tts.test.ts +44 -0
  299. package/src/live-voice/__tests__/live-voice-vad.test.ts +939 -75
  300. package/src/live-voice/__tests__/microphone-idle-gate.test.ts +87 -0
  301. package/src/live-voice/__tests__/session-controls.test.ts +23 -0
  302. package/src/live-voice/__tests__/task-outcomes.test.ts +117 -0
  303. package/src/live-voice/__tests__/voice-input-diagnostics.test.ts +117 -0
  304. package/src/live-voice/camera-mode-session.test.ts +265 -0
  305. package/src/live-voice/camera-mode-session.ts +277 -0
  306. package/src/live-voice/continuation-judge.ts +123 -0
  307. package/src/live-voice/live-activity-reporter.ts +115 -6
  308. package/src/live-voice/live-voice-manager.ts +36 -1
  309. package/src/live-voice/live-voice-photo.ts +88 -7
  310. package/src/live-voice/live-voice-session-manager.ts +15 -0
  311. package/src/live-voice/live-voice-session.ts +1443 -747
  312. package/src/live-voice/live-voice-tts.ts +22 -4
  313. package/src/live-voice/microphone-idle-gate.ts +56 -0
  314. package/src/live-voice/protocol.ts +171 -4
  315. package/src/live-voice/session-controls.ts +11 -3
  316. package/src/live-voice/task-outcomes.ts +131 -0
  317. package/src/live-voice/voice-input-diagnostics.ts +117 -0
  318. package/src/mcp/__tests__/manager-tool-caps.test.ts +63 -18
  319. package/src/mcp/__tests__/tool-caps.test.ts +28 -1
  320. package/src/mcp/manager.ts +3 -0
  321. package/src/mcp/tool-caps.ts +12 -4
  322. package/src/media/__tests__/image-models.test.ts +58 -0
  323. package/src/media/image-credentials.ts +5 -0
  324. package/src/media/image-models.ts +52 -0
  325. package/src/media/image-service.ts +8 -0
  326. package/src/media/openrouter-image-service.test.ts +133 -0
  327. package/src/media/openrouter-image-service.ts +153 -0
  328. package/src/media/types.ts +1 -1
  329. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +65 -11
  330. package/src/messaging/providers/discord/send.ts +4 -17
  331. package/src/messaging/providers/discord/transport.ts +2 -2
  332. package/src/messaging/providers/outlook/client.ts +16 -22
  333. package/src/messaging/providers/telegram-bot/send.test.ts +236 -1
  334. package/src/messaging/providers/telegram-bot/send.ts +10 -7
  335. package/src/messaging/providers/telegram-bot/transport.ts +3 -10
  336. package/src/messaging/providers/whatsapp/send.test.ts +169 -4
  337. package/src/monitoring/__tests__/control.test.ts +17 -5
  338. package/src/monitoring/control.ts +15 -5
  339. package/src/notifications/AGENTS.md +1 -1
  340. package/src/notifications/README.md +83 -99
  341. package/src/notifications/__tests__/broadcaster.test.ts +1 -1
  342. package/src/notifications/__tests__/channel-allowlist.test.ts +39 -0
  343. package/src/notifications/__tests__/copy-composer.test.ts +1 -1
  344. package/src/notifications/__tests__/decision-engine.test.ts +79 -2
  345. package/src/notifications/__tests__/deterministic-checks.test.ts +10 -10
  346. package/src/notifications/__tests__/home-feed-side-effect.test.ts +93 -4
  347. package/src/notifications/__tests__/notification-utils.test.ts +18 -0
  348. package/src/notifications/adapters/macos.ts +37 -46
  349. package/src/notifications/broadcaster.ts +5 -6
  350. package/src/notifications/channel-allowlist.ts +44 -0
  351. package/src/notifications/copy-composer.ts +0 -23
  352. package/src/notifications/decision-engine.ts +24 -14
  353. package/src/notifications/deterministic-checks.ts +1 -1
  354. package/src/notifications/emit-signal.ts +123 -37
  355. package/src/notifications/home-feed-side-effect.ts +9 -7
  356. package/src/notifications/notification-utils.ts +19 -0
  357. package/src/notifications/signal.ts +4 -19
  358. package/src/notifications/types.ts +15 -0
  359. package/src/oauth/connection-resolver.ts +1 -2
  360. package/src/oauth/oauth-store.ts +1 -3
  361. package/src/oauth/provider-serializer.ts +7 -0
  362. package/src/oauth/seed-providers.ts +30 -0
  363. package/src/onboarding/onboarding-events-store.ts +99 -12
  364. package/src/persistence/__tests__/client-connection-events-store.test.ts +322 -0
  365. package/src/persistence/client-connection-events-store.ts +377 -0
  366. package/src/persistence/conversation-crud.ts +55 -6
  367. package/src/persistence/conversation-mode-sessions.test.ts +424 -0
  368. package/src/persistence/conversation-mode-sessions.ts +460 -0
  369. package/src/persistence/conversation-queries.ts +6 -19
  370. package/src/persistence/llm-usage-store.ts +3 -9
  371. package/src/persistence/message-metadata.ts +25 -0
  372. package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
  373. package/src/persistence/migrations/382-create-client-connection-events.ts +40 -0
  374. package/src/persistence/migrations/__tests__/382-create-client-connection-events.test.ts +56 -0
  375. package/src/persistence/schema/conversations.ts +67 -0
  376. package/src/persistence/schema/infrastructure.ts +29 -0
  377. package/src/persistence/steps.ts +17 -0
  378. package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
  379. package/src/plugin-api/effective-context-window.ts +33 -0
  380. package/src/plugin-api/index.ts +24 -5
  381. package/src/plugin-api/plugin-name-env.ts +125 -0
  382. package/src/plugin-api/resolve-credential.ts +79 -20
  383. package/src/plugin-api/vision-support.test.ts +22 -0
  384. package/src/plugin-api/vision-support.ts +17 -5
  385. package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
  386. package/src/plugins/active-plugin-env.ts +51 -0
  387. package/src/plugins/defaults/index.ts +24 -0
  388. package/src/plugins/defaults/memory/__tests__/archive-removal-framing.test.ts +64 -0
  389. package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
  390. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +9 -12
  391. package/src/plugins/defaults/memory/context-search/agent-runner.ts +78 -22
  392. package/src/plugins/defaults/memory/context-search/format.ts +51 -7
  393. package/src/plugins/defaults/memory/context-search/limits.ts +10 -12
  394. package/src/plugins/defaults/memory/context-search/recall-input.ts +38 -0
  395. package/src/plugins/defaults/memory/context-search/search.ts +8 -14
  396. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +1 -0
  397. package/src/plugins/defaults/memory/context-search/types.ts +1 -11
  398. package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +11 -4
  399. package/src/plugins/defaults/memory/graph/tool-handlers.ts +12 -21
  400. package/src/plugins/defaults/memory/graph/tools.ts +35 -64
  401. package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
  402. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +5 -0
  403. package/src/plugins/defaults/memory/src/memory-item-routes.ts +2 -1
  404. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +2 -2
  405. package/src/plugins/defaults/memory/tools.test.ts +124 -9
  406. package/src/plugins/defaults/memory/tools.ts +38 -11
  407. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +165 -27
  408. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +626 -27
  409. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +46 -27
  410. package/src/plugins/defaults/memory/v3/orchestrate.ts +78 -38
  411. package/src/plugins/defaults/memory/v3/pool-log-store.ts +65 -79
  412. package/src/plugins/defaults/memory/v3/pool-select.test.ts +14 -5
  413. package/src/plugins/defaults/memory/v3/pool-select.ts +683 -44
  414. package/src/plugins/defaults/memory/worker-control.ts +15 -5
  415. package/src/plugins/defaults/task-progress/__tests__/registration.test.ts +70 -0
  416. package/src/plugins/defaults/task-progress/__tests__/task-progress-context.test.ts +380 -0
  417. package/src/plugins/defaults/task-progress/hooks/post-compact.ts +19 -0
  418. package/src/plugins/defaults/task-progress/hooks/user-prompt-submit.ts +24 -0
  419. package/src/plugins/defaults/task-progress/package.json +14 -0
  420. package/src/plugins/defaults/task-progress/src/apply-task-progress-context.ts +93 -0
  421. package/src/plugins/defaults/task-progress/src/format-task-progress-context.ts +67 -0
  422. package/src/providers/__tests__/context-overflow-error.test.ts +10 -0
  423. package/src/providers/__tests__/search-provider-catalog.test.ts +18 -0
  424. package/src/providers/anthropic/client.ts +10 -1
  425. package/src/providers/anthropic-gateway-shared.ts +6 -0
  426. package/src/providers/fetch-provider-catalog.ts +11 -0
  427. package/src/providers/gemini/client.ts +25 -8
  428. package/src/providers/inference/adapter-factory.ts +1 -1
  429. package/src/providers/jev/client.test.ts +10 -0
  430. package/src/providers/jev/client.ts +20 -4
  431. package/src/providers/model-catalog.ts +4 -3
  432. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +94 -1
  433. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +106 -1
  434. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +24 -0
  435. package/src/providers/openai/__tests__/google-thought-signature.test.ts +1 -2
  436. package/src/providers/openai/chat-completions-provider.ts +100 -29
  437. package/src/providers/openai/google-thought-signature.ts +0 -6
  438. package/src/providers/openai/responses-provider.ts +23 -1
  439. package/src/providers/ratelimit.ts +1 -3
  440. package/src/providers/search-provider-catalog.ts +24 -0
  441. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +24 -0
  442. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +7 -1
  443. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +50 -1
  444. package/src/providers/speech-to-text/deepgram-flux-frames.ts +11 -0
  445. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +13 -0
  446. package/src/providers/types.ts +5 -0
  447. package/src/routes/control.ts +7 -5
  448. package/src/runtime/AGENTS.md +4 -2
  449. package/src/runtime/actor-trust-resolver.ts +3 -3
  450. package/src/runtime/agent-wake.ts +1 -1
  451. package/src/runtime/approval-message-composer.ts +3 -69
  452. package/src/runtime/assistant-event-hub.ts +73 -75
  453. package/src/runtime/assistant-event-publish-options.ts +7 -0
  454. package/src/runtime/assistant-event-targeting.ts +79 -0
  455. package/src/runtime/assistant-stream-state.ts +13 -100
  456. package/src/runtime/background-job-runner.ts +5 -5
  457. package/src/runtime/channel-retry-sweep.ts +1 -0
  458. package/src/runtime/guardian-action-message-composer.ts +8 -161
  459. package/src/runtime/guardian-reply-router.ts +28 -32
  460. package/src/runtime/http-types.ts +6 -0
  461. package/src/runtime/message-composer-types.ts +3 -61
  462. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +67 -1
  463. package/src/runtime/migrations/vbundle-builder.ts +24 -1
  464. package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -4
  465. package/src/runtime/migrations/vbundle-import-policy.ts +10 -0
  466. package/src/runtime/migrations/vbundle-importer.ts +4 -1
  467. package/src/runtime/migrations/vbundle-streaming-importer.ts +4 -1
  468. package/src/runtime/migrations/vbundle-validator.ts +1 -0
  469. package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
  470. package/src/runtime/pending-interactions.ts +29 -0
  471. package/src/runtime/routes/__tests__/client-history-routes.test.ts +104 -0
  472. package/src/runtime/routes/__tests__/image-generation-routes.test.ts +97 -0
  473. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +1 -1
  474. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +29 -6
  475. package/src/runtime/routes/browser-context.ts +31 -11
  476. package/src/runtime/routes/browser-routes.ts +43 -4
  477. package/src/runtime/routes/btw-routes.ts +9 -4
  478. package/src/runtime/routes/client-routes.ts +108 -0
  479. package/src/runtime/routes/conversation-management-routes.ts +4 -1
  480. package/src/runtime/routes/conversation-routes.ts +113 -27
  481. package/src/runtime/routes/credential-prompt-routes.ts +4 -3
  482. package/src/runtime/routes/credential-routes.ts +1 -2
  483. package/src/runtime/routes/desktop-setup-routes.test.ts +9 -18
  484. package/src/runtime/routes/desktop-setup-routes.ts +4 -6
  485. package/src/runtime/routes/documents-routes.ts +51 -0
  486. package/src/runtime/routes/email-routes.ts +3 -7
  487. package/src/runtime/routes/events-routes.ts +75 -54
  488. package/src/runtime/routes/identity-routes.ts +1 -5
  489. package/src/runtime/routes/image-generation-routes.ts +8 -14
  490. package/src/runtime/routes/inbound-message-handler.ts +35 -38
  491. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.test.ts +168 -0
  492. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.ts +96 -0
  493. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +38 -9
  494. package/src/runtime/routes/inbound-stages/background-dispatch.ts +2 -5
  495. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +2 -0
  496. package/src/runtime/routes/llm-context-normalization.ts +1 -7
  497. package/src/runtime/routes/migration-routes.ts +73 -2
  498. package/src/runtime/routes/notification-routes.ts +10 -0
  499. package/src/runtime/routes/oauth-providers.ts +7 -0
  500. package/src/runtime/routes/plugins-routes.ts +6 -0
  501. package/src/runtime/routes/roadmap-routes.ts +47 -8
  502. package/src/runtime/routes/secret-routes.ts +1 -1
  503. package/src/runtime/routes/settings-routes.ts +10 -4
  504. package/src/runtime/routes/watcher-routes.ts +1 -3
  505. package/src/runtime/trust-class.test.ts +7 -6
  506. package/src/runtime/trust-class.ts +9 -17
  507. package/src/schedule/__tests__/worker-control.test.ts +36 -5
  508. package/src/schedule/worker-control.ts +15 -5
  509. package/src/security/secure-keys.ts +53 -103
  510. package/src/skills/clawhub.ts +3 -8
  511. package/src/skills/managed-store.ts +1 -5
  512. package/src/stt/types.ts +4 -0
  513. package/src/subagent/notify.ts +54 -34
  514. package/src/subagent/parent-notification.ts +6 -0
  515. package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
  516. package/src/telemetry/phone-call-funnel.ts +144 -0
  517. package/src/tools/__tests__/tool-schema-root-combinator-guard.test.ts +81 -0
  518. package/src/tools/ask-question/ask-question-tool.ts +6 -4
  519. package/src/tools/browser/browser-execution.ts +22 -59
  520. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +561 -39
  521. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +435 -39
  522. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
  523. package/src/tools/client-os.ts +15 -0
  524. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
  525. package/src/tools/computer-use/target.ts +42 -0
  526. package/src/tools/document/document-tool.ts +13 -18
  527. package/src/tools/execution-target.ts +14 -16
  528. package/src/tools/host-terminal/host-shell.ts +4 -1
  529. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
  530. package/src/tools/network/__tests__/web-search.test.ts +203 -0
  531. package/src/tools/network/web-fetch.ts +274 -7
  532. package/src/tools/network/web-search.ts +468 -6
  533. package/src/tools/permission-checker.ts +3 -3
  534. package/src/tools/policy-context.ts +3 -1
  535. package/src/tools/skills/load.ts +2 -1
  536. package/src/tools/skills/sandbox-runner.ts +11 -2
  537. package/src/tools/skills/skill-script-runner.ts +7 -0
  538. package/src/tools/skills/skill-tool-factory.ts +15 -1
  539. package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
  540. package/src/tools/terminal/safe-env.ts +4 -0
  541. package/src/tools/terminal/shell.ts +2 -0
  542. package/src/tools/tool-approval-handler.ts +5 -1
  543. package/src/tools/tool-types.ts +3 -3
  544. package/src/tools/types.ts +8 -2
  545. package/src/util/__tests__/worker-process-command.test.ts +5 -4
  546. package/src/util/abort-reasons.ts +18 -26
  547. package/src/util/canonicalize-identity.test.ts +21 -0
  548. package/src/util/canonicalize-identity.ts +16 -0
  549. package/src/util/errors.ts +13 -3
  550. package/src/util/provider-error-patterns.ts +3 -0
  551. package/src/util/worker-process.ts +77 -35
  552. package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
  553. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
  554. package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
  555. package/src/workspace/migrations/registry.ts +2 -0
  556. package/src/__tests__/computer-use-window-schema.test.ts +0 -24
  557. package/src/api/events/heartbeat-alert.ts +0 -20
  558. package/src/daemon/message-types/web-activity.ts +0 -65
  559. package/src/desktop/desktop-dependencies.test.ts +0 -131
  560. package/src/tools/computer-use/definitions.ts +0 -607
@@ -24,7 +24,12 @@ import { getConversationDirName } from "../persistence/conversation-directories.
24
24
  import type { UserPromptSubmitContext } from "../plugin-api/types.js";
25
25
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
26
26
  import { registerPlugin } from "../plugins/registry.js";
27
- import type { Message, Provider, ToolDefinition } from "../providers/types.js";
27
+ import type {
28
+ Message,
29
+ Provider,
30
+ SendMessageOptions,
31
+ ToolDefinition,
32
+ } from "../providers/types.js";
28
33
  import { ContextOverflowError } from "../providers/types.js";
29
34
  import {
30
35
  resolveUsageAttribution,
@@ -341,12 +346,18 @@ const deleteMessageByIdMock = mock(() => ({
341
346
  segmentIds: [],
342
347
  deletedSummaryIds: [],
343
348
  }));
344
- const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
349
+ const reserveMessageMock = mock(async () => ({
350
+ id: "msg-reserve",
351
+ createdAt: 1_700_000_000_050,
352
+ }));
345
353
  /** Persisted rows the loop reads back. Empty unless a test seeds one. */
346
354
  let mockStoredMessages: unknown[] = [];
347
355
  const updateMessageContentMock = mock(() => {});
348
356
  const finalizeMessageContentMock = mock(() => {});
349
- const addMessageMock = mock(() => ({ id: "mock-msg-id" }));
357
+ const addMessageMock = mock(() => ({
358
+ id: "mock-msg-id",
359
+ createdAt: 1_700_000_000_100,
360
+ }));
350
361
  const updateConversationContextWindowMock = mock(() => {});
351
362
  mock.module("../persistence/conversation-crud.js", () => ({
352
363
  setConversationProcessingStartedAt: () => {},
@@ -715,7 +726,7 @@ mock.module("../persistence/llm-request-log-store.js", () => ({
715
726
  // ── Imports (after mocks) ────────────────────────────────────────────
716
727
 
717
728
  import { AgentLoop } from "../agent/loop.js";
718
- import type { Conversation } from "../daemon/conversation.js";
729
+ import { Conversation } from "../daemon/conversation.js";
719
730
  import {
720
731
  applyCompactionResult,
721
732
  runAgentLoopImpl,
@@ -792,6 +803,19 @@ function makeCtx(
792
803
  toolExecutor,
793
804
  });
794
805
 
806
+ const modeSessions = {
807
+ getTurnOwner: () => undefined,
808
+ getTerminalDisposition: () => undefined,
809
+ keepsSessionOpenAfterTurn: () => false,
810
+ trackPersistedRow: () => false,
811
+ recordStructuralWait: () => false,
812
+ invalidateStructuralWait: mock(() => false),
813
+ beginDraining: () => false,
814
+ finalizeTurn: () => false,
815
+ releaseTurn: () => false,
816
+ transferTurn: () => false,
817
+ } as unknown as Conversation["modeSessions"];
818
+
795
819
  const ctx = asConversation({
796
820
  conversationId: "test-conv",
797
821
  messages: [
@@ -839,6 +863,7 @@ function makeCtx(
839
863
  pendingSurfaceActions: new Map(),
840
864
  surfaceActionRequestIds: new Set<string>(),
841
865
  currentTurnSurfaces: [],
866
+ modeSessions,
842
867
 
843
868
  workingDir: "/tmp",
844
869
  channelCapabilities: undefined,
@@ -1100,7 +1125,388 @@ beforeEach(() => {
1100
1125
  resetPluginRegistryAndRegisterDefaults();
1101
1126
  });
1102
1127
 
1128
+ function makeModeSessionDouble(options?: {
1129
+ terminalDisposition?: {
1130
+ status: "completed" | "interrupted";
1131
+ endReason: string;
1132
+ };
1133
+ structuralWait?: boolean;
1134
+ sourceLifetime?: boolean;
1135
+ }) {
1136
+ const owner = { id: "session-1", mode: "computer_use" as const };
1137
+ const trackPersistedRow = mock(() => true);
1138
+ const recordStructuralWait = mock(() => options?.structuralWait ?? false);
1139
+ const beginDraining = mock(() => true);
1140
+ const finalizeTurn = mock(() => true);
1141
+ const releaseTurn = mock(() => true);
1142
+ const transferTurn = mock(() => true);
1143
+ const coordinator = {
1144
+ getTurnOwner: () => owner,
1145
+ getTerminalDisposition: () => options?.terminalDisposition,
1146
+ keepsSessionOpenAfterTurn: () => options?.sourceLifetime ?? false,
1147
+ trackPersistedRow,
1148
+ recordStructuralWait,
1149
+ beginDraining,
1150
+ finalizeTurn,
1151
+ releaseTurn,
1152
+ transferTurn,
1153
+ } as unknown as Conversation["modeSessions"];
1154
+ return {
1155
+ coordinator,
1156
+ trackPersistedRow,
1157
+ recordStructuralWait,
1158
+ beginDraining,
1159
+ finalizeTurn,
1160
+ releaseTurn,
1161
+ transferTurn,
1162
+ };
1163
+ }
1164
+
1165
+ describe("prompt cache warming", () => {
1166
+ test("attributes provider usage to the conversation", async () => {
1167
+ const sendMessage = mock(
1168
+ async (_messages: Message[], _options?: SendMessageOptions) =>
1169
+ textResponse("unused"),
1170
+ );
1171
+ const conversation = Object.assign(
1172
+ Object.create(Conversation.prototype) as object,
1173
+ {
1174
+ conversationId: "conv-cache-warm-test",
1175
+ messages: [],
1176
+ provider: { name: "mock-provider", sendMessage },
1177
+ agentLoop: { getResolvedTools: () => [] },
1178
+ buildCurrentSystemPrompt: () => "system prompt",
1179
+ },
1180
+ ) as unknown as Conversation;
1181
+
1182
+ await conversation.warmPromptCache();
1183
+
1184
+ expect(sendMessage).toHaveBeenCalledTimes(1);
1185
+ expect(sendMessage.mock.calls[0]?.[1]?.config).toMatchObject({
1186
+ callSite: "mainAgent",
1187
+ conversationId: "conv-cache-warm-test",
1188
+ max_tokens: 16,
1189
+ selectionSeed: "conv-cache-warm-test",
1190
+ });
1191
+ expect(
1192
+ sendMessage.mock.calls[0]?.[1]?.config?.usageTracking,
1193
+ ).toBeUndefined();
1194
+ });
1195
+
1196
+ test("stays non-rejecting when request preparation fails", async () => {
1197
+ const sendMessage = mock(async () => textResponse("unused"));
1198
+ const conversation = Object.assign(
1199
+ Object.create(Conversation.prototype) as object,
1200
+ {
1201
+ conversationId: "conv-cache-warm-test",
1202
+ messages: [],
1203
+ provider: { name: "mock-provider", sendMessage },
1204
+ agentLoop: {
1205
+ getResolvedTools: () => {
1206
+ throw new Error("tool resolution failed");
1207
+ },
1208
+ },
1209
+ buildCurrentSystemPrompt: () => "system prompt",
1210
+ },
1211
+ ) as unknown as Conversation;
1212
+
1213
+ await expect(conversation.warmPromptCache()).resolves.toBeUndefined();
1214
+ expect(sendMessage).not.toHaveBeenCalled();
1215
+ });
1216
+
1217
+ test("does not rebuild a system prompt explicitly removed by a hook", async () => {
1218
+ const sendMessage = mock(
1219
+ async (_messages: Message[], _options?: SendMessageOptions) =>
1220
+ textResponse("unused"),
1221
+ );
1222
+ const buildCurrentSystemPrompt = mock(() => "rebuilt prompt");
1223
+ const conversation = Object.assign(
1224
+ Object.create(Conversation.prototype) as object,
1225
+ {
1226
+ conversationId: "conv-cache-warm-test",
1227
+ messages: [],
1228
+ provider: { name: "mock-provider", sendMessage },
1229
+ agentLoop: { getResolvedTools: () => [] },
1230
+ buildCurrentSystemPrompt,
1231
+ },
1232
+ ) as unknown as Conversation;
1233
+
1234
+ await conversation.warmPromptCache({ systemPrompt: null, tools: [] });
1235
+
1236
+ expect(buildCurrentSystemPrompt).not.toHaveBeenCalled();
1237
+ expect(sendMessage.mock.calls[0]?.[1]?.systemPrompt).toBeUndefined();
1238
+ });
1239
+ });
1240
+
1103
1241
  describe("session-agent-loop", () => {
1242
+ describe("mode session settlement", () => {
1243
+ test.each(["reply", "provider error"] as const)(
1244
+ "row tracking failures preserve tool output and the final %s",
1245
+ async (outcome) => {
1246
+ const sessions = makeModeSessionDouble();
1247
+ sessions.trackPersistedRow.mockImplementation(() => {
1248
+ throw new Error("session row tracking unavailable");
1249
+ });
1250
+ for (let index = 1; index <= 3; index += 1) {
1251
+ reserveMessageMock.mockImplementationOnce(async () => ({
1252
+ id: `msg-tracking-${index}`,
1253
+ createdAt: 1_700_000_000_050 + index,
1254
+ }));
1255
+ }
1256
+ mockMessageById = {
1257
+ id: "msg-tracking-2",
1258
+ conversationId: "test-conv",
1259
+ createdAt: 1_700_000_000_052,
1260
+ role: "user",
1261
+ content: "[]",
1262
+ metadata: null,
1263
+ };
1264
+ const scripted = createMockProvider([
1265
+ toolUseResponse("tool-123", "file_read", {}),
1266
+ outcome === "reply"
1267
+ ? textResponse("Finished reading")
1268
+ : new Error("upstream unavailable"),
1269
+ ]);
1270
+ let savedToolOutputBeforeFollowup = false;
1271
+ const provider: Provider = {
1272
+ ...scripted.provider,
1273
+ async sendMessage(messages, options) {
1274
+ if (scripted.calls.length === 1) {
1275
+ savedToolOutputBeforeFollowup = inflightDeltaFiles().some(
1276
+ (path) => readFileSync(path, "utf8").includes("file content"),
1277
+ );
1278
+ }
1279
+ return scripted.provider.sendMessage(messages, options);
1280
+ },
1281
+ };
1282
+ const executeTool = mock(async () => ({
1283
+ content: "file content",
1284
+ isError: false,
1285
+ }));
1286
+ const ctx = makeCtx({
1287
+ modeSessions: sessions.coordinator,
1288
+ loopProvider: provider,
1289
+ loopTools: [
1290
+ {
1291
+ name: "file_read",
1292
+ description: "Read a file",
1293
+ input_schema: { type: "object", properties: {} },
1294
+ },
1295
+ ],
1296
+ toolExecutor: executeTool,
1297
+ });
1298
+ const events: AssistantEvent[] = [];
1299
+
1300
+ await runAgentLoopImpl(ctx, "read the file", "msg-user-123", (event) =>
1301
+ events.push(event),
1302
+ );
1303
+
1304
+ expect(executeTool).toHaveBeenCalledTimes(1);
1305
+ expect(scripted.calls).toHaveLength(2);
1306
+ expect(savedToolOutputBeforeFollowup).toBe(true);
1307
+ expect(
1308
+ events.filter((event) => event.type === "assistant_turn_start"),
1309
+ ).toHaveLength(2);
1310
+ expect(
1311
+ events.filter((event) => event.type === "message_complete"),
1312
+ ).toHaveLength(1);
1313
+ expect(events.some((event) => event.type === "error")).toBe(false);
1314
+ expect(inflightDeltaFiles()).toHaveLength(0);
1315
+ expect(sessions.finalizeTurn).toHaveBeenCalledTimes(1);
1316
+ const finalized = (
1317
+ finalizeMessageContentMock.mock.calls as unknown as Array<
1318
+ [string, string, unknown]
1319
+ >
1320
+ ).map(([id, content]) => ({ id, content }));
1321
+ expect(finalized).toContainEqual({
1322
+ id: "msg-tracking-2",
1323
+ content: expect.stringContaining("file content"),
1324
+ });
1325
+ if (outcome === "reply") {
1326
+ expect(finalized).toContainEqual({
1327
+ id: "msg-tracking-3",
1328
+ content: expect.stringContaining("Finished reading"),
1329
+ });
1330
+ expect(addMessageMock).not.toHaveBeenCalled();
1331
+ } else {
1332
+ expect(addMessageMock).toHaveBeenCalledTimes(1);
1333
+ expect(backfillMessageIdOnLogsMock).toHaveBeenCalledWith(
1334
+ "test-conv",
1335
+ "mock-msg-id",
1336
+ );
1337
+ expect(ctx.messages.at(-1)?.content).toEqual([
1338
+ {
1339
+ type: "text",
1340
+ text: mockConversationErrorClassification.userMessage,
1341
+ },
1342
+ ]);
1343
+ }
1344
+ },
1345
+ );
1346
+
1347
+ test("finalizes an owned turn after its assistant output settles", async () => {
1348
+ const sessions = makeModeSessionDouble();
1349
+ const ctx = makeCtx({ modeSessions: sessions.coordinator });
1350
+
1351
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
1352
+
1353
+ expect(sessions.trackPersistedRow).toHaveBeenCalledWith(
1354
+ "test-req",
1355
+ expect.any(String),
1356
+ expect.anything(),
1357
+ );
1358
+ expect(sessions.beginDraining).not.toHaveBeenCalled();
1359
+ expect(sessions.finalizeTurn).toHaveBeenCalledWith({
1360
+ turnId: "test-req",
1361
+ status: "completed",
1362
+ endedAt: expect.any(Number),
1363
+ endReason: "turn_settled",
1364
+ lastActivityAt: expect.any(Number),
1365
+ });
1366
+ });
1367
+
1368
+ test.each(["throw", "conflict"] as const)(
1369
+ "keeps a delivered reply successful after a finalization %s",
1370
+ async (failedOperation) => {
1371
+ const sessions = makeModeSessionDouble();
1372
+ sessions.finalizeTurn.mockImplementation(() => {
1373
+ if (failedOperation === "throw") {
1374
+ throw new Error("session settlement unavailable");
1375
+ }
1376
+ return false;
1377
+ });
1378
+ const ctx = makeCtx({ modeSessions: sessions.coordinator });
1379
+ const events: AssistantEvent[] = [];
1380
+
1381
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (event) =>
1382
+ events.push(event),
1383
+ );
1384
+
1385
+ expect(
1386
+ events.filter((event) => event.type === "message_complete"),
1387
+ ).toHaveLength(1);
1388
+ expect(events.some((event) => event.type === "error")).toBe(false);
1389
+ expect(sessions.releaseTurn).toHaveBeenCalledWith("test-req", {
1390
+ status: "completed",
1391
+ endReason: "turn_settled",
1392
+ });
1393
+ },
1394
+ );
1395
+
1396
+ test("releases a retired source only after final output settles", async () => {
1397
+ const sessions = makeModeSessionDouble({
1398
+ terminalDisposition: {
1399
+ status: "interrupted",
1400
+ endReason: "browser_cancelled",
1401
+ },
1402
+ });
1403
+ const ctx = makeCtx({ modeSessions: sessions.coordinator });
1404
+
1405
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
1406
+
1407
+ expect(sessions.beginDraining).not.toHaveBeenCalled();
1408
+ expect(sessions.releaseTurn).toHaveBeenCalledWith("test-req", {
1409
+ status: "completed",
1410
+ endReason: "turn_settled",
1411
+ });
1412
+ expect(sessions.finalizeTurn).not.toHaveBeenCalled();
1413
+ });
1414
+
1415
+ test("keeps a source-lifetime session active across a cancelled turn", async () => {
1416
+ const abortController = new AbortController();
1417
+ const provider: Provider = {
1418
+ name: "mock",
1419
+ async sendMessage(_messages, options) {
1420
+ options?.onEvent?.({ type: "text_delta", text: "partial" });
1421
+ abortController.abort();
1422
+ return textResponse("partial");
1423
+ },
1424
+ };
1425
+ const sessions = makeModeSessionDouble({ sourceLifetime: true });
1426
+ const cancelledCtx = makeCtx({
1427
+ loopProvider: provider,
1428
+ abortController,
1429
+ modeSessions: sessions.coordinator,
1430
+ });
1431
+
1432
+ await runAgentLoopImpl(cancelledCtx, "hello", "msg-1", () => {});
1433
+
1434
+ expect(sessions.releaseTurn).toHaveBeenCalledWith("test-req");
1435
+ expect(sessions.beginDraining).not.toHaveBeenCalled();
1436
+ expect(sessions.finalizeTurn).not.toHaveBeenCalled();
1437
+
1438
+ const laterCtx = makeCtx({ modeSessions: sessions.coordinator });
1439
+ await runAgentLoopImpl(laterCtx, "later", "msg-2", () => {});
1440
+
1441
+ expect(sessions.releaseTurn).toHaveBeenCalledTimes(2);
1442
+ expect(sessions.finalizeTurn).not.toHaveBeenCalled();
1443
+ });
1444
+
1445
+ test("keeps a source-lifetime session active across a failed turn", async () => {
1446
+ const sessions = makeModeSessionDouble({ sourceLifetime: true });
1447
+ const failedCtx = makeCtx({
1448
+ loopProvider: {
1449
+ name: "mock",
1450
+ async sendMessage() {
1451
+ throw new Error("provider failure");
1452
+ },
1453
+ } as Provider,
1454
+ modeSessions: sessions.coordinator,
1455
+ });
1456
+
1457
+ await runAgentLoopImpl(failedCtx, "hello", "msg-1", () => {});
1458
+
1459
+ expect(sessions.releaseTurn).toHaveBeenCalledWith("test-req");
1460
+ expect(sessions.beginDraining).not.toHaveBeenCalled();
1461
+ expect(sessions.finalizeTurn).not.toHaveBeenCalled();
1462
+ });
1463
+
1464
+ test("keeps an owned turn open for an exact structural response", async () => {
1465
+ const sessions = makeModeSessionDouble({ structuralWait: true });
1466
+ const ctx = makeCtx({
1467
+ modeSessions: sessions.coordinator,
1468
+ pendingSurfaceActions: new Map([
1469
+ ["surface-1", { surfaceType: "form" }],
1470
+ ]) as Conversation["pendingSurfaceActions"],
1471
+ });
1472
+
1473
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
1474
+
1475
+ expect(sessions.recordStructuralWait).toHaveBeenCalledWith("test-req", {
1476
+ kind: "surface",
1477
+ responseId: "surface-1",
1478
+ });
1479
+ expect(sessions.releaseTurn).toHaveBeenCalledWith("test-req");
1480
+ expect(sessions.beginDraining).not.toHaveBeenCalled();
1481
+ expect(sessions.finalizeTurn).not.toHaveBeenCalled();
1482
+ });
1483
+
1484
+ test("transfers ownership to the queued turn at a checkpoint", async () => {
1485
+ const sessions = makeModeSessionDouble();
1486
+ const ctx = makeCtx({
1487
+ modeSessions: sessions.coordinator,
1488
+ providerResponses: [toolUseResponse("tu-1", "file_read", {})],
1489
+ loopTools: [
1490
+ {
1491
+ name: "file_read",
1492
+ description: "Read a file",
1493
+ input_schema: { type: "object", properties: {} },
1494
+ },
1495
+ ],
1496
+ toolExecutor: async () => ({ content: "file content", isError: false }),
1497
+ canHandoffAtCheckpoint: () => true,
1498
+ queue: {
1499
+ snapshot: () => [{ requestId: "msg-2" }],
1500
+ } as unknown as Conversation["queue"],
1501
+ });
1502
+
1503
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
1504
+
1505
+ expect(sessions.transferTurn).toHaveBeenCalledWith("test-req", "msg-2");
1506
+ expect(sessions.finalizeTurn).not.toHaveBeenCalled();
1507
+ });
1508
+ });
1509
+
1104
1510
  describe("user-prompt-submit hook failures", () => {
1105
1511
  test("passes the effective profile to hooks even when it was already announced", async () => {
1106
1512
  // Both profiles are complete (provider + model) so each is a usable
@@ -1820,9 +2226,18 @@ describe("session-agent-loop", () => {
1820
2226
  "isAssistantFeatureFlagEnabled",
1821
2227
  ).mockImplementation((key: string) => key === "send-user-message");
1822
2228
  reserveMessageMock
1823
- .mockImplementationOnce(async () => ({ id: "msg-delivered-reply" }))
1824
- .mockImplementationOnce(async () => ({ id: "msg-tool-result" }))
1825
- .mockImplementationOnce(async () => ({ id: "msg-final-private" }));
2229
+ .mockImplementationOnce(async () => ({
2230
+ id: "msg-delivered-reply",
2231
+ createdAt: 1_700_000_000_050,
2232
+ }))
2233
+ .mockImplementationOnce(async () => ({
2234
+ id: "msg-tool-result",
2235
+ createdAt: 1_700_000_000_050,
2236
+ }))
2237
+ .mockImplementationOnce(async () => ({
2238
+ id: "msg-final-private",
2239
+ createdAt: 1_700_000_000_050,
2240
+ }));
1826
2241
  mockTurnReplyMessageId = "msg-delivered-reply";
1827
2242
  resolveAssistantAttachmentsMock.mockImplementation(async () => ({
1828
2243
  assistantAttachments: [],
@@ -1880,9 +2295,18 @@ describe("session-agent-loop", () => {
1880
2295
  "isAssistantFeatureFlagEnabled",
1881
2296
  ).mockImplementation((key: string) => key === "send-user-message");
1882
2297
  reserveMessageMock
1883
- .mockImplementationOnce(async () => ({ id: "msg-delivered-empty" }))
1884
- .mockImplementationOnce(async () => ({ id: "msg-tool-result-empty" }))
1885
- .mockImplementationOnce(async () => ({ id: "msg-final-empty" }));
2298
+ .mockImplementationOnce(async () => ({
2299
+ id: "msg-delivered-empty",
2300
+ createdAt: 1_700_000_000_050,
2301
+ }))
2302
+ .mockImplementationOnce(async () => ({
2303
+ id: "msg-tool-result-empty",
2304
+ createdAt: 1_700_000_000_050,
2305
+ }))
2306
+ .mockImplementationOnce(async () => ({
2307
+ id: "msg-final-empty",
2308
+ createdAt: 1_700_000_000_050,
2309
+ }));
1886
2310
  mockTurnReplyMessageId = "msg-delivered-empty";
1887
2311
 
1888
2312
  try {
@@ -2165,6 +2589,43 @@ describe("session-agent-loop", () => {
2165
2589
  const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
2166
2590
  expect(call[5]).toBe("callAgent");
2167
2591
  });
2592
+
2593
+ test("reports only the first finalized model call", async () => {
2594
+ const tool: ToolDefinition = {
2595
+ name: "echo",
2596
+ description: "Echo",
2597
+ input_schema: { type: "object" },
2598
+ };
2599
+ const onFirstModelCallPrepared = mock(() => {});
2600
+ const ctx = makeCtx({
2601
+ providerResponses: [
2602
+ toolUseResponse("tool-1", "echo", {}),
2603
+ textResponse("done"),
2604
+ ],
2605
+ loopTools: [tool],
2606
+ toolExecutor: async () => ({ content: "ok", isError: false }),
2607
+ });
2608
+ const turnSignal = ctx.abortController?.signal;
2609
+
2610
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
2611
+ callSite: "callAgent",
2612
+ inferenceCallSite: "mainAgent",
2613
+ overrideProfile: "quality-optimized",
2614
+ forceOverrideProfile: true,
2615
+ onFirstModelCallPrepared,
2616
+ });
2617
+
2618
+ expect(onFirstModelCallPrepared).toHaveBeenCalledTimes(1);
2619
+ expect(onFirstModelCallPrepared).toHaveBeenCalledWith({
2620
+ callSite: "mainAgent",
2621
+ overrideProfile: "quality-optimized",
2622
+ forceOverrideProfile: true,
2623
+ signal: turnSignal,
2624
+ systemPrompt: "system prompt",
2625
+ tools: [tool],
2626
+ });
2627
+ expect(ctx.currentCallSite).toBe("callAgent");
2628
+ });
2168
2629
  });
2169
2630
 
2170
2631
  describe("usage accounting", () => {
@@ -2473,11 +2934,23 @@ describe("session-agent-loop", () => {
2473
2934
  },
2474
2935
  };
2475
2936
 
2476
- const ctx = makeCtx({ loopProvider: provider, abortController });
2937
+ const sessions = makeModeSessionDouble();
2938
+ const ctx = makeCtx({
2939
+ loopProvider: provider,
2940
+ abortController,
2941
+ modeSessions: sessions.coordinator,
2942
+ });
2477
2943
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2478
2944
 
2479
2945
  const cancelled = events.find((e) => e.type === "generation_cancelled");
2480
2946
  expect(cancelled).toBeDefined();
2947
+ expect(sessions.finalizeTurn).toHaveBeenCalledWith({
2948
+ turnId: "test-req",
2949
+ status: "interrupted",
2950
+ endedAt: expect.any(Number),
2951
+ endReason: "cancelled",
2952
+ lastActivityAt: expect.any(Number),
2953
+ });
2481
2954
  });
2482
2955
 
2483
2956
  // A `task_progress` card mid-run. `data` mirrors what `ui_show` stores for
@@ -2871,6 +3344,38 @@ describe("session-agent-loop", () => {
2871
3344
  expect(ctx.pendingSurfaceActions.has("page-1")).toBe(true);
2872
3345
  expect(ctx.pendingSurfaceActions.has("stale-table-1")).toBe(false);
2873
3346
  expect(ctx.pendingSurfaceActions.has("stale-form-1")).toBe(false);
3347
+ expect(ctx.modeSessions.invalidateStructuralWait).toHaveBeenCalledTimes(
3348
+ 2,
3349
+ );
3350
+ expect(ctx.modeSessions.invalidateStructuralWait).toHaveBeenCalledWith({
3351
+ kind: "surface",
3352
+ responseId: "stale-table-1",
3353
+ });
3354
+ expect(ctx.modeSessions.invalidateStructuralWait).toHaveBeenCalledWith({
3355
+ kind: "surface",
3356
+ responseId: "stale-form-1",
3357
+ });
3358
+ });
3359
+
3360
+ test("tracking cleanup failure does not prevent a new user turn", async () => {
3361
+ const events: AssistantEvent[] = [];
3362
+ const ctx = makeCtx();
3363
+ ctx.pendingSurfaceActions.set("stale-surface", { surfaceType: "form" });
3364
+ ctx.modeSessions.invalidateStructuralWait = () => {
3365
+ throw new Error("session tracking unavailable");
3366
+ };
3367
+ await runAgentLoopImpl(
3368
+ ctx,
3369
+ "hello",
3370
+ "msg-1",
3371
+ (event) => events.push(event),
3372
+ { isUserMessage: true },
3373
+ );
3374
+ expect(ctx.pendingSurfaceActions.has("stale-surface")).toBe(false);
3375
+ expect(
3376
+ events.filter((event) => event.type === "message_complete"),
3377
+ ).toHaveLength(1);
3378
+ expect(events.some((event) => event.type === "error")).toBe(false);
2874
3379
  });
2875
3380
 
2876
3381
  test("withholds the dismissal event and keeps the surface pending when its persisted write fails", async () => {
@@ -2904,6 +3409,7 @@ describe("session-agent-loop", () => {
2904
3409
  // The card stays live on the client, so the daemon must keep treating it
2905
3410
  // as pending; the sweep retries on the next user message.
2906
3411
  expect(ctx.pendingSurfaceActions.has("stale-table-2")).toBe(true);
3412
+ expect(ctx.modeSessions.invalidateStructuralWait).not.toHaveBeenCalled();
2907
3413
  });
2908
3414
 
2909
3415
  test("dismisses the remaining surfaces when one write fails", async () => {
@@ -2967,6 +3473,7 @@ describe("session-agent-loop", () => {
2967
3473
  expect(completeEvents).toHaveLength(0);
2968
3474
  // The pending surface should still be there
2969
3475
  expect(ctx.pendingSurfaceActions.has("active-table-1")).toBe(true);
3476
+ expect(ctx.modeSessions.invalidateStructuralWait).not.toHaveBeenCalled();
2970
3477
  });
2971
3478
 
2972
3479
  test("no-op when no pending surfaces exist", async () => {
@@ -3025,6 +3532,11 @@ describe("session-agent-loop", () => {
3025
3532
  isUserMessage: true,
3026
3533
  });
3027
3534
 
3535
+ expect(ctx.pendingSurfaceActions.has("stale-table-1")).toBe(false);
3536
+ expect(ctx.modeSessions.invalidateStructuralWait).toHaveBeenCalledWith({
3537
+ kind: "surface",
3538
+ responseId: "stale-table-1",
3539
+ });
3028
3540
  expect(ctx.isProcessing()).toBe(false);
3029
3541
  expect(ctx.abortController).toBeNull();
3030
3542
  expect(ctx.currentRequestId).toBeUndefined();
@@ -3292,9 +3804,18 @@ describe("session-agent-loop", () => {
3292
3804
  // shape too, so a reload explains why the assistant stopped instead of
3293
3805
  // ending on a bare tool call.
3294
3806
  reserveMessageMock
3295
- .mockImplementationOnce(async () => ({ id: "msg-reserve-1" }))
3296
- .mockImplementationOnce(async () => ({ id: "msg-reserve-2" }))
3297
- .mockImplementationOnce(async () => ({ id: "msg-reserve-3" }));
3807
+ .mockImplementationOnce(async () => ({
3808
+ id: "msg-reserve-1",
3809
+ createdAt: 1_700_000_000_051,
3810
+ }))
3811
+ .mockImplementationOnce(async () => ({
3812
+ id: "msg-reserve-2",
3813
+ createdAt: 1_700_000_000_052,
3814
+ }))
3815
+ .mockImplementationOnce(async () => ({
3816
+ id: "msg-reserve-3",
3817
+ createdAt: 1_700_000_000_053,
3818
+ }));
3298
3819
  mockConversationErrorClassification = DAILY_LIMIT_CLASSIFICATION;
3299
3820
 
3300
3821
  // GIVEN a run whose first call asks for a tool, the tool succeeds, and
@@ -3806,8 +4327,14 @@ describe("session-agent-loop", () => {
3806
4327
  // `llm_call_started` must delete the stranded row so the transcript
3807
4328
  // does not accumulate empty assistant bubbles.
3808
4329
  reserveMessageMock
3809
- .mockImplementationOnce(async () => ({ id: "msg-strand-A" }))
3810
- .mockImplementationOnce(async () => ({ id: "msg-strand-B" }));
4330
+ .mockImplementationOnce(async () => ({
4331
+ id: "msg-strand-A",
4332
+ createdAt: 1_700_000_000_051,
4333
+ }))
4334
+ .mockImplementationOnce(async () => ({
4335
+ id: "msg-strand-B",
4336
+ createdAt: 1_700_000_000_052,
4337
+ }));
3811
4338
  // Indexer/projector mocks default to no-op; no finalized row in this
3812
4339
  // test, so `mockMessageById` stays null.
3813
4340
 
@@ -3869,6 +4396,7 @@ describe("session-agent-loop", () => {
3869
4396
  // reservation id.
3870
4397
  reserveMessageMock.mockImplementationOnce(async () => ({
3871
4398
  id: "msg-orphaned-reservation",
4399
+ createdAt: 1_700_000_000_051,
3872
4400
  }));
3873
4401
 
3874
4402
  // GIVEN a real loop that reserves an assistant row at
@@ -3911,6 +4439,7 @@ describe("session-agent-loop", () => {
3911
4439
  test("managed-key provider-error cleanup publishes message invalidation after deleting the reservation", async () => {
3912
4440
  reserveMessageMock.mockImplementationOnce(async () => ({
3913
4441
  id: "msg-managed-key-reservation",
4442
+ createdAt: 1_700_000_000_051,
3914
4443
  }));
3915
4444
  mockConversationErrorClassification = {
3916
4445
  code: "MANAGED_KEY_INVALID",
@@ -4247,6 +4776,7 @@ describe("session-agent-loop", () => {
4247
4776
  // error message lands.
4248
4777
  reserveMessageMock.mockImplementationOnce(async () => ({
4249
4778
  id: "msg-orphan-with-partial",
4779
+ createdAt: 1_700_000_000_051,
4250
4780
  }));
4251
4781
 
4252
4782
  // GIVEN a real loop whose provider streams a delta — landing a debounced