@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
@@ -0,0 +1,1008 @@
1
+ import { v7 as uuidv7 } from "uuid";
2
+
3
+ import type {
4
+ ModeSession,
5
+ ModeSessionDescriptor,
6
+ ModeSessionMode,
7
+ ModeSessionSummary,
8
+ } from "../api/mode-session.js";
9
+ import { modeSessionRowStartsDisplay } from "../api/mode-session.js";
10
+ import { updateMessageMetadata } from "../persistence/conversation-crud.js";
11
+ import {
12
+ advanceConversationModeSessionRevision,
13
+ beginConversationModeSession,
14
+ finalizeConversationModeSession,
15
+ getConversationModeSession,
16
+ type ModeSessionWriteResult,
17
+ updateConversationModeSessionActivity,
18
+ updateConversationModeSessionBoundaries,
19
+ } from "../persistence/conversation-mode-sessions.js";
20
+ import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
21
+ import { getLogger } from "../util/logger.js";
22
+ import { bestEffortModeSessionTracking } from "./mode-session-tracking.js";
23
+
24
+ const log = getLogger("conversation-mode-session");
25
+
26
+ export type ModeSessionStructuralKind =
27
+ | "question"
28
+ | "confirmation"
29
+ | "secret"
30
+ | "surface";
31
+
32
+ interface ModeSessionSourceRegistration {
33
+ sourceId: string;
34
+ generation: number;
35
+ session: ModeSessionSummary;
36
+ lifetime: "turn" | "source";
37
+ }
38
+
39
+ export interface ModeSessionSourceReference {
40
+ sourceId: string;
41
+ generation: number;
42
+ activation: number;
43
+ }
44
+
45
+ export interface ModeSessionSourceHandle
46
+ extends ModeSessionSourceReference, ModeSession {}
47
+
48
+ export interface ModeSessionSourceActivation {
49
+ sourceId: string;
50
+ generation: number;
51
+ mode: ModeSessionMode;
52
+ sourceStartedAt: number;
53
+ lifetime?: "turn" | "source";
54
+ }
55
+
56
+ export interface ModeSessionStructuralResponse {
57
+ kind: ModeSessionStructuralKind;
58
+ responseId: string;
59
+ }
60
+
61
+ export interface ModeSessionTerminalDisposition {
62
+ status: "completed" | "interrupted";
63
+ endReason: string;
64
+ }
65
+
66
+ interface TrackedRow {
67
+ id: string;
68
+ at: number;
69
+ stamped: boolean;
70
+ startsDisplayBoundary: boolean;
71
+ }
72
+
73
+ interface TurnState {
74
+ owner?: ModeSession;
75
+ rows: TrackedRow[];
76
+ runtimeState?: "waiting" | "finishing";
77
+ terminalDisposition?: ModeSessionTerminalDisposition;
78
+ }
79
+
80
+ interface SourceState {
81
+ generation: number;
82
+ activation: number;
83
+ session: ModeSession;
84
+ lifetime: "turn" | "source";
85
+ }
86
+
87
+ interface StructuralAssociation {
88
+ conversationId: string;
89
+ kind: ModeSessionStructuralKind;
90
+ owner: ModeSession;
91
+ }
92
+
93
+ export interface ConversationModeSessionCoordinatorDependencies {
94
+ createId(): string;
95
+ beginSession(input: {
96
+ id: string;
97
+ conversationId: string;
98
+ mode: ModeSessionMode;
99
+ sourceStartedAt: number;
100
+ }): ModeSessionWriteResult;
101
+ getSession(conversationId: string, id: string): ModeSessionSummary | null;
102
+ updateActivity(input: {
103
+ id: string;
104
+ conversationId: string;
105
+ expectedRevision: number;
106
+ lastActivityAt: number;
107
+ lastOwnedMessageId?: string | null;
108
+ }): ModeSessionWriteResult;
109
+ updateBoundaries(input: {
110
+ id: string;
111
+ conversationId: string;
112
+ expectedRevision: number;
113
+ firstIncluded: { at: number; messageId: string } | null;
114
+ lastActivityAt: number;
115
+ lastOwnedMessageId: string | null;
116
+ }): ModeSessionWriteResult;
117
+ advanceRevision(input: {
118
+ id: string;
119
+ conversationId: string;
120
+ expectedRevision: number;
121
+ }): ModeSessionWriteResult;
122
+ finalize(input: {
123
+ id: string;
124
+ conversationId: string;
125
+ expectedRevision: number;
126
+ status: "completed" | "interrupted";
127
+ endedAt: number | null;
128
+ endReason: string;
129
+ lastActivityAt?: number;
130
+ lastOwnedMessageId?: string | null;
131
+ }): ModeSessionWriteResult;
132
+ stampMessage(messageId: string, owner: ModeSession): void;
133
+ publishMessagesChanged(conversationId: string): void;
134
+ }
135
+
136
+ const defaultDependencies: ConversationModeSessionCoordinatorDependencies = {
137
+ createId: uuidv7,
138
+ beginSession: beginConversationModeSession,
139
+ getSession: getConversationModeSession,
140
+ updateActivity: updateConversationModeSessionActivity,
141
+ updateBoundaries: updateConversationModeSessionBoundaries,
142
+ advanceRevision: advanceConversationModeSessionRevision,
143
+ finalize: (input) => {
144
+ if (input.status === "completed") {
145
+ if (input.endedAt === null) {
146
+ throw new Error("Completed mode sessions require an end time");
147
+ }
148
+ return finalizeConversationModeSession({
149
+ id: input.id,
150
+ conversationId: input.conversationId,
151
+ expectedRevision: input.expectedRevision,
152
+ status: "completed",
153
+ endedAt: input.endedAt,
154
+ endReason: input.endReason,
155
+ ...(input.lastActivityAt !== undefined
156
+ ? { lastActivityAt: input.lastActivityAt }
157
+ : {}),
158
+ ...(input.lastOwnedMessageId !== undefined
159
+ ? { lastOwnedMessageId: input.lastOwnedMessageId }
160
+ : {}),
161
+ });
162
+ }
163
+ return finalizeConversationModeSession({
164
+ id: input.id,
165
+ conversationId: input.conversationId,
166
+ expectedRevision: input.expectedRevision,
167
+ status: "interrupted",
168
+ endedAt: input.endedAt,
169
+ endReason: input.endReason,
170
+ ...(input.lastActivityAt !== undefined
171
+ ? { lastActivityAt: input.lastActivityAt }
172
+ : {}),
173
+ ...(input.lastOwnedMessageId !== undefined
174
+ ? { lastOwnedMessageId: input.lastOwnedMessageId }
175
+ : {}),
176
+ });
177
+ },
178
+ stampMessage: (messageId, owner) => {
179
+ updateMessageMetadata(messageId, { modeSession: owner });
180
+ },
181
+ publishMessagesChanged: publishConversationMessagesChanged,
182
+ };
183
+
184
+ function associationKey(
185
+ kind: ModeSessionStructuralKind,
186
+ responseId: string,
187
+ ): string {
188
+ return `${kind}:${responseId}`;
189
+ }
190
+
191
+ function lastStampedRow(rows: TrackedRow[]): TrackedRow | undefined {
192
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
193
+ if (rows[index]?.stamped) {
194
+ return rows[index];
195
+ }
196
+ }
197
+ return undefined;
198
+ }
199
+
200
+ /**
201
+ * Conversation-local membership bookkeeping. Mode producers register their
202
+ * durable active session and call these mechanical ownership operations; this
203
+ * helper never decides when a mode should activate.
204
+ */
205
+ export class ConversationModeSessionCoordinator {
206
+ readonly #conversationId: string;
207
+ readonly #dependencies: ConversationModeSessionCoordinatorDependencies;
208
+ readonly #sources = new Map<string, SourceState>();
209
+ readonly #sourceActivations = new Map<string, number>();
210
+ readonly #turns = new Map<string, TurnState>();
211
+ readonly #structuralAssociations = new Map<string, StructuralAssociation>();
212
+ readonly #retiredDispositions = new Map<
213
+ string,
214
+ ModeSessionTerminalDisposition & { completeAtLastActivity?: boolean }
215
+ >();
216
+
217
+ constructor(
218
+ conversationId: string,
219
+ dependencies: ConversationModeSessionCoordinatorDependencies = defaultDependencies,
220
+ ) {
221
+ this.#conversationId = conversationId;
222
+ this.#dependencies = dependencies;
223
+ }
224
+
225
+ #registerSource(
226
+ registration: ModeSessionSourceRegistration,
227
+ ): ModeSessionSourceHandle | undefined {
228
+ const { session } = registration;
229
+ if (
230
+ session.conversationId !== this.#conversationId ||
231
+ session.status !== "active"
232
+ ) {
233
+ return undefined;
234
+ }
235
+ const key = registration.sourceId;
236
+ const current = this.#sources.get(key);
237
+ if (current && current.generation >= registration.generation) {
238
+ return undefined;
239
+ }
240
+ const activation = (this.#sourceActivations.get(key) ?? 0) + 1;
241
+ this.#sourceActivations.set(key, activation);
242
+ this.#sources.set(key, {
243
+ generation: registration.generation,
244
+ activation,
245
+ session: { id: session.id, mode: session.mode },
246
+ lifetime: registration.lifetime,
247
+ });
248
+ return {
249
+ sourceId: registration.sourceId,
250
+ generation: registration.generation,
251
+ activation,
252
+ id: session.id,
253
+ mode: session.mode,
254
+ };
255
+ }
256
+
257
+ activateSource(
258
+ activation: ModeSessionSourceActivation,
259
+ ): ModeSessionSourceHandle | undefined {
260
+ const current = this.#sources.get(activation.sourceId);
261
+ if (current && current.generation === activation.generation) {
262
+ return this.#readActiveSession(current.session.id)
263
+ ? {
264
+ sourceId: activation.sourceId,
265
+ generation: current.generation,
266
+ activation: current.activation,
267
+ ...current.session,
268
+ }
269
+ : undefined;
270
+ }
271
+ if (current && current.generation > activation.generation) {
272
+ return undefined;
273
+ }
274
+
275
+ const result = this.#dependencies.beginSession({
276
+ id: this.#dependencies.createId(),
277
+ conversationId: this.#conversationId,
278
+ mode: activation.mode,
279
+ sourceStartedAt: activation.sourceStartedAt,
280
+ });
281
+ if (!result.ok || result.session.status !== "active") {
282
+ return undefined;
283
+ }
284
+ return this.#registerSource({
285
+ sourceId: activation.sourceId,
286
+ generation: activation.generation,
287
+ session: result.session,
288
+ lifetime: activation.lifetime ?? "turn",
289
+ });
290
+ }
291
+
292
+ retireSource(
293
+ source: ModeSessionSourceReference,
294
+ disposition?: ModeSessionTerminalDisposition,
295
+ ): boolean {
296
+ const key = source.sourceId;
297
+ const currentSource = this.#sources.get(key);
298
+ if (
299
+ !currentSource ||
300
+ currentSource.generation !== source.generation ||
301
+ currentSource.activation !== source.activation
302
+ ) {
303
+ return false;
304
+ }
305
+ this.#sources.delete(key);
306
+ if (disposition) {
307
+ this.#retiredDispositions.set(currentSource.session.id, disposition);
308
+ for (const turn of this.#turns.values()) {
309
+ if (turn.owner?.id === currentSource.session.id) {
310
+ turn.terminalDisposition = disposition;
311
+ turn.runtimeState = "finishing";
312
+ }
313
+ }
314
+ }
315
+ const removedWait = this.#deleteAssociationsForOwner(
316
+ currentSource.session.id,
317
+ );
318
+ if (removedWait && !disposition) {
319
+ this.#clearRuntimeState(currentSource.session.id);
320
+ }
321
+ try {
322
+ if (removedWait || disposition) {
323
+ this.#publishRuntimeChange(currentSource.session.id);
324
+ }
325
+ } finally {
326
+ if (disposition) {
327
+ this.#finalizeRetiredSessionIfSettled(currentSource.session.id);
328
+ }
329
+ }
330
+ return true;
331
+ }
332
+
333
+ acceptTurn(
334
+ turnId: string,
335
+ structuralResponse?: ModeSessionStructuralResponse,
336
+ ): ModeSession | undefined {
337
+ const turn = this.#turns.get(turnId) ?? { rows: [] };
338
+ this.#turns.set(turnId, turn);
339
+ if (!structuralResponse || turn.owner) {
340
+ return turn.owner;
341
+ }
342
+
343
+ const key = associationKey(
344
+ structuralResponse.kind,
345
+ structuralResponse.responseId,
346
+ );
347
+ const association = this.#structuralAssociations.get(key);
348
+ if (!association || association.conversationId !== this.#conversationId) {
349
+ return undefined;
350
+ }
351
+ const session = this.#readActiveSession(association.owner.id);
352
+ this.#structuralAssociations.delete(key);
353
+ if (!session || session.mode !== association.owner.mode) {
354
+ return undefined;
355
+ }
356
+ turn.owner = association.owner;
357
+ this.#clearRuntimeState(association.owner.id);
358
+ this.#publishRuntimeChange(association.owner.id);
359
+ this.#repairTrackedRows(turn);
360
+ return turn.owner;
361
+ }
362
+
363
+ trackPersistedRow(
364
+ turnId: string,
365
+ messageId: string,
366
+ at: number,
367
+ options?: {
368
+ startsDisplayBoundary?: boolean;
369
+ publishMessagesChanged?: boolean;
370
+ },
371
+ ): void {
372
+ const turn = this.#turns.get(turnId) ?? { rows: [] };
373
+ this.#turns.set(turnId, turn);
374
+ if (turn.rows.some((row) => row.id === messageId)) {
375
+ return;
376
+ }
377
+ const row = {
378
+ id: messageId,
379
+ at,
380
+ stamped: false,
381
+ startsDisplayBoundary: options?.startsDisplayBoundary ?? true,
382
+ };
383
+ turn.rows.push(row);
384
+ if (turn.owner) {
385
+ this.#repairTrackedRows(turn, options?.publishMessagesChanged ?? true);
386
+ }
387
+ }
388
+
389
+ claimTurn(
390
+ turnId: string,
391
+ sourceReference: ModeSessionSourceReference,
392
+ activityAt: number,
393
+ ): ModeSession | undefined {
394
+ const turn = this.#turns.get(turnId) ?? { rows: [] };
395
+ this.#turns.set(turnId, turn);
396
+ if (turn.owner) {
397
+ return turn.owner;
398
+ }
399
+
400
+ const source = this.#sources.get(sourceReference.sourceId);
401
+ if (
402
+ !source ||
403
+ source.generation !== sourceReference.generation ||
404
+ source.activation !== sourceReference.activation
405
+ ) {
406
+ return undefined;
407
+ }
408
+ const session = this.#readActiveSession(source.session.id);
409
+ if (!session || session.mode !== source.session.mode) {
410
+ return undefined;
411
+ }
412
+
413
+ turn.owner = source.session;
414
+ this.#repairTrackedRows(turn);
415
+ this.recordActivity(turnId, activityAt);
416
+ return turn.owner;
417
+ }
418
+
419
+ getTurnOwner(turnId: string): ModeSession | undefined {
420
+ return this.#turns.get(turnId)?.owner;
421
+ }
422
+
423
+ /** True while eviction would discard live ownership or continuation state. */
424
+ hasResidentWork(): boolean {
425
+ if (
426
+ this.#sources.size > 0 ||
427
+ this.#structuralAssociations.size > 0 ||
428
+ this.#retiredDispositions.size > 0
429
+ ) {
430
+ return true;
431
+ }
432
+ for (const turn of this.#turns.values()) {
433
+ if (turn.owner) {
434
+ return true;
435
+ }
436
+ }
437
+ return false;
438
+ }
439
+
440
+ transferTurn(fromTurnId: string, toTurnId: string): ModeSession | undefined {
441
+ if (fromTurnId === toTurnId) {
442
+ return this.#turns.get(toTurnId)?.owner;
443
+ }
444
+ const turn = this.#turns.get(fromTurnId);
445
+ const destination = this.#turns.get(toTurnId);
446
+ if (!turn?.owner) {
447
+ return destination?.owner;
448
+ }
449
+ if (destination?.owner && destination.owner.id !== turn.owner.id) {
450
+ bestEffortModeSessionTracking("handoff origin settlement", () =>
451
+ this.releaseTurn(fromTurnId, {
452
+ status: "completed",
453
+ endReason: "handoff_settled",
454
+ }),
455
+ );
456
+ return destination.owner;
457
+ }
458
+ bestEffortModeSessionTracking("handoff origin rows", () =>
459
+ this.#repairTrackedRows(turn),
460
+ );
461
+ const transferred: TurnState = destination ?? { rows: [] };
462
+ transferred.owner ??= turn.owner;
463
+ transferred.runtimeState ??= turn.runtimeState;
464
+ transferred.terminalDisposition ??= turn.terminalDisposition;
465
+ this.#turns.set(toTurnId, transferred);
466
+ this.#turns.delete(fromTurnId);
467
+ bestEffortModeSessionTracking("handoff destination rows", () =>
468
+ this.#repairTrackedRows(transferred),
469
+ );
470
+ return transferred.owner;
471
+ }
472
+
473
+ getTerminalDisposition(
474
+ turnId: string,
475
+ ): ModeSessionTerminalDisposition | undefined {
476
+ return this.#turns.get(turnId)?.terminalDisposition;
477
+ }
478
+
479
+ keepsSessionOpenAfterTurn(turnId: string): boolean {
480
+ const owner = this.#turns.get(turnId)?.owner;
481
+ if (!owner || this.#retiredDispositions.has(owner.id)) {
482
+ return false;
483
+ }
484
+ return this.#hasSourceLifetimeSource(owner.id);
485
+ }
486
+
487
+ recordActivity(turnId: string, at: number): boolean {
488
+ const turn = this.#turns.get(turnId);
489
+ if (!turn?.owner) {
490
+ return false;
491
+ }
492
+ const lastOwnedMessageId = lastStampedRow(turn.rows)?.id;
493
+ return this.#mutateActiveSession(turn.owner.id, (session) =>
494
+ this.#dependencies.updateActivity({
495
+ id: session.id,
496
+ conversationId: this.#conversationId,
497
+ expectedRevision: session.revision,
498
+ lastActivityAt: at,
499
+ ...(lastOwnedMessageId ? { lastOwnedMessageId } : {}),
500
+ }),
501
+ );
502
+ }
503
+
504
+ recordStructuralWait(
505
+ turnId: string,
506
+ response: ModeSessionStructuralResponse,
507
+ ): boolean {
508
+ const turn = this.#turns.get(turnId);
509
+ if (
510
+ !turn?.owner ||
511
+ !this.#readActiveSession(turn.owner.id) ||
512
+ !this.#hasEligibleSource(turn.owner.id)
513
+ ) {
514
+ return false;
515
+ }
516
+ this.#structuralAssociations.set(
517
+ associationKey(response.kind, response.responseId),
518
+ {
519
+ conversationId: this.#conversationId,
520
+ kind: response.kind,
521
+ owner: turn.owner,
522
+ },
523
+ );
524
+ turn.runtimeState = "waiting";
525
+ this.#publishRuntimeChange(turn.owner.id);
526
+ return true;
527
+ }
528
+
529
+ invalidateStructuralWait(response: ModeSessionStructuralResponse): boolean {
530
+ const association = this.#takeStructuralAssociation(response);
531
+ if (!association) {
532
+ return false;
533
+ }
534
+ if (this.#hasAssociationForOwner(association.owner.id)) {
535
+ return true;
536
+ }
537
+ this.#settleStructuralOwner(
538
+ association.owner.id,
539
+ {
540
+ status: "completed",
541
+ endReason: "structural_wait_abandoned",
542
+ },
543
+ true,
544
+ );
545
+ return true;
546
+ }
547
+
548
+ settleStructuralWait(
549
+ response: ModeSessionStructuralResponse,
550
+ disposition: ModeSessionTerminalDisposition,
551
+ ): boolean {
552
+ const association = this.#takeStructuralAssociation(response);
553
+ if (!association) {
554
+ return false;
555
+ }
556
+ if (this.#hasAssociationForOwner(association.owner.id)) {
557
+ return true;
558
+ }
559
+ this.#settleStructuralOwner(association.owner.id, disposition);
560
+ return true;
561
+ }
562
+
563
+ #settleStructuralOwner(
564
+ sessionId: string,
565
+ disposition: ModeSessionTerminalDisposition,
566
+ completeAtLastActivity = false,
567
+ ): void {
568
+ if (this.#hasSourceLifetimeSource(sessionId)) {
569
+ this.#clearRuntimeState(sessionId);
570
+ this.#publishRuntimeChange(sessionId);
571
+ return;
572
+ }
573
+ const existing = this.#retiredDispositions.get(sessionId);
574
+ this.#retiredDispositions.set(
575
+ sessionId,
576
+ existing ?? {
577
+ ...disposition,
578
+ completeAtLastActivity,
579
+ },
580
+ );
581
+ for (const turn of this.#turns.values()) {
582
+ if (turn.owner?.id === sessionId) {
583
+ turn.terminalDisposition ??= existing ?? disposition;
584
+ turn.runtimeState = "finishing";
585
+ }
586
+ }
587
+ this.#retireSourcesForOwner(sessionId);
588
+ try {
589
+ this.#publishRuntimeChange(sessionId);
590
+ } finally {
591
+ this.#finalizeRetiredSessionIfSettled(sessionId);
592
+ }
593
+ }
594
+
595
+ invalidateAllStructuralWaits(): number {
596
+ const ownerIds = new Set(
597
+ [...this.#structuralAssociations.values()].map(
598
+ (association) => association.owner.id,
599
+ ),
600
+ );
601
+ const count = this.#structuralAssociations.size;
602
+ this.#structuralAssociations.clear();
603
+ let settlementError: unknown;
604
+ for (const ownerId of ownerIds) {
605
+ try {
606
+ this.#settleStructuralOwner(
607
+ ownerId,
608
+ {
609
+ status: "completed",
610
+ endReason: "structural_wait_abandoned",
611
+ },
612
+ true,
613
+ );
614
+ } catch (err) {
615
+ settlementError ??= err;
616
+ }
617
+ }
618
+ if (settlementError) {
619
+ throw settlementError;
620
+ }
621
+ return count;
622
+ }
623
+
624
+ beginDraining(turnId: string): boolean {
625
+ const turn = this.#turns.get(turnId);
626
+ if (!turn?.owner || !this.#readActiveSession(turn.owner.id)) {
627
+ return false;
628
+ }
629
+ turn.runtimeState = "finishing";
630
+ this.#publishRuntimeChange(turn.owner.id);
631
+ return true;
632
+ }
633
+
634
+ finalizeTurn(input: {
635
+ turnId: string;
636
+ status: "completed" | "interrupted";
637
+ endedAt: number | null;
638
+ endReason: string;
639
+ lastActivityAt?: number;
640
+ }): boolean {
641
+ const turn = this.#turns.get(input.turnId);
642
+ if (!turn?.owner) {
643
+ return false;
644
+ }
645
+ this.#repairTrackedRows(turn);
646
+ const lastOwnedMessageId = lastStampedRow(turn.rows)?.id;
647
+ const finalized = this.#mutateActiveSession(turn.owner.id, (session) =>
648
+ this.#dependencies.finalize({
649
+ id: session.id,
650
+ conversationId: this.#conversationId,
651
+ expectedRevision: session.revision,
652
+ status: input.status,
653
+ endedAt: input.endedAt,
654
+ endReason: input.endReason,
655
+ ...(input.lastActivityAt !== undefined
656
+ ? { lastActivityAt: input.lastActivityAt }
657
+ : {}),
658
+ ...(lastOwnedMessageId ? { lastOwnedMessageId } : {}),
659
+ }),
660
+ );
661
+ if (finalized) {
662
+ this.#turns.delete(input.turnId);
663
+ this.#deleteAssociationsForOwner(turn.owner.id);
664
+ this.#retireSourcesForOwner(turn.owner.id);
665
+ this.#retiredDispositions.delete(turn.owner.id);
666
+ this.#dependencies.publishMessagesChanged(this.#conversationId);
667
+ }
668
+ return finalized;
669
+ }
670
+
671
+ releaseTurn(
672
+ turnId: string,
673
+ fallbackDisposition?: ModeSessionTerminalDisposition,
674
+ ): void {
675
+ const turn = this.#turns.get(turnId);
676
+ let repairError: unknown;
677
+ if (turn?.owner) {
678
+ try {
679
+ this.#repairTrackedRows(turn);
680
+ } catch (err) {
681
+ repairError = err;
682
+ }
683
+ }
684
+ const owner = turn?.owner;
685
+ if (
686
+ owner &&
687
+ fallbackDisposition?.status === "interrupted" &&
688
+ this.#retiredDispositions.get(owner.id)?.status === "completed"
689
+ ) {
690
+ this.#retiredDispositions.set(owner.id, fallbackDisposition);
691
+ for (const currentTurn of this.#turns.values()) {
692
+ if (currentTurn.owner?.id === owner.id) {
693
+ currentTurn.terminalDisposition = fallbackDisposition;
694
+ }
695
+ }
696
+ }
697
+ const shouldRetireOwner =
698
+ owner !== undefined &&
699
+ fallbackDisposition !== undefined &&
700
+ !this.#hasAssociationForOwner(owner.id) &&
701
+ !this.#hasSourceLifetimeSource(owner.id) &&
702
+ !this.#retiredDispositions.has(owner.id);
703
+ if (shouldRetireOwner) {
704
+ this.#retiredDispositions.set(owner.id, fallbackDisposition);
705
+ for (const currentTurn of this.#turns.values()) {
706
+ if (currentTurn.owner?.id === owner.id) {
707
+ currentTurn.terminalDisposition = fallbackDisposition;
708
+ currentTurn.runtimeState = "finishing";
709
+ }
710
+ }
711
+ this.#retireSourcesForOwner(owner.id);
712
+ }
713
+ this.#turns.delete(turnId);
714
+ let retirementError: unknown;
715
+ if (owner) {
716
+ if (shouldRetireOwner) {
717
+ try {
718
+ this.#publishRuntimeChange(owner.id);
719
+ } catch (err) {
720
+ retirementError = err;
721
+ }
722
+ }
723
+ try {
724
+ this.#finalizeRetiredSessionIfSettled(owner.id);
725
+ } catch (err) {
726
+ retirementError ??= err;
727
+ }
728
+ }
729
+ if (repairError) {
730
+ throw repairError;
731
+ }
732
+ if (retirementError) {
733
+ throw retirementError;
734
+ }
735
+ }
736
+
737
+ #descriptorFor(
738
+ summary: ModeSessionSummary,
739
+ ): ModeSessionDescriptor | undefined {
740
+ const sessionId = summary.id;
741
+ if (summary.status !== "active") {
742
+ return { summary };
743
+ }
744
+ let hasOwnedTurn = false;
745
+ for (const turn of this.#turns.values()) {
746
+ if (turn.owner?.id !== sessionId) {
747
+ continue;
748
+ }
749
+ hasOwnedTurn = true;
750
+ if (turn.runtimeState) {
751
+ return { summary, runtimeState: turn.runtimeState };
752
+ }
753
+ }
754
+ if (!hasOwnedTurn && this.#hasAssociationForOwner(sessionId)) {
755
+ return { summary, runtimeState: "waiting" };
756
+ }
757
+ return hasOwnedTurn || this.#hasEligibleSource(sessionId)
758
+ ? { summary }
759
+ : undefined;
760
+ }
761
+
762
+ describeSummary(
763
+ summary: ModeSessionSummary,
764
+ ): ModeSessionDescriptor | undefined {
765
+ if (summary.conversationId !== this.#conversationId) {
766
+ return { summary };
767
+ }
768
+ return this.#descriptorFor(summary);
769
+ }
770
+
771
+ #readActiveSession(sessionId: string): ModeSessionSummary | null {
772
+ const session = this.#dependencies.getSession(
773
+ this.#conversationId,
774
+ sessionId,
775
+ );
776
+ return session?.status === "active" ? session : null;
777
+ }
778
+
779
+ #mutateActiveSession(
780
+ sessionId: string,
781
+ mutate: (session: ModeSessionSummary) => ModeSessionWriteResult,
782
+ ): boolean {
783
+ let session = this.#readActiveSession(sessionId);
784
+ if (!session) {
785
+ return false;
786
+ }
787
+ for (let attempt = 0; attempt < 2; attempt += 1) {
788
+ const result = mutate(session);
789
+ if (result.ok) {
790
+ return true;
791
+ }
792
+ if (
793
+ result.reason !== "stale_revision" ||
794
+ !result.session ||
795
+ result.session.status !== "active"
796
+ ) {
797
+ return false;
798
+ }
799
+ session = result.session;
800
+ }
801
+ return false;
802
+ }
803
+
804
+ #repairTrackedRows(turn: TurnState, publishMessagesChanged = true): boolean {
805
+ if (!turn.owner) {
806
+ return false;
807
+ }
808
+ let changed = false;
809
+ for (const row of turn.rows) {
810
+ changed = this.#stampRow(row, turn.owner) || changed;
811
+ }
812
+ const stampedRows = turn.rows.filter((row) => row.stamped);
813
+ if (stampedRows.length > 0) {
814
+ changed =
815
+ this.#updateOrdinaryBoundaries(turn.owner, stampedRows) || changed;
816
+ }
817
+ if (changed && publishMessagesChanged) {
818
+ this.#dependencies.publishMessagesChanged(this.#conversationId);
819
+ }
820
+ return changed;
821
+ }
822
+
823
+ #stampRow(row: TrackedRow, owner: ModeSession): boolean {
824
+ if (row.stamped) {
825
+ return false;
826
+ }
827
+ try {
828
+ this.#dependencies.stampMessage(row.id, owner);
829
+ row.stamped = true;
830
+ return true;
831
+ } catch {
832
+ return false;
833
+ }
834
+ }
835
+
836
+ #updateOrdinaryBoundaries(owner: ModeSession, rows: TrackedRow[]): boolean {
837
+ if (rows.length === 0) {
838
+ return false;
839
+ }
840
+ return this.#mutateActiveSession(owner.id, (session) => {
841
+ const eligibleFirstRows = rows.filter((row) =>
842
+ modeSessionRowStartsDisplay(owner.mode, row.startsDisplayBoundary),
843
+ );
844
+ const earliest = eligibleFirstRows.reduce<TrackedRow | undefined>(
845
+ (current, row) => (!current || row.at < current.at ? row : current),
846
+ undefined,
847
+ );
848
+ const firstIncluded =
849
+ session.firstIncludedAt !== null &&
850
+ session.firstIncludedMessageId !== null &&
851
+ (!earliest || session.firstIncludedAt <= earliest.at)
852
+ ? {
853
+ at: session.firstIncludedAt,
854
+ messageId: session.firstIncludedMessageId,
855
+ }
856
+ : earliest
857
+ ? { at: earliest.at, messageId: earliest.id }
858
+ : null;
859
+ const lastOwnedMessageId = rows.at(-1)?.id ?? session.lastOwnedMessageId;
860
+ const lastActivityAt = rows.reduce(
861
+ (latest, row) => Math.max(latest, row.at),
862
+ session.lastActivityAt,
863
+ );
864
+ if (
865
+ session.firstIncludedAt === (firstIncluded?.at ?? null) &&
866
+ session.firstIncludedMessageId === (firstIncluded?.messageId ?? null) &&
867
+ session.lastActivityAt === lastActivityAt &&
868
+ session.lastOwnedMessageId === lastOwnedMessageId
869
+ ) {
870
+ return { ok: true, session };
871
+ }
872
+ return this.#dependencies.updateBoundaries({
873
+ id: session.id,
874
+ conversationId: this.#conversationId,
875
+ expectedRevision: session.revision,
876
+ firstIncluded,
877
+ lastActivityAt,
878
+ lastOwnedMessageId,
879
+ });
880
+ });
881
+ }
882
+
883
+ #deleteAssociationsForOwner(sessionId: string): boolean {
884
+ let deleted = false;
885
+ for (const [key, association] of this.#structuralAssociations) {
886
+ if (association.owner.id === sessionId) {
887
+ this.#structuralAssociations.delete(key);
888
+ deleted = true;
889
+ }
890
+ }
891
+ return deleted;
892
+ }
893
+
894
+ #clearRuntimeState(sessionId: string): void {
895
+ for (const turn of this.#turns.values()) {
896
+ if (turn.owner?.id === sessionId) {
897
+ turn.runtimeState = undefined;
898
+ }
899
+ }
900
+ }
901
+
902
+ #hasEligibleSource(sessionId: string): boolean {
903
+ for (const source of this.#sources.values()) {
904
+ if (source.session.id === sessionId) {
905
+ return true;
906
+ }
907
+ }
908
+ return false;
909
+ }
910
+
911
+ #retireSourcesForOwner(sessionId: string): void {
912
+ for (const [key, source] of this.#sources) {
913
+ if (source.session.id === sessionId) {
914
+ this.#sources.delete(key);
915
+ }
916
+ }
917
+ }
918
+
919
+ #finalizeRetiredSessionIfSettled(sessionId: string): boolean {
920
+ const disposition = this.#retiredDispositions.get(sessionId);
921
+ if (!disposition) {
922
+ return false;
923
+ }
924
+ for (const turn of this.#turns.values()) {
925
+ if (turn.owner?.id === sessionId) {
926
+ return false;
927
+ }
928
+ }
929
+ const endedAt = Date.now();
930
+ let finalized = false;
931
+ try {
932
+ finalized = this.#mutateActiveSession(sessionId, (session) =>
933
+ this.#dependencies.finalize({
934
+ id: session.id,
935
+ conversationId: this.#conversationId,
936
+ expectedRevision: session.revision,
937
+ status: disposition.status,
938
+ endedAt: disposition.completeAtLastActivity
939
+ ? session.lastActivityAt
940
+ : endedAt,
941
+ endReason: disposition.endReason,
942
+ ...(disposition.completeAtLastActivity
943
+ ? {}
944
+ : { lastActivityAt: endedAt }),
945
+ }),
946
+ );
947
+ } finally {
948
+ this.#retiredDispositions.delete(sessionId);
949
+ if (!finalized) {
950
+ log.warn(
951
+ { conversationId: this.#conversationId, sessionId },
952
+ "Mode-session retirement did not persist a terminal update",
953
+ );
954
+ bestEffortModeSessionTracking(
955
+ "retired session descriptor invalidation",
956
+ () => this.#dependencies.publishMessagesChanged(this.#conversationId),
957
+ );
958
+ }
959
+ }
960
+ if (finalized) {
961
+ this.#dependencies.publishMessagesChanged(this.#conversationId);
962
+ }
963
+ return finalized;
964
+ }
965
+
966
+ #hasAssociationForOwner(sessionId: string): boolean {
967
+ for (const association of this.#structuralAssociations.values()) {
968
+ if (association.owner.id === sessionId) {
969
+ return true;
970
+ }
971
+ }
972
+ return false;
973
+ }
974
+
975
+ #hasSourceLifetimeSource(sessionId: string): boolean {
976
+ for (const source of this.#sources.values()) {
977
+ if (source.session.id === sessionId && source.lifetime === "source") {
978
+ return true;
979
+ }
980
+ }
981
+ return false;
982
+ }
983
+
984
+ #takeStructuralAssociation(
985
+ response: ModeSessionStructuralResponse,
986
+ ): StructuralAssociation | undefined {
987
+ const key = associationKey(response.kind, response.responseId);
988
+ const association = this.#structuralAssociations.get(key);
989
+ if (!association || !this.#structuralAssociations.delete(key)) {
990
+ return undefined;
991
+ }
992
+ return association;
993
+ }
994
+
995
+ #publishRuntimeChange(sessionId: string): boolean {
996
+ const advanced = this.#mutateActiveSession(sessionId, (session) =>
997
+ this.#dependencies.advanceRevision({
998
+ id: session.id,
999
+ conversationId: this.#conversationId,
1000
+ expectedRevision: session.revision,
1001
+ }),
1002
+ );
1003
+ if (advanced) {
1004
+ this.#dependencies.publishMessagesChanged(this.#conversationId);
1005
+ }
1006
+ return advanced;
1007
+ }
1008
+ }