@vellumai/assistant 0.11.3 → 0.11.4-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 (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -60,9 +60,11 @@ import {
60
60
  } from "./conversation-slash.js";
61
61
  import { getModelInfo } from "./handlers/config-model.js";
62
62
  import { preactivateHostProxySkills } from "./host-proxy-preactivation.js";
63
+ import { bindInteractiveTurnSender } from "./interactive-turn-sender.js";
63
64
  import type { UserMessageAttachment } from "./message-protocol.js";
64
65
  import { buildTransportHints } from "./transport-hints.js";
65
66
  import { sameTrustIdentity, type TrustContext } from "./trust-context-types.js";
67
+ import { turnOrRestingTrust } from "./trust-context-types.js";
66
68
  import { resolveVerificationSessionIntent } from "./verification-session-intent.js";
67
69
 
68
70
  const log = getLogger("conversation-process");
@@ -823,7 +825,7 @@ async function drainSingleMessage(
823
825
  if (slashResult.kind === "unknown") {
824
826
  try {
825
827
  const drainProvenance = provenanceFromTrustContext(
826
- conversation.trustContext,
828
+ turnOrRestingTrust(conversation),
827
829
  );
828
830
  const drainImageSourcePaths: Record<string, string> = {};
829
831
  for (let i = 0; i < next.attachments.length; i++) {
@@ -938,7 +940,7 @@ async function drainSingleMessage(
938
940
  let persistedCompactMessage = false;
939
941
  try {
940
942
  const drainProvenance = provenanceFromTrustContext(
941
- conversation.trustContext,
943
+ turnOrRestingTrust(conversation),
942
944
  );
943
945
  const drainChannelMeta = {
944
946
  ...drainProvenance,
@@ -1031,7 +1033,7 @@ async function drainSingleMessage(
1031
1033
  let persistedCleanMessage = false;
1032
1034
  try {
1033
1035
  const drainProvenance = provenanceFromTrustContext(
1034
- conversation.trustContext,
1036
+ turnOrRestingTrust(conversation),
1035
1037
  );
1036
1038
  const drainChannelMeta = {
1037
1039
  ...drainProvenance,
@@ -1291,11 +1293,26 @@ async function drainSingleMessage(
1291
1293
  drainLoopOptions.isHiddenPrompt = true;
1292
1294
  }
1293
1295
 
1296
+ // By the time the drain runs, the conversation-level sender is usually the
1297
+ // no-op the just-finished interactive turn's restore (or conversation
1298
+ // birth) left behind, and the drain bypasses `processMessage`, so without
1299
+ // a fresh bind the PermissionPrompter's confirmation_request (which reads
1300
+ // the conversation-level sender, not the turn's `onEvent`) would be
1301
+ // emitted into nothing and hang until the permission timeout. Same
1302
+ // bind/restore contract as `processMessage`.
1303
+ const restoreSender =
1304
+ next.isInteractive === true
1305
+ ? bindInteractiveTurnSender(conversation)
1306
+ : undefined;
1307
+
1294
1308
  conversation
1295
1309
  .runAgentLoop(agentLoopContent, userMessageId, {
1296
1310
  ...drainLoopOptions,
1297
1311
  onEvent: next.onEvent,
1298
1312
  })
1313
+ .finally(() => {
1314
+ restoreSender?.();
1315
+ })
1299
1316
  .catch((err) => {
1300
1317
  const message = err instanceof Error ? err.message : String(err);
1301
1318
  log.error(
@@ -1778,6 +1795,14 @@ async function drainBatch(
1778
1795
  drainLoopOptions.isHiddenPrompt = true;
1779
1796
  }
1780
1797
 
1798
+ // Same sender contract as drainSingleMessage: an interactive drained batch
1799
+ // must rebind the conversation-level sender or prompter confirmations
1800
+ // raised during this turn reach no client.
1801
+ const restoreSender =
1802
+ drainLoopOptions.isInteractive === true
1803
+ ? bindInteractiveTurnSender(conversation)
1804
+ : undefined;
1805
+
1781
1806
  // Fire-and-forget: runAgentLoop's finally block recursively calls drainQueue
1782
1807
  // when this run completes. Mirrors drainSingleMessage.
1783
1808
  conversation
@@ -1785,6 +1810,9 @@ async function drainBatch(
1785
1810
  ...drainLoopOptions,
1786
1811
  onEvent: fanOutOnEvent,
1787
1812
  })
1813
+ .finally(() => {
1814
+ restoreSender?.();
1815
+ })
1788
1816
  .catch((err) => {
1789
1817
  const message = err instanceof Error ? err.message : String(err);
1790
1818
  log.error(
@@ -2024,7 +2052,9 @@ export async function processMessage(
2024
2052
  if (slashResult.kind === "unknown") {
2025
2053
  const pmTurnCtx = conversation.getTurnChannelContext();
2026
2054
  const pmInterfaceCtx = conversation.getTurnInterfaceContext();
2027
- const pmProvenance = provenanceFromTrustContext(conversation.trustContext);
2055
+ const pmProvenance = provenanceFromTrustContext(
2056
+ turnOrRestingTrust(conversation),
2057
+ );
2028
2058
  const pmImageSourcePaths: Record<string, string> = {};
2029
2059
  for (let i = 0; i < attachments.length; i++) {
2030
2060
  const a = attachments[i];
@@ -2116,7 +2146,7 @@ export async function processMessage(
2116
2146
  const pmTurnCtx = conversation.getTurnChannelContext();
2117
2147
  const pmInterfaceCtx = conversation.getTurnInterfaceContext();
2118
2148
  const pmProvenance = provenanceFromTrustContext(
2119
- conversation.trustContext,
2149
+ turnOrRestingTrust(conversation),
2120
2150
  );
2121
2151
  const pmChannelMeta = {
2122
2152
  ...pmProvenance,
@@ -2197,7 +2227,7 @@ export async function processMessage(
2197
2227
  const pmTurnCtx = conversation.getTurnChannelContext();
2198
2228
  const pmInterfaceCtx = conversation.getTurnInterfaceContext();
2199
2229
  const pmProvenance = provenanceFromTrustContext(
2200
- conversation.trustContext,
2230
+ turnOrRestingTrust(conversation),
2201
2231
  );
2202
2232
  const pmChannelMeta = {
2203
2233
  ...pmProvenance,
@@ -2319,7 +2319,7 @@ export async function applyRuntimeInjections(
2319
2319
  // folded into the summary are excluded.
2320
2320
  const slackActiveThreadFocusBlock = channelCapabilities
2321
2321
  ? loadSlackActiveThreadFocusBlock(conversationId, channelCapabilities, {
2322
- trustClass: liveConversation?.trustContext?.trustClass,
2322
+ trustClass: liveConversation?.getTrustContext()?.trustClass,
2323
2323
  contextCompactedMessageCount:
2324
2324
  liveConversation?.contextCompactedMessageCount,
2325
2325
  slackContextCompactionWatermarkTs:
@@ -2337,7 +2337,7 @@ export async function applyRuntimeInjections(
2337
2337
  // boundary without the orchestrator threading a snapshot in.
2338
2338
  const slackChronologicalMessages = channelCapabilities
2339
2339
  ? loadSlackChronologicalMessages(conversationId, channelCapabilities, {
2340
- trustClass: liveConversation?.trustContext?.trustClass,
2340
+ trustClass: liveConversation?.getTrustContext()?.trustClass,
2341
2341
  contextSummary: liveConversation?.contextSummary,
2342
2342
  contextCompactedMessageCount:
2343
2343
  liveConversation?.contextCompactedMessageCount,
@@ -2380,8 +2380,7 @@ export async function applyRuntimeInjections(
2380
2380
  turnIndex: options.turnIndex ?? liveConversation?.turnCount ?? 0,
2381
2381
  trust:
2382
2382
  options.trust ??
2383
- liveConversation?.currentTurnTrustContext ??
2384
- liveConversation?.trustContext ??
2383
+ liveConversation?.getTurnOrRestingTrust() ??
2385
2384
  fallbackTurnTrust(channelCapabilities),
2386
2385
  callSite: options.callSite ?? liveConversation?.currentCallSite,
2387
2386
  ...injectionInputs,
@@ -140,14 +140,28 @@ const MAX_UNDO_DEPTH = 10;
140
140
 
141
141
  /**
142
142
  * Pending surface types that do not hold the one-interactive-surface-at-a-time
143
- * lock. Both render content the user reads rather than a question they must
144
- * answer, so a live one must not block the next surface.
143
+ * lock. Each renders content the user reads (or settles on its own) rather
144
+ * than a question they must answer, so a live one must not block the next
145
+ * surface.
145
146
  */
146
147
  const NON_BLOCKING_PENDING_SURFACE_TYPES = new Set<SurfaceType>([
147
148
  "dynamic_page",
148
149
  "visual",
150
+ "voice_picker",
149
151
  ]);
150
152
 
153
+ /**
154
+ * Surface types that carry no terminal action: the card settles when the user
155
+ * interacts with it, so no click could ever satisfy an attached `actions`
156
+ * entry or an explicit `await_action`. Both are generic ui_show params though,
157
+ * so nothing stops the model attaching them here, and doing so wedges the
158
+ * turn: the client latches `awaiting_user_input` on the presence of actions
159
+ * alone and no action ever arrives to clear it, leaving the composer disabled
160
+ * and Stop hidden while the daemon is still streaming. Stripping them makes
161
+ * the "never blocks a turn" contract structural rather than advisory.
162
+ */
163
+ const ACTIONLESS_SURFACE_TYPES = new Set<SurfaceType>(["voice_picker"]);
164
+
151
165
  /**
152
166
  * Debounce window for persisting `ui_surface_update` data back to the
153
167
  * message row. Surfaces typically receive bursts of updates (e.g. a
@@ -3279,8 +3293,12 @@ export async function surfaceProxyResolver(
3279
3293
  }
3280
3294
  inputActions = valid.length > 0 ? valid : undefined;
3281
3295
  }
3282
- const actions =
3283
- choiceData !== undefined ? buildChoiceActions(choiceData) : inputActions;
3296
+ const isActionless = ACTIONLESS_SURFACE_TYPES.has(surfaceType);
3297
+ const actions = isActionless
3298
+ ? undefined
3299
+ : choiceData !== undefined
3300
+ ? buildChoiceActions(choiceData)
3301
+ : inputActions;
3284
3302
  const hasActions = Array.isArray(actions) && actions.length > 0;
3285
3303
  if (surfaceType === "choice" && !hasActions) {
3286
3304
  return {
@@ -3332,7 +3350,11 @@ export async function surfaceProxyResolver(
3332
3350
  : surfaceType === "table"
3333
3351
  ? hasActions
3334
3352
  : INTERACTIVE_SURFACE_TYPES.includes(surfaceType);
3335
- const awaitAction = (input.await_action as boolean) ?? isInteractive;
3353
+ // An explicit `await_action: true` is honored for every other type; an
3354
+ // actionless surface has nothing to await, so it is forced false.
3355
+ const awaitAction = isActionless
3356
+ ? false
3357
+ : ((input.await_action as boolean) ?? isInteractive);
3336
3358
 
3337
3359
  // Only one non-persistent interactive surface at a time. If another
3338
3360
  // surface is already awaiting user input, reject this one so the LLM
@@ -389,8 +389,7 @@ export function createToolExecutor(
389
389
  // Per-turn trust snapshot: prefer the snapshot captured at turn start so
390
390
  // a concurrent owner meta command (/status, /clean) that mutates the live
391
391
  // trustContext cannot elevate the in-flight turn to guardian.
392
- const turnTrust =
393
- ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
392
+ const turnTrust = ctx.getTurnOrRestingTrust() ?? FALLBACK_TURN_TRUST;
394
393
 
395
394
  const toolContext: ToolContext = {
396
395
  workingDir: ctx.workingDir,
@@ -2488,6 +2488,54 @@ export class Conversation {
2488
2488
  return this.authContext;
2489
2489
  }
2490
2490
 
2491
+ /**
2492
+ * Trust the in-flight turn is executing under.
2493
+ *
2494
+ * Use this for authorization and for routing a reply to the requester:
2495
+ * cases where substituting the conversation's owner would be wrong rather
2496
+ * than approximate. Provenance is not such a case; see
2497
+ * {@link getTurnOrRestingTrust} and `docs/architecture/turn-actor.md`.
2498
+ *
2499
+ * `undefined` when no turn recorded one, which is a gap in the entry point
2500
+ * rather than an answer. Deliberately does not fall back to the
2501
+ * conversation's trust: a caller that can accept the conversation's owner
2502
+ * instead spells `?? getTrustContext()`, so the substitution is visible
2503
+ * where it happens.
2504
+ */
2505
+ getTurnTrust(): TrustContext | undefined {
2506
+ return this.currentTurnTrustContext;
2507
+ }
2508
+
2509
+ /**
2510
+ * Trust of the actor the conversation belongs to, independent of any turn.
2511
+ *
2512
+ * Use this where there is no turn to speak of: routes reporting on a
2513
+ * conversation, hydration, and persisting conversation-level options. A
2514
+ * caller that wants the conversation's owner *rather than* whoever is
2515
+ * currently acting should be obviously doing so; if it is not obvious,
2516
+ * {@link getTurnTrust} is probably the one meant.
2517
+ */
2518
+ getTrustContext(): TrustContext | undefined {
2519
+ return this.trustContext;
2520
+ }
2521
+
2522
+ /**
2523
+ * Trust of the in-flight turn, or the conversation's owner when the turn
2524
+ * recorded none. The substitution is in the name: callers that can accept
2525
+ * the owner as a stand-in ask this, including provenance stamping, whose
2526
+ * readers treat an absent trust class as more trusted than `"unknown"`.
2527
+ * Callers for which the owner would be wrong rather than approximate call
2528
+ * {@link getTurnTrust} and handle `undefined`.
2529
+ *
2530
+ * The fallback half is load-bearing, not transitional politeness: a
2531
+ * deferred wake fires with no inbound actor, and refusing it an answer
2532
+ * denies every sensitive tool in the resumed turn (LUM-2929). It becomes
2533
+ * removable in one place when every entry point records a turn actor.
2534
+ */
2535
+ getTurnOrRestingTrust(): TrustContext | undefined {
2536
+ return this.currentTurnTrustContext ?? this.trustContext;
2537
+ }
2538
+
2491
2539
  /**
2492
2540
  * The actor principal that owns the current turn, for host-proxy routing.
2493
2541
  * Prefers the in-flight turn's actor over the conversation's resting
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The interactive-turn sender contract, in one place.
3
+ *
4
+ * `Conversation.sendToClient` is conversation-level mutable state: born a
5
+ * no-op, bound to the SSE hub for the duration of an interactive turn, and
6
+ * restored when that turn ends. Some subsystems read this conversation-level
7
+ * sender instead of the turn's own `onEvent` sink; the `PermissionPrompter`
8
+ * is the load-bearing one, so a `confirmation_request` raised by a turn that
9
+ * never bound the sender is emitted into the no-op, reaches no client, and
10
+ * hangs until the permission timeout auto-denies. Every path that runs an
11
+ * interactive agent turn MUST therefore bind before the loop and restore
12
+ * after it. The queue drain bypasses `processMessage`, so it uses this
13
+ * helper directly.
14
+ *
15
+ * The restore is a snapshot of the sender state taken at bind time, not an
16
+ * unconditional reset to a no-op: a live binding that predated the turn
17
+ * (e.g. installed by the send route while a non-interactive turn was
18
+ * running) survives the turn, so out-of-turn producers that read the live
19
+ * sender (call transcript and completion notifiers) keep their client. In
20
+ * the ordinary interactive flow the snapshot is the no-op the previous
21
+ * turn's restore left behind, so restoring it is identical to the reset
22
+ * this contract has always performed. The restore is also identity-guarded:
23
+ * it only replaces the hub binding this contract installed, so a sender
24
+ * some other subsystem rebound mid-turn to anything else is left alone.
25
+ *
26
+ * TRANSITIONAL: this helper is the paved road for a pattern we want less
27
+ * of. Bind/restore of shared mutable sender state is why the drain, the
28
+ * compaction card, and the host-browser proxy have each needed their own
29
+ * restore step. The durable design is a per-turn sink threaded to the
30
+ * prompter and the other conversation-level-sender readers, at which point
31
+ * this module and every call site of it should be deleted. Prefer that
32
+ * refactor over adding new bind/restore call sites.
33
+ */
34
+
35
+ import { broadcastMessage } from "../runtime/assistant-event-hub.js";
36
+ import { getSubagentManager } from "../subagent/index.js";
37
+ import type { Conversation } from "./conversation.js";
38
+
39
+ /**
40
+ * Point the conversation-level sender (and the subagent parent sender) at
41
+ * the SSE hub for the duration of an interactive turn. Returns the restore
42
+ * closure the turn's cleanup must invoke.
43
+ */
44
+ export function bindInteractiveTurnSender(
45
+ conversation: Conversation,
46
+ ): () => void {
47
+ const previousSender = conversation.getCurrentSender();
48
+ const previousHasNoClient = conversation.hasNoClient;
49
+ conversation.updateClient(broadcastMessage, false);
50
+ getSubagentManager().updateParentSender(
51
+ conversation.conversationId,
52
+ broadcastMessage,
53
+ );
54
+ return () => {
55
+ if (conversation.getCurrentSender() === broadcastMessage) {
56
+ conversation.updateClient(previousSender, previousHasNoClient);
57
+ }
58
+ };
59
+ }
@@ -6,6 +6,10 @@
6
6
  */
7
7
 
8
8
  import { getConfig, invalidateConfigCache } from "../config/loader.js";
9
+ import {
10
+ buildEffectiveMcpConfig,
11
+ pluginMcpServersChangedSinceLastBuild,
12
+ } from "../mcp/effective-config.js";
9
13
  import { getMcpServerManager } from "../mcp/manager.js";
10
14
  import { migrateLegacyMcpHeaders } from "../mcp/mcp-header-store.js";
11
15
  import { createMcpToolsFromServer } from "../tools/mcp/mcp-tool-factory.js";
@@ -52,6 +56,29 @@ export function reloadMcpServers(): Promise<McpReloadResult> {
52
56
  return reloadInProgress;
53
57
  }
54
58
 
59
+ /**
60
+ * Reload MCP servers if — and only if — the plugin-declared set moved.
61
+ *
62
+ * Called by the plugin source reconcile, which every install / uninstall /
63
+ * upgrade / enable / disable already funnels through, so a plugin's MCP
64
+ * tools come up and go down with the plugin instead of lingering until an
65
+ * explicit reload, a config edit, or a restart.
66
+ *
67
+ * The guard matters: a reconcile also fires for plugin edits that touch no
68
+ * `mcp.json` at all, and an unconditional reload would tear down every
69
+ * healthy workspace connection to re-establish it for nothing.
70
+ *
71
+ * Never throws — the reconcile is best-effort and must not fail an install
72
+ * whose files already landed on disk.
73
+ */
74
+ export async function reconcilePluginMcpServers(): Promise<void> {
75
+ if (!pluginMcpServersChangedSinceLastBuild()) {
76
+ return;
77
+ }
78
+ log.info("Plugin-declared MCP servers changed; reloading");
79
+ await reloadMcpServers();
80
+ }
81
+
55
82
  async function doReload(): Promise<McpReloadResult> {
56
83
  try {
57
84
  const manager = getMcpServerManager();
@@ -74,17 +101,20 @@ async function doReload(): Promise<McpReloadResult> {
74
101
  // 2. Stop existing MCP servers + unregister their tools
75
102
  await manager.stop();
76
103
  unregisterAllMcpTools();
77
- const serverIds = config.mcp?.servers
78
- ? Object.keys(config.mcp.servers)
79
- : [];
104
+
105
+ // Plugins are re-read here too: installing or removing one changes the
106
+ // server set exactly like editing config.json does, and both arrive
107
+ // through this same reload.
108
+ const mcpConfig = buildEffectiveMcpConfig(config.mcp);
109
+ const serverIds = Object.keys(mcpConfig.servers);
80
110
 
81
111
  // 3. Restart MCP servers
82
112
  let serverCount = 0;
83
113
  let toolCount = 0;
84
114
  const servers: McpReloadServerResult[] = [];
85
115
 
86
- if (config.mcp?.servers && Object.keys(config.mcp.servers).length > 0) {
87
- const serverToolInfos = await manager.start(config.mcp);
116
+ if (serverIds.length > 0) {
117
+ const serverToolInfos = await manager.start(mcpConfig);
88
118
  for (const { serverId, serverConfig, tools } of serverToolInfos) {
89
119
  const mcpTools = createMcpToolsFromServer(
90
120
  tools,
@@ -105,7 +135,7 @@ async function doReload(): Promise<McpReloadResult> {
105
135
  // Include servers that were configured but failed to connect or are disabled
106
136
  for (const id of serverIds) {
107
137
  if (!servers.some((s) => s.id === id)) {
108
- const serverConfig = config.mcp!.servers![id];
138
+ const serverConfig = mcpConfig.servers[id];
109
139
  const isDisabled = serverConfig?.enabled === false;
110
140
  servers.push({
111
141
  id,
@@ -34,7 +34,6 @@ import { updateMetaFile } from "../persistence/conversation-disk-view.js";
34
34
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
35
35
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
36
36
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
37
- import { getSubagentManager } from "../subagent/index.js";
38
37
  import {
39
38
  readTurnFailure,
40
39
  type TurnFailure,
@@ -70,7 +69,9 @@ import {
70
69
  preactivateHostProxySkills,
71
70
  shouldAttachHostProxyForCapability,
72
71
  } from "./host-proxy-preactivation.js";
72
+ import { bindInteractiveTurnSender } from "./interactive-turn-sender.js";
73
73
  import type { SubagentToolGateMode } from "./tool-setup-types.js";
74
+ import { restingTrust } from "./trust-context-types.js";
74
75
 
75
76
  const log = getLogger("process-message");
76
77
 
@@ -455,7 +456,10 @@ export async function processMessage(
455
456
  if (slashResult.kind === "unknown") {
456
457
  const serverTurnCtx = conversation.getTurnChannelContext();
457
458
  const serverProvenance = provenanceFromTrustContext(
458
- conversation.trustContext,
459
+ // Ingress persists before any per-turn stamp; the slot was just written
460
+ // by this message's own resolution, and the per-turn field may still
461
+ // hold the previous turn's actor.
462
+ restingTrust(conversation),
459
463
  );
460
464
  const imageSourcePaths: Record<string, string> = {};
461
465
  for (let i = 0; i < attachments.length; i++) {
@@ -557,7 +561,10 @@ export async function processMessage(
557
561
  if (slashResult.kind === "compact") {
558
562
  const serverTurnCtx = conversation.getTurnChannelContext();
559
563
  const serverProvenance = provenanceFromTrustContext(
560
- conversation.trustContext,
564
+ // Ingress persists before any per-turn stamp; the slot was just written
565
+ // by this message's own resolution, and the per-turn field may still
566
+ // hold the previous turn's actor.
567
+ restingTrust(conversation),
561
568
  );
562
569
  const compactChannelMeta = {
563
570
  ...serverProvenance,
@@ -612,7 +619,10 @@ export async function processMessage(
612
619
  if (slashResult.kind === "clean") {
613
620
  const serverTurnCtx = conversation.getTurnChannelContext();
614
621
  const serverProvenance = provenanceFromTrustContext(
615
- conversation.trustContext,
622
+ // Ingress persists before any per-turn stamp; the slot was just written
623
+ // by this message's own resolution, and the per-turn field may still
624
+ // hold the previous turn's actor.
625
+ restingTrust(conversation),
616
626
  );
617
627
  const cleanChannelMeta = {
618
628
  ...serverProvenance,
@@ -699,10 +709,10 @@ export async function processMessage(
699
709
  };
700
710
  }
701
711
 
702
- if (options?.isInteractive === true) {
703
- conversation.updateClient(broadcastMessage, false);
704
- getSubagentManager().updateParentSender(conversationId, broadcastMessage);
705
- }
712
+ const restoreSender =
713
+ options?.isInteractive === true
714
+ ? bindInteractiveTurnSender(conversation)
715
+ : undefined;
706
716
 
707
717
  const restoreToolScope = applyTurnToolAllowlist(conversation, options);
708
718
  try {
@@ -721,12 +731,7 @@ export async function processMessage(
721
731
  });
722
732
  } finally {
723
733
  restoreToolScope();
724
- if (
725
- options?.isInteractive === true &&
726
- conversation.getCurrentSender() === broadcastMessage
727
- ) {
728
- conversation.updateClient(() => {}, true);
729
- }
734
+ restoreSender?.();
730
735
  }
731
736
 
732
737
  // Read the just-finished turn's outcome from the stamp `runAgentLoop`'s
@@ -785,10 +790,10 @@ export async function processMessageInBackground(
785
790
  return { messageId };
786
791
  }
787
792
 
788
- if (options?.isInteractive === true) {
789
- conversation.updateClient(broadcastMessage, false);
790
- getSubagentManager().updateParentSender(conversationId, broadcastMessage);
791
- }
793
+ const restoreSender =
794
+ options?.isInteractive === true
795
+ ? bindInteractiveTurnSender(conversation)
796
+ : undefined;
792
797
 
793
798
  const restoreToolScope = applyTurnToolAllowlist(conversation, options);
794
799
  conversation
@@ -804,12 +809,7 @@ export async function processMessageInBackground(
804
809
  })
805
810
  .finally(() => {
806
811
  restoreToolScope();
807
- if (
808
- options?.isInteractive === true &&
809
- conversation.getCurrentSender() === broadcastMessage
810
- ) {
811
- conversation.updateClient(() => {}, true);
812
- }
812
+ restoreSender?.();
813
813
  })
814
814
  .catch((err) => {
815
815
  log.error({ err, conversationId }, "Background agent loop failed");
@@ -1,6 +1,7 @@
1
1
  import { maybeDefaultSpeechToManaged } from "../config/managed-speech-defaults.js";
2
2
  import { rehydratePlatformCredentials } from "../config/platform-rehydration.js";
3
3
  import type { AssistantConfig } from "../config/types.js";
4
+ import { buildEffectiveMcpConfig } from "../mcp/effective-config.js";
4
5
  import { getMcpServerManager } from "../mcp/manager.js";
5
6
  import { gmailMessagingProvider } from "../messaging/providers/gmail/adapter.js";
6
7
  import { outlookMessagingProvider } from "../messaging/providers/outlook/adapter.js";
@@ -61,11 +62,13 @@ export async function initializeProvidersAndTools(
61
62
  );
62
63
  }
63
64
 
64
- // Start MCP servers and register their tools
65
- if (config.mcp?.servers && Object.keys(config.mcp.servers).length > 0) {
65
+ // Start MCP servers — workspace-configured and plugin-declared alike —
66
+ // and register their tools.
67
+ const mcpConfig = buildEffectiveMcpConfig(config.mcp);
68
+ if (Object.keys(mcpConfig.servers).length > 0) {
66
69
  const manager = getMcpServerManager();
67
70
  try {
68
- const serverToolInfos = await manager.start(config.mcp);
71
+ const serverToolInfos = await manager.start(mcpConfig);
69
72
  for (const { serverId, serverConfig, tools } of serverToolInfos) {
70
73
  const mcpTools = createMcpToolsFromServer(
71
74
  tools,
@@ -106,3 +106,32 @@ export function sameTrustIdentity(
106
106
  a.guardianPrincipalId === b.guardianPrincipalId
107
107
  );
108
108
  }
109
+
110
+ /** The two trust fields a conversation-shaped value carries. */
111
+ export interface TrustCarrier {
112
+ currentTurnTrustContext?: TrustContext;
113
+ trustContext?: TrustContext;
114
+ }
115
+
116
+ /**
117
+ * The acting turn's trust, else the owner's. Structural counterpart of
118
+ * `Conversation.getTurnOrRestingTrust` for call sites handed a
119
+ * conversation-shaped context rather than the class, so a partial test double
120
+ * needs only the fields it already has.
121
+ */
122
+ export function turnOrRestingTrust(
123
+ carrier: TrustCarrier | undefined,
124
+ ): TrustContext | undefined {
125
+ return carrier?.currentTurnTrustContext ?? carrier?.trustContext;
126
+ }
127
+
128
+ /**
129
+ * The owner's trust, independent of any turn. Structural counterpart of
130
+ * `Conversation.getTrustContext`; see `docs/architecture/turn-actor.md` for
131
+ * when the owner is the right actor.
132
+ */
133
+ export function restingTrust(
134
+ carrier: Pick<TrustCarrier, "trustContext"> | undefined,
135
+ ): TrustContext | undefined {
136
+ return carrier?.trustContext;
137
+ }
@@ -32,6 +32,7 @@ import type {
32
32
  SubagentToolGateMode,
33
33
  WakeToolContextPin,
34
34
  } from "./tool-setup-types.js";
35
+ import { restingTrust } from "./trust-context-types.js";
35
36
 
36
37
  const log = getLogger("wake-conversation-ops");
37
38
 
@@ -222,7 +223,7 @@ export async function persistWakeTailMessage(
222
223
  const turnChannelCtx = conversation.getTurnChannelContext();
223
224
  const turnInterfaceCtx = conversation.getTurnInterfaceContext();
224
225
  const metadata: Record<string, unknown> = {
225
- ...provenanceFromTrustContext(conversation.trustContext),
226
+ ...provenanceFromTrustContext(restingTrust(conversation)),
226
227
  userMessageChannel: turnChannelCtx?.userMessageChannel ?? "vellum",
227
228
  assistantMessageChannel:
228
229
  turnChannelCtx?.assistantMessageChannel ?? "vellum",
@@ -301,7 +302,7 @@ export async function persistWakeTriggerMessage(
301
302
  const turnChannelCtx = conversation.getTurnChannelContext();
302
303
  const turnInterfaceCtx = conversation.getTurnInterfaceContext();
303
304
  const metadata: Record<string, unknown> = {
304
- ...provenanceFromTrustContext(conversation.trustContext),
305
+ ...provenanceFromTrustContext(restingTrust(conversation)),
305
306
  userMessageChannel: turnChannelCtx?.userMessageChannel ?? "vellum",
306
307
  assistantMessageChannel:
307
308
  turnChannelCtx?.assistantMessageChannel ?? "vellum",