@vellumai/assistant 0.11.3 → 0.11.4-dev.202608190019.b94dbf2

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 (596) hide show
  1. package/AGENTS.md +9 -3
  2. package/ARCHITECTURE.md +32 -10
  3. package/docs/architecture/memory.md +43 -5
  4. package/docs/architecture/security.md +96 -152
  5. package/docs/architecture/turn-actor.md +70 -0
  6. package/docs/browser-use-architecture-phase2.md +128 -56
  7. package/docs/flux-turn-detection-spike.md +248 -0
  8. package/docs/guardian-request-flow.md +35 -0
  9. package/docs/stt-provider-onboarding.md +3 -1
  10. package/knip.json +3 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  24. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  25. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  26. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  28. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  29. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  30. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  31. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  32. package/openapi.yaml +289 -113
  33. package/package.json +1 -1
  34. package/scripts/voice-ttft-spike.ts +3 -3
  35. package/scripts/write-plugin-api-shim.ts +10 -0
  36. package/src/__tests__/app-compiler.test.ts +38 -3
  37. package/src/__tests__/app-control-flow.test.ts +1 -0
  38. package/src/__tests__/approval-routes-http.test.ts +2 -2
  39. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  40. package/src/__tests__/attachments-store.test.ts +21 -12
  41. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  42. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  43. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  44. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  45. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  46. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  47. package/src/__tests__/channel-inbound-disk-pressure.test.ts +4 -4
  48. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  49. package/src/__tests__/checker.test.ts +20 -314
  50. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  51. package/src/__tests__/compaction-events.test.ts +8 -10
  52. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  53. package/src/__tests__/config-schema.test.ts +25 -10
  54. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  55. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  56. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  57. package/src/__tests__/conversation-agent-loop.test.ts +32 -21
  58. package/src/__tests__/conversation-attachments.test.ts +0 -1
  59. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  61. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  63. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  64. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  65. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  66. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  67. package/src/__tests__/conversation-queue.test.ts +154 -4
  68. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  69. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  71. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +187 -102
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  75. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  82. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  83. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  85. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  86. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  87. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  88. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  89. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  90. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  91. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  92. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  93. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  94. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  95. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  96. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  97. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  98. package/src/__tests__/document-sync-tags.test.ts +0 -75
  99. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  100. package/src/__tests__/edit-propagation.test.ts +107 -11
  101. package/src/__tests__/file-ops-service.test.ts +163 -30
  102. package/src/__tests__/filesystem-tools.test.ts +23 -24
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  104. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  105. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  106. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  107. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  108. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  109. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  110. package/src/__tests__/injector-chain.test.ts +63 -41
  111. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  112. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  113. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  114. package/src/__tests__/llm-schema.test.ts +5 -2
  115. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  116. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  117. package/src/__tests__/media-generate-image.test.ts +131 -21
  118. package/src/__tests__/memory-retrieval-hook.test.ts +100 -7
  119. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  120. package/src/__tests__/mtime-cache.test.ts +3 -1
  121. package/src/__tests__/non-member-access-request.test.ts +0 -20
  122. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  123. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  124. package/src/__tests__/plugin-api-store-credential.test.ts +261 -0
  125. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  126. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  127. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  128. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  129. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  130. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -4
  131. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  132. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  133. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  134. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  135. package/src/__tests__/qdrant-manager.test.ts +14 -1
  136. package/src/__tests__/reaction-persistence.test.ts +200 -7
  137. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  138. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  139. package/src/__tests__/run-conversation-turn-persistence.test.ts +460 -105
  140. package/src/__tests__/run-due-schedules.test.ts +21 -0
  141. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  142. package/src/__tests__/schedule-routes.test.ts +23 -0
  143. package/src/__tests__/schedule-store.test.ts +17 -0
  144. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  145. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  146. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  147. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  148. package/src/__tests__/skills.test.ts +32 -0
  149. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  150. package/src/__tests__/starter-task-flow.test.ts +5 -4
  151. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  152. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  153. package/src/__tests__/subagent-spawn-and-await.test.ts +18 -17
  154. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  155. package/src/__tests__/subagent-tools.test.ts +81 -101
  156. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  157. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  158. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  159. package/src/__tests__/tool-executor.test.ts +68 -39
  160. package/src/__tests__/turn-events-store.test.ts +43 -0
  161. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  162. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  163. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  164. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  165. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  166. package/src/__tests__/user-plugin-loader.test.ts +3 -1
  167. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  168. package/src/__tests__/visible-app-context.test.ts +16 -9
  169. package/src/__tests__/voice-config-update.test.ts +40 -0
  170. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  171. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  172. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  173. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  174. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  175. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  176. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  177. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  178. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  179. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  180. package/src/acp/__tests__/auth-required.test.ts +161 -0
  181. package/src/acp/acp-claude-oauth.ts +19 -2
  182. package/src/acp/agent-process.test.ts +100 -0
  183. package/src/acp/agent-process.ts +29 -26
  184. package/src/acp/auth-required.ts +102 -0
  185. package/src/acp/session-manager.test.ts +119 -0
  186. package/src/acp/session-manager.ts +76 -3
  187. package/src/api/events/acp-auth-required.ts +55 -0
  188. package/src/api/events/host-file.ts +2 -2
  189. package/src/api/index.ts +7 -0
  190. package/src/api/surfaces.ts +12 -3
  191. package/src/apps/app-store.ts +3 -0
  192. package/src/bundler/package-resolver.ts +2 -30
  193. package/src/calls/__tests__/voice-session-bridge.test.ts +194 -11
  194. package/src/calls/__tests__/voice-triage-escalate.test.ts +102 -0
  195. package/src/calls/call-controller.ts +19 -3
  196. package/src/calls/call-setup-flow.ts +0 -1
  197. package/src/calls/media-stream-stt-session.ts +15 -0
  198. package/src/calls/voice-session-bridge.ts +115 -41
  199. package/src/calls/voice-triage-escalate.ts +105 -2
  200. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  201. package/src/channels/config.ts +13 -0
  202. package/src/channels/plugin-channel-declarations.ts +108 -0
  203. package/src/channels/types.ts +30 -0
  204. package/src/cli/AGENTS.md +5 -2
  205. package/src/cli/bundled-modules.ts +29 -0
  206. package/src/cli/commands/credentials.help.ts +2 -2
  207. package/src/cli/commands/db/repair.ts +4 -8
  208. package/src/cli/commands/domain.ts +6 -3
  209. package/src/cli/commands/email.ts +6 -3
  210. package/src/cli/commands/inference-providers.ts +1 -1
  211. package/src/cli/commands/keys.ts +8 -3
  212. package/src/cli/commands/mcp.help.ts +13 -4
  213. package/src/cli/commands/mcp.ts +9 -0
  214. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  215. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  216. package/src/cli/commands/memory/index.help.ts +84 -28
  217. package/src/cli/commands/memory/index.ts +2 -0
  218. package/src/cli/commands/memory/memory-v2.ts +58 -54
  219. package/src/cli/commands/memory/memory-v3.ts +64 -0
  220. package/src/cli/commands/memory/memory-validate.ts +18 -0
  221. package/src/cli/commands/plugins.ts +85 -36
  222. package/src/cli/commands/schedules.ts +35 -1
  223. package/src/cli/commands/stt.help.ts +27 -2
  224. package/src/cli/commands/trust.ts +3 -14
  225. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  226. package/src/cli/lib/bundled-marketplace.json +14 -1
  227. package/src/cli/lib/upgrade-plugin.ts +42 -0
  228. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  229. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  230. package/src/config/__tests__/default-provider.test.ts +6 -1
  231. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  232. package/src/config/assistant-feature-flags.ts +36 -15
  233. package/src/config/balanced-model-experiment.ts +35 -0
  234. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  235. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  236. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  237. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  238. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  239. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  240. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  241. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  242. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  243. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  244. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  245. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  246. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  247. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  248. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  249. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  250. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  251. package/src/config/call-site-defaults.ts +11 -5
  252. package/src/config/default-profile-catalog.ts +179 -16
  253. package/src/config/default-profile-names.ts +4 -1
  254. package/src/config/default-provider-resolution.ts +4 -0
  255. package/src/config/feature-flag-registry.json +12 -12
  256. package/src/config/llm-context-resolution.ts +11 -3
  257. package/src/config/llm-resolver.ts +28 -1
  258. package/src/config/profile-materialization.ts +70 -22
  259. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  260. package/src/config/schemas/call-site-catalog.ts +4 -4
  261. package/src/config/schemas/live-voice.ts +57 -23
  262. package/src/config/schemas/llm.ts +59 -32
  263. package/src/config/schemas/mcp.ts +23 -0
  264. package/src/config/schemas/plugin-updates.ts +6 -2
  265. package/src/config/schemas/stt.ts +1 -0
  266. package/src/config/skills.ts +9 -2
  267. package/src/context/outbound-sanitize.ts +96 -1
  268. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  269. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  270. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  271. package/src/daemon/conversation-agent-loop.ts +31 -19
  272. package/src/daemon/conversation-messaging.ts +5 -1
  273. package/src/daemon/conversation-notifiers.ts +20 -10
  274. package/src/daemon/conversation-process.ts +20 -6
  275. package/src/daemon/conversation-runtime-assembly.ts +12 -6
  276. package/src/daemon/conversation-store.ts +14 -4
  277. package/src/daemon/conversation-surfaces.ts +75 -26
  278. package/src/daemon/conversation-tool-setup.ts +3 -6
  279. package/src/daemon/conversation.ts +154 -51
  280. package/src/daemon/doordash-steps.ts +2 -2
  281. package/src/daemon/lifecycle.ts +14 -1
  282. package/src/daemon/mcp-reload-service.ts +36 -6
  283. package/src/daemon/process-message.ts +13 -26
  284. package/src/daemon/providers-setup.ts +6 -3
  285. package/src/daemon/trust-context-types.ts +29 -0
  286. package/src/daemon/trust-context.ts +12 -7
  287. package/src/daemon/wake-conversation-ops.ts +3 -2
  288. package/src/daemon/windows-compiled-entry.ts +4 -0
  289. package/src/documents/document-store.ts +143 -240
  290. package/src/hooks/hook-loader.ts +3 -3
  291. package/src/hooks/registry.ts +103 -45
  292. package/src/hooks/types.ts +5 -0
  293. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  294. package/src/inbound/oauth-callback-url.ts +61 -0
  295. package/src/ipc/gateway-client.test.ts +1 -1
  296. package/src/ipc/gateway-client.ts +14 -21
  297. package/src/ipc/gateway-flag-listener.ts +17 -3
  298. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  299. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  300. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +1050 -0
  301. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  302. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  303. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  304. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  305. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  306. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  307. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  308. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  309. package/src/live-voice/live-voice-archive.ts +2 -0
  310. package/src/live-voice/live-voice-manager.ts +16 -3
  311. package/src/live-voice/live-voice-metrics.ts +57 -32
  312. package/src/live-voice/live-voice-photo.ts +1 -2
  313. package/src/live-voice/live-voice-session.ts +598 -323
  314. package/src/live-voice/progress-narration.ts +277 -0
  315. package/src/live-voice/protocol.ts +21 -1
  316. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  317. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  318. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  319. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  320. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  321. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  322. package/src/mcp/client.ts +16 -11
  323. package/src/mcp/effective-config.ts +113 -0
  324. package/src/mcp/manager.ts +11 -6
  325. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  326. package/src/mcp/mcp-oauth-provider.ts +205 -240
  327. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  328. package/src/monitoring/control.ts +1 -0
  329. package/src/monitoring/db-integrity-sample.ts +4 -5
  330. package/src/monitoring/plugin-auto-update.ts +128 -24
  331. package/src/notifications/AGENTS.md +2 -0
  332. package/src/notifications/approval-card-data.ts +33 -0
  333. package/src/notifications/signal.ts +1 -0
  334. package/src/permissions/AGENTS.md +16 -0
  335. package/src/permissions/checker.test.ts +75 -122
  336. package/src/permissions/checker.ts +44 -560
  337. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  338. package/src/permissions/confirmation-guardian-request.ts +2 -2
  339. package/src/permissions/prompter.ts +1 -5
  340. package/src/permissions/question-guardian-request.test.ts +14 -6
  341. package/src/permissions/question-guardian-request.ts +1 -2
  342. package/src/persistence/attachments-store.ts +8 -1
  343. package/src/persistence/bookmark-crud.ts +3 -7
  344. package/src/persistence/conversation-attention-store.ts +16 -45
  345. package/src/persistence/conversation-crud.ts +33 -4
  346. package/src/persistence/conversation-lineage.ts +9 -0
  347. package/src/persistence/conversation-queries.ts +182 -52
  348. package/src/persistence/delivery-crud.ts +150 -36
  349. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  350. package/src/persistence/external-conversation-store.ts +32 -4
  351. package/src/persistence/llm-request-log-store.ts +4 -10
  352. package/src/persistence/llm-usage-store.ts +8 -3
  353. package/src/persistence/message-reads.test.ts +197 -0
  354. package/src/persistence/message-reads.ts +211 -0
  355. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  356. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  357. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  358. package/src/persistence/real-user-turn-filter.ts +27 -3
  359. package/src/persistence/schema/documents.ts +4 -4
  360. package/src/persistence/steps.ts +9 -0
  361. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  362. package/src/plugin-api/constants.ts +8 -0
  363. package/src/plugin-api/conversation-turn.ts +195 -5
  364. package/src/plugin-api/index.ts +38 -6
  365. package/src/plugin-api/resolve-credential.ts +3 -2
  366. package/src/plugin-api/store-credential.ts +146 -0
  367. package/src/plugin-api/vision-support.test.ts +1 -1
  368. package/src/plugin-api/webhook-url.ts +13 -11
  369. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  370. package/src/plugins/defaults/main.ts +21 -7
  371. package/src/plugins/defaults/memory/AGENTS.md +15 -3
  372. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  373. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  374. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  375. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  376. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  377. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  378. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  379. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  380. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  381. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  382. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  383. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  384. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +17 -24
  385. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  386. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  387. package/src/plugins/defaults/memory/indexer.ts +3 -1
  388. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  389. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  390. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  391. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  392. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  393. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +147 -0
  394. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  395. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  396. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  397. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  398. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  399. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  400. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +195 -58
  401. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +93 -24
  402. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  403. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  404. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  405. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  406. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  407. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  408. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  409. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -33
  410. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  411. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  412. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  413. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  414. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  415. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  416. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  417. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  418. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  419. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  420. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  421. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  422. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  423. package/src/plugins/defaults/memory/worker.ts +6 -3
  424. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  425. package/src/plugins/external-plugin-loader.ts +47 -0
  426. package/src/plugins/mcp-servers.ts +361 -0
  427. package/src/plugins/mtime-cache.ts +37 -46
  428. package/src/plugins/pipeline.ts +14 -7
  429. package/src/plugins/plugin-execution-context.ts +35 -11
  430. package/src/plugins/worker-plugin-surface.ts +33 -0
  431. package/src/prompts/templates/system-sections.ts +0 -7
  432. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  433. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  434. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  435. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  436. package/src/providers/__tests__/retry-callsite.test.ts +25 -2
  437. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  438. package/src/providers/anthropic/client.ts +332 -241
  439. package/src/providers/call-site-routing.ts +30 -3
  440. package/src/providers/connection-resolution.ts +194 -11
  441. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +4 -0
  442. package/src/providers/inference/adapter-factory.ts +3 -0
  443. package/src/providers/inference/auth.ts +6 -0
  444. package/src/providers/inference/connection-availability.ts +24 -2
  445. package/src/providers/inference/connections.ts +2 -0
  446. package/src/providers/model-catalog.ts +3 -3
  447. package/src/providers/model-intents.ts +28 -8
  448. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +163 -40
  449. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  450. package/src/providers/openai/chat-completions-provider.ts +133 -36
  451. package/src/providers/openai/codex-models.ts +2 -1
  452. package/src/providers/provider-send-message.ts +32 -3
  453. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  454. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +625 -0
  455. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  456. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  457. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  458. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +679 -0
  459. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  460. package/src/providers/speech-to-text/resolve.ts +25 -2
  461. package/src/routes/control.ts +1 -0
  462. package/src/routes/route-host-client.ts +1 -0
  463. package/src/routes/route-host-protocol.ts +7 -0
  464. package/src/routes/worker.ts +48 -15
  465. package/src/runtime/AGENTS.md +16 -17
  466. package/src/runtime/access-request-helper.ts +9 -12
  467. package/src/runtime/agent-wake.ts +18 -15
  468. package/src/runtime/effective-capabilities.test.ts +43 -0
  469. package/src/runtime/effective-capabilities.ts +27 -0
  470. package/src/runtime/guardian-reply-router.ts +6 -2
  471. package/src/runtime/pre-first-message-gate.ts +4 -0
  472. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  473. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +522 -2
  474. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  475. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  476. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  477. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  478. package/src/runtime/routes/__tests__/plugins-routes.test.ts +132 -35
  479. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  480. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  481. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  482. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  483. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +133 -1
  484. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  485. package/src/runtime/routes/channel-route-shared.ts +0 -6
  486. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  487. package/src/runtime/routes/conversation-list-routes.ts +181 -23
  488. package/src/runtime/routes/conversation-management-routes.ts +17 -12
  489. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  490. package/src/runtime/routes/conversation-routes.ts +43 -29
  491. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  492. package/src/runtime/routes/credential-routes.ts +26 -82
  493. package/src/runtime/routes/default-provider-routes.ts +15 -0
  494. package/src/runtime/routes/documents-routes.ts +3 -222
  495. package/src/runtime/routes/inbound-message-handler.ts +21 -45
  496. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  497. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  498. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  499. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  500. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  501. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -79
  502. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  503. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -7
  504. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -8
  505. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -79
  506. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  507. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  508. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  509. package/src/runtime/routes/log-export-routes.ts +3 -0
  510. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  511. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  512. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  513. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  514. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  515. package/src/runtime/routes/plugins-routes.ts +38 -8
  516. package/src/runtime/routes/schedule-routes.ts +94 -7
  517. package/src/runtime/routes/settings-routes.ts +6 -6
  518. package/src/runtime/routes/stt-routes.ts +31 -25
  519. package/src/runtime/routes/surface-action-routes.ts +20 -19
  520. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  521. package/src/runtime/routes/user-route-dispatcher.ts +69 -15
  522. package/src/runtime/routes/user-route-import.ts +108 -0
  523. package/src/runtime/routes/user-route-resolution.ts +18 -1
  524. package/src/runtime/routes/workspace-routes.ts +0 -9
  525. package/src/runtime/routes/workspace-utils.ts +3 -13
  526. package/src/runtime/services/conversation-serializer.ts +7 -2
  527. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  528. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  529. package/src/schedule/plugin-schedule-availability.ts +58 -0
  530. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  531. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  532. package/src/schedule/schedule-store.ts +5 -1
  533. package/src/schedule/scheduler.ts +9 -4
  534. package/src/schedule/worker-control.ts +1 -0
  535. package/src/schedule/worker.ts +6 -0
  536. package/src/security/oauth2.ts +6 -22
  537. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  538. package/src/stt/__tests__/types.test.ts +94 -0
  539. package/src/stt/daemon-batch-transcriber.ts +10 -0
  540. package/src/stt/stt-stream-session.ts +8 -4
  541. package/src/stt/types.ts +103 -0
  542. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  543. package/src/subagent/consult-context.ts +11 -11
  544. package/src/subagent/consult-prompt.ts +26 -35
  545. package/src/subagent/manager.ts +21 -40
  546. package/src/subagent/notify.ts +7 -1
  547. package/src/subagent/types.ts +15 -12
  548. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  549. package/src/tools/acp/spawn.test.ts +97 -0
  550. package/src/tools/acp/spawn.ts +38 -4
  551. package/src/tools/credentials/ref-parse.ts +35 -0
  552. package/src/tools/credentials/resolve.ts +4 -10
  553. package/src/tools/credentials/store.ts +168 -0
  554. package/src/tools/document/document-tool.ts +12 -3
  555. package/src/tools/executor.ts +53 -15
  556. package/src/tools/filesystem/read.ts +27 -10
  557. package/src/tools/host-filesystem/read.ts +27 -15
  558. package/src/tools/network/url-safety.ts +9 -16
  559. package/src/tools/permission-checker.ts +43 -52
  560. package/src/tools/registry.ts +2 -1
  561. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  562. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  563. package/src/tools/shared/filesystem/types.ts +5 -5
  564. package/src/tools/skills/scaffold-managed.ts +25 -7
  565. package/src/tools/subagent/spawn.ts +28 -88
  566. package/src/tools/tool-types.ts +26 -13
  567. package/src/tools/types.ts +6 -6
  568. package/src/tools/ui-surface/surface-shape-docs.ts +12 -1
  569. package/src/tools/workflows/run-workflow.ts +1 -2
  570. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  571. package/src/tts/reasoning-tag-filter.ts +89 -0
  572. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  573. package/src/util/logger.ts +16 -0
  574. package/src/util/think-tag-stream.ts +95 -0
  575. package/src/util/worker-process.ts +37 -4
  576. package/src/windows-compiled-cli.ts +32 -0
  577. package/src/windows-compiled-entry.ts +4 -0
  578. package/src/windows-compiled-logger.ts +6 -0
  579. package/src/windows-compiled-worker-entry.ts +29 -0
  580. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  581. package/src/workspace/custom-profile-ensure.ts +4 -24
  582. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  583. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  584. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  585. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  586. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  587. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  588. package/src/workspace/migrations/registry.ts +10 -0
  589. package/src/workspace/provider-commit-message-generator.ts +7 -5
  590. package/src/__tests__/document-workspace-file.test.ts +0 -467
  591. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  592. package/src/live-voice/front-decision.ts +0 -476
  593. package/src/permissions/ipc-risk-types.ts +0 -143
  594. package/src/permissions/risk-types.ts +0 -76
  595. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  596. package/src/subagent/consult-transcript.ts +0 -90
@@ -2,7 +2,6 @@
2
2
  * Shared types, constants, and utilities used across channel route modules.
3
3
  */
4
4
  import type { ChannelId } from "../../channels/types.js";
5
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
6
5
  import {
7
6
  type ApprovalAction,
8
7
  type ApprovalDecisionResult,
@@ -10,11 +9,6 @@ import {
10
9
  isApprovalAction,
11
10
  } from "../channel-approval-types.js";
12
11
 
13
- /** Canonicalize assistantId for channel ingress paths. */
14
- export function canonicalChannelAssistantId(_assistantId: string): string {
15
- return DAEMON_INTERNAL_ASSISTANT_ID;
16
- }
17
-
18
12
  // ---------------------------------------------------------------------------
19
13
  // Actor role
20
14
  // ---------------------------------------------------------------------------
@@ -159,13 +159,13 @@ async function handleExchange(args: RouteHandlerArgs) {
159
159
 
160
160
  const existing = getConnection(db, CONNECTION_NAME);
161
161
  if (existing) {
162
- // Stamp the provider with the auth: this row is the subscription
163
- // connection by name, and dispatch derives auth from the provider for
164
- // managed rows, so a claiming row with e.g. provider "vellum" would
165
- // otherwise strand the fresh token behind derived platform auth.
162
+ // Stamp the provider with the auth: this row IS the subscription route
163
+ // (auth modality = provider identity), and dispatch derives the openai
164
+ // upstream from the identity per-request. Stamping also heals a
165
+ // claiming row whose provider would otherwise misroute the fresh token.
166
166
  const updateResult = updateConnection(db, CONNECTION_NAME, {
167
167
  auth: authInput,
168
- provider: "openai",
168
+ provider: "chatgpt",
169
169
  });
170
170
  if (!updateResult.ok) {
171
171
  log.error(
@@ -177,7 +177,7 @@ async function handleExchange(args: RouteHandlerArgs) {
177
177
  } else {
178
178
  const createResult = createConnection(db, {
179
179
  name: CONNECTION_NAME,
180
- provider: "openai",
180
+ provider: "chatgpt",
181
181
  auth: authInput,
182
182
  });
183
183
  if (!createResult.ok) {
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * GET /v1/conversations — paginated conversation list
5
5
  * GET /v1/conversations/unread-count - count of unseen foreground conversations
6
+ * GET /v1/conversations/sections - per-section totals for the sidebar index
6
7
  * POST /v1/conversations/seen — record a seen signal (single)
7
8
  * POST /v1/conversations/seen/bulk — record seen signals (batch)
8
9
  * POST /v1/conversations/unread — mark a conversation unread
@@ -29,6 +30,7 @@ import { resolveConversationId } from "../../persistence/conversation-key-store.
29
30
  import {
30
31
  type ConversationListFilter,
31
32
  countConversations,
33
+ countConversationSections,
32
34
  countUnreadConversations,
33
35
  listConversations,
34
36
  listPinnedConversations,
@@ -155,6 +157,42 @@ const unreadConversationCountResponseSchema = z.object({
155
157
  count: z.number(),
156
158
  });
157
159
 
160
+ const sectionCountFields = {
161
+ total: z.number(),
162
+ unread: z.number(),
163
+ };
164
+
165
+ /**
166
+ * One renderable sidebar section. Discriminated by `kind` so clients get a
167
+ * typed union: group sections carry their metadata inline (one consistent
168
+ * snapshot, no join against `GET /v1/groups` that could disagree with it),
169
+ * channel sections carry only the id (labels are client-side i18n), and
170
+ * `chats` is the ungrouped-native bucket. In a view without channel sections
171
+ * the Chats card holds every ungrouped row: the buckets are disjoint, so
172
+ * that reading is `chats` plus the sum of `channel` entries.
173
+ */
174
+ const conversationSectionSchema = z.discriminatedUnion("kind", [
175
+ z.object({ kind: z.literal("pinned"), ...sectionCountFields }),
176
+ z.object({
177
+ kind: z.literal("group"),
178
+ groupId: z.string(),
179
+ name: z.string(),
180
+ icon: z.string().nullable(),
181
+ sortPosition: z.number(),
182
+ ...sectionCountFields,
183
+ }),
184
+ z.object({
185
+ kind: z.literal("channel"),
186
+ channelId: z.string(),
187
+ ...sectionCountFields,
188
+ }),
189
+ z.object({ kind: z.literal("chats"), ...sectionCountFields }),
190
+ ]);
191
+
192
+ const conversationSectionsResponseSchema = z.object({
193
+ sections: z.array(conversationSectionSchema),
194
+ });
195
+
158
196
  // ---------------------------------------------------------------------------
159
197
  // Helpers
160
198
  // ---------------------------------------------------------------------------
@@ -171,28 +209,41 @@ function resolveOrThrow(rawId: string): string {
171
209
  // Handlers
172
210
  // ---------------------------------------------------------------------------
173
211
 
212
+ /**
213
+ * Read a query parameter that admits a closed set of values. Absent or empty
214
+ * reads as `undefined`; anything outside `accepted` is a 400. The strictness
215
+ * is the point: silently coercing a typo or a newer client's value to "no
216
+ * filter" would hand back the full list where a subset was asked for, and
217
+ * that skew is invisible to the client (it masks version skew too).
218
+ */
219
+ function parseEnumQueryParam<const T extends readonly string[]>(
220
+ queryParams: Record<string, string | undefined>,
221
+ name: string,
222
+ accepted: T,
223
+ ): T[number] | undefined {
224
+ const raw = queryParams[name];
225
+ if (raw === undefined || raw === "") {
226
+ return undefined;
227
+ }
228
+ if ((accepted as readonly string[]).includes(raw)) {
229
+ return raw as T[number];
230
+ }
231
+ throw new BadRequestError(
232
+ `Unknown ${name} "${raw}"; expected ${accepted.map((v) => `"${v}"`).join(" or ")}.`,
233
+ );
234
+ }
235
+
174
236
  function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
175
237
  const limit = Number(queryParams.limit ?? 50);
176
238
  const offset = Number(queryParams.offset ?? 0);
177
239
  // "background" is the back-compat umbrella (background + scheduled); newer
178
240
  // clients can pass "scheduled" to load only the Scheduled section. Absent
179
- // defaults to the standard foreground list. Any other value is rejected so
180
- // an unrecognized type surfaces as a 400 rather than being silently coerced
181
- // to the foreground list (which would mask client/daemon version skew).
182
- const rawConversationType = queryParams.conversationType;
183
- let conversationType: ConversationType = "standard";
184
- if (rawConversationType !== undefined && rawConversationType !== "") {
185
- if (
186
- rawConversationType === "background" ||
187
- rawConversationType === "scheduled"
188
- ) {
189
- conversationType = rawConversationType;
190
- } else {
191
- throw new BadRequestError(
192
- `Unknown conversationType "${rawConversationType}"; expected "background" or "scheduled".`,
193
- );
194
- }
195
- }
241
+ // defaults to the standard foreground list.
242
+ const conversationType: ConversationType =
243
+ parseEnumQueryParam(queryParams, "conversationType", [
244
+ "background",
245
+ "scheduled",
246
+ ]) ?? "standard";
196
247
  // Defaults to `active` so sidebar restores no longer pull archived rows.
197
248
  // The Archive page opts into `archived` to render only archived rows
198
249
  // without dragging the entire live history through pagination first.
@@ -213,11 +264,23 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
213
264
  ? queryParams.groupId
214
265
  : undefined;
215
266
 
267
+ const needsAttention =
268
+ parseEnumQueryParam(queryParams, "needsAttention", ["true"]) === "true"
269
+ ? true
270
+ : undefined;
271
+
272
+ const foregroundOnly =
273
+ parseEnumQueryParam(queryParams, "foregroundOnly", ["true"]) === "true"
274
+ ? true
275
+ : undefined;
276
+
216
277
  const filter: ConversationListFilter = {
217
278
  conversationType,
218
279
  archiveStatus,
219
280
  originChannel,
220
281
  groupId,
282
+ needsAttention,
283
+ foregroundOnly,
221
284
  };
222
285
  let rows = listConversations({ ...filter, limit, offset });
223
286
  const totalCount = countConversations(filter);
@@ -232,15 +295,20 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
232
295
  // Skipped for group-scoped queries for the same reason: a caller asking
233
296
  // for one group gets that group, and a client that fetches the Pinned
234
297
  // section via `groupId=system:pinned` has no use for rows appended to
235
- // some other group's page. This is the compatibility shim for clients
236
- // that still read Pinned out of the unfiltered list; it goes away once
237
- // every section fetches its own group.
298
+ // some other group's page. Skipped for attention-scoped and
299
+ // foreground-only queries too: the caller asked for a subset, and a
300
+ // pinned row outside it appended to the page would sit there while
301
+ // `hasMore` is computed from the filtered count. This is the
302
+ // compatibility shim for clients that still read Pinned out of the
303
+ // unfiltered list; it goes away once every section fetches its own group.
238
304
  if (
239
305
  offset === 0 &&
240
306
  conversationType === "standard" &&
241
307
  archiveStatus === "active" &&
242
308
  originChannel === undefined &&
243
- groupId === undefined
309
+ groupId === undefined &&
310
+ needsAttention === undefined &&
311
+ foregroundOnly === undefined
244
312
  ) {
245
313
  const pinned = listPinnedConversations(archiveStatus);
246
314
  const seen = new Set(rows.map((c) => c.id));
@@ -292,6 +360,64 @@ function handleGetUnreadConversationCount() {
292
360
  return { count: countUnreadConversations() };
293
361
  }
294
362
 
363
+ /**
364
+ * One row per non-empty sidebar section, no conversation rows. What this
365
+ * exists to answer without a list drain: which sections render at all,
366
+ * and what their rail badges say.
367
+ *
368
+ * A count row whose `group_id` names no existing non-system group is
369
+ * skipped rather than surfaced: those rows are unreachable as a section
370
+ * (the sidebar builds group sections from the groups table), and a
371
+ * dangling id is a transient state around group deletion, not a section.
372
+ */
373
+ function handleGetConversationSections() {
374
+ const counts = countConversationSections();
375
+ const groupsById = new Map(listGroups().map((g) => [g.id, g]));
376
+ const sections: Array<z.infer<typeof conversationSectionSchema>> = [];
377
+
378
+ for (const row of counts.groups) {
379
+ if (row.groupId === "system:pinned") {
380
+ sections.push({ kind: "pinned", total: row.total, unread: row.unread });
381
+ continue;
382
+ }
383
+ const meta = groupsById.get(row.groupId);
384
+ if (!meta || meta.isSystemGroup) {
385
+ continue;
386
+ }
387
+ sections.push({
388
+ kind: "group",
389
+ groupId: row.groupId,
390
+ name: meta.name,
391
+ icon: meta.icon,
392
+ sortPosition: meta.sortPosition,
393
+ total: row.total,
394
+ unread: row.unread,
395
+ });
396
+ }
397
+
398
+ for (const row of counts.channels) {
399
+ if (row.channel === "vellum") {
400
+ sections.push({ kind: "chats", total: row.total, unread: row.unread });
401
+ continue;
402
+ }
403
+ sections.push({
404
+ kind: "channel",
405
+ channelId: row.channel,
406
+ total: row.total,
407
+ unread: row.unread,
408
+ });
409
+ }
410
+
411
+ /* Every other section exists only when it has rows; Chats is the leftover
412
+ bucket and renders regardless, so its counts are part of the contract
413
+ even at zero. */
414
+ if (!sections.some((s) => s.kind === "chats")) {
415
+ sections.push({ kind: "chats", total: 0, unread: 0 });
416
+ }
417
+
418
+ return { sections };
419
+ }
420
+
295
421
  function handleRecordSeen({ body = {}, headers }: RouteHandlerArgs) {
296
422
  const rawConversationId = body.conversationId as string | undefined;
297
423
  if (!rawConversationId) {
@@ -482,7 +608,23 @@ export const ROUTES: RouteDefinition[] = [
482
608
  type: "string",
483
609
  required: false,
484
610
  description:
485
- 'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is ordered by the user\'s arrangement (display order, then recency) and never has pinned rows appended to it. Omit to span every group.',
611
+ 'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is recency-ordered like every list read (COALESCE(last_message_at, updated_at) descending) and never has pinned rows appended to it. Omit to span every group.',
612
+ },
613
+ {
614
+ name: "needsAttention",
615
+ type: "string",
616
+ required: false,
617
+ description:
618
+ 'Pass "true" to return only conversations whose latest assistant message the user has not seen: the same predicate behind the unread count and the section index, so a client that keeps no complete conversation list can ask for exactly the rows its attention surfaces need. Composes with every other filter. Any value other than "true" is rejected. Omit to span every conversation.',
619
+ schema: { type: "string", enum: ["true"] },
620
+ },
621
+ {
622
+ name: "foregroundOnly",
623
+ type: "string",
624
+ required: false,
625
+ description:
626
+ 'Pass "true" to return only user-facing foreground conversations, dropping the automated background and scheduled runs the standard listing admits when they are filed in a custom group (a surfaced run stays). This is the same predicate the unread count and the section index apply, so a client can ask for the newest conversation a user can open in one row instead of paging past runs it would skip. Composes with every other filter; a foreground-only first page never has pinned rows appended to it. Any value other than "true" is rejected. Omit to keep every visible row.',
627
+ schema: { type: "string", enum: ["true"] },
486
628
  },
487
629
  ],
488
630
  responseBody: listConversationsResponseSchema,
@@ -503,6 +645,21 @@ export const ROUTES: RouteDefinition[] = [
503
645
  responseBody: unreadConversationCountResponseSchema,
504
646
  handler: handleGetUnreadConversationCount,
505
647
  },
648
+ {
649
+ operationId: "getConversationSections",
650
+ endpoint: "conversations/sections",
651
+ method: "GET",
652
+ policy: {
653
+ requiredScopes: ["chat.read"],
654
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
655
+ },
656
+ summary: "Sidebar section index",
657
+ description:
658
+ "One row per renderable sidebar section (Pinned, each non-empty custom group, each origin channel with unassigned conversations, Chats) with total and unread counts and no conversation rows. Lets a client know which sections exist, and what their badges say, without fetching any conversation list. Totals follow the standard listing visibility; unread applies the same rules as GET /v1/conversations/unread-count scoped to the section. Chats is always present, even at zero: it is the leftover bucket and renders regardless.",
659
+ tags: ["conversations"],
660
+ responseBody: conversationSectionsResponseSchema,
661
+ handler: handleGetConversationSections,
662
+ },
506
663
  {
507
664
  operationId: "recordConversationSeen",
508
665
  endpoint: "conversations/seen",
@@ -572,7 +729,8 @@ export const ROUTES: RouteDefinition[] = [
572
729
  },
573
730
  pathParams: [{ name: "id", type: "uuid" }],
574
731
  summary: "Get conversation detail",
575
- description: "Retrieve a single conversation with full metadata.",
732
+ description:
733
+ "Retrieve a single conversation with full metadata. Rows the listing hides by type (legacy private rows) are not found here either.",
576
734
  tags: ["conversations"],
577
735
  responseBody: conversationDetailResponseSchema,
578
736
  handler: handleGetConversation,
@@ -308,9 +308,8 @@ async function handleSummarizeConversation({ body = {} }: RouteHandlerArgs) {
308
308
  statusText: "Summarizing conversation",
309
309
  });
310
310
  // The context-window usage push goes out on the same broadcast path as
311
- // the result card below: this route resolves its conversation outside
312
- // the send path, so the instance may still hold the store's no-op
313
- // sender and a `sendToClient` emit would reach nobody.
311
+ // the result card below, so the two can never be delivered to different
312
+ // places.
314
313
  const result = await conversation.summarizeUpToMessage(
315
314
  beforeMessageId,
316
315
  broadcastMessage,
@@ -692,17 +691,16 @@ async function handleRetryLastAssistantTurn({
692
691
  const conversation = await getOrCreateConversationInstance(conversationId);
693
692
  touchConversation(conversationId);
694
693
 
695
- // Bind the requesting actor's trust before the turn. A freshly hydrated
696
- // conversation has no trust context, and `loadFromDb` under an unset
697
- // context filters guardian-provenance history — the re-run would see an
698
- // empty transcript.
694
+ // Resolved before the busy gate so the awaits stay outside the
695
+ // check-then-claim window below, but bound only once the claim succeeds: a
696
+ // retry that loses the race is rejected, and a rejected request must not
697
+ // repoint the conversation's actor at its requester.
699
698
  const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
700
- conversation.setTrustContext(
701
- await resolveVellumActorTrustContext(actorPrincipalId, {
702
- healResetDrift: true,
703
- }),
699
+ const retryTrustContext = await resolveVellumActorTrustContext(
700
+ actorPrincipalId,
701
+ { healResetDrift: true },
704
702
  );
705
- conversation.currentTurnSourceActorPrincipalId =
703
+ const retrySourceActorPrincipalId =
706
704
  await resolveActorPrincipalIdForLocalGuardian(actorPrincipalId);
707
705
 
708
706
  // Synchronous check-then-claim (no await between them) so a concurrent
@@ -715,6 +713,13 @@ async function handleRetryLastAssistantTurn({
715
713
  }
716
714
  conversation.setProcessing(true);
717
715
 
716
+ // Bind the requesting actor's trust for the turn we just claimed. A freshly
717
+ // hydrated conversation has no trust context, and `loadFromDb` under an
718
+ // unset context filters guardian-provenance history — the re-run would see
719
+ // an empty transcript.
720
+ conversation.setTrustContext(retryTrustContext);
721
+ conversation.currentTurnSourceActorPrincipalId = retrySourceActorPrincipalId;
722
+
718
723
  const requestId = uuidv7();
719
724
  const abortController = new AbortController();
720
725
  let discarded: ReturnType<typeof discardLastAssistantDisplayTurn>;
@@ -41,7 +41,10 @@ import {
41
41
  withSuppressedConfigDiskWrites,
42
42
  withSuppressedConfigDiskWritesSync,
43
43
  } from "../../config/loader.js";
44
- import { completeCustomProfile } from "../../config/profile-materialization.js";
44
+ import {
45
+ completeCustomProfile,
46
+ mergePreservingUnknownKeys,
47
+ } from "../../config/profile-materialization.js";
45
48
  import { AssistantConfigSchema } from "../../config/schema.js";
46
49
  import { getSchemaAtPath } from "../../config/schema-utils.js";
47
50
  import {
@@ -50,6 +53,7 @@ import {
50
53
  LLMConfigFragment,
51
54
  ProfileEntry,
52
55
  routingIdentityModelIssue,
56
+ unknownLlmProviderIssue,
53
57
  } from "../../config/schemas/llm.js";
54
58
  import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
55
59
  import { ServiceModeSchema } from "../../config/schemas/services.js";
@@ -96,6 +100,7 @@ import { type LogRow } from "../../persistence/llm-request-log-store.js";
96
100
  import { getMemoryRecallLogByMessageIds } from "../../plugins/defaults/memory/memory-recall-log-store.js";
97
101
  import { getMemoryV2ActivationLogByMessageIds } from "../../plugins/defaults/memory/v2/activation-log-store.js";
98
102
  import { getMemoryV3SelectionForInspectorByMessageIds } from "../../plugins/defaults/memory/v3/selection-log-store.js";
103
+ import { writableProfileProviderIssue } from "../../providers/connection-resolution.js";
99
104
  import { ROUTING_IDENTITY_PROVIDERS } from "../../providers/inference/auth.js";
100
105
  import { PROVIDERS_REQUIRING_BASE_URL_AND_MODELS } from "../../providers/inference/auth.js";
101
106
  import {
@@ -1252,30 +1257,6 @@ function assertInvariantProfilesPreserved(
1252
1257
  }
1253
1258
  }
1254
1259
 
1255
- /**
1256
- * `{...raw, ...completed}` recursively: completed (schema-known) values win,
1257
- * raw keys the schema stripped survive at every depth.
1258
- */
1259
- function mergePreservingUnknownKeys(
1260
- raw: Record<string, unknown>,
1261
- completed: Record<string, unknown>,
1262
- ): Record<string, unknown> {
1263
- const out: Record<string, unknown> = { ...raw, ...completed };
1264
- for (const [key, value] of Object.entries(completed)) {
1265
- const rawValue = raw[key];
1266
- if (
1267
- readPlainObject(value) !== undefined &&
1268
- readPlainObject(rawValue) !== undefined
1269
- ) {
1270
- out[key] = mergePreservingUnknownKeys(
1271
- rawValue as Record<string, unknown>,
1272
- value as Record<string, unknown>,
1273
- );
1274
- }
1275
- }
1276
- return out;
1277
- }
1278
-
1279
1260
  /**
1280
1261
  * Custom profiles persist as complete overrides: any user-source, non-mix
1281
1262
  * profile entry this write creates or changes is completed against the
@@ -1343,7 +1324,9 @@ function completeChangedCustomProfiles(
1343
1324
  * would let the pair reach disk and be stripped on the next read — a silent
1344
1325
  * no-op where the user expects a saved override.
1345
1326
  */
1346
- function assertRoutableIdentityEntries(raw: Record<string, unknown>): void {
1327
+ function collectProviderEntries(
1328
+ raw: Record<string, unknown>,
1329
+ ): [string, { provider?: unknown; model?: unknown }][] {
1347
1330
  const llm = raw.llm as
1348
1331
  | {
1349
1332
  default?: { provider?: unknown; model?: unknown } | null;
@@ -1371,10 +1354,40 @@ function assertRoutableIdentityEntries(raw: Record<string, unknown>): void {
1371
1354
  }
1372
1355
  }
1373
1356
  }
1357
+ return entries;
1358
+ }
1359
+
1360
+ function assertRoutableIdentityEntries(
1361
+ preWrite: Record<string, unknown>,
1362
+ raw: Record<string, unknown>,
1363
+ ): void {
1364
+ const entries = collectProviderEntries(raw);
1365
+ const priorProviders = new Map(
1366
+ collectProviderEntries(preWrite).flatMap(([label, entry]) =>
1367
+ typeof entry.provider === "string" ? [[label, entry.provider]] : [],
1368
+ ),
1369
+ );
1374
1370
  for (const [label, entry] of entries) {
1375
1371
  if (typeof entry.provider !== "string") {
1376
1372
  continue;
1377
1373
  }
1374
+ // The provider schema is an open string (a stored value outside the
1375
+ // known set parses instead of stripping its profile), so write-time
1376
+ // membership is enforced here and at the profiles route. Scoped to
1377
+ // providers this write introduces or changes: a stored value outside
1378
+ // the known set is readable and dispatchable by design, and
1379
+ // re-validating it on every write would make all later settings saves
1380
+ // fail with no in-product repair path. Profiles accept entry names;
1381
+ // call-site fragments and the legacy default blob stay vendor-only
1382
+ // (overrides become model-only with the entries demolition).
1383
+ if (entry.provider !== priorProviders.get(label)) {
1384
+ const providerIssue = label.startsWith("llm.profiles.")
1385
+ ? writableProfileProviderIssue(entry.provider)
1386
+ : unknownLlmProviderIssue(entry.provider);
1387
+ if (providerIssue) {
1388
+ throw new BadRequestError(`${providerIssue} (${label})`);
1389
+ }
1390
+ }
1378
1391
  const issue = routingIdentityModelIssue(
1379
1392
  entry.provider,
1380
1393
  typeof entry.model === "string" ? entry.model : undefined,
@@ -1396,7 +1409,7 @@ export async function commitConfigWrite(
1396
1409
  const preWrite = loadRawConfig();
1397
1410
  completeChangedCustomProfiles(preWrite, raw);
1398
1411
  assertInvariantProfilesPreserved(preWrite, raw);
1399
- assertRoutableIdentityEntries(raw);
1412
+ assertRoutableIdentityEntries(preWrite, raw);
1400
1413
 
1401
1414
  // Suppress the file-watcher callback for the duration of the debounce
1402
1415
  // window. Without this, the ConfigWatcher detects the config.json write
@@ -1849,6 +1849,11 @@ export async function handleSendMessage(
1849
1849
 
1850
1850
  // Resolve guardian context from the AuthContext's actorPrincipalId via the
1851
1851
  // gateway guardian binding: a vellum principal is the guardian or nobody.
1852
+ //
1853
+ // Resolved into a local; the conversation's slot is stamped only where this
1854
+ // request commits to running a turn, since that write is what supplies the
1855
+ // acting actor for a run and must not fire for a send that merely queues.
1856
+ let resolvedTrustCtx: TrustContext;
1852
1857
  if (actorPrincipalId) {
1853
1858
  // Dev bypass (HTTP auth disabled): the synthetic "dev-bypass" principal
1854
1859
  // won't match any guardian binding. Resolve the real guardian principal and
@@ -1874,7 +1879,7 @@ export async function handleSendMessage(
1874
1879
  trustCtx = healed;
1875
1880
  }
1876
1881
  }
1877
- conversation.setTrustContext(trustCtx);
1882
+ resolvedTrustCtx = trustCtx;
1878
1883
  } else {
1879
1884
  let trustCtx = withSourceChannel(
1880
1885
  sourceChannel,
@@ -1907,22 +1912,19 @@ export async function handleSendMessage(
1907
1912
  );
1908
1913
  }
1909
1914
  }
1910
- conversation.setTrustContext(trustCtx);
1915
+ resolvedTrustCtx = trustCtx;
1911
1916
  }
1912
1917
  } else {
1913
1918
  // Service principals (svc_gateway) or tokens without an actor ID
1914
1919
  // get a minimal guardian context so downstream code has something.
1915
- conversation.setTrustContext({ trustClass: "guardian", sourceChannel });
1920
+ resolvedTrustCtx = { trustClass: "guardian", sourceChannel };
1916
1921
  }
1917
1922
 
1918
- // The trust this request's turn runs under, captured here rather than read
1919
- // back at the agent loop below. Every branch above has just written it and
1920
- // nothing awaits in between, so this is the resolved sender. Between here
1921
- // and the loop the conversation slot is writable by paths that do not own
1922
- // this turn (channel ingress for another actor, live-voice hydration,
1923
- // pointer elevation, the voice bridge), and the loop would otherwise
1924
- // re-read it and run this turn as whoever wrote last.
1925
- const turnTrustContext = conversation.trustContext;
1923
+ // The trust this request's turn runs under: the sender resolved above,
1924
+ // never a read of the shared slot. Between here and the loop that slot is
1925
+ // writable by paths that do not own this turn (channel ingress for another
1926
+ // actor, live-voice hydration, pointer elevation, the voice bridge).
1927
+ const turnTrustContext = resolvedTrustCtx;
1926
1928
 
1927
1929
  const isInteractive = isInteractiveInterface(sourceInterface);
1928
1930
  // Translate the dev-bypass actor principal to the real guardian principal
@@ -1979,20 +1981,18 @@ export async function handleSendMessage(
1979
1981
  sourceActorPrincipalId,
1980
1982
  );
1981
1983
  }
1982
- // Wire sendToClient to the SSE hub so all subsystems can reach the HTTP client.
1983
- // hasNoClient must remain `!isInteractive` so downstream tool gating
1984
- // (`isToolActiveForContext` for HOST_TOOL_NAMES, `createToolExecutor`'s
1985
- // `isInteractive: !ctx.hasNoClient`) keeps host_bash/host_file/host_cu
1986
- // tools gated for non-desktop interfaces. The chrome-extension interface
1987
- // is non-interactive (no SSE prompter UI) but still has a connected client
1988
- // that can service host_browser_request events; we restore that single
1989
- // proxy explicitly below without relaxing `hasNoClient`.
1990
- conversation.updateClient(broadcastMessage, !isInteractive);
1984
+ // Delivery needs no wiring: the conversation's sink is the SSE hub for its
1985
+ // whole life. Presence travels with the turn (`isInteractive` below), which
1986
+ // is what keeps host_bash/host_file/host_cu gated for non-desktop
1987
+ // interfaces. The chrome-extension interface is non-interactive (no SSE
1988
+ // prompter UI) but still has a connected client that can service
1989
+ // host_browser_request events; that single proxy is restored explicitly
1990
+ // below without claiming a human is present.
1991
1991
  if (isInteractive) {
1992
- getSubagentManager().updateParentSender(
1993
- mapping.conversationId,
1994
- broadcastMessage,
1995
- );
1992
+ // A client that reconnected mid-phase resyncs the current activity state
1993
+ // and any subagent card it left stale while disconnected.
1994
+ conversation.replayActivityState();
1995
+ getSubagentManager().reannounceChildStatuses(mapping.conversationId);
1996
1996
  }
1997
1997
 
1998
1998
  // ── URL scan path: rewrite first message for scan onboarding ──
@@ -2055,12 +2055,15 @@ export async function handleSendMessage(
2055
2055
  requestId: uuidv7(),
2056
2056
  metadata: greetingMeta,
2057
2057
  clientMessageId,
2058
+ // This path answers and returns without starting a turn, so it never
2059
+ // reaches the stamp below; name the sender on the row directly.
2060
+ trustContext: resolvedTrustCtx,
2058
2061
  ...(clientOs ? { requestClientOs: clientOs } : {}),
2059
2062
  });
2060
2063
 
2061
2064
  const conversationId = mapping.conversationId;
2062
2065
  const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
2063
- trustContext: conversation.trustContext,
2066
+ trustContext: resolvedTrustCtx,
2064
2067
  });
2065
2068
 
2066
2069
  const assistantMsg = createAssistantMessage(cannedGreeting);
@@ -2167,10 +2170,9 @@ export async function handleSendMessage(
2167
2170
 
2168
2171
  // Resolve the verified actor's external user ID and principal for inline
2169
2172
  // approval routing from the conversation's guardian context.
2170
- const verifiedActorExternalUserId =
2171
- conversation.trustContext?.guardianExternalUserId;
2173
+ const verifiedActorExternalUserId = resolvedTrustCtx.guardianExternalUserId;
2172
2174
  const verifiedActorPrincipalId =
2173
- conversation.trustContext?.guardianPrincipalId ?? undefined;
2175
+ resolvedTrustCtx.guardianPrincipalId ?? undefined;
2174
2176
 
2175
2177
  // Try to consume the message as a guardian approval/rejection reply.
2176
2178
  // On failure, degrade to the existing queue/auto-deny path rather than
@@ -2244,6 +2246,10 @@ export async function handleSendMessage(
2244
2246
  sourceActorPrincipalId,
2245
2247
  transport,
2246
2248
  clientMessageId,
2249
+ // The sender's own trust, so the drain runs this message as the actor
2250
+ // who sent it rather than as whoever the slot happens to hold when the
2251
+ // queue is worked.
2252
+ trustContext: resolvedTrustCtx,
2247
2253
  });
2248
2254
  if (enqueueResult.rejected) {
2249
2255
  return new RouteResponse(
@@ -2342,7 +2348,15 @@ export async function handleSendMessage(
2342
2348
  // matching in-memory pending interaction (e.g. prompter timeouts).
2343
2349
  await expireOrphanedGuardianRequests(mapping.conversationId);
2344
2350
 
2345
- // Conversation is idle — persist and fire agent loop immediately
2351
+ // Conversation is idle — persist and fire agent loop immediately.
2352
+ //
2353
+ // Stamping the sender here rather than at resolution is what keeps the two
2354
+ // in step: the slot hydrates and scopes the turn started just below
2355
+ // (`ensureActorScopedHistory`, persisted provenance, the loop's own trust),
2356
+ // so it must name whoever this request is about to run as. A request that
2357
+ // queues instead returns above without stamping — it is not starting a run,
2358
+ // and its actor rides the queue item to the drain.
2359
+ conversation.setTrustContext(resolvedTrustCtx);
2346
2360
  conversation.setTurnChannelContext({
2347
2361
  userMessageChannel: sourceChannel,
2348
2362
  assistantMessageChannel: sourceChannel,