@vellumai/assistant 0.10.9 → 0.10.10-dev.202607162206.d08e98e

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 (456) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +8 -0
  3. package/docs/activation-funnel-telemetry.md +13 -7
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  12. package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  13. package/openapi.yaml +530 -107
  14. package/package.json +1 -1
  15. package/scripts/generate-openapi.ts +8 -0
  16. package/src/__tests__/activation-early-marking.test.ts +6 -5
  17. package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
  18. package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
  19. package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
  20. package/src/__tests__/app-bundler.test.ts +4 -12
  21. package/src/__tests__/app-executors.test.ts +2 -49
  22. package/src/__tests__/app-routes-csp.test.ts +114 -146
  23. package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
  24. package/src/__tests__/build-persisted-content.test.ts +96 -0
  25. package/src/__tests__/bundle-scanner.test.ts +27 -1
  26. package/src/__tests__/call-controller.test.ts +291 -0
  27. package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
  29. package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
  30. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  31. package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +9 -4
  33. package/src/__tests__/config-schema-cmd.test.ts +10 -11
  34. package/src/__tests__/config-schema.test.ts +190 -257
  35. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
  37. package/src/__tests__/conversation-agent-loop.test.ts +22 -9
  38. package/src/__tests__/conversation-error.test.ts +31 -0
  39. package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
  40. package/src/__tests__/conversation-process-callsite.test.ts +12 -19
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
  42. package/src/__tests__/conversation-summarize-route.test.ts +36 -44
  43. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  44. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
  45. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
  46. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
  47. package/src/__tests__/conversation-usage.test.ts +4 -12
  48. package/src/__tests__/credential-routes.test.ts +250 -0
  49. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  50. package/src/__tests__/db-migration-rollback.test.ts +22 -0
  51. package/src/__tests__/empty-response-hook.test.ts +186 -1
  52. package/src/__tests__/external-plugin-loader.test.ts +11 -5
  53. package/src/__tests__/heartbeat-service.test.ts +0 -28
  54. package/src/__tests__/host-shell-tool.test.ts +2 -0
  55. package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
  56. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
  57. package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
  58. package/src/__tests__/invite-routes-http.test.ts +12 -16
  59. package/src/__tests__/list-all-apps.test.ts +0 -4
  60. package/src/__tests__/llm-context-resolution.test.ts +32 -56
  61. package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
  62. package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
  63. package/src/__tests__/llm-resolver.test.ts +342 -602
  64. package/src/__tests__/llm-schema.test.ts +79 -37
  65. package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
  66. package/src/__tests__/media-stream-output.test.ts +259 -3
  67. package/src/__tests__/media-stream-server-integration.test.ts +22 -1
  68. package/src/__tests__/media-stream-stt-session.test.ts +47 -0
  69. package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
  70. package/src/__tests__/memory-recall-log-store.test.ts +47 -13
  71. package/src/__tests__/mock-gateway-ipc.ts +46 -1
  72. package/src/__tests__/mtime-cache.test.ts +61 -0
  73. package/src/__tests__/navigate-settings-tab.test.ts +2 -0
  74. package/src/__tests__/normalize-onboarding.test.ts +33 -0
  75. package/src/__tests__/onboarding-persona-write.test.ts +26 -0
  76. package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
  77. package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
  78. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
  79. package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
  80. package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
  81. package/src/__tests__/provider-connections-backfill.test.ts +138 -0
  82. package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
  83. package/src/__tests__/provider-registry-ollama.test.ts +8 -18
  84. package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
  85. package/src/__tests__/provider-usage-tracking.test.ts +9 -19
  86. package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
  87. package/src/__tests__/published-app-updater.test.ts +22 -16
  88. package/src/__tests__/registry.test.ts +5 -16
  89. package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
  90. package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
  91. package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
  92. package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
  93. package/src/__tests__/reveal-success-registry.test.ts +123 -0
  94. package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
  95. package/src/__tests__/secret-fixtures.ts +9 -0
  96. package/src/__tests__/server-history-render.test.ts +28 -0
  97. package/src/__tests__/skills.test.ts +9 -4
  98. package/src/__tests__/slack-share-routes.test.ts +0 -1
  99. package/src/__tests__/stt-stream-session.test.ts +6 -5
  100. package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
  101. package/src/__tests__/subagent-disposal.test.ts +2 -0
  102. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  103. package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
  104. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  105. package/src/__tests__/subagent-role-registry.test.ts +37 -0
  106. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  107. package/src/__tests__/subagent-terminal-message.test.ts +50 -0
  108. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
  109. package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
  110. package/src/__tests__/telemetry-routes.test.ts +99 -19
  111. package/src/__tests__/tool-audit.test.ts +34 -4
  112. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  113. package/src/__tests__/tool-profiler.test.ts +72 -1
  114. package/src/__tests__/tool-result-spool.test.ts +49 -4
  115. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
  116. package/src/__tests__/ui-channel-variants.test.ts +108 -0
  117. package/src/__tests__/ui-shape-teaching.test.ts +255 -0
  118. package/src/__tests__/usage-attribution.test.ts +18 -41
  119. package/src/__tests__/user-plugin-loader.test.ts +4 -4
  120. package/src/__tests__/voice-config-update.test.ts +46 -0
  121. package/src/__tests__/voice-session-bridge.test.ts +216 -84
  122. package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
  123. package/src/agent/loop.ts +4 -3
  124. package/src/api/events/open-conversation.test.ts +64 -0
  125. package/src/api/events/open-conversation.ts +33 -0
  126. package/src/api/index.ts +6 -0
  127. package/src/api/responses/conversation-message.ts +5 -0
  128. package/src/apps/app-store.ts +25 -35
  129. package/src/bundler/app-bundler.ts +34 -48
  130. package/src/bundler/app-compiler.ts +39 -4
  131. package/src/bundler/bundle-scanner.ts +13 -0
  132. package/src/bundler/manifest.ts +1 -1
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
  134. package/src/calls/call-constants.ts +5 -0
  135. package/src/calls/call-controller.ts +100 -32
  136. package/src/calls/call-transport.ts +9 -0
  137. package/src/calls/media-stream-output.ts +107 -4
  138. package/src/calls/media-stream-server.ts +29 -9
  139. package/src/calls/media-stream-stt-session.ts +7 -1
  140. package/src/calls/media-turn-detector.ts +11 -1
  141. package/src/calls/voice-session-bridge.ts +84 -63
  142. package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
  143. package/src/cli/commands/__tests__/notifications.test.ts +24 -3
  144. package/src/cli/commands/config.help.ts +6 -6
  145. package/src/cli/commands/credentials.help.ts +13 -0
  146. package/src/cli/commands/credentials.ts +6 -1
  147. package/src/cli/commands/email.help.ts +7 -0
  148. package/src/cli/commands/email.ts +35 -1
  149. package/src/cli/commands/inference-providers.ts +168 -107
  150. package/src/cli/commands/inference.help.ts +118 -46
  151. package/src/cli/commands/memory/index.help.ts +23 -0
  152. package/src/cli/commands/memory/nodes.ts +146 -0
  153. package/src/cli/commands/notifications.help.ts +10 -10
  154. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
  155. package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
  156. package/src/cli/commands/oauth/connect.test.ts +126 -0
  157. package/src/cli/commands/oauth/connect.ts +29 -6
  158. package/src/cli/commands/oauth/index.help.ts +7 -1
  159. package/src/cli/commands/oauth/status.test.ts +69 -3
  160. package/src/cli/commands/oauth/status.ts +50 -12
  161. package/src/cli/commands/plugins.help.ts +13 -2
  162. package/src/cli/commands/plugins.ts +61 -7
  163. package/src/cli/commands/telemetry.help.ts +13 -0
  164. package/src/cli/commands/telemetry.ts +45 -5
  165. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
  166. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
  167. package/src/cli/lib/bundled-marketplace.json +93 -36
  168. package/src/cli/lib/inspect-plugin.ts +5 -1
  169. package/src/cli/lib/install-from-github.ts +68 -4
  170. package/src/cli/lib/plugin-catalog-local.ts +6 -2
  171. package/src/cli/lib/plugin-fingerprint.ts +3 -3
  172. package/src/cli/lib/upgrade-plugin.ts +236 -35
  173. package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
  174. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
  175. package/src/config/__tests__/profile-materialization.test.ts +1 -88
  176. package/src/config/bundled-skills/AGENTS.md +3 -30
  177. package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
  178. package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
  180. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
  181. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
  182. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  183. package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
  184. package/src/config/bundled-skills/settings/TOOLS.json +3 -1
  185. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
  186. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
  187. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  188. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  189. package/src/config/call-site-defaults.ts +3 -3
  190. package/src/config/feature-flag-registry.json +24 -39
  191. package/src/config/llm-resolver.ts +88 -475
  192. package/src/config/profile-materialization.ts +11 -11
  193. package/src/config/schema.ts +93 -127
  194. package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
  195. package/src/config/schemas/__tests__/stt.test.ts +31 -3
  196. package/src/config/schemas/live-voice.ts +10 -4
  197. package/src/config/schemas/llm.ts +52 -15
  198. package/src/config/schemas/memory-lifecycle.ts +1 -1
  199. package/src/config/schemas/memory-retrospective.ts +8 -0
  200. package/src/config/schemas/memory-v2.ts +2 -2
  201. package/src/config/schemas/memory-v3.ts +1 -1
  202. package/src/config/schemas/services.ts +6 -3
  203. package/src/config/schemas/stt.ts +38 -21
  204. package/src/config/schemas/tts.ts +13 -18
  205. package/src/config/skills.ts +31 -21
  206. package/src/context/compactor.ts +44 -7
  207. package/src/context/post-turn-tool-result-truncation.ts +4 -2
  208. package/src/context/tool-result-spool.ts +26 -5
  209. package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
  210. package/src/conversations/message-consolidation.ts +22 -2
  211. package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
  212. package/src/daemon/app-source-watcher.ts +17 -23
  213. package/src/daemon/chat-credential-redaction.ts +1365 -0
  214. package/src/daemon/conversation-agent-loop-handlers.ts +627 -34
  215. package/src/daemon/conversation-agent-loop.ts +71 -31
  216. package/src/daemon/conversation-error.ts +36 -14
  217. package/src/daemon/conversation-process.ts +22 -0
  218. package/src/daemon/conversation-store.ts +35 -0
  219. package/src/daemon/conversation-surfaces.ts +25 -25
  220. package/src/daemon/conversation-tool-setup.ts +33 -7
  221. package/src/daemon/conversation.ts +55 -5
  222. package/src/daemon/handlers/shared.ts +33 -2
  223. package/src/daemon/lifecycle.ts +4 -4
  224. package/src/daemon/message-types/conversations.ts +6 -17
  225. package/src/daemon/providers-setup.ts +8 -0
  226. package/src/daemon/tool-setup-types.ts +7 -1
  227. package/src/daemon/wake-conversation-ops.ts +10 -1
  228. package/src/hooks/types.ts +9 -0
  229. package/src/ipc/__tests__/email-ipc.test.ts +90 -0
  230. package/src/ipc/gateway-client.test.ts +59 -0
  231. package/src/ipc/gateway-client.ts +70 -27
  232. package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
  233. package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
  234. package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
  235. package/src/live-voice/__tests__/protocol.test.ts +122 -0
  236. package/src/live-voice/live-voice-session.ts +443 -40
  237. package/src/live-voice/protocol.ts +143 -1
  238. package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
  239. package/src/monitoring/plugin-source-watch.ts +3 -62
  240. package/src/notifications/README.md +1 -1
  241. package/src/permissions/checker.ts +8 -4
  242. package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
  243. package/src/persistence/conversation-crud.ts +104 -2
  244. package/src/persistence/db-init.ts +14 -3
  245. package/src/persistence/job-handlers/cleanup.ts +15 -7
  246. package/src/persistence/llm-request-log-store.ts +88 -52
  247. package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
  248. package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
  249. package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
  250. package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
  251. package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
  252. package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
  253. package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
  254. package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
  255. package/src/persistence/migrations/helpers/relocation.ts +44 -1
  256. package/src/persistence/migrations/run-migrations.ts +25 -1
  257. package/src/persistence/schema/infrastructure.ts +9 -0
  258. package/src/persistence/schema/memory-core.ts +3 -0
  259. package/src/persistence/schema/memory-injection.ts +2 -0
  260. package/src/persistence/steps.ts +36 -0
  261. package/src/platform/client.test.ts +1 -44
  262. package/src/platform/client.ts +10 -20
  263. package/src/platform/consent-cache.test.ts +89 -23
  264. package/src/platform/consent-cache.ts +71 -33
  265. package/src/plugin-api/constants.ts +12 -0
  266. package/src/plugin-api/conversation-turn.ts +37 -14
  267. package/src/plugin-api/index.ts +11 -1
  268. package/src/plugin-api/resolve-credential.ts +75 -0
  269. package/src/plugin-api/vision-support.test.ts +8 -19
  270. package/src/plugin-api/vision-support.ts +26 -17
  271. package/src/plugins/collect-source-versions.ts +77 -0
  272. package/src/plugins/defaults/compaction/compact.ts +6 -0
  273. package/src/plugins/defaults/compaction/window-manager.ts +9 -0
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
  275. package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
  276. package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
  277. package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
  278. package/src/plugins/defaults/index.ts +8 -1
  279. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
  280. package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
  281. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
  282. package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
  285. package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
  286. package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
  287. package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
  288. package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
  289. package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
  290. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
  291. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
  292. package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
  293. package/src/plugins/defaults/memory/graph/store.ts +114 -0
  294. package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
  295. package/src/plugins/defaults/memory/graph/tools.ts +47 -13
  296. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
  297. package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
  298. package/src/plugins/defaults/memory/memory-db.ts +3 -2
  299. package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
  300. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
  301. package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
  302. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
  303. package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
  304. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
  305. package/src/plugins/defaults/memory/path-containment.ts +21 -0
  306. package/src/plugins/defaults/memory/prompt-override.ts +51 -13
  307. package/src/plugins/defaults/memory/tools.test.ts +34 -0
  308. package/src/plugins/defaults/memory/tools.ts +12 -2
  309. package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
  310. package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
  311. package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
  312. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
  313. package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
  314. package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
  315. package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
  316. package/src/plugins/defaults/memory/v2/migration.ts +46 -16
  317. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
  318. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
  319. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
  320. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
  321. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
  322. package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
  323. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
  324. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
  325. package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
  326. package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
  327. package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
  328. package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
  329. package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
  330. package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
  331. package/src/plugins/defaults/memory/v3/prune.ts +17 -11
  332. package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
  333. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
  334. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
  335. package/src/plugins/external-plugin-loader.ts +15 -15
  336. package/src/plugins/mtime-cache.ts +47 -0
  337. package/src/plugins/pipeline.ts +9 -1
  338. package/src/plugins/plugin-execution-context.ts +44 -0
  339. package/src/plugins/plugin-tree-walk.ts +31 -24
  340. package/src/plugins/source-fingerprint.ts +3 -4
  341. package/src/prompts/normalize-onboarding.ts +12 -0
  342. package/src/prompts/persona-resolver.ts +8 -0
  343. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
  344. package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
  345. package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
  346. package/src/providers/__tests__/retry-callsite.test.ts +235 -215
  347. package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
  348. package/src/providers/atlascloud/client.ts +10 -49
  349. package/src/providers/baseten/client.ts +43 -0
  350. package/src/providers/call-site-routing.ts +26 -13
  351. package/src/providers/connection-resolution.ts +9 -11
  352. package/src/providers/fetch-provider-catalog.ts +4 -2
  353. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
  354. package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
  355. package/src/providers/inference/adapter-factory.ts +27 -6
  356. package/src/providers/inference/auth.ts +29 -1
  357. package/src/providers/inference/backfill.ts +61 -10
  358. package/src/providers/inference/connection-availability.ts +3 -2
  359. package/src/providers/inference/resolve-auth.ts +9 -1
  360. package/src/providers/model-catalog.ts +37 -0
  361. package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
  362. package/src/providers/openai/api-key-validation.ts +70 -0
  363. package/src/providers/retry.ts +2 -0
  364. package/src/providers/types.ts +11 -4
  365. package/src/providers/vellum-model-routing.test.ts +26 -0
  366. package/src/providers/vellum-model-routing.ts +30 -0
  367. package/src/providers/voice-error-copy.ts +47 -0
  368. package/src/runtime/agent-wake.ts +40 -24
  369. package/src/runtime/for-chat-mint-registry.ts +118 -0
  370. package/src/runtime/reveal-nonce.ts +49 -0
  371. package/src/runtime/reveal-success-registry.ts +306 -0
  372. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
  373. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
  374. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
  375. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
  376. package/src/runtime/routes/app-management-routes.ts +152 -45
  377. package/src/runtime/routes/app-routes.ts +16 -77
  378. package/src/runtime/routes/canned-message-complete.ts +16 -16
  379. package/src/runtime/routes/conversation-management-routes.ts +17 -14
  380. package/src/runtime/routes/conversation-query-routes.ts +54 -6
  381. package/src/runtime/routes/conversation-routes.ts +48 -2
  382. package/src/runtime/routes/credential-routes.ts +116 -3
  383. package/src/runtime/routes/email-routes.ts +17 -1
  384. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
  385. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
  386. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
  387. package/src/runtime/routes/inference-send-routes.ts +1 -1
  388. package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
  389. package/src/runtime/routes/plugins-routes.ts +37 -6
  390. package/src/runtime/routes/publish-routes.ts +15 -18
  391. package/src/runtime/routes/schedule-worker-routes.ts +9 -0
  392. package/src/runtime/routes/secret-routes.ts +10 -0
  393. package/src/runtime/routes/telemetry-routes.ts +149 -45
  394. package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
  395. package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
  396. package/src/schedule/schedule-store.ts +12 -1
  397. package/src/schedule/schedule-timezone.ts +63 -0
  398. package/src/schedule/scheduler.ts +100 -6
  399. package/src/schedule/worker-control.ts +19 -0
  400. package/src/security/auth-fallback-events-store.ts +7 -6
  401. package/src/security/secret-scanner.ts +26 -1
  402. package/src/services/published-app-updater.ts +6 -11
  403. package/src/stt/stt-stream-session.ts +36 -16
  404. package/src/subagent/manager.ts +34 -8
  405. package/src/telemetry/AGENTS.md +30 -1
  406. package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
  407. package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
  408. package/src/telemetry/config-setting-snapshot.ts +44 -10
  409. package/src/telemetry/telemetry-event-sources.test.ts +124 -30
  410. package/src/telemetry/telemetry-event-sources.ts +137 -83
  411. package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
  412. package/src/telemetry/telemetry-events-outbox.ts +60 -9
  413. package/src/telemetry/telemetry-wire-source.json +1 -1
  414. package/src/telemetry/telemetry-wire-validation.ts +39 -2
  415. package/src/telemetry/telemetry-wire.generated.ts +8 -0
  416. package/src/telemetry/tool-audit.ts +15 -9
  417. package/src/telemetry/tool-executed-events-store.test.ts +1 -1
  418. package/src/telemetry/turn-events-store.ts +20 -0
  419. package/src/telemetry/types.ts +45 -12
  420. package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
  421. package/src/telemetry/usage-telemetry-reporter.ts +117 -18
  422. package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
  423. package/src/telemetry/watchdog-direct-emit.ts +11 -6
  424. package/src/tools/apps/executors.ts +11 -36
  425. package/src/tools/executor.ts +14 -1
  426. package/src/tools/host-terminal/host-shell.ts +6 -2
  427. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
  428. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
  429. package/src/tools/network/web-fetch.ts +6 -2
  430. package/src/tools/skills/sandbox-runner.ts +5 -2
  431. package/src/tools/subagent/spawn.ts +7 -11
  432. package/src/tools/terminal/shell.ts +5 -2
  433. package/src/tools/tool-manifest.ts +0 -2
  434. package/src/tools/tool-profiler.ts +37 -6
  435. package/src/tools/ui-surface/channel-variants.ts +101 -0
  436. package/src/tools/ui-surface/definitions.ts +23 -67
  437. package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
  438. package/src/tts/__tests__/provider-adapters.test.ts +18 -0
  439. package/src/tts/provider-catalog.ts +2 -4
  440. package/src/tts/providers/deepgram-provider.ts +10 -1
  441. package/src/types/onboarding-context.ts +8 -0
  442. package/src/usage/attribution.ts +18 -112
  443. package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
  444. package/src/util/provider-error-patterns.ts +7 -1
  445. package/src/util/worker-process.ts +105 -0
  446. package/src/watcher/__tests__/telemetry.test.ts +17 -4
  447. package/src/watcher/telemetry.ts +5 -5
  448. package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
  449. package/src/workspace/migrations/registry.ts +2 -0
  450. package/src/workspace/provider-commit-message-generator.ts +6 -4
  451. package/src/__tests__/app-open-proxy.test.ts +0 -67
  452. package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
  453. package/src/onboarding/onboarding-research-events-store.ts +0 -104
  454. package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
  455. package/src/tools/apps/definitions.ts +0 -73
  456. package/src/tools/apps/open-proxy.ts +0 -43
@@ -15,6 +15,12 @@ import type {
15
15
  VoiceTurnHandle,
16
16
  VoiceTurnOptions,
17
17
  } from "../calls/voice-session-bridge.js";
18
+ import {
19
+ getConversationTurnTeardown,
20
+ resolveProcessingWaitMs,
21
+ VOICE_NO_SETUP_FLOWS_RULE,
22
+ waitForPriorTurnTeardown,
23
+ } from "../calls/voice-session-bridge.js";
18
24
  import {
19
25
  ESCALATION_CONTINUATION_CONTENT,
20
26
  ESCALATION_PROFILE,
@@ -26,6 +32,8 @@ import {
26
32
  } from "../calls/voice-triage-escalate.js";
27
33
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
28
34
  import { getConfig } from "../config/loader.js";
35
+ import { ABORT_WATCHDOG_MS } from "../daemon/abort-watchdog.js";
36
+ import { findConversation } from "../daemon/conversation-registry.js";
29
37
  import type { TrustContext } from "../daemon/trust-context-types.js";
30
38
  import { ensureConversationExists } from "../persistence/conversation-crud.js";
31
39
  import {
@@ -40,6 +48,7 @@ import type {
40
48
  SttStreamServerErrorEvent,
41
49
  SttStreamServerEvent,
42
50
  } from "../stt/types.js";
51
+ import { getSubagentManager } from "../subagent/index.js";
43
52
  import { extractSpeakableSegments } from "../tts/speakable-segments.js";
44
53
  import { getLogger } from "../util/logger.js";
45
54
  import type {
@@ -67,6 +76,7 @@ import type {
67
76
  } from "./live-voice-tts.js";
68
77
  import {
69
78
  type LiveVoiceClientFrame,
79
+ type LiveVoiceClientUpdateConfigFrame,
70
80
  LiveVoiceProtocolErrorCode,
71
81
  type LiveVoiceServerFramePayload,
72
82
  } from "./protocol.js";
@@ -93,10 +103,12 @@ const SERVER_VAD_PRE_ROLL_MAX_CHUNKS = 25;
93
103
  // provider keeps its own, longer, finalize fallback).
94
104
  const FINALIZE_GRACE_MS = 1_000;
95
105
  // Consecutive speech (ms) required before speech during assistant playback
96
- // flushes it (speech_started) and cancels the turn, so a cough or noise blip
97
- // cannot kill a reply mid-sentence. Mirrors the liveVoice.vad.bargeInMinSpeechMs
98
- // schema default; 0 disables the guard for instant barge-in.
99
- const DEFAULT_BARGE_IN_MIN_SPEECH_MS = 60;
106
+ // flushes it (speech_started) and cancels the turn, so a cough, a filler word,
107
+ // or the assistant's own TTS bleeding through imperfect browser echo
108
+ // cancellation cannot kill a reply mid-sentence. Mirrors the
109
+ // liveVoice.vad.bargeInMinSpeechMs schema default; 0 disables the guard for
110
+ // instant barge-in.
111
+ const DEFAULT_BARGE_IN_MIN_SPEECH_MS = 250;
100
112
  // At most this many TTS segment jobs are open (provider stream started,
101
113
  // frames not yet fully emitted) per turn: the emitting job plus one
102
114
  // prefetching job. The prefetch buffers its chunks in memory until promoted;
@@ -119,6 +131,18 @@ export type LiveVoiceTtsStreamer = (
119
131
  options: LiveVoiceTtsOptions,
120
132
  ) => Promise<LiveVoiceTtsResult>;
121
133
 
134
+ // Runs an interrupted live-voice turn to completion on a background subagent
135
+ // (true-duplex handoff, gated behind voice-duplex-handoff). The run is silent
136
+ // (no parent notification) until the resurface flow lands, and aborts when
137
+ // `signal` fires. Injected for testability; the factory wires the real
138
+ // SubagentManager-backed implementation.
139
+ export type LiveVoiceBackgroundContinuationSpawner = (args: {
140
+ parentConversationId: string;
141
+ objective: string;
142
+ label: string;
143
+ signal: AbortSignal;
144
+ }) => Promise<void>;
145
+
122
146
  export interface LiveVoiceSessionArchiveAudioInput {
123
147
  messageId?: string | null;
124
148
  sessionId: string;
@@ -175,6 +199,27 @@ export interface LiveVoiceSessionOptions {
175
199
  * flush in persistent mode (test hook). Defaults to `FINALIZE_GRACE_MS`.
176
200
  */
177
201
  finalizeGraceMs?: number;
202
+ /**
203
+ * Spawns the background continuation for a barged-in turn when the
204
+ * `voice-duplex-handoff` flag is on. The factory wires the real
205
+ * SubagentManager-backed implementation; tests inject a stub.
206
+ */
207
+ spawnBackgroundContinuation?: LiveVoiceBackgroundContinuationSpawner;
208
+ /**
209
+ * Returns the pending teardown promise for a conversation's most recent
210
+ * turn. The barge-in path awaits it before forking the background
211
+ * continuation so the fork snapshots history only after the interrupted
212
+ * turn's completed tool calls have settled in. The factory wires the
213
+ * bridge's `getConversationTurnTeardown`; tests inject a controllable
214
+ * promise to exercise the ordering.
215
+ */
216
+ getTurnTeardown?: (conversationId: string) => Promise<void> | undefined;
217
+ /**
218
+ * Overrides the bounded wait for the interrupted turn's teardown before the
219
+ * background continuation forks (test hook). Defaults to the bridge's
220
+ * teardown budget (`resolveProcessingWaitMs`).
221
+ */
222
+ detachTeardownSettleTimeoutMs?: number;
178
223
  }
179
224
 
180
225
  type LiveVoiceUtterancePhase =
@@ -260,10 +305,15 @@ interface ActiveAssistantTurn {
260
305
  handle: VoiceTurnHandle | null;
261
306
  assistantCompleted: boolean;
262
307
  ttsDone: boolean;
263
- // A tts_audio frame actually went out to the client — the barge-in gate:
264
- // speech only cancels a turn that has audibly started speaking.
308
+ // A tts_audio frame actually went out to the client — latches on the first
309
+ // forwarded chunk so the firstTtsAudio metric is marked exactly once per turn.
265
310
  ttsAudioStarted: boolean;
266
311
  finalized: boolean;
312
+ // When this turn started from a barge-in, the interrupted request's
313
+ // transcript. Appended to the turn's control prompt (both legs) so the model
314
+ // merges it with this turn's utterance instead of treating that utterance as
315
+ // a fresh follow-up. Null for an ordinary (non-barge-in) turn.
316
+ interruptedRequest: string | null;
267
317
  // Triage-and-escalate (Voice Mode): the front-door leg emitted [ESCALATE]
268
318
  // and the strong "escalated" leg has taken over this same turn. Guards the
269
319
  // front-door leg's trailing completion from finalizing the turn, and makes
@@ -285,6 +335,51 @@ interface ActiveAssistantTurn {
285
335
  assistantAudioSampleRate?: number;
286
336
  }
287
337
 
338
+ // Base control prompt for every live-voice turn. When a turn starts from a
339
+ // barge-in, the interruption merge note is appended to it (see
340
+ // buildInterruptionMergeNote) so the model reconciles the interrupted request
341
+ // with the new utterance.
342
+ const LIVE_VOICE_CONTROL_PROMPT =
343
+ "You are speaking in a local live voice session. Keep replies brief and conversational. You cannot display cards, forms, or any on-screen UI during the call — convey everything in speech. " +
344
+ VOICE_NO_SETUP_FLOWS_RULE;
345
+
346
+ // System-level guidance appended to a barge-in turn's control prompt so the
347
+ // model treats the new utterance as a continuation of the request it was cut
348
+ // off answering, rather than a fresh follow-up. Reaches the model only; it is
349
+ // not a user message and never renders as a transcript bubble.
350
+ function buildInterruptionMergeNote(interruptedRequest: string): string {
351
+ return `The user interrupted your previous, unfinished reply. Their earlier request was: "${interruptedRequest}". Treat their current message as a continuation of that request and address both together, or stay silent if they only want you to stop.`;
352
+ }
353
+
354
+ // Objective handed to the background subagent that continues a barged-in turn.
355
+ // The subagent forks the live conversation, so it already sees the interrupted
356
+ // turn's completed tool calls in history and resumes from there. The request
357
+ // text is embedded so the continuation still knows what to finish even in the
358
+ // pre-persist window where the interrupted user message has not yet landed in
359
+ // the forked history.
360
+ function buildDuplexContinuationObjective(interruptedRequest: string): string {
361
+ const base =
362
+ "You were in the middle of responding to the user's most recent request when they interrupted you. Finish that response now. Do not repeat any tool calls whose results are already present in the conversation.";
363
+ return interruptedRequest.length > 0
364
+ ? `${base} Their request was: "${interruptedRequest}".`
365
+ : base;
366
+ }
367
+
368
+ // Upper bound on how long a barge-in waits for the interrupted turn's teardown
369
+ // to settle before giving up on the continuation. The teardown settles once the
370
+ // aborted turn's agent loop reaches its `finally`, which can wait out BOTH the
371
+ // abort-unwind watchdog and the turn-boundary commit — so bound the wait by the
372
+ // same budget the bridge uses to wait for a prior turn's teardown
373
+ // (resolveProcessingWaitMs). That lets a legitimately slow abort+commit still
374
+ // fork, while a genuinely wedged teardown times out — and on timeout the fork is
375
+ // SKIPPED (not run against stale history); see detachInterruptedTurn.
376
+ function defaultDetachTeardownSettleTimeoutMs(): number {
377
+ return resolveProcessingWaitMs(
378
+ getConfig().workspaceGit?.turnCommitMaxWaitMs ?? 4000,
379
+ ABORT_WATCHDOG_MS,
380
+ );
381
+ }
382
+
288
383
  export class LiveVoiceSession implements LiveVoiceSessionContract {
289
384
  private readonly context: LiveVoiceSessionFactoryContext;
290
385
  private readonly resolveTranscriber: LiveVoiceStreamingTranscriberResolver;
@@ -292,6 +387,22 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
292
387
  private readonly startVoiceTurn: LiveVoiceTurnStarter | null;
293
388
  private readonly streamTtsAudio: LiveVoiceTtsStreamer | null;
294
389
  private readonly archiveAudio: LiveVoiceSessionAudioArchiver | null;
390
+ private readonly spawnBackgroundContinuation: LiveVoiceBackgroundContinuationSpawner | null;
391
+ // Reads the interrupted turn's teardown promise so the barge-in path can wait
392
+ // for it to settle before forking the continuation (voice-duplex-handoff).
393
+ private readonly getTurnTeardown:
394
+ | ((conversationId: string) => Promise<void> | undefined)
395
+ | null;
396
+ private readonly detachTeardownSettleTimeoutMs: number;
397
+ // Abort handles for background continuations started when a barge-in detached
398
+ // an interrupted turn (voice-duplex-handoff). Each controller is registered
399
+ // synchronously before its spawn, so interrupt()/close() abort a continuation
400
+ // even if a stop lands while it is still spawning.
401
+ private readonly detachControllers = new Set<AbortController>();
402
+ // Bumped whenever a stop (interrupt/close) fires. A barge-in captures this
403
+ // before its async teardown; if it has changed by the time the detach would
404
+ // spawn, a stop landed during the gap and the continuation is not started.
405
+ private detachStopGeneration = 0;
295
406
  private readonly emitMetrics: boolean;
296
407
  private readonly metrics: LiveVoiceMetricsCollector;
297
408
  private readonly createTurnId: () => string;
@@ -306,7 +417,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
306
417
  // Energy gate for server-VAD speech classification; undefined defers to
307
418
  // DEFAULT_SPEECH_ENERGY_THRESHOLD.
308
419
  private readonly speechEnergyThreshold: number | undefined;
309
- private readonly bargeInMinSpeechMs: number;
420
+ // Mutable so a mid-session `update_config` frame can retune "interrupt
421
+ // sensitivity" live (see applyConfigUpdate).
422
+ private bargeInMinSpeechMs: number;
310
423
  // Sustained-speech barge-in guard, armed at speech onset while the
311
424
  // assistant turn is audibly speaking: consecutive speech-chunk duration
312
425
  // accumulates until it reaches bargeInMinSpeechMs, then the deferred
@@ -326,6 +439,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
326
439
  // the chunk's PCM duration; zeroed whenever the client flushes playback
327
440
  // (speech_started, turn_cancelled, interrupt, close).
328
441
  private assistantPlaybackTailUntilMs = 0;
442
+ // Set when barge-in cancels an in-flight turn: the interrupted request's
443
+ // transcript, carried into the next turn so the model merges the two.
444
+ // Consumed (and cleared) when that turn launches; cleared if the barge-in
445
+ // utterance is discarded, so it can never attach to a later, unrelated turn.
446
+ private pendingInterruptedRequest: string | null = null;
329
447
  private readonly maxPendingAudioBytes: number;
330
448
  // Set on VAD speech onset; consumed when the first speech chunk is routed
331
449
  // to an utterance so the metric lands on the right turn.
@@ -375,6 +493,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
375
493
  this.startVoiceTurn = options.startVoiceTurn ?? null;
376
494
  this.streamTtsAudio = options.streamTtsAudio ?? null;
377
495
  this.archiveAudio = options.archiveAudio ?? null;
496
+ this.spawnBackgroundContinuation =
497
+ options.spawnBackgroundContinuation ?? null;
498
+ this.getTurnTeardown = options.getTurnTeardown ?? null;
499
+ this.detachTeardownSettleTimeoutMs =
500
+ options.detachTeardownSettleTimeoutMs ??
501
+ defaultDetachTeardownSettleTimeoutMs();
378
502
  this.emitMetrics = options.emitMetrics ?? false;
379
503
  this.createTurnId = options.createTurnId ?? randomUUID;
380
504
  this.conversationId =
@@ -386,12 +510,23 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
386
510
  ...(options.metricsClock ? { clock: options.metricsClock } : {}),
387
511
  });
388
512
  this.speechEnergyThreshold = options.speechEnergyThreshold;
513
+ // Precedence for the two sensitivity knobs: per-session start-frame
514
+ // override (the client's user setting) > daemon `liveVoice.vad` config
515
+ // (seeded into `options` by the factory) > in-code default.
389
516
  this.bargeInMinSpeechMs =
390
- options.bargeInMinSpeechMs ?? DEFAULT_BARGE_IN_MIN_SPEECH_MS;
517
+ context.startFrame.bargeInMinSpeechMs ??
518
+ options.bargeInMinSpeechMs ??
519
+ DEFAULT_BARGE_IN_MIN_SPEECH_MS;
391
520
  this.finalizeGraceMs = options.finalizeGraceMs ?? FINALIZE_GRACE_MS;
521
+ const turnDetectorConfig: TurnDetectorConfig = {
522
+ ...(options.turnDetectorConfig ?? {}),
523
+ ...(context.startFrame.silenceThresholdMs !== undefined
524
+ ? { silenceThresholdMs: context.startFrame.silenceThresholdMs }
525
+ : {}),
526
+ };
392
527
  this.turnDetector =
393
528
  context.startFrame.turnDetection === "server_vad"
394
- ? new MediaTurnDetector(options.turnDetectorConfig ?? {}, {
529
+ ? new MediaTurnDetector(turnDetectorConfig, {
395
530
  onTurnStart: () => this.handleVadSpeechStart(),
396
531
  onTurnEnd: (reason) => this.handleVadUtteranceEnd(reason),
397
532
  })
@@ -461,6 +596,25 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
461
596
  return;
462
597
  case "start":
463
598
  return;
599
+ case "update_config":
600
+ this.applyConfigUpdate(frame);
601
+ return;
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Apply a mid-session `update_config` frame: retune the live turn detector's
607
+ * pause ("pause before reply") and/or the barge-in guard ("interrupt
608
+ * sensitivity") without reconnecting. Each field is optional and independent;
609
+ * changes take effect from the next utterance. A no-op on manual (non-
610
+ * server_vad) sessions, which have no turn detector.
611
+ */
612
+ private applyConfigUpdate(frame: LiveVoiceClientUpdateConfigFrame): void {
613
+ if (frame.silenceThresholdMs !== undefined) {
614
+ this.turnDetector?.setSilenceThresholdMs(frame.silenceThresholdMs);
615
+ }
616
+ if (frame.bargeInMinSpeechMs !== undefined) {
617
+ this.bargeInMinSpeechMs = frame.bargeInMinSpeechMs;
464
618
  }
465
619
  }
466
620
 
@@ -477,6 +631,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
477
631
  this.state = "closed";
478
632
  this.turnDetector?.dispose();
479
633
  this.stopSessionTranscriber();
634
+ this.abortDetachedRuns();
480
635
  await this.cancelAssistantTurn("session_closed");
481
636
  if (shouldEmitSessionEndMetrics) {
482
637
  await this.emitSessionEndMetrics();
@@ -867,12 +1022,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
867
1022
  }
868
1023
 
869
1024
  // VAD speech onset. Contract: speech_started tells the client to flush
870
- // tail playback immediately; barge-in then cancels the active turn only
871
- // once its first tts_audio chunk was forwarded speech during a pre-TTS
872
- // "thinking" turn never kills the unspoken reply. While a turn is audibly
873
- // speaking, both are deferred behind the sustained-speech guard so a
874
- // cough or noise blip cannot kill the reply; onset while listening keeps
875
- // the instant speech_started (turn-taking latency is untouched).
1025
+ // tail playback immediately; barge-in then cancels any in-flight,
1026
+ // non-finalized turn including a pre-TTS "thinking" turn whose reply is
1027
+ // still being generated, so a user can cut in before the assistant starts
1028
+ // talking (JARVIS-1266). Speaking over a thinking or audibly speaking turn
1029
+ // is deferred behind the same sustained-speech guard, so a cough or noise
1030
+ // blip cannot kill an unspoken reply or clip a spoken one; sustained speech
1031
+ // aborts the turn. Onset while listening keeps the instant speech_started
1032
+ // (turn-taking latency is untouched).
876
1033
  private handleVadSpeechStart(): void {
877
1034
  if (this.isClosed || this.state === "failed") {
878
1035
  return;
@@ -881,25 +1038,27 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
881
1038
  this.vadSpeechStartPending = true;
882
1039
 
883
1040
  const turn = this.activeAssistantTurn;
884
- const speakingTurn =
885
- turn && !turn.finalized && turn.ttsAudioStarted ? turn : null;
1041
+ // Any in-flight, non-finalized turn is interruptible, whether it is still
1042
+ // "thinking" (pre-TTS) or audibly speaking, so a user can cut in before the
1043
+ // assistant starts talking.
1044
+ const bargeableTurn = turn && !turn.finalized ? turn : null;
886
1045
  // The client can still be draining audible playback after tts_done
887
1046
  // (the turn is already cleared server-side) — that tail deserves the
888
1047
  // same guard, or a noise blip clips the reply's last words.
889
1048
  const drainingPlayback = Date.now() < this.assistantPlaybackTailUntilMs;
890
1049
 
891
- if ((speakingTurn || drainingPlayback) && this.bargeInMinSpeechMs > 0) {
1050
+ if ((bargeableTurn || drainingPlayback) && this.bargeInMinSpeechMs > 0) {
892
1051
  // Onset audio keeps flowing into the cycle/pre-roll while the guard
893
1052
  // accumulates (trackBargeInGuard), so no speech is lost either way.
894
- this.pendingBargeIn = { turn: speakingTurn, speechMs: 0 };
1053
+ this.pendingBargeIn = { turn: bargeableTurn, speechMs: 0 };
895
1054
  return;
896
1055
  }
897
1056
 
898
1057
  this.pendingBargeIn = null;
899
1058
  this.assistantPlaybackTailUntilMs = 0;
900
1059
  void this.sendFrame({ type: "speech_started" });
901
- if (speakingTurn) {
902
- this.bargeIn(speakingTurn);
1060
+ if (bargeableTurn) {
1061
+ this.bargeIn(bargeableTurn);
903
1062
  }
904
1063
  }
905
1064
 
@@ -938,8 +1097,25 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
938
1097
  // do not collide with it in the collector. turn_cancelled flushes
939
1098
  // client playback, so the drain estimate resets with it.
940
1099
  this.assistantPlaybackTailUntilMs = 0;
1100
+ // Carry the interrupted request into the next turn so it merges with the
1101
+ // barge-in utterance rather than being answered as a fresh follow-up.
1102
+ const interruptedRequest = turn.utterance.finalTranscriptSegments
1103
+ .join(" ")
1104
+ .trim();
1105
+ this.pendingInterruptedRequest =
1106
+ interruptedRequest.length > 0 ? interruptedRequest : null;
941
1107
  turn.abortController.abort();
942
1108
  this.metrics.markBargeIn(turn.turnId);
1109
+ // Capture the interrupted turn's teardown promise synchronously, before the
1110
+ // barge-in utterance's own startVoiceTurn overwrites the bridge's
1111
+ // per-conversation entry (that utterance is not transcribed yet, so its turn
1112
+ // has not started — this read is race-free). The detach awaits it so the
1113
+ // fork snapshots history only after this turn's completed tool calls have
1114
+ // settled in (see detachInterruptedTurn).
1115
+ const teardownWait = this.getTurnTeardown?.(this.conversationId);
1116
+ // Snapshot the stop generation before the async teardown: a stop that lands
1117
+ // during it must cancel the pending detach (checked in detachInterruptedTurn).
1118
+ const stopGeneration = this.detachStopGeneration;
943
1119
  void (async () => {
944
1120
  await this.finishMetricsTurn(
945
1121
  turn.utterance,
@@ -949,9 +1125,118 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
949
1125
  );
950
1126
  await this.sendFrame({ type: "turn_cancelled", turnId: turn.turnId });
951
1127
  await this.cancelAssistantTurn("barge_in");
1128
+ // Keep the interrupted turn's work alive on a background subagent
1129
+ // (voice-duplex-handoff); the detach waits for its teardown to settle the
1130
+ // partial into history before forking.
1131
+ this.detachInterruptedTurn(turn, stopGeneration, teardownWait);
952
1132
  })().catch(() => {});
953
1133
  }
954
1134
 
1135
+ // True-duplex handoff (voice-duplex-handoff): keep a barged-in turn's work
1136
+ // alive by continuing it on a background subagent instead of discarding it.
1137
+ // Waits for the interrupted turn's bridge teardown (captured at barge-in) to
1138
+ // settle before forking, so its partial — including any completed tool calls —
1139
+ // is already in the conversation the subagent forks from and a side-effecting
1140
+ // continuation cannot repeat a call the interrupted turn already ran.
1141
+ // Resurfacing the subagent's result is a follow-up; for now it runs silently
1142
+ // and a later stop/interrupt aborts it.
1143
+ private detachInterruptedTurn(
1144
+ turn: ActiveAssistantTurn,
1145
+ stopGeneration: number,
1146
+ teardownWait: Promise<void> | undefined,
1147
+ ): void {
1148
+ const spawn = this.spawnBackgroundContinuation;
1149
+ if (
1150
+ !spawn ||
1151
+ this.isClosed ||
1152
+ // The model already finished generating (barge-in during TTS playback of a
1153
+ // complete reply): there is nothing to continue, so a continuation would
1154
+ // just re-do a finished answer.
1155
+ turn.assistantCompleted ||
1156
+ // A stop (interrupt/close) landed during the barge-in teardown: honor it
1157
+ // and do not start the continuation.
1158
+ this.detachStopGeneration !== stopGeneration ||
1159
+ !isAssistantFeatureFlagEnabled("voice-duplex-handoff", getConfig())
1160
+ ) {
1161
+ return;
1162
+ }
1163
+ // Embed the interrupted request in the objective so the continuation knows
1164
+ // what to finish even if the forked history predates the user message being
1165
+ // persisted (barge-in can fire while the turn is still acquiring the lock).
1166
+ const interruptedRequest = turn.utterance.finalTranscriptSegments
1167
+ .join(" ")
1168
+ .trim();
1169
+ // Register the abort handle synchronously so a stop that lands while the
1170
+ // continuation is still spawning still aborts it (abortDetachedRuns fires
1171
+ // controller.abort(), which the spawn's signal wiring honors).
1172
+ const controller = new AbortController();
1173
+ this.detachControllers.add(controller);
1174
+ void (async () => {
1175
+ try {
1176
+ // Wait for the interrupted turn's teardown to settle its partial into
1177
+ // conversation history before the fork snapshots it. This is
1178
+ // turn-scoped — it waits for THIS turn only (captured at barge-in), not
1179
+ // the conversation's overall idle state, so the interrupting turn's own
1180
+ // work still proceeds in parallel (conversation.waitForIdle would block
1181
+ // on it and defeat the background handoff).
1182
+ if (teardownWait) {
1183
+ let settled = false;
1184
+ try {
1185
+ settled = await waitForPriorTurnTeardown(
1186
+ teardownWait,
1187
+ this.detachTeardownSettleTimeoutMs,
1188
+ controller.signal,
1189
+ );
1190
+ } catch {
1191
+ // Aborted mid-wait (stop/interrupt); handled by the skip below.
1192
+ }
1193
+ // Fork only once the teardown has settled. On timeout (false) or
1194
+ // abort (throw) we cannot guarantee the fork would see the interrupted
1195
+ // turn's completed tool calls, so skip the continuation rather than
1196
+ // snapshot stale history and risk repeating a side effect — the bridge
1197
+ // refuses the next turn on an unsettled teardown for the same reason.
1198
+ // The continuation is best-effort, so a rare dropped one is the safe
1199
+ // trade.
1200
+ if (!settled) {
1201
+ return;
1202
+ }
1203
+ }
1204
+ if (controller.signal.aborted || this.isClosed) {
1205
+ return;
1206
+ }
1207
+ await spawn({
1208
+ parentConversationId: this.conversationId,
1209
+ objective: buildDuplexContinuationObjective(interruptedRequest),
1210
+ label: `voice-continue-${turn.turnId}`,
1211
+ signal: controller.signal,
1212
+ });
1213
+ } catch (err) {
1214
+ // A stop/interrupt aborts via the signal; that rejection is expected.
1215
+ if (!controller.signal.aborted) {
1216
+ log.warn(
1217
+ { err, turnId: turn.turnId },
1218
+ "Voice duplex handoff continuation failed",
1219
+ );
1220
+ }
1221
+ } finally {
1222
+ this.detachControllers.delete(controller);
1223
+ }
1224
+ })();
1225
+ }
1226
+
1227
+ // Abort every background continuation this session started and drop its
1228
+ // handle. A client interrupt or session close is a hard stop for detached
1229
+ // work; the continuation's own `.finally` removes it from the set too.
1230
+ private abortDetachedRuns(): void {
1231
+ // Bump the generation so a barge-in whose async teardown is still in flight
1232
+ // (its detach not yet spawned) sees the stop and skips the continuation.
1233
+ this.detachStopGeneration += 1;
1234
+ for (const controller of this.detachControllers) {
1235
+ controller.abort();
1236
+ }
1237
+ this.detachControllers.clear();
1238
+ }
1239
+
955
1240
  // VAD closed the utterance — the analog of ptt_release: emit
956
1241
  // utterance_end, then run the standard release path.
957
1242
  private handleVadUtteranceEnd(reason: "silence" | "max-duration"): void {
@@ -1384,6 +1669,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1384
1669
  this.assistantPlaybackTailUntilMs = 0;
1385
1670
  this.takeVadPreRoll();
1386
1671
  this.vadPendingTurnEnd = null;
1672
+ // A client interrupt is a hard reset: any barge-in merge context waiting for
1673
+ // the next turn is now stale (the interrupted utterance may be discarded
1674
+ // without ever reaching finalizePendingUtterance).
1675
+ this.pendingInterruptedRequest = null;
1676
+ // ...and it hard-stops any detached background continuations.
1677
+ this.abortDetachedRuns();
1387
1678
  const utterance = this.currentUtterance;
1388
1679
  this.stopSessionTranscriber();
1389
1680
  if (utterance) {
@@ -1456,6 +1747,24 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1456
1747
  return;
1457
1748
  }
1458
1749
 
1750
+ // Consume any pending barge-in merge context for this turn (one barge-in
1751
+ // feeds exactly the next launched turn).
1752
+ const interruptedRequest = this.pendingInterruptedRequest;
1753
+ this.pendingInterruptedRequest = null;
1754
+ await this.launchAssistantTurn(utterance, content, { interruptedRequest });
1755
+ }
1756
+
1757
+ // Build the ActiveAssistantTurn for a released utterance and drive its model
1758
+ // leg.
1759
+ private async launchAssistantTurn(
1760
+ utterance: UtteranceCycle,
1761
+ content: string,
1762
+ opts?: {
1763
+ // Set on a barge-in follow-up turn: the interrupted request's transcript,
1764
+ // appended to the turn's control prompt so the model merges the two.
1765
+ interruptedRequest?: string | null;
1766
+ },
1767
+ ): Promise<void> {
1459
1768
  utterance.assistantTurnStarted = true;
1460
1769
  const token = Symbol("live-voice-assistant-turn");
1461
1770
  const turnId = this.ensureTurnId(utterance);
@@ -1471,6 +1780,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1471
1780
  ttsDone: false,
1472
1781
  ttsAudioStarted: false,
1473
1782
  finalized: false,
1783
+ interruptedRequest: opts?.interruptedRequest ?? null,
1474
1784
  escalationHandedOff: false,
1475
1785
  ttsBuffer: "",
1476
1786
  ttsSegmentEnqueued: false,
@@ -1551,7 +1861,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1551
1861
  return;
1552
1862
  }
1553
1863
  this.markFirstAssistantDelta(utterance, turnId);
1554
- void this.sendFrame({ type: "assistant_text_delta", text: chunk });
1864
+ // Same send-time abort gate as the default-leg delta path: a front-door
1865
+ // delta queued behind a backed-up outbound frame must not be written
1866
+ // once barge-in aborts the turn. Escalation aborts the front-door handle,
1867
+ // not this turn's controller, so legitimate front-door text still sends.
1868
+ void this.sendFrame(
1869
+ { type: "assistant_text_delta", text: chunk },
1870
+ () => !activeTurn.abortController.signal.aborted && !this.isClosed,
1871
+ );
1555
1872
  this.bufferAssistantTextForTts(token, chunk);
1556
1873
  };
1557
1874
 
@@ -1596,9 +1913,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1596
1913
  assistantMessageChannel: "vellum",
1597
1914
  userMessageInterface: "macos",
1598
1915
  assistantMessageInterface: "macos",
1599
- voiceControlPrompt:
1600
- "You are speaking in a local live voice session. Keep replies brief and conversational.",
1601
- approvalMode: "local-live-voice",
1916
+ voiceControlPrompt: activeTurn.interruptedRequest
1917
+ ? `${LIVE_VOICE_CONTROL_PROMPT}\n\n${buildInterruptionMergeNote(
1918
+ activeTurn.interruptedRequest,
1919
+ )}`
1920
+ : LIVE_VOICE_CONTROL_PROMPT,
1602
1921
  content: leg.content,
1603
1922
  isInbound: true,
1604
1923
  signal: activeTurn.abortController.signal,
@@ -1622,10 +1941,21 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1622
1941
  return;
1623
1942
  }
1624
1943
  this.markFirstAssistantDelta(utterance, turnId);
1625
- void this.sendFrame({
1626
- type: "assistant_text_delta",
1627
- text: msg.text,
1628
- });
1944
+ void this.sendFrame(
1945
+ {
1946
+ type: "assistant_text_delta",
1947
+ text: msg.text,
1948
+ },
1949
+ // Re-check at send time (mirrors the tts_audio path): a delta
1950
+ // already queued behind a backed-up outbound frame must not be
1951
+ // written once barge-in has aborted the turn, or the cancelled
1952
+ // reply's text leaks ahead of turn_cancelled. Key off this turn's
1953
+ // own abort signal — a normal message_complete finalizes and
1954
+ // clears activeAssistantTurn while trailing deltas may still be
1955
+ // draining, so an activeAssistantTurn-based guard would drop them.
1956
+ () =>
1957
+ !activeTurn.abortController.signal.aborted && !this.isClosed,
1958
+ );
1629
1959
  this.bufferAssistantTextForTts(token, msg.text);
1630
1960
  },
1631
1961
  message_complete: (msg) => {
@@ -1798,7 +2128,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1798
2128
  private isActiveAssistantTurn(token: symbol): boolean {
1799
2129
  const activeTurn = this.activeAssistantTurn;
1800
2130
  return (
1801
- activeTurn?.token === token && !activeTurn.finalized && !this.isClosed
2131
+ activeTurn?.token === token &&
2132
+ !activeTurn.finalized &&
2133
+ // Barge-in aborts synchronously but finalizes through an async
2134
+ // cancelAssistantTurn chain; the abort makes the turn dead at once so a
2135
+ // rejected startVoiceTurn or a trailing onError in that window does not
2136
+ // treat it as live (and emit a stray error frame or double-finalize).
2137
+ !activeTurn.abortController.signal.aborted &&
2138
+ !this.isClosed
1802
2139
  );
1803
2140
  }
1804
2141
 
@@ -1808,6 +2145,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1808
2145
  activeTurn?.token === token &&
1809
2146
  !activeTurn.assistantCompleted &&
1810
2147
  !activeTurn.finalized &&
2148
+ // Fence a late first assistant_text_delta once barge-in aborts a pre-TTS
2149
+ // turn, before its async teardown finalizes — mirrors isForwardingTts so
2150
+ // no cancelled-turn text leaks after turn_cancelled.
2151
+ !activeTurn.abortController.signal.aborted &&
1811
2152
  !this.isClosed
1812
2153
  );
1813
2154
  }
@@ -2072,9 +2413,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
2072
2413
  if (activeTurn?.token !== token) {
2073
2414
  return;
2074
2415
  }
2075
- activeTurn.assistantAudioChunks.push(
2076
- Buffer.from(chunk.dataBase64, "base64"),
2077
- );
2416
+ // Only retain the assistant TTS audio when it will be archived (see
2417
+ // collectUserAudio); the mime/sample-rate are cheap and left unconditional.
2418
+ if (this.archiveAudio) {
2419
+ activeTurn.assistantAudioChunks.push(
2420
+ Buffer.from(chunk.dataBase64, "base64"),
2421
+ );
2422
+ }
2078
2423
  activeTurn.assistantAudioMimeType = chunk.contentType;
2079
2424
  activeTurn.assistantAudioSampleRate = chunk.sampleRate;
2080
2425
  job.frames = job.frames.then(async () => {
@@ -2087,10 +2432,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
2087
2432
  },
2088
2433
  () => this.isForwardingTts(token),
2089
2434
  );
2090
- // Arm the barge-in gate only once a tts_audio frame was actually
2091
- // written a backed-up outbound queue must not let speech cancel a
2092
- // still-unspoken reply. Token match keeps a stale turn's late send
2093
- // from arming a newer turn.
2435
+ // Skip a frame that wasn't actually written a backed-up outbound
2436
+ // queue hasn't reached the client, so it must not extend the
2437
+ // playback-tail estimate or latch first-audio state. Token match keeps
2438
+ // a stale turn's late send from latching a newer turn.
2094
2439
  if (!sent) {
2095
2440
  return;
2096
2441
  }
@@ -2114,7 +2459,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
2114
2459
  }
2115
2460
 
2116
2461
  private collectUserAudio(utterance: UtteranceCycle, chunk: Buffer): void {
2117
- utterance.userAudioChunks.push(Buffer.from(chunk));
2462
+ // Only retain the raw audio when it will actually be archived — otherwise
2463
+ // buffering a whole turn's PCM is dead memory. The first-audio metric is
2464
+ // independent of archiving, so it stays unconditional.
2465
+ if (this.archiveAudio) {
2466
+ utterance.userAudioChunks.push(Buffer.from(chunk));
2467
+ }
2118
2468
  this.markUtteranceMetric(utterance, "firstAudioAtMs", (turnId) =>
2119
2469
  this.metrics.markFirstAudio(turnId),
2120
2470
  );
@@ -2229,6 +2579,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
2229
2579
  utterance: UtteranceCycle,
2230
2580
  reason: string,
2231
2581
  ): Promise<void> {
2582
+ // An utterance that finalizes here never became a turn (empty transcript,
2583
+ // client interrupt, transcriber close, error), so it ends the window a
2584
+ // barge-in's merge context was waiting to attach to. Drop that context so
2585
+ // it can't leak into a later, unrelated turn. The barged turn itself
2586
+ // finalizes through finalizeAssistantTurn, not here, so this never clears a
2587
+ // request that the barge-in follow-up turn is still about to consume.
2588
+ this.pendingInterruptedRequest = null;
2232
2589
  utterance.completed = true;
2233
2590
  const turnId = utterance.turnId;
2234
2591
  if (!turnId) {
@@ -2509,7 +2866,8 @@ export function createLiveVoiceSession(
2509
2866
  // unchanged. Optional-chained
2510
2867
  // because hand-built test configs may predate the liveVoice namespace;
2511
2868
  // absent config falls through to the in-code defaults.
2512
- const vadConfig = getConfig().liveVoice?.vad;
2869
+ const liveVoiceConfig = getConfig().liveVoice;
2870
+ const vadConfig = liveVoiceConfig?.vad;
2513
2871
  return new LiveVoiceSession(context, {
2514
2872
  ...options,
2515
2873
  turnDetectorConfig:
@@ -2533,14 +2891,59 @@ export function createLiveVoiceSession(
2533
2891
  options.streamTtsAudio === undefined
2534
2892
  ? defaultStreamLiveVoiceTtsAudio
2535
2893
  : options.streamTtsAudio,
2894
+ spawnBackgroundContinuation:
2895
+ options.spawnBackgroundContinuation ?? defaultSpawnBackgroundContinuation,
2896
+ getTurnTeardown: options.getTurnTeardown ?? getConversationTurnTeardown,
2897
+ // Off by default (see the `liveVoice.archiveAudio` schema): voice turns
2898
+ // persist only their transcribed text, so the recorded audio never lands
2899
+ // as an attachment on the conversation messages. Enable via config for
2900
+ // playback/debugging. An explicit option (incl. `null`) always wins — the
2901
+ // test seam and any future caller override.
2536
2902
  archiveAudio:
2537
2903
  options.archiveAudio === undefined
2538
- ? defaultArchiveLiveVoiceAudio
2904
+ ? liveVoiceConfig?.archiveAudio
2905
+ ? defaultArchiveLiveVoiceAudio
2906
+ : null
2539
2907
  : options.archiveAudio,
2540
2908
  emitMetrics: options.emitMetrics ?? true,
2541
2909
  });
2542
2910
  }
2543
2911
 
2912
+ // Forks the live voice conversation into a background subagent that continues
2913
+ // the interrupted turn. The fork inherits the conversation's current messages
2914
+ // (which include the interrupted turn's completed tool calls after teardown),
2915
+ // so it resumes without repeating them. Uses spawnAndAwait (synchronous mode)
2916
+ // so the terminal parent-notification is skipped — the continuation stays
2917
+ // silent until the resurface flow lands, and this call ignores its result. The
2918
+ // `signal` aborts the child on a stop/interrupt.
2919
+ async function defaultSpawnBackgroundContinuation(args: {
2920
+ parentConversationId: string;
2921
+ objective: string;
2922
+ label: string;
2923
+ signal: AbortSignal;
2924
+ }): Promise<void> {
2925
+ const parentConversation = findConversation(args.parentConversationId);
2926
+ if (!parentConversation) {
2927
+ throw new Error(
2928
+ `Cannot detach interrupted voice turn: conversation ${args.parentConversationId} is not resident.`,
2929
+ );
2930
+ }
2931
+ await getSubagentManager().spawnAndAwait(
2932
+ {
2933
+ parentConversationId: args.parentConversationId,
2934
+ label: args.label,
2935
+ objective: args.objective,
2936
+ fork: true,
2937
+ sendResultToUser: false,
2938
+ parentMessages: [...parentConversation.messages],
2939
+ parentSystemPrompt: parentConversation.getCurrentSystemPrompt(),
2940
+ },
2941
+ // No client-facing events: the continuation is silent until resurface.
2942
+ () => {},
2943
+ { signal: args.signal },
2944
+ );
2945
+ }
2946
+
2544
2947
  async function defaultResolveStreamingTranscriber(
2545
2948
  options: ResolveStreamingTranscriberOptions,
2546
2949
  ): Promise<StreamingTranscriber | null> {