@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
@@ -0,0 +1,248 @@
1
+ # Deepgram Flux Turn Detection: Local Spike Runbook
2
+
3
+ How to enable Deepgram Flux end-of-turn in live voice on your own machine, run it against the existing front-door path, and read a latency comparison that is actually honest.
4
+
5
+ Flux is a spike. `liveVoice.flux.turnEnd.enabled` defaults to `false`, the existing local-VAD-plus-front-door hold machinery is untouched and still the default, and it is still the fallback when Flux goes quiet. Nothing here is a rollout.
6
+
7
+ ---
8
+
9
+ ## Read this before you read any number
10
+
11
+ **`endpointCommitLatencyMs` is the headline, and it is the only endpoint field that is like-for-like across the two arms.** It measures the local VAD speech-stop mark to the moment the turn committed. Take its median per arm and compare the two medians. There is no arithmetic to do and no source-specific semantics to remember.
12
+
13
+ It is stamped in `releaseUtterance` (`live-voice-session.ts`), which every committed turn passes through whichever decider released it, from the same speech-stop anchor on both. So the two arms are one population measured over one span. It is absent only on a turn that never committed, and in push-to-talk, which this spike does not run.
14
+
15
+ It is also the only endpoint number that a Flux socket teardown stays out of: `releaseUtterance` stamps it before any stop. On a turn Flux itself closed there is no teardown to carry; on a caller-side release `roundTripMs`, `llmFirstDeltaMs`, and `totalMs` still carry one, and so does what the caller hears. Read "The socket teardown, and when it is load-bearing" in section 3 before you compare any of those three against a `deepgram` run.
16
+
17
+ ### Why the other endpoint fields are not the comparison
18
+
19
+ `endpointDecisionMaxLatencyMs`, `endpointHoldCount`, and `endpointDecisionSource` all still exist and are all still worth reading. But `endpointDecisionMaxLatencyMs` is **not** comparable between arms, in two independent ways, and comparing it flatters Flux by roughly a full second with nothing in the output to tell you.
20
+
21
+ **It spans different things.**
22
+
23
+ | Path | What `endpointDecisionMaxLatencyMs` actually spans |
24
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `endpointDecisionSource: "provider"` | Local VAD speech-stop mark to the `turn-end` event. This is the **whole** end-of-turn latency. |
26
+ | `endpointDecisionSource: "front-door"` | Speculative dispatch to the hold verdict, i.e. the endpoint-decider LLM roundtrip **only**. The trailing-silence wait that had to elapse before that dispatch is not in the number, and neither is the hold extension that follows it. |
27
+
28
+ Source: `handleProviderTurnEnd` passes `this.msSinceLocalSpeechStop()` (`live-voice-session.ts`), while `holdSpeculativeTurn` passes `Date.now() - turn.speculativeDispatchedAtMs`, and the dispatch happens at the silence boundary.
29
+
30
+ **It samples different populations.** `markEndpointDecision` is called from exactly two places: the Flux commit, and the hold verdict. A front-door turn that committed straight through the boundary emits **no** `endpoint_decision` at all, and its metrics frame carries none of the three endpoint fields. So the front-door sample is "turns the model judged mid-thought", the slow tail by construction, while the Flux sample is every turn. Two denominators that cannot be reconciled after the fact is exactly why `endpointCommitLatencyMs` exists.
31
+
32
+ Read `endpointDecisionMaxLatencyMs` as a **breakdown of** the headline, not against the other arm: on a Flux turn it isolates how much of the commit latency was Flux's own decision, and on a held front-door turn it isolates the decider LLM roundtrip.
33
+
34
+ ### What the front door actually costs
35
+
36
+ You do not need this to read the headline. You need it when a front-door number surprises you.
37
+
38
+ - On an **unheld** turn the added end-of-turn cost is `silenceThresholdMs` alone. The speculative leg dispatched at the boundary _is_ the assistant turn, so if the model does not return the hold token, generation has been running since the boundary and the decider roundtrip is not added latency. That is the common case, and the real bar Flux has to beat.
39
+ - On a **held** turn, add `endpointExtensionMs` (default **1500**) per hold, capped at `endpointMaxExtensions` (default **2**), so up to 3000ms on top.
40
+ - `silenceThresholdMs` is the trailing-silence wait the boundary cost. Its value is, in precedence order: the client's explicit "pause before reply" setting sent on the start frame, else `liveVoice.vad.silenceThresholdMs` (schema default **1200**, `config/schemas/live-voice.ts`), else the in-code `DEFAULT_SILENCE_THRESHOLD_MS` of **800** (`live-voice-session.ts`). In a real daemon run the factory always seeds the config value, so **1200 is the number in force** unless you moved the web client's pause slider or overrode the config key. The 800 constant governs only a session built with no `liveVoice` config at all, which in practice means tests.
41
+
42
+ ### The cross-check that needs no correction
43
+
44
+ The per-turn timestamps in the metrics snapshot are absolute and come from one clock, and both paths stamp `utteranceEndAtMs` at the moment the turn commits (`markUtteranceReleased` runs inside `releaseUtterance`, which both paths call). `speechStartAtMs` is stamped at local VAD onset and is first-wins, so it survives a pause the front door held across.
45
+
46
+ ```
47
+ utteranceEndAtMs - speechStartAtMs
48
+ ```
49
+
50
+ Say the **same scripted sentence** on both arms, and the difference between the arms in that figure is the endpointing cost. It is an independent path to the same answer as `endpointCommitLatencyMs`: that field anchors at the speech-stop mark and this one at the VAD speech onset, so a run where the two disagree means the mic was picking up something the script did not say. Use it to sanity-check the headline.
51
+
52
+ ---
53
+
54
+ ## 1. Credentials
55
+
56
+ Flux shares the existing Deepgram credential. `credentialProvider: "deepgram"` in `providers/speech-to-text/provider-catalog.ts`, so there is **no new key to obtain and nowhere new to put it**. If `deepgram` already transcribes for you, Flux already has what it needs.
57
+
58
+ If it does not, set the Deepgram key the ordinary way (client Settings, Speech-to-text card) and pick either Deepgram entry; both write the same `deepgram` credential.
59
+
60
+ ## 2. Enable Flux
61
+
62
+ > **Setting `services.stt.provider` to `deepgram-flux` turns off batch transcription for the whole workspace, not just live voice.** `services.stt.provider` is the single source of truth for every STT route, and Flux is the only provider in the catalog with no `daemon-batch` boundary. For as long as it is set, these all stop working: voice-message and inbound-attachment transcription, the `transcribe` skill, the `media-processing` skill's audio segments, `POST /v1/stt/transcribe`, and phone-call transcription. Each reports a message naming Flux as the cause rather than failing silently, but they do not fall back to `deepgram` on their own. **Set `services.stt.provider` back to `deepgram` when the spike is over**, and do not run the spike on an assistant that is also taking calls or handling voice messages.
63
+
64
+ Two keys in `config.json`, which lives at `$VELLUM_WORKSPACE_DIR/config.json` (default `~/.vellum/workspace/config.json`):
65
+
66
+ ```json
67
+ {
68
+ "services": {
69
+ "stt": {
70
+ "provider": "deepgram-flux"
71
+ }
72
+ },
73
+ "liveVoice": {
74
+ "flux": {
75
+ "turnEnd": {
76
+ "enabled": true
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ Restart the daemon so the config is reloaded, then start a live-voice session **hands-free**. Push-to-talk is deliberately excluded: the latch requires a server-VAD turn detector, because in PTT the client's release already is the boundary and there is nothing for Flux to decide.
84
+
85
+ The rest of `liveVoice.flux` is optional and defaulted (`config/schemas/live-voice.ts`):
86
+
87
+ | Key | Default | Range | Notes |
88
+ | ------------------- | ----------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | `model` | `flux-general-en` | any string | English only in this spike. |
90
+ | `eotThreshold` | `0.7` | 0.5 to 0.9 | Lower commits sooner and cuts speakers off more; higher adds latency. |
91
+ | `eagerEotThreshold` | _unset_ | 0.3 to 0.9 | Leave it unset. See "Known gaps". |
92
+ | `eotTimeoutMs` | `5000` | 500 to 60000 | Silence after which Flux force-ends a turn it never got confident about. Lower it to 1500 to 2000 for a measurement run; see section 4. |
93
+
94
+ ## 3. Run the A/B
95
+
96
+ **Flip `turnEnd.enabled` between runs and leave `services.stt.provider` on `deepgram-flux` in both arms.** That holds the STT engine, the model, the socket, and the transcriber lifecycle constant, so the only thing that changes is which signal commits the turn.
97
+
98
+ - **Arm A (control):** `provider: "deepgram-flux"`, `turnEnd.enabled: false`. Flux transcribes; the four turn-detection events are ignored; the local silence boundary and the front-door hold path run exactly as they do today.
99
+ - **Arm B (treatment):** `provider: "deepgram-flux"`, `turnEnd.enabled: true`.
100
+
101
+ **Do not A/B by switching the provider between `deepgram` and `deepgram-flux`.** That confounds two changes at once:
102
+
103
+ 1. It swaps the STT model. `deepgram` runs `nova-2` (`DEFAULT_MODEL`, `deepgram-realtime.ts`), so any transcript-quality or first-partial difference lands in your latency numbers.
104
+ 2. It swaps the transcriber lifecycle. `deepgram` implements `finalizeUtterance`, so the session adopts it as a persistent stream shared across the whole session (`sharedTranscriber`) and never tears it down between turns. Flux implements no `finalizeUtterance`, so every utterance owns its own `/v2/listen` socket and every release closes it. That per-turn socket churn is inside `roundTripMs` and `totalMs` on the Flux side and absent on the `deepgram` side, so a naive provider-swap A/B attributes it to turn detection. The next section is what it costs and why it cannot be removed.
105
+
106
+ Say the same scripted set of utterances in both arms. Include at least a few deliberate mid-sentence thinking pauses, because that is the case the hold path exists for and the case Flux has to not regress.
107
+
108
+ ### The socket teardown, and when it is load-bearing
109
+
110
+ The Flux adapter implements no `finalizeUtterance`. That is forced by Flux's wire protocol, not a statement that Flux owns the turn boundary, and adding the method as a no-op would break transcript correctness.
111
+
112
+ `parseFluxFrame` emits `final` only on `EndOfTurn`, and that is the adapter's sole source of `final`. Flux offers no mid-stream flush, so `CloseStream` is the only message that makes it answer for a turn still in progress, which is exactly what `stop()` sends. A no-op `finalizeUtterance` would report `finalized` without flushing anything, so a turn released on a caller-side boundary would dispatch on an empty transcript while its real text arrived afterwards and was dropped as a late final segment. With `turnEnd.enabled` at its default `false` that is every turn, because the release always comes from the local silence path. With the latch on it is still every fail-open fallback, every max-duration force-end, and every barge-in, all of which release with a Flux turn open.
113
+
114
+ That reasoning holds for a turn the **caller** releases, and only for those. A turn Flux itself closes needs no flush at all: `EndOfTurn` emits the `final` immediately before `turn-end`, so the transcript is already complete when the release runs, and the stream can stay open (JARVIS-1538). The session reflects that split:
115
+
116
+ - **Provider-closed turns seal in place.** `handleProviderTurnEnd` marks the cycle `providerClosedTurn`, and the release moves it straight to `transcriber_closed` without stopping the transcriber. The stream serves the whole session, and `rearmAfterTurn` re-arms onto it synchronously.
117
+ - **Caller-side releases still close it.** The fail-open deadline, a max-duration force-end, and barge-in all release with a Flux turn potentially still open, and `CloseStream` remains the only message that makes Flux answer for one. Those releases retire the shared stream and tear it down; the next arm dials a fresh one.
118
+
119
+ Consequences for the numbers, all now specific to the caller-side path:
120
+
121
+ - **`endpointCommitLatencyMs` never contains a teardown.** `releaseUtterance` stamps the commit latency and `utteranceEndAtMs` before any stop, so the headline comparison is clean on both arms.
122
+ - **`roundTripMs`, `llmFirstDeltaMs`, and `totalMs` contain it only when the caller released the turn,** which with the latch on means the exceptional paths rather than every turn.
123
+
124
+ Before JARVIS-1538 every utterance dialed its own socket, and the audio arriving during that handshake was lost rather than replayed from the VAD pre-roll buffer. The opening words of each turn after the first went missing ("How many days are in February?" transcribed as "many days are in February?"). A persistent stream removes the handshake, and with it the gap.
125
+
126
+ ### What this A/B still does not hold constant
127
+
128
+ With Flux as the provider in both arms, the transcript **final** arrives at different moments. In arm B the `EndOfTurn` frame emits `final` immediately before `turn-end`, so the final is already in hand at commit. In arm A the local boundary fires first and release calls `stop()`, which sends `CloseStream` and waits for Flux to flush. So `sttMs` (the `utteranceEnd` to `finalTranscript` span) is not comparable between arms. It is not the headline number, but do not read a regression into it.
129
+
130
+ ## 4. Read the numbers
131
+
132
+ The daemon sends a `metrics` frame over the live-voice WebSocket on `turn_completed`, `turn_cancelled`, and `session_ended`. The endpoint fields are flattened onto that frame by `getLiveVoiceMetricsAggregateFields`:
133
+
134
+ | Field | Meaning |
135
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `endpointCommitLatencyMs` | **The headline.** Local VAD speech-stop mark to the commit. Present on every committed turn on both arms. Compare medians across arms. |
137
+ | `endpointDecisionSource` | `"provider"` or `"front-door"`. Present only when an endpoint decision was recorded. |
138
+ | `endpointDecisionMaxLatencyMs` | Worst single decision latency in the turn. A breakdown of the headline, never a cross-arm comparison. See the first section. |
139
+ | `endpointHoldCount` | Hold verdicts in the turn. Always 0 on a Flux-committed turn. |
140
+
141
+ The last three are **absent** unless a decision was recorded, which is deliberate: the absence is itself the signal, and it is what makes them useless as a cross-arm comparison. `endpointCommitLatencyMs` is absent only on a turn that never committed.
142
+
143
+ **How to see them.** The web client stores the whole frame but its `console.debug("[live-voice] turn latency", ...)` line does not print the endpoint fields. Read them from the raw frame instead: DevTools, Network, WS, select the live-voice socket, filter frames for `"type":"metrics"`, and read the `turn_completed` frames. The per-turn `metrics.recentTurns[]` array in the same frame carries the `timestamps` you need for the cross-check in the first section.
144
+
145
+ ### Reading an arm-B turn
146
+
147
+ | What you see | What happened |
148
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | `endpointDecisionSource: "provider"` | Flux committed the turn. This is the measurement you came for. |
150
+ | No endpoint fields at all | The fail-open deadline fired, the utterance fell back to the silence path, and the front door released it without holding. Not a Flux sample. |
151
+ | `endpointDecisionSource: "front-door"` | Same fallback, but the front door then held. Not a Flux sample either. |
152
+
153
+ Fallbacks are logged at `warn`, so they are visible at the default log level:
154
+
155
+ ```
156
+ No provider end-of-turn is coming; falling back to the silence boundary for this utterance
157
+ ```
158
+
159
+ The deadline is `liveVoice.flux.eotTimeoutMs` plus a 1000ms margin (`PROVIDER_TURN_END_FALLBACK_MARGIN_MS`), measured from the local speech-stop mark. If you see these routinely, you are measuring the fallback path, not Flux.
160
+
161
+ #### Lower `eotTimeoutMs` for the run
162
+
163
+ With the shipped defaults the fail-open budget is `eotTimeoutMs` (**5000**) plus `PROVIDER_TURN_END_FALLBACK_MARGIN_MS` (**1000**), so **6000ms from the local speech-stop mark**. The local silence boundary fires at ~1200ms and then hands the turn to Flux, so a stalled turn is roughly **4.8 seconds of dead air** before the utterance replays onto the hold path and anyone answers.
164
+
165
+ That is the worst case by design (the budget has to clear Flux's own force-end), but it is a bad property to carry through a measurement run. It makes a stall expensive to sit through, which biases you toward not reproducing one, and it means arm B's slow turns are dominated by a timeout rather than by turn detection.
166
+
167
+ Set `liveVoice.flux.eotTimeoutMs` to **1500 to 2000** for the duration of the run. The budget drops to 2500 to 3000ms, a stall costs about 1.3 to 1.8 seconds past the silence boundary instead of 4.8, and arm B stays representative of what Flux does rather than of what the deadline does. Put it back before drawing any conclusion about the shipped configuration: it also changes how long Flux itself waits before force-ending a turn it never got confident about.
168
+
169
+ ### Prefer medians, and do not chase a single bad sample
170
+
171
+ `isStaleProviderTurnEnd` decides on Flux's own `turn_index`, which `parseFluxFrame` carries onto `turn-start` and `turn-end`. The cycle records the newest turn Flux opened, so a delayed end-of-turn for an older index is recognized as stale and dropped, even though the caller has resumed speaking since. An end-of-turn for the turn still in progress is never stale: the mid-thought pause is what Flux's turn model exists to judge, its verdict covers the resumed speech, and the newest speech-stop mark is the right anchor for it.
172
+
173
+ The local VAD generation counter is only the fallback, for an event that carries no turn number. There the outlier mode survives: if the caller resumes and stops again before a delayed `turn-end` lands, the second boundary re-stamps `turnBoundaryGeneration` to the current generation, the event stops looking stale, and it is accepted against the newer speech-stop mark. The **commit is still correct**, but the recorded latency is understated for that turn, in `endpointCommitLatencyMs` and `endpointDecisionMaxLatencyMs` alike, since they share the anchor.
174
+
175
+ Drops log at `info` and so are visible at the default log level, carrying `turnIndex`, `openTurnIndex`, `boundaryGeneration`, and `speechGeneration`:
176
+
177
+ ```
178
+ Dropping a stale provider end-of-turn: the caller resumed speaking past the boundary it closed
179
+ ```
180
+
181
+ A drop is not itself an error: the cycle stays open and still commits, on the end-of-turn for the resumed speech or on the fail-open deadline. Read the line's fields. A drop with both `turnIndex` and `openTurnIndex` present is the turn-index path working, and it also drops the silent variant of the same race. A drop with `turnIndex` absent means Deepgram is sending unnumbered events and the run is on the generation fallback, where individual samples can be skewed and legitimate fast end-of-turns are dropped conservatively. Either way, report medians over a run of turns and ignore individual extremes.
182
+
183
+ ## 5. Confirm the session is really running Flux
184
+
185
+ The dialed URL is logged at `info` on every session open, and it carries the clamped query parameters. The API key travels in an `Authorization` header, never in the URL, so the log line is safe to read and paste.
186
+
187
+ ```
188
+ grep "Opening Deepgram Flux session" ~/.vellum/workspace/data/logs/assistant-*.log
189
+ ```
190
+
191
+ Check the URL is `/v2/listen` and contains `model=flux-general-en`, `eot_threshold`, `eot_timeout_ms`, and **no** `eager_eot_threshold`. That is the cheapest confirmation that the tuning you wrote is the tuning in force.
192
+
193
+ Deepgram's authoritative thresholds would come back on a `ConfigureSuccess` frame, but the adapter never sends `Configure` and the parser has no case for the response, so nothing echoes the tuning back. The dialed URL is the confirmation you have.
194
+
195
+ ### Log levels
196
+
197
+ Every diagnostic this runbook tells you to read is at `info` or `warn`, and the pino level is `info` (`src/util/logger.ts`), so nothing here needs a source edit or a rebuild. That includes the stale-turn-end drop in section 4 and the chunk-cadence line in section 6.
198
+
199
+ One Flux line does sit at `debug`: `buildFluxQueryParams` reports clamping `eager_eot_threshold` down to the effective `eot_threshold`. It fires only if you set `eagerEotThreshold`, which this spike says to leave unset.
200
+
201
+ ## 6. Chunk cadence
202
+
203
+ Deepgram recommends 80ms audio chunks for Flux. Capture cadence is a client concern and this spike does not change it, but the adapter makes it measurable from the daemon side: once per session, on the first audio frame, it logs at `info`
204
+
205
+ ```
206
+ Deepgram Flux audio chunk cadence
207
+ ```
208
+
209
+ with `byteLength`, `sampleRate`, `encoding`, `observedChunkMs`, and `recommendedChunkMs: 80`. `observedChunkMs` is derived from byte length and the negotiated sample rate and is only present for `linear16`, which is the default encoding. Read it before anyone proposes tuning the client; if it is already near 80 there is nothing to win there.
210
+
211
+ ## 7. Known gaps
212
+
213
+ Do not rediscover these.
214
+
215
+ - **English only.** The spike pins `flux-general-en`. No language is forwarded to the adapter, because `language_hint` means nothing to a monolingual model. The catalog entry carries `languageSelection: "auto"`, so the Settings speech-to-text card renders no language picker for Flux. Read that as "no picker", not as detection: audio in another language transcribes as English rather than being detected.
216
+ - **No telephony, and no fallback either.** The catalog entry sets `telephonyMode: "none"`, so `resolveTelephonySttCapability` reports Flux as unsupported and the call session reports that as its error. Nothing reroutes the call to the `deepgram` provider: with Flux configured, calls on this assistant are not transcribed at all.
217
+ - **No batch, workspace-wide.** `supportedBoundaries` is `daemon-streaming` only, and `resolveBatchTranscriber` throws for it rather than returning the `null` that every batch caller reports as "no speech-to-text provider is configured". Callers surface the thrown message instead: `Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".` See the warning in section 2 for the full list of surfaces this takes down.
218
+ - **No managed / velay path.** This is BYOK through the daemon only. The relay pins the model server-side, so managed rollout is a relay change.
219
+ - **Eager end-of-turn is off, and its being off is load-bearing.** `eagerEotThreshold` is optional with no default, and leaving it unset is precisely what stops Deepgram emitting `EagerEndOfTurn` / `TurnResumed` at all. The parser handles both frames and the session no-ops them, so the follow-up is small, but Deepgram warns that enabling speculation raises LLM calls by 50 to 70 percent. Note also that `buildFluxQueryParams` clamps `eager_eot_threshold` **down** to the effective `eot_threshold`, because Deepgram rejects the inverse combination.
220
+ - **The hold machinery is present and unchanged.** `HOLD_VERDICT_TOKEN`, the `includeHold` branch, the speculative dispatch and rollback state, `endpointExtensionMs`, and `endpointMaxExtensions` are all still there. The latch only skips them.
221
+ - **Local VAD still owns barge-in in both modes,** and must. A local energy gate on audio already in hand beats a provider roundtrip for an interrupt during playback, and the echo-adaptive part of the guard has to stay upstream of Flux, which hears only the microphone and cannot tell our own TTS bleeding through imperfect echo cancellation from a real caller turn.
222
+ - **Self-hosted bundles and `KeepAlive`.** The adapter sends a `KeepAlive` control frame every 5s, which cloud `/v2/listen` accepts and which is the only thing that resets Deepgram's server-side inactivity timer during silence. Self-hosted SageMaker Flux bundles reject it as a fatal `UNPARSABLE_CLIENT_MESSAGE`. The escape hatch is the adapter's `keepaliveIntervalMs: 0` option, but it is a constructor option only: `resolve.ts` passes just `sampleRate`, so pointing the adapter at a self-hosted bundle means editing that call site. There is no config key for it.
223
+
224
+ ### Two invariants the measurement rests on
225
+
226
+ Check these if you port the spike anywhere else, because a run that violates either produces numbers that look plausible and are not.
227
+
228
+ - **The latch is up before the session's first boundary.** `start()` sends `ready` without waiting on the transcriber resolve, so the caller can speak and close a whole silence boundary while the handshake is in flight. `beginUtterance` seeds the latch from the **configured** provider before the dial and reconciles it against the resolved provider afterwards, so the opening turn is a Flux turn like every other one rather than a front-door turn inside a session that looks like a Flux session.
229
+ - **Every recorded latency is anchored at a real speech-stop.** `localSpeechStopAtMs` is stamped on every above-gate chunk in every server-VAD session, and `markEndpointCommit` records nothing at all when there is no mark, which is push-to-talk. `msSinceLocalSpeechStop()` therefore never reports a turn measured from zero, and both arms share the anchor.
230
+
231
+ ## 8. Falling back
232
+
233
+ Set `liveVoice.flux.turnEnd.enabled` back to `false` (or delete the key) and restart. That is the whole rollback: the latch goes down, the turn-detection events become no-ops, and the silence-boundary path runs unchanged. You can leave `services.stt.provider` on `deepgram-flux` or move it back to `deepgram`; either is a working configuration.
234
+
235
+ Runtime fallback needs no action. A Flux stream that never emits `turn-end` is caught by the fail-open deadline and the utterance replays onto the silence path, so an outage degrades to today's behavior rather than to a hung turn.
236
+
237
+ ## 9. What would justify deleting the hold path
238
+
239
+ The follow-up cleanup is not small: `HOLD_VERDICT_TOKEN`, the `includeHold` branch of `frontDoorDecisionRule`, the `holdEnabled` parameter on `classifyFrontDoorLeading`, six pieces of speculative-dispatch state in the session, and the `endpointExtensionMs` / `endpointMaxExtensions` config surface. It is worth doing only if Flux clears a real bar. Deepgram claims Flux decides in under 400ms; that claim is the hypothesis under test here, not an input.
240
+
241
+ Suggested bar, all measured on `endpointCommitLatencyMs` over enough turns for a median to mean something. That field deliberately excludes the per-turn socket teardown, which keeps the arms comparable but also means a bar cleared here is not by itself a case for running Flux in front of users: the teardown is a cost Flux keeps paying and `deepgram` does not.
242
+
243
+ 1. **Median `endpointCommitLatencyMs` in arm B beats the median in arm A.** Most arm-A turns are unheld, so that median sits near `silenceThresholdMs`, about 1200ms with the defaults. Do not set the bar against arm A's held turns: beating those is easy and proves nothing, because most turns are not held.
244
+ 2. **No regression on thinking pauses.** The hold path exists so a mid-sentence pause does not trigger a premature reply. Count premature commits on the scripted pause utterances in both arms. Flux has to be at least as good, not merely faster. A fast path that interrupts people is worse than the slow one.
245
+ 3. **The fallback is rare.** If `warn`-level fallback lines appear on a meaningful fraction of turns, the hold path is not dead code, it is the live path, and deleting it removes the thing keeping the feature usable.
246
+ 4. **Escalate and the spoken ack / progress phrasing still behave.** Flux bypasses only the `[0]` hold branch. Confirm `[1]` escalate still fires and acks still land before concluding the front door has nothing left to do.
247
+
248
+ If 1 and 2 both hold and 3 is clean, the cleanup is justified. If 1 holds but 2 does not, the answer is to tune `eotThreshold` upward and re-measure, not to delete anything.
@@ -54,6 +54,41 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
54
54
  | Kind-specific follow-through | resolver registry (`kind` → resolver) | switch statements in the router |
55
55
  | Reply understanding | guardian reply router (codes, buttons, reactions, modes) | per-feature inbound intercepts |
56
56
 
57
+ ## Cards are not conversation history
58
+
59
+ `pairDeliveryWithConversation` persists one message row per delivery so the
60
+ card renders and deep-links. For a guardian card that row is addressed to a
61
+ conversation the request is _about_, not one the assistant is speaking in:
62
+ `buildVellumCardAffinity` pins the vellum card to the originating
63
+ conversation, and a channel card lands in whatever conversation the guardian's
64
+ chat binds to. Either way the row is written straight to the DB by the
65
+ notification pipeline, so the live turn's in-memory history never sees it.
66
+
67
+ That row must never be replayed to the model. The conversation it lands in is
68
+ typically parked mid-approval, with its last assistant message carrying the
69
+ `tool_use` still waiting on this very decision. Replayed, the card sits
70
+ between that `tool_use` and its `tool_result`; history repair reads the pair
71
+ as broken, synthesizes a stub result, and downgrades the real one to text. A
72
+ card left as the tail row instead ends the history on an assistant message,
73
+ which extended-thinking models reject outright ("does not support assistant
74
+ message prefill").
75
+
76
+ `isGuardianCardRow` (`notifications/approval-card-data.ts`) is the one
77
+ definition of which rows those are, read off the card's own `ui_surface` id
78
+ using the same prefixes `approvalCardSurfaceId` recomputes for withdrawal. It
79
+ is derived rather than stored so a row written before the rule existed is
80
+ recognized on the same terms as a new one, with no marker to backfill.
81
+
82
+ **Both history assemblers must consult it.** `Conversation.loadFromDb` builds
83
+ `this.messages`, but Slack conversations do not use that list:
84
+ `loadSlackChronologicalContext` re-reads the rows. A rule applied to only one
85
+ exempts the other channel. Each applies it _after_ its own compaction boundary,
86
+ since both boundaries are computed against unfiltered row lists.
87
+
88
+ Surface state is deliberately NOT filtered this way. `restoreSurfaceStateFromHistory`
89
+ takes the pre-filter window, because a card's Approve/Reject buttons must keep
90
+ routing after a restart even though the card is absent from the model's history.
91
+
57
92
  Two instruction modes exist per request kind (`notifications/guardian-question-mode.ts`):
58
93
  **approval** ("CODE approve" / approve–reject buttons) and **answer**
59
94
  ("CODE <your answer>" / option buttons). `pending_question` is answer-mode.
@@ -13,6 +13,7 @@ Add a new entry to the `CATALOG` map with:
13
13
  - `supportedBoundaries` — the set of `SttBoundaryId` values the provider supports. Valid values are `"daemon-batch"` (post-recording transcription) and `"daemon-streaming"` (real-time streaming transcription during conversation).
14
14
  - `conversationStreamingMode` — how the provider handles streaming transcription in conversation mode: `"realtime-ws"` (provider supports real-time streaming natively via WebSocket), `"incremental-batch"` (streaming emulated via throttled polling), or `"none"` (no streaming support). Required for all providers.
15
15
  - `telephonyMode` — how the provider participates in real-time telephony STT: `"realtime-ws"`, `"batch-only"`, or `"none"`. The telephony capability resolver (`resolveTelephonySttCapability()` in `src/providers/speech-to-text/resolve.ts`) reads this field plus credential availability to decide whether phone calls can run with the provider.
16
+ - `turnDetection`: whether the provider decides end-of-turn itself, `"provider"` or `"none"`. Use `"provider"` only when the adapter emits `turn-start` / `turn-end` on its transcript stream; a live-voice session reads this via `supportsProviderTurnDetection()` to decide whether to arm its provider turn-end path. Default to `"none"`: a provider that declares `"provider"` but never emits the events makes every turn wait out the fail-open deadline before falling back to the silence boundary. A provider declaring `"provider"` should also number its turns, because the staleness check prefers the turn index on the event and degrades to the session's VAD generation counter without one.
16
17
 
17
18
  ## 2. Type-system registration
18
19
 
@@ -71,10 +72,11 @@ Native clients fetch this metadata at launch via `GET /v1/stt/providers`. No sep
71
72
  | ---------------- | -------------------- | ------------- |
72
73
  | `openai-whisper` | `openai` | shared |
73
74
  | `deepgram` | `deepgram` | exclusive |
75
+ | `deepgram-flux` | `deepgram` | shared |
74
76
  | `google-gemini` | `gemini` | shared |
75
77
  | `xai` | `xai` | exclusive |
76
78
 
77
- When the provider ID differs from the credential provider name (e.g. `google-gemini` maps to `gemini`), the key is **shared** with other services that use the same credential.
79
+ When the provider ID differs from the credential provider name (e.g. `google-gemini` maps to `gemini`), the key is **shared** with other services that use the same credential. Two STT providers may also name the same credential: `deepgram-flux` is a model on the same Deepgram account as `deepgram`, so it reads that key rather than introducing one of its own. Reuse an existing `credentialProvider` whenever the new provider authenticates against an account the catalog already covers, and keep the `credentialsGuide` text identical across them (`DEEPGRAM_CREDENTIALS_GUIDE` is shared by both entries for that reason).
78
80
 
79
81
  ### Client settings key behavior
80
82
 
package/knip.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "src/**/__tests__/**/*.ts",
5
5
  "scripts/**/*.ts",
6
6
  "src/daemon/main.ts!",
7
+ "src/daemon/windows-compiled-entry.ts!",
8
+ "src/windows-compiled-entry.ts!",
9
+ "src/windows-compiled-worker-entry.ts!",
7
10
  "src/plugin-api/index.ts!",
8
11
  "src/api/index.ts!",
9
12
  "src/config/bundled-skills/**/tools/**/*.ts!"
@@ -14,6 +14,7 @@
14
14
  "./remote-web-pairing": "./src/remote-web-pairing.ts",
15
15
  "./twilio-ingress": "./src/twilio-ingress.ts",
16
16
  "./trust-rules": "./src/trust-rules.ts",
17
+ "./url-normalization": "./src/url-normalization.ts",
17
18
  "./handles": "./src/handles.ts",
18
19
  "./rpc": "./src/rpc.ts",
19
20
  "./attachment-naming": "./src/attachment-naming.ts",
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ canonicalizeWebUrl,
5
+ looksLikeHostPortShorthand,
6
+ looksLikePathOnlyInput,
7
+ normalizeWebUrl,
8
+ } from "../url-normalization.js";
9
+
10
+ /**
11
+ * These are the rules a trust rule is saved and matched under. A change here
12
+ * changes which saved rules still match, so each case states the target the
13
+ * two spellings must agree on.
14
+ */
15
+ describe("normalizeWebUrl", () => {
16
+ test("keeps an ordinary https URL as-is", () => {
17
+ expect(normalizeWebUrl("https://example.com/docs/page")?.href).toBe(
18
+ "https://example.com/docs/page",
19
+ );
20
+ });
21
+
22
+ test("drops the fragment: it never reaches the server", () => {
23
+ expect(normalizeWebUrl("https://example.com/docs#section")?.href).toBe(
24
+ "https://example.com/docs",
25
+ );
26
+ });
27
+
28
+ test("drops userinfo so credentials cannot land in a saved rule", () => {
29
+ const credentialed = new URL("https://example.com/docs/page");
30
+ credentialed.username = "demo";
31
+ credentialed.password = ["c", "r", "e", "d", "1", "2", "3"].join("");
32
+
33
+ const normalized = normalizeWebUrl(credentialed.href);
34
+ expect(normalized?.href).toBe("https://example.com/docs/page");
35
+ expect(normalized?.username).toBe("");
36
+ expect(normalized?.password).toBe("");
37
+ });
38
+
39
+ test("strips a trailing root dot from the hostname", () => {
40
+ expect(normalizeWebUrl("https://example.com./docs/page")?.href).toBe(
41
+ "https://example.com/docs/page",
42
+ );
43
+ });
44
+
45
+ test("decodes escaped path segments so one path has one spelling", () => {
46
+ // Without this, a rule scoped to /private is bypassed by /%70rivate.
47
+ expect(normalizeWebUrl("https://example.com/%70rivate")?.href).toBe(
48
+ "https://example.com/private",
49
+ );
50
+ });
51
+
52
+ test("reads scheme-less input as https", () => {
53
+ expect(normalizeWebUrl("example.com/docs")?.href).toBe(
54
+ "https://example.com/docs",
55
+ );
56
+ });
57
+
58
+ test("reads host:port shorthand as an https origin, not a scheme", () => {
59
+ expect(normalizeWebUrl("example.com:8443/status")?.origin).toBe(
60
+ "https://example.com:8443",
61
+ );
62
+ expect(normalizeWebUrl("[2001:db8::1]:8443/status")?.origin).toBe(
63
+ "https://[2001:db8::1]:8443",
64
+ );
65
+ });
66
+
67
+ test("rejects path-only input rather than coercing it to a host", () => {
68
+ for (const input of ["/etc/passwd", "./rel", "../up", "?q=1", "#frag"]) {
69
+ expect(normalizeWebUrl(input)).toBeNull();
70
+ }
71
+ });
72
+
73
+ test("rejects every non-http scheme", () => {
74
+ for (const input of [
75
+ "file:///etc/passwd",
76
+ "data:text/html,<script>",
77
+ "javascript:alert(1)",
78
+ "ftp://example.com/f",
79
+ ]) {
80
+ expect(normalizeWebUrl(input)).toBeNull();
81
+ }
82
+ });
83
+
84
+ test("rejects empty and whitespace-only input", () => {
85
+ expect(normalizeWebUrl("")).toBeNull();
86
+ expect(normalizeWebUrl(" ")).toBeNull();
87
+ });
88
+
89
+ test("trims surrounding whitespace", () => {
90
+ expect(normalizeWebUrl(" https://example.com/a ")?.href).toBe(
91
+ "https://example.com/a",
92
+ );
93
+ });
94
+
95
+ test("returns null rather than throwing on an unparseable authority", () => {
96
+ expect(normalizeWebUrl("https://")).toBeNull();
97
+ });
98
+ });
99
+
100
+ describe("canonicalizeWebUrl", () => {
101
+ test("keeps the parser's form when the path is not decodable, without throwing", () => {
102
+ // `%zz` and a truncated UTF-8 sequence are not valid escapes; the path
103
+ // must survive unchanged rather than the call throwing.
104
+ expect(canonicalizeWebUrl(new URL("https://example.com/100%zz")).href).toBe(
105
+ "https://example.com/100%zz",
106
+ );
107
+ expect(
108
+ canonicalizeWebUrl(new URL("https://example.com/%E0%A4%A")).href,
109
+ ).toBe("https://example.com/%E0%A4%A");
110
+ });
111
+
112
+ test("decodes an escaped percent, so `%25` and `%` are one path", () => {
113
+ expect(canonicalizeWebUrl(new URL("https://example.com/100%25")).href).toBe(
114
+ "https://example.com/100%",
115
+ );
116
+ });
117
+ });
118
+
119
+ describe("input shape predicates", () => {
120
+ test("host:port shorthand is recognized, scheme-prefixed input is not", () => {
121
+ expect(looksLikeHostPortShorthand("example.com:8443/x")).toBe(true);
122
+ expect(looksLikeHostPortShorthand("[2001:db8::1]:443")).toBe(true);
123
+ expect(looksLikeHostPortShorthand("https://example.com/x")).toBe(false);
124
+ expect(looksLikeHostPortShorthand("example.com/x")).toBe(false);
125
+ });
126
+
127
+ test("path-only input is recognized", () => {
128
+ expect(looksLikePathOnlyInput("/abs")).toBe(true);
129
+ expect(looksLikePathOnlyInput("./rel")).toBe(true);
130
+ expect(looksLikePathOnlyInput("../up")).toBe(true);
131
+ expect(looksLikePathOnlyInput("?q=1")).toBe(true);
132
+ expect(looksLikePathOnlyInput("#frag")).toBe(true);
133
+ expect(looksLikePathOnlyInput("example.com/x")).toBe(false);
134
+ });
135
+ });
@@ -6,6 +6,16 @@
6
6
  * assistant through (Slack, Telegram, WhatsApp, phone, …) plus a couple of
7
7
  * internal ids (`vellum` for native app conversations, `platform` for the
8
8
  * internal control plane). This is the single source of truth for that set:
9
+ *
10
+ * One id, `plugin`, does not name a surface: it names *every* surface a plugin
11
+ * brings. A plugin channel's real identity is the plugin, which is workspace
12
+ * state and cannot be a compile-time union member, so the plugin name travels
13
+ * in `sourceMetadata.plugin` and is prefixed onto every external id the gateway
14
+ * forwards (`imessage:+15551234567`). Two plugins therefore share a channel
15
+ * row — one admission floor, one set of channel-wide defaults — while their
16
+ * conversations, contacts, and trust records stay disjoint. See
17
+ * `gateway/src/channels/plugin-inbound.ts` for what that concedes.
18
+ *
9
19
  * the assistant adopts it wholesale as its `ChannelId`, and the gateway
10
20
  * asserts its own (narrower) inbound list is a subset of it so the two sides
11
21
  * cannot silently drift.
@@ -30,6 +40,7 @@ export const CHANNEL_IDS = [
30
40
  "platform",
31
41
  "a2a",
32
42
  "discord",
43
+ "plugin",
33
44
  ] as const;
34
45
 
35
46
  export type ChannelId = (typeof CHANNEL_IDS)[number];
@@ -29,3 +29,4 @@ export * from "./trust-rules.js";
29
29
  export * from "./ingress.js";
30
30
  export * from "./remote-web-pairing.js";
31
31
  export * from "./twilio-ingress.js";
32
+ export * from "./url-normalization.js";
@@ -11,6 +11,12 @@
11
11
  * (`gateway/src/http/routes/remote-web-pairing-verification.ts`)
12
12
  * - `POST /v1/remote-web/pairing-token` poll + exchange device code
13
13
  * (`gateway/src/http/routes/remote-web-pairing-token.ts`)
14
+ * - `GET /v1/remote-web/pairing-requests` list pending challenges
15
+ * (loopback-only)
16
+ * - `POST /v1/remote-web/pairing-requests/approve` approve by request id
17
+ * (loopback-only)
18
+ * - `POST /v1/remote-web/pairing-requests/deny` deny (delete) by request id
19
+ * (loopback-only)
14
20
  *
15
21
  * These shapes mirror those handlers' request/response bodies exactly so the
16
22
  * gateway, the `vellum pair` CLI (`cli/src/commands/pair.ts`), and the web SPA
@@ -68,6 +74,60 @@ export interface RemoteWebPairingVerificationResponse {
68
74
  expiresAt: string;
69
75
  }
70
76
 
77
+ /**
78
+ * One pending challenge as shown on a host approval surface.
79
+ *
80
+ * The requesting device already sees the plaintext `userCode` in its own
81
+ * challenge response ({@link RemoteWebPairingChallengeResponse.userCode});
82
+ * the loopback-gated list route is the only host-side re-exposure. Displaying
83
+ * it there is what lets the approver match the code against the requesting
84
+ * device's screen: the device-flow anti-phishing binding.
85
+ */
86
+ export interface RemoteWebPairingRequestSummary {
87
+ /** Opaque server-side id used to approve or deny this request. */
88
+ requestId: string;
89
+ /** The human-readable code the requesting device is displaying (e.g. "ABCD-EFGH"). */
90
+ userCode: string;
91
+ /** Public base URL the challenge was minted for. */
92
+ publicBaseUrl: string;
93
+ /** ISO-8601 instant the challenge was minted. */
94
+ requestedAt: string;
95
+ /** ISO-8601 instant the challenge expires. */
96
+ expiresAt: string;
97
+ /**
98
+ * Client IP of the mint request: the loopback/host address when minted
99
+ * locally, or the edge-observed client address when the mint arrived
100
+ * through the nginx tunnel edge (which stamps it via `proxy_set_header`,
101
+ * so a remote client cannot smuggle a value).
102
+ */
103
+ requesterIp: string;
104
+ /** User-Agent header of the mint request, or null when absent. */
105
+ requesterUserAgent: string | null;
106
+ /**
107
+ * Whether the mint arrived through the public tunnel edge rather than the
108
+ * host itself.
109
+ */
110
+ viaEdgeProxy: boolean;
111
+ }
112
+
113
+ /** `GET /v1/remote-web/pairing-requests` success response body (200). */
114
+ export interface RemoteWebPairingRequestListResponse {
115
+ requests: RemoteWebPairingRequestSummary[];
116
+ }
117
+
118
+ /**
119
+ * Request body for the pairing-request approve and deny routes. The approve
120
+ * route's success body reuses {@link RemoteWebPairingVerificationResponse}.
121
+ */
122
+ export interface RemoteWebPairingRequestActionRequest {
123
+ requestId: string;
124
+ }
125
+
126
+ /** `POST /v1/remote-web/pairing-requests/deny` success response body (200). */
127
+ export interface RemoteWebPairingRequestDenyResponse {
128
+ status: "denied";
129
+ }
130
+
71
131
  /** `POST /v1/remote-web/pairing-token` request body. */
72
132
  export interface RemoteWebPairingTokenRequest {
73
133
  /** The `deviceCode` from the challenge. */