@vellumai/assistant 0.12.2 → 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 (772) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/ARCHITECTURE.md +43 -29
  4. package/Dockerfile +63 -0
  5. package/docs/architecture/integrations.md +7 -0
  6. package/docs/architecture/memory.md +17 -2
  7. package/docs/architecture/security.md +10 -5
  8. package/docs/architecture/turn-actor.md +2 -0
  9. package/docs/credential-execution-service.md +1 -1
  10. package/docs/desktop-browser-cli.md +50 -5
  11. package/docs/live-voice-task-outcomes.md +100 -0
  12. package/docs/mode-sessions.md +183 -0
  13. package/docs/voice-input-diagnostics.md +58 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  19. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  20. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  26. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  28. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
  29. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
  30. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
  31. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  32. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  35. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  36. package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
  37. package/openapi.yaml +716 -16
  38. package/package.json +1 -1
  39. package/scripts/smoke-desktop-browser-cli.ts +11 -26
  40. package/scripts/smoke-desktop-window-theme.ts +2 -3
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/scripts/voice-ttft-spike.ts +2 -2
  43. package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
  44. package/src/__tests__/agent-loop-provider-error-recording.test.ts +87 -19
  45. package/src/__tests__/agent-loop.test.ts +259 -0
  46. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  47. package/src/__tests__/annotate-activity-metadata.test.ts +3 -1
  48. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  49. package/src/__tests__/answered-question-persistence.test.ts +2 -0
  50. package/src/__tests__/anthropic-gateway-shared.test.ts +22 -0
  51. package/src/__tests__/anthropic-provider.test.ts +55 -0
  52. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  53. package/src/__tests__/approval-message-composer.test.ts +1 -118
  54. package/src/__tests__/approval-routes-http.test.ts +3 -0
  55. package/src/__tests__/assistant-event-hub-connection-history.test.ts +111 -0
  56. package/src/__tests__/assistant-event-hub-targeted.test.ts +50 -0
  57. package/src/__tests__/assistant-event-targeting.test.ts +186 -0
  58. package/src/__tests__/assistant-stream-state.test.ts +29 -9
  59. package/src/__tests__/attachments-store.test.ts +22 -3
  60. package/src/__tests__/browser-fill-credential.test.ts +11 -22
  61. package/src/__tests__/btw-routes.test.ts +4 -1
  62. package/src/__tests__/build-persisted-content.test.ts +1 -1
  63. package/src/__tests__/call-conversation-messages.test.ts +5 -0
  64. package/src/__tests__/call-recovery.test.ts +7 -2
  65. package/src/__tests__/call-routes-http.test.ts +12 -0
  66. package/src/__tests__/call-store.test.ts +7 -2
  67. package/src/__tests__/channel-approval.test.ts +9 -14
  68. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  69. package/src/__tests__/channel-retry-sweep.test.ts +9 -0
  70. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  71. package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
  72. package/src/__tests__/checker.test.ts +11 -3
  73. package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
  74. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  75. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  76. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +0 -25
  77. package/src/__tests__/computer-use-tools.test.ts +1 -233
  78. package/src/__tests__/config-model-image-provider.test.ts +39 -0
  79. package/src/__tests__/config-schema.test.ts +16 -0
  80. package/src/__tests__/config-watcher.test.ts +24 -0
  81. package/src/__tests__/context-search-agent-runner.test.ts +260 -1
  82. package/src/__tests__/context-search-fanout.test.ts +1 -1
  83. package/src/__tests__/context-search-types.test.ts +7 -7
  84. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
  85. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
  86. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  87. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  88. package/src/__tests__/conversation-agent-loop.test.ts +797 -16
  89. package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
  90. package/src/__tests__/conversation-attachments.test.ts +106 -0
  91. package/src/__tests__/conversation-evictor.test.ts +62 -37
  92. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  93. package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
  94. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
  95. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  96. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  97. package/src/__tests__/conversation-retry-route.test.ts +22 -0
  98. package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
  99. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
  100. package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
  101. package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
  102. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  103. package/src/__tests__/conversation-runtime-assembly.test.ts +29 -0
  104. package/src/__tests__/conversation-surface-snapshots.test.ts +543 -0
  105. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
  106. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
  107. package/src/__tests__/conversation-tool-setup-attribution.test.ts +23 -0
  108. package/src/__tests__/conversation-undo.test.ts +61 -0
  109. package/src/__tests__/credential-execution-client.test.ts +72 -55
  110. package/src/__tests__/credential-prompt-route.test.ts +35 -3
  111. package/src/__tests__/cu-unified-flow.test.ts +82 -1
  112. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  113. package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
  114. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
  115. package/src/__tests__/dm-backfill.test.ts +91 -1
  116. package/src/__tests__/document-create-route.test.ts +88 -0
  117. package/src/__tests__/document-sync-tags.test.ts +27 -0
  118. package/src/__tests__/emit-signal-routing-intent.test.ts +142 -2
  119. package/src/__tests__/events-tail-route.test.ts +58 -0
  120. package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
  121. package/src/__tests__/guardian-action-sweep.test.ts +16 -0
  122. package/src/__tests__/guardian-dispatch.test.ts +13 -0
  123. package/src/__tests__/headless-browser-interactions.test.ts +64 -20
  124. package/src/__tests__/heartbeat-service.test.ts +2 -59
  125. package/src/__tests__/helpers/mock-conversation.ts +21 -0
  126. package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
  127. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +5 -0
  128. package/src/__tests__/history-repair.test.ts +53 -0
  129. package/src/__tests__/host-cu-proxy.test.ts +46 -2
  130. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  131. package/src/__tests__/image-credentials.test.ts +49 -0
  132. package/src/__tests__/image-service-dispatcher.test.ts +45 -0
  133. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  134. package/src/__tests__/interrupt-on-send.test.ts +57 -43
  135. package/src/__tests__/interrupt-turn-note.test.ts +5 -6
  136. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  137. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  138. package/src/__tests__/media-generate-image.test.ts +49 -1
  139. package/src/__tests__/message-provenance-contact-id.test.ts +69 -0
  140. package/src/__tests__/migration-export-to-gcs.test.ts +142 -0
  141. package/src/__tests__/mode-session-history.test.ts +245 -0
  142. package/src/__tests__/mode-session-recovery.test.ts +222 -0
  143. package/src/__tests__/mode-session-wire.test.ts +141 -0
  144. package/src/__tests__/notification-guardian-path.test.ts +7 -0
  145. package/src/__tests__/notification-telegram-adapter.test.ts +2 -2
  146. package/src/__tests__/notification-vellum-adapter.test.ts +34 -0
  147. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  148. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  149. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  150. package/src/__tests__/oauth-provider-serializer.test.ts +13 -0
  151. package/src/__tests__/oauth-providers-routes.test.ts +3 -0
  152. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  153. package/src/__tests__/persist-media-references.test.ts +50 -0
  154. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  155. package/src/__tests__/plugin-api-provider.test.ts +5 -0
  156. package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
  157. package/src/__tests__/plugin-import-boundary-guard.test.ts +9 -1
  158. package/src/__tests__/process-message-display-content.test.ts +56 -0
  159. package/src/__tests__/reaction-persistence.test.ts +5 -0
  160. package/src/__tests__/request-secret-standalone-channel-gate.test.ts +25 -9
  161. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  162. package/src/__tests__/runtime-events-sse-reconnect.test.ts +1 -1
  163. package/src/__tests__/schedule-routes.test.ts +20 -0
  164. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  165. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  166. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +73 -0
  167. package/src/__tests__/secret-ingress-http.test.ts +2 -0
  168. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  169. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  170. package/src/__tests__/secret-scanner.test.ts +20 -0
  171. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  172. package/src/__tests__/secure-keys.test.ts +50 -4
  173. package/src/__tests__/send-endpoint-busy.test.ts +163 -1
  174. package/src/__tests__/server-history-render.test.ts +91 -1
  175. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  176. package/src/__tests__/settings-routes.test.ts +50 -1
  177. package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
  178. package/src/__tests__/skills.test.ts +5 -4
  179. package/src/__tests__/subagent-fork-notifications.test.ts +1 -2
  180. package/src/__tests__/subagent-manager-notify.test.ts +1 -2
  181. package/src/__tests__/subagent-notify-parent.test.ts +146 -39
  182. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +2 -4
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  184. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
  185. package/src/__tests__/terminal-tools.test.ts +8 -0
  186. package/src/__tests__/thread-backfill.test.ts +53 -0
  187. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  188. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  189. package/src/__tests__/tool-result-metadata-plumbing.test.ts +66 -1
  190. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  191. package/src/__tests__/twilio-routes.test.ts +1 -0
  192. package/src/__tests__/unicode.test.ts +36 -0
  193. package/src/__tests__/web-search-backend-failure.test.ts +1 -1
  194. package/src/activation/progress-store.test.ts +12 -7
  195. package/src/agent/loop.ts +136 -53
  196. package/src/agent/tool-result-follow-up.ts +70 -0
  197. package/src/api/attachment-provenance.test.ts +68 -0
  198. package/src/api/computer-use-tool.test.ts +55 -0
  199. package/src/api/computer-use-tool.ts +27 -0
  200. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  201. package/src/api/events/assistant-turn-start.ts +3 -0
  202. package/src/api/events/desktop-activity-changed.ts +10 -0
  203. package/src/api/events/generation-handoff.ts +2 -0
  204. package/src/api/events/message-complete.ts +2 -0
  205. package/src/api/events/notification-conversation-created.ts +14 -16
  206. package/src/api/events/notification-intent.ts +4 -2
  207. package/src/api/events/question-request.ts +1 -0
  208. package/src/api/events/tool-result.ts +85 -0
  209. package/src/api/events/tool-use-start.ts +3 -0
  210. package/src/api/events/user-message-echo.ts +3 -0
  211. package/src/api/index.ts +42 -6
  212. package/src/api/mode-session.test.ts +131 -0
  213. package/src/api/mode-session.ts +148 -0
  214. package/src/api/remember-tool.ts +15 -0
  215. package/src/api/responses/conversation-message.ts +9 -0
  216. package/src/approvals/AGENTS.md +4 -2
  217. package/src/approvals/approval-primitive.ts +5 -2
  218. package/src/approvals/scoped-approval-grants.ts +6 -2
  219. package/src/browser/__tests__/operations.test.ts +29 -0
  220. package/src/browser/operation-meta.ts +2 -1
  221. package/src/browser/operations.ts +29 -0
  222. package/src/browser/virtual-desktop-target.ts +2 -9
  223. package/src/bundler/bundle-signer.ts +1 -3
  224. package/src/calls/__tests__/barge-in-guard.test.ts +3 -0
  225. package/src/calls/__tests__/call-controller.test.ts +94 -0
  226. package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
  227. package/src/calls/__tests__/voice-control-protocol.test.ts +15 -0
  228. package/src/calls/__tests__/voice-escalation-judge.test.ts +209 -0
  229. package/src/calls/__tests__/voice-leg-coordinator.test.ts +168 -0
  230. package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
  231. package/src/calls/__tests__/voice-session-bridge.test.ts +576 -122
  232. package/src/calls/barge-in-guard.ts +5 -0
  233. package/src/calls/call-controller.ts +196 -1
  234. package/src/calls/call-domain.ts +4 -0
  235. package/src/calls/call-store.ts +87 -2
  236. package/src/calls/guardian-action-sweep.ts +22 -8
  237. package/src/calls/media-stream-audio-transcode.ts +1 -3
  238. package/src/calls/typesafe-noul.ts +156 -0
  239. package/src/calls/voice-control-protocol.ts +6 -0
  240. package/src/calls/voice-escalation-judge.ts +145 -0
  241. package/src/calls/voice-escalation-target.ts +10 -0
  242. package/src/calls/voice-leg-coordinator.ts +121 -3
  243. package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
  244. package/src/calls/voice-session-bridge.ts +256 -112
  245. package/src/calls/voice-triage-escalate.ts +7 -0
  246. package/src/cli/__tests__/roadmap-cli.test.ts +4 -41
  247. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  248. package/src/cli/commands/__tests__/notifications.test.ts +35 -10
  249. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  250. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  251. package/src/cli/commands/bash.help.ts +4 -3
  252. package/src/cli/commands/browser.help.ts +9 -6
  253. package/src/cli/commands/browser.ts +0 -1
  254. package/src/cli/commands/clients.help.ts +46 -0
  255. package/src/cli/commands/clients.ts +116 -0
  256. package/src/cli/commands/credentials.help.ts +9 -4
  257. package/src/cli/commands/notifications.help.ts +16 -2
  258. package/src/cli/commands/notifications.ts +30 -8
  259. package/src/cli/commands/oauth/index.help.ts +10 -0
  260. package/src/cli/commands/oauth/providers.ts +12 -0
  261. package/src/cli/commands/oauth/request.test.ts +179 -2
  262. package/src/cli/commands/oauth/request.ts +41 -10
  263. package/src/cli/commands/plugins.ts +12 -5
  264. package/src/cli/commands/roadmap.help.ts +18 -21
  265. package/src/cli/commands/roadmap.ts +4 -21
  266. package/src/cli/commands/schedules.ts +2 -0
  267. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  268. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  269. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +5 -2
  270. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  271. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  272. package/src/cli/lib/bundled-marketplace.json +906 -1
  273. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  274. package/src/cli/lib/inspect-plugin.ts +11 -4
  275. package/src/cli/lib/plugin-marketplace.ts +8 -0
  276. package/src/cli/lib/publish-plugin.ts +1 -2
  277. package/src/cli/lib/upgrade-plugin.ts +19 -11
  278. package/src/cli/output.ts +18 -4
  279. package/src/config/__tests__/default-provider.test.ts +6 -0
  280. package/src/config/__tests__/memory-retrospective-schema.test.ts +14 -1
  281. package/src/config/bundled-skills/computer-use/SKILL.md +1 -1
  282. package/src/config/bundled-skills/computer-use/TOOLS.json +89 -1
  283. package/src/config/bundled-skills/image-studio/SKILL.md +17 -2
  284. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +7 -11
  285. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  286. package/src/config/bundled-skills/sequences/TOOLS.json +1 -5
  287. package/src/config/call-site-defaults.ts +9 -0
  288. package/src/config/feature-flag-registry.json +16 -0
  289. package/src/config/loader.ts +9 -17
  290. package/src/config/platform-identity.ts +8 -8
  291. package/src/config/profile-text-generation.test.ts +51 -0
  292. package/src/config/profile-text-generation.ts +51 -0
  293. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  294. package/src/config/schemas/__tests__/tools.test.ts +31 -0
  295. package/src/config/schemas/call-site-catalog.ts +14 -0
  296. package/src/config/schemas/channels.ts +0 -18
  297. package/src/config/schemas/llm.ts +9 -2
  298. package/src/config/schemas/mcp.ts +6 -1
  299. package/src/config/schemas/memory-retrospective.ts +11 -2
  300. package/src/config/schemas/memory-v3.ts +12 -0
  301. package/src/config/schemas/services.ts +15 -6
  302. package/src/config/schemas/tools.ts +10 -0
  303. package/src/config/session-groups-gate.ts +21 -0
  304. package/src/config/skills.ts +2 -6
  305. package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
  306. package/src/conversations/message-consolidation.ts +102 -5
  307. package/src/credential-execution/ces-connect.test.ts +69 -0
  308. package/src/credential-execution/ces-connect.ts +115 -0
  309. package/src/credential-execution/ces-runtime.ts +55 -93
  310. package/src/credential-execution/executable-discovery.ts +34 -142
  311. package/src/credential-execution/process-manager.test.ts +20 -24
  312. package/src/credential-execution/process-manager.ts +24 -19
  313. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
  314. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  315. package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
  316. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  317. package/src/daemon/assistant-attachments.ts +20 -12
  318. package/src/daemon/browser-mode-session.test.ts +402 -0
  319. package/src/daemon/browser-mode-session.ts +186 -0
  320. package/src/daemon/chat-credential-redaction.ts +16 -1
  321. package/src/daemon/computer-use-mode-session.test.ts +210 -0
  322. package/src/daemon/computer-use-mode-session.ts +105 -0
  323. package/src/daemon/config-watcher.ts +10 -0
  324. package/src/daemon/conversation-agent-loop-handlers.ts +132 -13
  325. package/src/daemon/conversation-agent-loop.ts +231 -25
  326. package/src/daemon/conversation-attachments.ts +93 -12
  327. package/src/daemon/conversation-evictor.ts +5 -24
  328. package/src/daemon/conversation-history.ts +10 -0
  329. package/src/daemon/conversation-interrupt-repair.ts +4 -43
  330. package/src/daemon/conversation-interrupt.ts +7 -11
  331. package/src/daemon/conversation-messaging.ts +58 -5
  332. package/src/daemon/conversation-mode-session.test.ts +1667 -0
  333. package/src/daemon/conversation-mode-session.ts +1008 -0
  334. package/src/daemon/conversation-process.ts +5 -0
  335. package/src/daemon/conversation-runtime-assembly.ts +10 -20
  336. package/src/daemon/conversation-skill-tools.ts +1 -0
  337. package/src/daemon/conversation-store.ts +4 -4
  338. package/src/daemon/conversation-surface-snapshots.ts +135 -0
  339. package/src/daemon/conversation-surfaces.ts +113 -5
  340. package/src/daemon/conversation-tool-setup.ts +79 -6
  341. package/src/daemon/conversation-turn-finalize.ts +26 -13
  342. package/src/daemon/conversation.ts +215 -58
  343. package/src/daemon/daemon-control.ts +3 -7
  344. package/src/daemon/handlers/config-model.ts +7 -5
  345. package/src/daemon/handlers/shared.ts +55 -16
  346. package/src/daemon/host-cu-proxy.ts +51 -22
  347. package/src/daemon/lifecycle.ts +30 -13
  348. package/src/daemon/mcp-reload-service.ts +1 -1
  349. package/src/daemon/message-protocol.ts +0 -1
  350. package/src/daemon/message-provenance.ts +28 -4
  351. package/src/daemon/message-types/shared.ts +1 -0
  352. package/src/daemon/mode-session-startup-recovery.ts +23 -0
  353. package/src/daemon/mode-session-tracking.ts +54 -0
  354. package/src/daemon/orphan-reaper.ts +4 -3
  355. package/src/daemon/persist-media-references.ts +20 -3
  356. package/src/daemon/process-message.ts +13 -4
  357. package/src/daemon/tool-setup-types.ts +6 -0
  358. package/src/daemon/virtual-desktop-context.ts +21 -0
  359. package/src/daemon/wake-conversation-ops.ts +50 -15
  360. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  361. package/src/desktop/desktop-accessibility-script.ts +88 -0
  362. package/src/desktop/desktop-accessibility.test.ts +160 -0
  363. package/src/desktop/desktop-accessibility.ts +169 -0
  364. package/src/desktop/desktop-automation-lease.test.ts +302 -64
  365. package/src/desktop/desktop-automation-lease.ts +129 -54
  366. package/src/desktop/desktop-browser-endpoint.ts +4 -0
  367. package/src/desktop/desktop-computer-use-routing.test.ts +330 -0
  368. package/src/desktop/desktop-computer-use.test.ts +449 -0
  369. package/src/desktop/desktop-computer-use.ts +502 -0
  370. package/src/desktop/desktop-dependencies.ts +47 -293
  371. package/src/desktop/desktop-display.ts +2 -2
  372. package/src/desktop/desktop-help.ts +15 -0
  373. package/src/desktop/desktop-panel-config.test.ts +13 -3
  374. package/src/desktop/desktop-panel-config.ts +109 -15
  375. package/src/desktop/desktop-session-manager.test.ts +69 -21
  376. package/src/desktop/desktop-session-manager.ts +76 -22
  377. package/src/desktop/desktop-stream-bridge.test.ts +76 -132
  378. package/src/desktop/desktop-stream-bridge.ts +9 -11
  379. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  380. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  381. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  382. package/src/desktop/desktop-wallpaper.ts +34 -102
  383. package/src/desktop/desktop-window-theme.ts +5 -25
  384. package/src/desktop/virtual-desktop-feature.ts +53 -3
  385. package/src/desktop/virtual-desktop-platform.test.ts +5 -9
  386. package/src/documents/document-store.ts +23 -0
  387. package/src/heartbeat/__tests__/heartbeat-service.test.ts +22 -26
  388. package/src/heartbeat/heartbeat-service.ts +5 -33
  389. package/src/ipc/__tests__/browser-ipc.test.ts +215 -6
  390. package/src/ipc/assistant-server.ts +2 -0
  391. package/src/ipc/routes/__tests__/channel-reply-ipc-routes.test.ts +111 -0
  392. package/src/ipc/routes/channel-reply-ipc-routes.ts +73 -0
  393. package/src/live-voice/__tests__/continuation-judge.test.ts +151 -0
  394. package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
  395. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +743 -31
  396. package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +191 -10
  397. package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
  398. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +5 -1
  399. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
  400. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
  401. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +224 -1
  402. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +101 -0
  403. package/src/live-voice/__tests__/live-voice-tts.test.ts +44 -0
  404. package/src/live-voice/__tests__/live-voice-vad.test.ts +939 -75
  405. package/src/live-voice/__tests__/microphone-idle-gate.test.ts +87 -0
  406. package/src/live-voice/__tests__/session-controls.test.ts +23 -0
  407. package/src/live-voice/__tests__/task-outcomes.test.ts +117 -0
  408. package/src/live-voice/__tests__/voice-input-diagnostics.test.ts +117 -0
  409. package/src/live-voice/camera-mode-session.test.ts +265 -0
  410. package/src/live-voice/camera-mode-session.ts +277 -0
  411. package/src/live-voice/continuation-judge.ts +123 -0
  412. package/src/live-voice/live-activity-reporter.ts +115 -6
  413. package/src/live-voice/live-voice-manager.ts +36 -1
  414. package/src/live-voice/live-voice-photo.ts +88 -7
  415. package/src/live-voice/live-voice-session-manager.ts +15 -0
  416. package/src/live-voice/live-voice-session.ts +1443 -747
  417. package/src/live-voice/live-voice-tts.ts +22 -4
  418. package/src/live-voice/microphone-idle-gate.ts +56 -0
  419. package/src/live-voice/protocol.ts +171 -4
  420. package/src/live-voice/session-controls.ts +11 -3
  421. package/src/live-voice/task-outcomes.ts +131 -0
  422. package/src/live-voice/voice-input-diagnostics.ts +117 -0
  423. package/src/mcp/__tests__/manager-tool-caps.test.ts +156 -0
  424. package/src/mcp/__tests__/startup.test.ts +30 -7
  425. package/src/mcp/__tests__/tool-caps.test.ts +134 -0
  426. package/src/mcp/manager.ts +171 -101
  427. package/src/mcp/startup.ts +33 -13
  428. package/src/mcp/tool-caps.ts +184 -0
  429. package/src/media/__tests__/image-models.test.ts +58 -0
  430. package/src/media/image-credentials.ts +5 -0
  431. package/src/media/image-models.ts +52 -0
  432. package/src/media/image-service.ts +8 -0
  433. package/src/media/openrouter-image-service.test.ts +133 -0
  434. package/src/media/openrouter-image-service.ts +153 -0
  435. package/src/media/types.ts +1 -1
  436. package/src/messaging/provider-message-metadata.ts +3 -3
  437. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +65 -11
  438. package/src/messaging/providers/discord/send.ts +4 -17
  439. package/src/messaging/providers/discord/transport.ts +2 -2
  440. package/src/messaging/providers/outlook/client.ts +16 -22
  441. package/src/messaging/providers/telegram-bot/send.test.ts +236 -1
  442. package/src/messaging/providers/telegram-bot/send.ts +10 -7
  443. package/src/messaging/providers/telegram-bot/transport.ts +3 -10
  444. package/src/messaging/providers/whatsapp/send.test.ts +169 -4
  445. package/src/monitoring/__tests__/control.test.ts +17 -5
  446. package/src/monitoring/control.ts +15 -5
  447. package/src/notifications/AGENTS.md +1 -1
  448. package/src/notifications/README.md +83 -99
  449. package/src/notifications/__tests__/broadcaster.test.ts +1 -1
  450. package/src/notifications/__tests__/channel-allowlist.test.ts +39 -0
  451. package/src/notifications/__tests__/copy-composer.test.ts +71 -1
  452. package/src/notifications/__tests__/decision-engine.test.ts +79 -2
  453. package/src/notifications/__tests__/deterministic-checks.test.ts +10 -10
  454. package/src/notifications/__tests__/home-feed-side-effect.test.ts +93 -4
  455. package/src/notifications/__tests__/notification-utils.test.ts +18 -0
  456. package/src/notifications/adapters/macos.ts +37 -46
  457. package/src/notifications/broadcaster.ts +5 -6
  458. package/src/notifications/channel-allowlist.ts +44 -0
  459. package/src/notifications/copy-composer.ts +11 -26
  460. package/src/notifications/decision-engine.ts +24 -14
  461. package/src/notifications/deterministic-checks.ts +1 -1
  462. package/src/notifications/emit-signal.ts +123 -37
  463. package/src/notifications/home-feed-side-effect.ts +9 -7
  464. package/src/notifications/notification-utils.ts +19 -0
  465. package/src/notifications/schedule-result-producer.ts +4 -6
  466. package/src/notifications/signal.ts +4 -19
  467. package/src/notifications/types.ts +15 -0
  468. package/src/oauth/AGENTS.md +2 -0
  469. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  470. package/src/oauth/connection-resolver.ts +1 -2
  471. package/src/oauth/identity-verifier.ts +24 -0
  472. package/src/oauth/oauth-store.ts +12 -4
  473. package/src/oauth/provider-serializer.ts +8 -0
  474. package/src/oauth/seed-providers.ts +42 -0
  475. package/src/onboarding/onboarding-events-store.ts +99 -12
  476. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  477. package/src/permissions/confirmation-guardian-request.ts +2 -3
  478. package/src/permissions/question-prompter.test.ts +31 -0
  479. package/src/permissions/question-prompter.ts +2 -0
  480. package/src/persistence/__tests__/client-connection-events-store.test.ts +322 -0
  481. package/src/persistence/attachments-store.ts +84 -44
  482. package/src/persistence/client-connection-events-store.ts +377 -0
  483. package/src/persistence/conversation-crud.ts +166 -26
  484. package/src/persistence/conversation-mode-sessions.test.ts +424 -0
  485. package/src/persistence/conversation-mode-sessions.ts +460 -0
  486. package/src/persistence/conversation-plugin-facade.ts +30 -0
  487. package/src/persistence/conversation-queries.ts +6 -19
  488. package/src/persistence/conversation-tool-surface.ts +135 -0
  489. package/src/persistence/conversation-types.test.ts +32 -0
  490. package/src/persistence/conversation-types.ts +26 -4
  491. package/src/persistence/llm-usage-store.ts +3 -9
  492. package/src/persistence/message-metadata.ts +25 -0
  493. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  494. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  495. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  496. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  497. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  498. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  499. package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
  500. package/src/persistence/migrations/382-create-client-connection-events.ts +40 -0
  501. package/src/persistence/migrations/__tests__/382-create-client-connection-events.test.ts +56 -0
  502. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  503. package/src/persistence/schema/conversations.ts +67 -0
  504. package/src/persistence/schema/index.ts +1 -0
  505. package/src/persistence/schema/infrastructure.ts +29 -0
  506. package/src/persistence/schema/oauth.ts +1 -0
  507. package/src/persistence/steps.ts +41 -3
  508. package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
  509. package/src/plugin-api/conversation-turn.ts +31 -7
  510. package/src/plugin-api/effective-context-window.ts +33 -0
  511. package/src/plugin-api/index.ts +35 -6
  512. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  513. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  514. package/src/plugin-api/plugin-name-env.ts +125 -0
  515. package/src/plugin-api/resolve-credential.ts +79 -20
  516. package/src/plugin-api/vision-support.test.ts +22 -0
  517. package/src/plugin-api/vision-support.ts +17 -5
  518. package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
  519. package/src/plugins/active-plugin-env.ts +51 -0
  520. package/src/plugins/defaults/index.ts +24 -0
  521. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  522. package/src/plugins/defaults/memory/__tests__/archive-removal-framing.test.ts +64 -0
  523. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  524. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  525. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  526. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  527. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  528. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  530. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  531. package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
  532. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  533. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  534. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +9 -12
  535. package/src/plugins/defaults/memory/context-search/agent-runner.ts +77 -22
  536. package/src/plugins/defaults/memory/context-search/format.ts +53 -8
  537. package/src/plugins/defaults/memory/context-search/limits.ts +10 -12
  538. package/src/plugins/defaults/memory/context-search/recall-input.ts +38 -0
  539. package/src/plugins/defaults/memory/context-search/search.ts +8 -14
  540. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +1 -0
  541. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  542. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  543. package/src/plugins/defaults/memory/context-search/types.ts +1 -11
  544. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  545. package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +11 -4
  546. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  547. package/src/plugins/defaults/memory/graph/tool-handlers.ts +13 -63
  548. package/src/plugins/defaults/memory/graph/tools.ts +35 -64
  549. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  550. package/src/plugins/defaults/memory/injectors.ts +4 -3
  551. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  552. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  553. package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
  554. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +6 -1
  555. package/src/plugins/defaults/memory/src/memory-item-routes.ts +2 -1
  556. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  557. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  558. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  559. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  560. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  561. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  562. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +91 -51
  563. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  564. package/src/plugins/defaults/memory/tools.test.ts +124 -9
  565. package/src/plugins/defaults/memory/tools.ts +38 -11
  566. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  567. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  568. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  570. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  571. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  572. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  573. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  574. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  575. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +332 -15
  576. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +626 -27
  577. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +148 -28
  578. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  579. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  580. package/src/plugins/defaults/memory/v3/orchestrate.ts +159 -32
  581. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  582. package/src/plugins/defaults/memory/v3/pool-log-store.ts +275 -37
  583. package/src/plugins/defaults/memory/v3/pool-select.test.ts +47 -5
  584. package/src/plugins/defaults/memory/v3/pool-select.ts +710 -59
  585. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  586. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  587. package/src/plugins/defaults/memory/worker-control.ts +15 -5
  588. package/src/plugins/defaults/task-progress/__tests__/registration.test.ts +70 -0
  589. package/src/plugins/defaults/task-progress/__tests__/task-progress-context.test.ts +380 -0
  590. package/src/plugins/defaults/task-progress/hooks/post-compact.ts +19 -0
  591. package/src/plugins/defaults/task-progress/hooks/user-prompt-submit.ts +24 -0
  592. package/src/plugins/defaults/task-progress/package.json +14 -0
  593. package/src/plugins/defaults/task-progress/src/apply-task-progress-context.ts +93 -0
  594. package/src/plugins/defaults/task-progress/src/format-task-progress-context.ts +67 -0
  595. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  596. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  597. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  598. package/src/prompts/delegation-gate.ts +57 -0
  599. package/src/prompts/system-prompt.ts +14 -32
  600. package/src/providers/__tests__/context-overflow-error.test.ts +10 -0
  601. package/src/providers/__tests__/search-provider-catalog.test.ts +18 -0
  602. package/src/providers/anthropic/client.ts +10 -1
  603. package/src/providers/anthropic-gateway-shared.ts +6 -0
  604. package/src/providers/fetch-provider-catalog.ts +11 -0
  605. package/src/providers/gemini/client.ts +25 -8
  606. package/src/providers/inference/adapter-factory.ts +6 -0
  607. package/src/providers/jev/client.test.ts +270 -0
  608. package/src/providers/jev/client.ts +534 -0
  609. package/src/providers/model-catalog.ts +59 -3
  610. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +94 -1
  611. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +106 -1
  612. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +24 -0
  613. package/src/providers/openai/__tests__/google-thought-signature.test.ts +1 -2
  614. package/src/providers/openai/chat-completions-provider.ts +100 -29
  615. package/src/providers/openai/google-thought-signature.ts +0 -6
  616. package/src/providers/openai/responses-provider.ts +23 -1
  617. package/src/providers/ratelimit.ts +1 -3
  618. package/src/providers/search-provider-catalog.ts +24 -0
  619. package/src/providers/server-tool-pairing.ts +16 -7
  620. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +24 -0
  621. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +7 -1
  622. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +50 -1
  623. package/src/providers/speech-to-text/deepgram-flux-frames.ts +11 -0
  624. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +13 -0
  625. package/src/providers/types.ts +5 -0
  626. package/src/routes/control.ts +7 -5
  627. package/src/runtime/AGENTS.md +6 -4
  628. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  629. package/src/runtime/actor-trust-resolver.ts +3 -3
  630. package/src/runtime/agent-wake.ts +32 -5
  631. package/src/runtime/approval-message-composer.ts +3 -69
  632. package/src/runtime/assistant-event-hub.ts +73 -75
  633. package/src/runtime/assistant-event-publish-options.ts +7 -0
  634. package/src/runtime/assistant-event-targeting.ts +79 -0
  635. package/src/runtime/assistant-stream-state.ts +13 -100
  636. package/src/runtime/background-job-runner.ts +5 -5
  637. package/src/runtime/channel-retry-sweep.ts +1 -0
  638. package/src/runtime/guardian-action-message-composer.ts +8 -161
  639. package/src/runtime/guardian-action-service.ts +2 -17
  640. package/src/runtime/guardian-reply-router.ts +29 -40
  641. package/src/runtime/http-server.ts +2 -2
  642. package/src/runtime/http-types.ts +6 -0
  643. package/src/runtime/message-composer-types.ts +3 -61
  644. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +67 -1
  645. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  646. package/src/runtime/migrations/vbundle-builder.ts +24 -1
  647. package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -4
  648. package/src/runtime/migrations/vbundle-import-policy.ts +43 -11
  649. package/src/runtime/migrations/vbundle-importer.ts +4 -1
  650. package/src/runtime/migrations/vbundle-streaming-importer.ts +4 -1
  651. package/src/runtime/migrations/vbundle-validator.ts +1 -0
  652. package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
  653. package/src/runtime/pending-interactions.ts +29 -0
  654. package/src/runtime/routes/__tests__/client-history-routes.test.ts +104 -0
  655. package/src/runtime/routes/__tests__/image-generation-routes.test.ts +97 -0
  656. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  657. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +29 -6
  658. package/src/runtime/routes/browser-context.ts +31 -11
  659. package/src/runtime/routes/browser-routes.ts +43 -4
  660. package/src/runtime/routes/btw-routes.ts +9 -4
  661. package/src/runtime/routes/channel-route-shared.ts +1 -9
  662. package/src/runtime/routes/client-routes.ts +108 -0
  663. package/src/runtime/routes/conversation-management-routes.ts +4 -1
  664. package/src/runtime/routes/conversation-routes.ts +125 -14
  665. package/src/runtime/routes/credential-prompt-routes.ts +4 -3
  666. package/src/runtime/routes/credential-routes.ts +1 -2
  667. package/src/runtime/routes/desktop-setup-routes.test.ts +9 -18
  668. package/src/runtime/routes/desktop-setup-routes.ts +8 -7
  669. package/src/runtime/routes/documents-routes.ts +51 -0
  670. package/src/runtime/routes/email-routes.ts +3 -7
  671. package/src/runtime/routes/events-routes.ts +75 -54
  672. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  673. package/src/runtime/routes/identity-routes.ts +1 -5
  674. package/src/runtime/routes/image-generation-routes.ts +8 -14
  675. package/src/runtime/routes/inbound-message-handler.ts +37 -41
  676. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.test.ts +168 -0
  677. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.ts +96 -0
  678. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +39 -10
  679. package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -9
  680. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +2 -0
  681. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  682. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  683. package/src/runtime/routes/llm-context-normalization.ts +1 -7
  684. package/src/runtime/routes/migration-routes.ts +73 -2
  685. package/src/runtime/routes/notification-routes.ts +10 -0
  686. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  687. package/src/runtime/routes/oauth-providers.ts +11 -0
  688. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  689. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  690. package/src/runtime/routes/plugins-routes.ts +6 -0
  691. package/src/runtime/routes/roadmap-routes.ts +47 -8
  692. package/src/runtime/routes/schedule-routes.ts +2 -0
  693. package/src/runtime/routes/secret-routes.ts +99 -119
  694. package/src/runtime/routes/settings-routes.ts +10 -4
  695. package/src/runtime/routes/watcher-routes.ts +1 -3
  696. package/src/runtime/trust-class.test.ts +7 -6
  697. package/src/runtime/trust-class.ts +9 -17
  698. package/src/schedule/__tests__/worker-control.test.ts +36 -5
  699. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  700. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  701. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  702. package/src/schedule/run-script.ts +2 -2
  703. package/src/schedule/scheduler.ts +47 -11
  704. package/src/schedule/tool-surface-readiness.ts +65 -0
  705. package/src/schedule/worker-control.ts +15 -5
  706. package/src/schedule/worker-mcp.ts +85 -0
  707. package/src/schedule/worker.ts +9 -31
  708. package/src/security/secure-keys.ts +74 -112
  709. package/src/skills/clawhub.ts +3 -8
  710. package/src/skills/managed-store.ts +1 -5
  711. package/src/stt/types.ts +4 -0
  712. package/src/subagent/notify.ts +54 -34
  713. package/src/subagent/parent-notification.ts +6 -0
  714. package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
  715. package/src/telemetry/phone-call-funnel.ts +144 -0
  716. package/src/tools/__tests__/tool-schema-root-combinator-guard.test.ts +81 -0
  717. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  718. package/src/tools/ask-question/ask-question-tool.ts +120 -22
  719. package/src/tools/browser/browser-execution.ts +29 -60
  720. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +561 -39
  721. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +435 -39
  722. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
  723. package/src/tools/client-os.ts +15 -0
  724. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
  725. package/src/tools/computer-use/target.ts +42 -0
  726. package/src/tools/document/document-tool.ts +13 -18
  727. package/src/tools/execution-target.ts +14 -16
  728. package/src/tools/host-terminal/host-shell.ts +16 -7
  729. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
  730. package/src/tools/network/__tests__/web-search.test.ts +203 -0
  731. package/src/tools/network/web-fetch.ts +274 -7
  732. package/src/tools/network/web-search.ts +468 -6
  733. package/src/tools/permission-checker.ts +3 -3
  734. package/src/tools/policy-context.ts +3 -1
  735. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  736. package/src/tools/shared/shell-output.test.ts +10 -0
  737. package/src/tools/shared/shell-output.ts +14 -2
  738. package/src/tools/skills/load.ts +2 -1
  739. package/src/tools/skills/sandbox-runner.ts +24 -4
  740. package/src/tools/skills/scaffold-managed.ts +2 -1
  741. package/src/tools/skills/skill-script-runner.ts +7 -0
  742. package/src/tools/skills/skill-tool-factory.ts +15 -1
  743. package/src/tools/terminal/__tests__/safe-env.test.ts +44 -4
  744. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  745. package/src/tools/terminal/safe-env.ts +39 -24
  746. package/src/tools/terminal/sanitized-bash.ts +15 -2
  747. package/src/tools/terminal/shell-launch.test.ts +162 -0
  748. package/src/tools/terminal/shell.test.ts +29 -0
  749. package/src/tools/terminal/shell.ts +15 -7
  750. package/src/tools/tool-approval-handler.ts +5 -1
  751. package/src/tools/tool-types.ts +3 -3
  752. package/src/tools/types.ts +8 -2
  753. package/src/util/__tests__/worker-process-command.test.ts +5 -4
  754. package/src/util/abort-reasons.ts +18 -26
  755. package/src/util/browser-human-verification.ts +14 -0
  756. package/src/util/canonicalize-identity.test.ts +21 -0
  757. package/src/util/canonicalize-identity.ts +16 -0
  758. package/src/util/errors.ts +13 -3
  759. package/src/util/host-process.test.ts +17 -1
  760. package/src/util/host-process.ts +24 -0
  761. package/src/util/provider-error-patterns.ts +3 -0
  762. package/src/util/unicode.ts +29 -0
  763. package/src/util/worker-process.ts +77 -35
  764. package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
  765. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
  766. package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
  767. package/src/workspace/migrations/registry.ts +2 -0
  768. package/src/__tests__/computer-use-window-schema.test.ts +0 -24
  769. package/src/api/events/heartbeat-alert.ts +0 -20
  770. package/src/daemon/message-types/web-activity.ts +0 -65
  771. package/src/desktop/desktop-dependencies.test.ts +0 -131
  772. package/src/tools/computer-use/definitions.ts +0 -607
@@ -14,15 +14,22 @@ import {
14
14
  import type { LoopToolExecutor } from "../agent/loop.js";
15
15
  import type { AssistantEvent } from "../api/index.js";
16
16
  import { stripInjectionsForCompaction } from "../context/strip-injections.js";
17
+ import type { AttachmentResolutionResult } from "../daemon/conversation-attachments.js";
17
18
  import {
18
19
  queueConversationNotice,
19
20
  resetConversationNoticesForTests,
20
21
  } from "../daemon/conversation-notices.js";
22
+ import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
21
23
  import { getConversationDirName } from "../persistence/conversation-directories.js";
22
24
  import type { UserPromptSubmitContext } from "../plugin-api/types.js";
23
25
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
24
26
  import { registerPlugin } from "../plugins/registry.js";
25
- 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";
26
33
  import { ContextOverflowError } from "../providers/types.js";
27
34
  import {
28
35
  resolveUsageAttribution,
@@ -42,6 +49,11 @@ const conversationDiskViewRealSnapshot = {
42
49
  "../persistence/conversation-disk-view.js",
43
50
  ) as Record<string, unknown>),
44
51
  };
52
+ const channelReplyDeliveryRealSnapshot = {
53
+ ...(createRequire(import.meta.url)(
54
+ "../runtime/channel-reply-delivery.js",
55
+ ) as Record<string, unknown>),
56
+ };
45
57
  // Disable the catalog default so resolution lands on llm.default.
46
58
  const disabledCatalogDefaultProfiles: Record<string, unknown> = {
47
59
  balanced: { source: "managed", status: "disabled" },
@@ -334,12 +346,18 @@ const deleteMessageByIdMock = mock(() => ({
334
346
  segmentIds: [],
335
347
  deletedSummaryIds: [],
336
348
  }));
337
- const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
349
+ const reserveMessageMock = mock(async () => ({
350
+ id: "msg-reserve",
351
+ createdAt: 1_700_000_000_050,
352
+ }));
338
353
  /** Persisted rows the loop reads back. Empty unless a test seeds one. */
339
354
  let mockStoredMessages: unknown[] = [];
340
355
  const updateMessageContentMock = mock(() => {});
341
356
  const finalizeMessageContentMock = mock(() => {});
342
- const addMessageMock = mock(() => ({ id: "mock-msg-id" }));
357
+ const addMessageMock = mock(() => ({
358
+ id: "mock-msg-id",
359
+ createdAt: 1_700_000_000_100,
360
+ }));
343
361
  const updateConversationContextWindowMock = mock(() => {});
344
362
  mock.module("../persistence/conversation-crud.js", () => ({
345
363
  setConversationProcessingStartedAt: () => {},
@@ -418,6 +436,10 @@ afterAll(() => {
418
436
  "../persistence/conversation-disk-view.js",
419
437
  () => conversationDiskViewRealSnapshot,
420
438
  );
439
+ mock.module(
440
+ "../runtime/channel-reply-delivery.js",
441
+ () => channelReplyDeliveryRealSnapshot,
442
+ );
421
443
  });
422
444
 
423
445
  const syncMessageToDiskMock = mock(() => {});
@@ -428,6 +450,19 @@ mock.module("../persistence/conversation-disk-view.js", () => ({
428
450
  rebuildConversationDiskViewFromDbStateMock,
429
451
  }));
430
452
 
453
+ let mockTurnReplyMessageId: string | undefined;
454
+ const resolveTurnReplyMessageIdMock = mock(
455
+ (
456
+ _conversationId: string,
457
+ _userMessageId: string | undefined,
458
+ fallbackMessageId: string,
459
+ ) => mockTurnReplyMessageId ?? fallbackMessageId,
460
+ );
461
+ mock.module("../runtime/channel-reply-delivery.js", () => ({
462
+ ...channelReplyDeliveryRealSnapshot,
463
+ resolveTurnReplyMessageId: resolveTurnReplyMessageIdMock,
464
+ }));
465
+
431
466
  mock.module("../apps/app-store.js", () => ({
432
467
  getApp: () => null,
433
468
  listAppFiles: () => [],
@@ -580,12 +615,16 @@ mock.module("../daemon/conversation-usage.js", () => ({
580
615
  recordUsage: recordUsageMock,
581
616
  }));
582
617
 
583
- const resolveAssistantAttachmentsMock = mock(async () => ({
584
- assistantAttachments: [],
585
- emittedAttachments: [],
586
- directiveWarnings: [],
587
- persistedFiles: [],
588
- }));
618
+ const resolveAssistantAttachmentsMock = mock(
619
+ async (): Promise<AttachmentResolutionResult> => ({
620
+ assistantAttachments: [],
621
+ emittedAttachments: [],
622
+ directiveWarnings: [],
623
+ persistedFiles: [],
624
+ linkedAttachmentIds: [],
625
+ computerUseScreenshotAttachmentIds: [],
626
+ }),
627
+ );
589
628
  mock.module("../daemon/conversation-attachments.js", () => ({
590
629
  resolveAssistantAttachments: resolveAssistantAttachmentsMock,
591
630
  approveHostAttachmentRead: async () => true,
@@ -687,7 +726,7 @@ mock.module("../persistence/llm-request-log-store.js", () => ({
687
726
  // ── Imports (after mocks) ────────────────────────────────────────────
688
727
 
689
728
  import { AgentLoop } from "../agent/loop.js";
690
- import type { Conversation } from "../daemon/conversation.js";
729
+ import { Conversation } from "../daemon/conversation.js";
691
730
  import {
692
731
  applyCompactionResult,
693
732
  runAgentLoopImpl,
@@ -764,6 +803,19 @@ function makeCtx(
764
803
  toolExecutor,
765
804
  });
766
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
+
767
819
  const ctx = asConversation({
768
820
  conversationId: "test-conv",
769
821
  messages: [
@@ -811,6 +863,7 @@ function makeCtx(
811
863
  pendingSurfaceActions: new Map(),
812
864
  surfaceActionRequestIds: new Set<string>(),
813
865
  currentTurnSurfaces: [],
866
+ modeSessions,
814
867
 
815
868
  workingDir: "/tmp",
816
869
  channelCapabilities: undefined,
@@ -894,6 +947,26 @@ function makeCtx(
894
947
  return ctx;
895
948
  }
896
949
 
950
+ function makeSendUserMessageCtx(): Conversation {
951
+ return makeCtx({
952
+ currentCallSite: "mainAgent",
953
+ providerResponses: [
954
+ toolUseResponse("tu_1", "send_user_message", {
955
+ message: "Here is the result.",
956
+ }),
957
+ textResponse("Finished delivery."),
958
+ ],
959
+ loopTools: [
960
+ {
961
+ name: "send_user_message",
962
+ description: "deliver",
963
+ input_schema: { type: "object" },
964
+ },
965
+ ],
966
+ toolExecutor: async () => ({ content: "Delivered.", isError: false }),
967
+ });
968
+ }
969
+
897
970
  /**
898
971
  * What `classifyConversationError` returns for a daily-credit-limit 402 (see
899
972
  * `dailyLimitClassification` in conversation-error.ts). Its `userMessage` is
@@ -987,6 +1060,8 @@ beforeEach(() => {
987
1060
  setAgentLoopExitReasonOnLatestLogMock.mockClear();
988
1061
  syncMessageToDiskMock.mockClear();
989
1062
  rebuildConversationDiskViewFromDbStateMock.mockClear();
1063
+ mockTurnReplyMessageId = undefined;
1064
+ resolveTurnReplyMessageIdMock.mockClear();
990
1065
  emitAssistantReplyNotificationMock.mockClear();
991
1066
  updateMessageMetadataMock.mockClear();
992
1067
  updateMessageMetadataMock.mockImplementation(() => {});
@@ -1038,6 +1113,8 @@ beforeEach(() => {
1038
1113
  emittedAttachments: [],
1039
1114
  directiveWarnings: [],
1040
1115
  persistedFiles: [],
1116
+ linkedAttachmentIds: [],
1117
+ computerUseScreenshotAttachmentIds: [],
1041
1118
  }));
1042
1119
  mockMessageById = null;
1043
1120
  resetConversationNoticesForTests();
@@ -1048,7 +1125,388 @@ beforeEach(() => {
1048
1125
  resetPluginRegistryAndRegisterDefaults();
1049
1126
  });
1050
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
+
1051
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
+
1052
1510
  describe("user-prompt-submit hook failures", () => {
1053
1511
  test("passes the effective profile to hooks even when it was already announced", async () => {
1054
1512
  // Both profiles are complete (provider + model) so each is a usable
@@ -1663,6 +2121,30 @@ describe("session-agent-loop", () => {
1663
2121
  });
1664
2122
  });
1665
2123
 
2124
+ test("releases desktop control before a completed turn accepts another message", async () => {
2125
+ const events: AssistantEvent[] = [];
2126
+ const ctx = makeCtx();
2127
+ const release = spyOn(desktopAutomationLease, "releaseForConversation");
2128
+ const setProcessing = ctx.setProcessing.bind(ctx);
2129
+ ctx.setProcessing = (processing) => {
2130
+ if (!processing) {
2131
+ expect(release).toHaveBeenCalledWith(ctx.conversationId);
2132
+ }
2133
+ setProcessing(processing);
2134
+ };
2135
+ try {
2136
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (event) =>
2137
+ events.push(event),
2138
+ );
2139
+ expect(events.some((event) => event.type === "message_complete")).toBe(
2140
+ true,
2141
+ );
2142
+ expect(release).toHaveBeenCalledTimes(1);
2143
+ } finally {
2144
+ release.mockRestore();
2145
+ }
2146
+ });
2147
+
1666
2148
  describe("tool execution errors via agent loop", () => {
1667
2149
  test("error events from agent loop are classified and emitted", async () => {
1668
2150
  const events: AssistantEvent[] = [];
@@ -1698,6 +2180,157 @@ describe("session-agent-loop", () => {
1698
2180
  const complete = events.find((e) => e.type === "message_complete");
1699
2181
  expect(complete).toBeDefined();
1700
2182
  });
2183
+
2184
+ test("carries automatic screenshot provenance on message completion", async () => {
2185
+ resolveAssistantAttachmentsMock.mockImplementation(async () => ({
2186
+ assistantAttachments: [],
2187
+ emittedAttachments: [
2188
+ {
2189
+ id: "screenshot-1",
2190
+ filename: "computer-use-click.png",
2191
+ mimeType: "image/png",
2192
+ data: "c2NyZWVuc2hvdA==",
2193
+ sourceType: "tool_block" as const,
2194
+ computerUseScreenshot: true,
2195
+ },
2196
+ ],
2197
+ directiveWarnings: [],
2198
+ persistedFiles: [],
2199
+ linkedAttachmentIds: ["screenshot-1"],
2200
+ computerUseScreenshotAttachmentIds: ["screenshot-1"],
2201
+ }));
2202
+ const events: AssistantEvent[] = [];
2203
+ const ctx = makeCtx({ providerResponses: [textResponse("Done")] });
2204
+
2205
+ await runAgentLoopImpl(ctx, "click it", "msg-1", (event) =>
2206
+ events.push(event),
2207
+ );
2208
+
2209
+ const complete = events.find(
2210
+ (event) => event.type === "message_complete",
2211
+ );
2212
+ expect(complete?.attachments?.[0]?.computerUseScreenshot).toBe(true);
2213
+ const syncCalls = syncMessageToDiskMock.mock.calls as unknown as Array<
2214
+ [string, string, number]
2215
+ >;
2216
+ const finalRowSyncs = syncCalls.filter(
2217
+ (call) => call[1] === "msg-reserve",
2218
+ );
2219
+ expect(finalRowSyncs).toHaveLength(1);
2220
+ });
2221
+
2222
+ test("defers an earlier delivered attachment reply to ordered turn settlement", async () => {
2223
+ const featureFlags = await import("../config/assistant-feature-flags.js");
2224
+ const flagSpy = spyOn(
2225
+ featureFlags,
2226
+ "isAssistantFeatureFlagEnabled",
2227
+ ).mockImplementation((key: string) => key === "send-user-message");
2228
+ reserveMessageMock
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
+ }));
2241
+ mockTurnReplyMessageId = "msg-delivered-reply";
2242
+ resolveAssistantAttachmentsMock.mockImplementation(async () => ({
2243
+ assistantAttachments: [],
2244
+ emittedAttachments: [],
2245
+ directiveWarnings: [],
2246
+ persistedFiles: [],
2247
+ linkedAttachmentIds: ["attachment-1"],
2248
+ computerUseScreenshotAttachmentIds: [],
2249
+ }));
2250
+ const assistantSyncsAtTerminal: string[][] = [];
2251
+ const ctx = makeSendUserMessageCtx();
2252
+
2253
+ try {
2254
+ await runAgentLoopImpl(ctx, "click it", "msg-1", (event) => {
2255
+ if (event.type !== "message_complete") {
2256
+ return;
2257
+ }
2258
+ const calls = syncMessageToDiskMock.mock.calls as unknown as Array<
2259
+ [string, string, number]
2260
+ >;
2261
+ assistantSyncsAtTerminal.push(
2262
+ calls
2263
+ .map((call) => call[1])
2264
+ .filter((id) =>
2265
+ ["msg-delivered-reply", "msg-final-private"].includes(id),
2266
+ ),
2267
+ );
2268
+ });
2269
+ } finally {
2270
+ flagSpy.mockRestore();
2271
+ }
2272
+
2273
+ expect(resolveTurnReplyMessageIdMock).toHaveBeenCalledWith(
2274
+ "test-conv",
2275
+ "msg-1",
2276
+ "msg-final-private",
2277
+ );
2278
+ expect(assistantSyncsAtTerminal.at(-1)).toEqual([]);
2279
+ const calls = syncMessageToDiskMock.mock.calls as unknown as Array<
2280
+ [string, string, number]
2281
+ >;
2282
+ expect(
2283
+ calls
2284
+ .map((call) => call[1])
2285
+ .filter((id) =>
2286
+ ["msg-delivered-reply", "msg-final-private"].includes(id),
2287
+ ),
2288
+ ).toEqual(["msg-delivered-reply", "msg-final-private"]);
2289
+ });
2290
+
2291
+ test("does not queue an earlier delivered reply without a linked attachment", async () => {
2292
+ const featureFlags = await import("../config/assistant-feature-flags.js");
2293
+ const flagSpy = spyOn(
2294
+ featureFlags,
2295
+ "isAssistantFeatureFlagEnabled",
2296
+ ).mockImplementation((key: string) => key === "send-user-message");
2297
+ reserveMessageMock
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
+ }));
2310
+ mockTurnReplyMessageId = "msg-delivered-empty";
2311
+
2312
+ try {
2313
+ await runAgentLoopImpl(
2314
+ makeSendUserMessageCtx(),
2315
+ "click it",
2316
+ "msg-1",
2317
+ () => {},
2318
+ );
2319
+ } finally {
2320
+ flagSpy.mockRestore();
2321
+ }
2322
+
2323
+ const calls = syncMessageToDiskMock.mock.calls as unknown as Array<
2324
+ [string, string, number]
2325
+ >;
2326
+ expect(
2327
+ calls
2328
+ .map((call) => call[1])
2329
+ .filter((id) =>
2330
+ ["msg-delivered-empty", "msg-final-empty"].includes(id),
2331
+ ),
2332
+ ).toEqual(["msg-final-empty"]);
2333
+ });
1701
2334
  });
1702
2335
 
1703
2336
  describe("LLM request log persistence", () => {
@@ -1956,6 +2589,43 @@ describe("session-agent-loop", () => {
1956
2589
  const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
1957
2590
  expect(call[5]).toBe("callAgent");
1958
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
+ });
1959
2629
  });
1960
2630
 
1961
2631
  describe("usage accounting", () => {
@@ -2172,6 +2842,48 @@ describe("session-agent-loop", () => {
2172
2842
  );
2173
2843
  });
2174
2844
 
2845
+ test("carries automatic screenshot provenance on generation handoff", async () => {
2846
+ resolveAssistantAttachmentsMock.mockImplementation(async () => ({
2847
+ assistantAttachments: [],
2848
+ emittedAttachments: [
2849
+ {
2850
+ id: "screenshot-1",
2851
+ filename: "computer-use-click.png",
2852
+ mimeType: "image/png",
2853
+ data: "c2NyZWVuc2hvdA==",
2854
+ sourceType: "tool_block" as const,
2855
+ computerUseScreenshot: true,
2856
+ },
2857
+ ],
2858
+ directiveWarnings: [],
2859
+ persistedFiles: [],
2860
+ linkedAttachmentIds: ["screenshot-1"],
2861
+ computerUseScreenshotAttachmentIds: ["screenshot-1"],
2862
+ }));
2863
+ const events: AssistantEvent[] = [];
2864
+ const ctx = makeCtx({
2865
+ providerResponses: [toolUseResponse("tu-1", "file_read", {})],
2866
+ loopTools: [
2867
+ {
2868
+ name: "file_read",
2869
+ description: "Read a file",
2870
+ input_schema: { type: "object", properties: {} },
2871
+ },
2872
+ ],
2873
+ toolExecutor: async () => ({ content: "content", isError: false }),
2874
+ canHandoffAtCheckpoint: () => true,
2875
+ });
2876
+
2877
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (event) =>
2878
+ events.push(event),
2879
+ );
2880
+
2881
+ const handoff = events.find(
2882
+ (event) => event.type === "generation_handoff",
2883
+ );
2884
+ expect(handoff?.attachments?.[0]?.computerUseScreenshot).toBe(true);
2885
+ });
2886
+
2175
2887
  test("continues when canHandoffAtCheckpoint returns false", async () => {
2176
2888
  const events: AssistantEvent[] = [];
2177
2889
 
@@ -2222,11 +2934,23 @@ describe("session-agent-loop", () => {
2222
2934
  },
2223
2935
  };
2224
2936
 
2225
- const ctx = makeCtx({ loopProvider: provider, abortController });
2937
+ const sessions = makeModeSessionDouble();
2938
+ const ctx = makeCtx({
2939
+ loopProvider: provider,
2940
+ abortController,
2941
+ modeSessions: sessions.coordinator,
2942
+ });
2226
2943
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2227
2944
 
2228
2945
  const cancelled = events.find((e) => e.type === "generation_cancelled");
2229
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
+ });
2230
2954
  });
2231
2955
 
2232
2956
  // A `task_progress` card mid-run. `data` mirrors what `ui_show` stores for
@@ -2620,6 +3344,38 @@ describe("session-agent-loop", () => {
2620
3344
  expect(ctx.pendingSurfaceActions.has("page-1")).toBe(true);
2621
3345
  expect(ctx.pendingSurfaceActions.has("stale-table-1")).toBe(false);
2622
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);
2623
3379
  });
2624
3380
 
2625
3381
  test("withholds the dismissal event and keeps the surface pending when its persisted write fails", async () => {
@@ -2653,6 +3409,7 @@ describe("session-agent-loop", () => {
2653
3409
  // The card stays live on the client, so the daemon must keep treating it
2654
3410
  // as pending; the sweep retries on the next user message.
2655
3411
  expect(ctx.pendingSurfaceActions.has("stale-table-2")).toBe(true);
3412
+ expect(ctx.modeSessions.invalidateStructuralWait).not.toHaveBeenCalled();
2656
3413
  });
2657
3414
 
2658
3415
  test("dismisses the remaining surfaces when one write fails", async () => {
@@ -2716,6 +3473,7 @@ describe("session-agent-loop", () => {
2716
3473
  expect(completeEvents).toHaveLength(0);
2717
3474
  // The pending surface should still be there
2718
3475
  expect(ctx.pendingSurfaceActions.has("active-table-1")).toBe(true);
3476
+ expect(ctx.modeSessions.invalidateStructuralWait).not.toHaveBeenCalled();
2719
3477
  });
2720
3478
 
2721
3479
  test("no-op when no pending surfaces exist", async () => {
@@ -2774,6 +3532,11 @@ describe("session-agent-loop", () => {
2774
3532
  isUserMessage: true,
2775
3533
  });
2776
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
+ });
2777
3540
  expect(ctx.isProcessing()).toBe(false);
2778
3541
  expect(ctx.abortController).toBeNull();
2779
3542
  expect(ctx.currentRequestId).toBeUndefined();
@@ -3041,9 +3804,18 @@ describe("session-agent-loop", () => {
3041
3804
  // shape too, so a reload explains why the assistant stopped instead of
3042
3805
  // ending on a bare tool call.
3043
3806
  reserveMessageMock
3044
- .mockImplementationOnce(async () => ({ id: "msg-reserve-1" }))
3045
- .mockImplementationOnce(async () => ({ id: "msg-reserve-2" }))
3046
- .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
+ }));
3047
3819
  mockConversationErrorClassification = DAILY_LIMIT_CLASSIFICATION;
3048
3820
 
3049
3821
  // GIVEN a run whose first call asks for a tool, the tool succeeds, and
@@ -3555,8 +4327,14 @@ describe("session-agent-loop", () => {
3555
4327
  // `llm_call_started` must delete the stranded row so the transcript
3556
4328
  // does not accumulate empty assistant bubbles.
3557
4329
  reserveMessageMock
3558
- .mockImplementationOnce(async () => ({ id: "msg-strand-A" }))
3559
- .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
+ }));
3560
4338
  // Indexer/projector mocks default to no-op; no finalized row in this
3561
4339
  // test, so `mockMessageById` stays null.
3562
4340
 
@@ -3618,6 +4396,7 @@ describe("session-agent-loop", () => {
3618
4396
  // reservation id.
3619
4397
  reserveMessageMock.mockImplementationOnce(async () => ({
3620
4398
  id: "msg-orphaned-reservation",
4399
+ createdAt: 1_700_000_000_051,
3621
4400
  }));
3622
4401
 
3623
4402
  // GIVEN a real loop that reserves an assistant row at
@@ -3660,6 +4439,7 @@ describe("session-agent-loop", () => {
3660
4439
  test("managed-key provider-error cleanup publishes message invalidation after deleting the reservation", async () => {
3661
4440
  reserveMessageMock.mockImplementationOnce(async () => ({
3662
4441
  id: "msg-managed-key-reservation",
4442
+ createdAt: 1_700_000_000_051,
3663
4443
  }));
3664
4444
  mockConversationErrorClassification = {
3665
4445
  code: "MANAGED_KEY_INVALID",
@@ -3996,6 +4776,7 @@ describe("session-agent-loop", () => {
3996
4776
  // error message lands.
3997
4777
  reserveMessageMock.mockImplementationOnce(async () => ({
3998
4778
  id: "msg-orphan-with-partial",
4779
+ createdAt: 1_700_000_000_051,
3999
4780
  }));
4000
4781
 
4001
4782
  // GIVEN a real loop whose provider streams a delta — landing a debounced