@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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 (866) hide show
  1. package/ARCHITECTURE.md +94 -91
  2. package/Dockerfile +5 -16
  3. package/README.md +7 -10
  4. package/bun.lock +0 -4
  5. package/docs/architecture/security.md +20 -20
  6. package/docs/stt-provider-onboarding.md +4 -5
  7. package/docs/trusted-contact-access.md +7 -7
  8. package/eslint-rules/cli-no-daemon-internals.js +4 -0
  9. package/eslint.config.mjs +7 -1
  10. package/knip.json +0 -1
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
  13. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
  14. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
  15. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
  17. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
  19. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
  21. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
  22. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
  23. package/openapi.yaml +1073 -225
  24. package/package.json +1 -4
  25. package/scripts/test.sh +35 -13
  26. package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
  27. package/src/__tests__/acp-session.test.ts +4 -1
  28. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
  29. package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
  30. package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
  31. package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
  32. package/src/__tests__/app-source-watcher.test.ts +11 -0
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  34. package/src/__tests__/assistant-attachments.test.ts +62 -4
  35. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
  36. package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
  37. package/src/__tests__/attachments-store.test.ts +3 -0
  38. package/src/__tests__/background-shell-bash.test.ts +34 -0
  39. package/src/__tests__/background-shell-host-bash.test.ts +12 -0
  40. package/src/__tests__/background-tool-registry.test.ts +97 -0
  41. package/src/__tests__/background-tool-routes.test.ts +72 -1
  42. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  43. package/src/__tests__/btw-routes.test.ts +57 -3
  44. package/src/__tests__/call-controller.test.ts +474 -84
  45. package/src/__tests__/call-domain.test.ts +48 -1
  46. package/src/__tests__/call-pointer-messages.test.ts +57 -19
  47. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
  48. package/src/__tests__/call-routes-http.test.ts +15 -14
  49. package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
  50. package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
  51. package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
  52. package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
  53. package/src/__tests__/call-setup-flow.test.ts +311 -0
  54. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
  55. package/src/__tests__/channel-guardian.test.ts +4 -7
  56. package/src/__tests__/channel-readiness-service.test.ts +0 -6
  57. package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
  58. package/src/__tests__/channel-retry-sweep.test.ts +76 -64
  59. package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
  60. package/src/__tests__/clear-all-lexical.test.ts +90 -0
  61. package/src/__tests__/compactor-media-strip.test.ts +177 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  63. package/src/__tests__/config-schema.test.ts +17 -0
  64. package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
  65. package/src/__tests__/contacts-relay-reads.test.ts +188 -23
  66. package/src/__tests__/contacts-tools.test.ts +42 -1
  67. package/src/__tests__/contacts-write.test.ts +181 -0
  68. package/src/__tests__/context-search-conversations-source.test.ts +220 -81
  69. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  71. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  72. package/src/__tests__/conversation-agent-loop.test.ts +39 -0
  73. package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
  74. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
  75. package/src/__tests__/conversation-clear-safety.test.ts +0 -1
  76. package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
  77. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
  78. package/src/__tests__/conversation-disk-view.test.ts +1 -0
  79. package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
  80. package/src/__tests__/conversation-error.test.ts +0 -18
  81. package/src/__tests__/conversation-fork-crud.test.ts +67 -0
  82. package/src/__tests__/conversation-history-web-search.test.ts +7 -137
  83. package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
  84. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
  85. package/src/__tests__/conversation-process-callsite.test.ts +37 -0
  86. package/src/__tests__/conversation-queue.test.ts +149 -0
  87. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
  88. package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
  89. package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
  90. package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
  91. package/src/__tests__/conversation-skill-tools.test.ts +73 -0
  92. package/src/__tests__/conversation-wipe.test.ts +20 -5
  93. package/src/__tests__/credential-prompt-route.test.ts +69 -0
  94. package/src/__tests__/credential-security-invariants.test.ts +1 -3
  95. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  96. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
  97. package/src/__tests__/edit-propagation.test.ts +56 -2
  98. package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
  99. package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
  100. package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
  101. package/src/__tests__/guardian-question-mode.test.ts +57 -0
  102. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
  103. package/src/__tests__/guardian-wait-controller.test.ts +619 -0
  104. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
  105. package/src/__tests__/heic-fixture.ts +55 -0
  106. package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
  107. package/src/__tests__/http-conversation-lineage.test.ts +6 -1
  108. package/src/__tests__/image-conversion.test.ts +170 -0
  109. package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
  110. package/src/__tests__/introduction-card-resolver.test.ts +354 -0
  111. package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
  112. package/src/__tests__/invite-routes-http.test.ts +246 -420
  113. package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
  114. package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
  115. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
  116. package/src/__tests__/list-messages-attachments.test.ts +159 -0
  117. package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
  118. package/src/__tests__/list-messages-queued.test.ts +25 -0
  119. package/src/__tests__/managed-store.test.ts +129 -1
  120. package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
  121. package/src/__tests__/media-stream-output.test.ts +350 -19
  122. package/src/__tests__/media-stream-server-integration.test.ts +943 -169
  123. package/src/__tests__/media-stream-stt-session.test.ts +498 -1
  124. package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
  125. package/src/__tests__/model-intents.test.ts +1 -1
  126. package/src/__tests__/mtime-cache.test.ts +19 -0
  127. package/src/__tests__/non-member-access-request.test.ts +349 -3
  128. package/src/__tests__/notification-decision-fallback.test.ts +104 -8
  129. package/src/__tests__/notification-decision-strategy.test.ts +57 -8
  130. package/src/__tests__/onboarding-template-contract.test.ts +27 -43
  131. package/src/__tests__/openai-provider.test.ts +33 -0
  132. package/src/__tests__/openai-responses-provider.test.ts +40 -0
  133. package/src/__tests__/persistence-layering-guard.test.ts +29 -20
  134. package/src/__tests__/persona-resolver.test.ts +56 -2
  135. package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
  136. package/src/__tests__/phone.test.ts +1 -15
  137. package/src/__tests__/plugin-bootstrap.test.ts +25 -196
  138. package/src/__tests__/plugin-disabled-state.test.ts +33 -0
  139. package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
  140. package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
  141. package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
  142. package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
  143. package/src/__tests__/plugin-pipeline.test.ts +63 -1
  144. package/src/__tests__/plugin-route-contribution.test.ts +62 -115
  145. package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
  146. package/src/__tests__/plugin-types.test.ts +0 -1
  147. package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
  148. package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
  149. package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
  150. package/src/__tests__/public-ingress-urls.test.ts +0 -48
  151. package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
  152. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
  153. package/src/__tests__/resolve-trust-class.test.ts +28 -10
  154. package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
  155. package/src/__tests__/schedule-retry.test.ts +38 -14
  156. package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
  157. package/src/__tests__/schedule-routes.test.ts +14 -6
  158. package/src/__tests__/schedule-store.test.ts +3 -1
  159. package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
  160. package/src/__tests__/scheduler-recurrence.test.ts +57 -107
  161. package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
  162. package/src/__tests__/scheduler-wake.test.ts +13 -23
  163. package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
  164. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  165. package/src/__tests__/shell-observability.test.ts +1 -0
  166. package/src/__tests__/skill-boundary-guard.test.ts +3 -4
  167. package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
  168. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
  169. package/src/__tests__/skill-script-runner-host.test.ts +53 -0
  170. package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
  171. package/src/__tests__/skill-tool-factory.test.ts +73 -1
  172. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
  173. package/src/__tests__/slack-inbound-verification.test.ts +36 -0
  174. package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
  175. package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
  176. package/src/__tests__/stt-hints.test.ts +2 -256
  177. package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
  178. package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
  179. package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
  180. package/src/__tests__/subagent-persistence.test.ts +122 -0
  181. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  182. package/src/__tests__/subagent-terminal-message.test.ts +99 -0
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
  184. package/src/__tests__/system-prompt.test.ts +17 -118
  185. package/src/__tests__/task-scheduler.test.ts +68 -89
  186. package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
  187. package/src/__tests__/telephony-tts-guard.test.ts +538 -0
  188. package/src/__tests__/title-generate-hook.test.ts +16 -0
  189. package/src/__tests__/tool-approval-handler.test.ts +87 -1
  190. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
  191. package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
  192. package/src/__tests__/trust-context-guards.test.ts +21 -10
  193. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
  194. package/src/__tests__/twilio-provider.test.ts +17 -17
  195. package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
  196. package/src/__tests__/twilio-routes.test.ts +257 -387
  197. package/src/__tests__/twilio-validation.test.ts +1 -1
  198. package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
  199. package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
  200. package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
  201. package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
  202. package/src/__tests__/voice-quality.test.ts +2 -2
  203. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
  204. package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
  205. package/src/__tests__/voice-session-bridge.test.ts +1 -10
  206. package/src/__tests__/workspace-git-service.test.ts +1 -1
  207. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
  208. package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
  209. package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
  210. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  211. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  212. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  213. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
  214. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
  215. package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
  216. package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
  217. package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
  218. package/src/acp/__tests__/session-manager.test.ts +6 -0
  219. package/src/acp/agent-process.test.ts +116 -0
  220. package/src/acp/agent-process.ts +71 -0
  221. package/src/acp/auto-install.ts +12 -8
  222. package/src/acp/failure-error.test.ts +109 -0
  223. package/src/acp/failure-error.ts +111 -0
  224. package/src/acp/prepare-agent-env.ts +0 -12
  225. package/src/acp/resolve-agent.test.ts +6 -45
  226. package/src/acp/resolve-agent.ts +2 -4
  227. package/src/acp/session-manager.test.ts +312 -0
  228. package/src/acp/session-manager.ts +102 -46
  229. package/src/agent/ax-tree-compaction.test.ts +4 -1
  230. package/src/agent/image-optimize.ts +17 -127
  231. package/src/agent/loop.ts +45 -192
  232. package/src/api/events/background-tool-completed.test.ts +51 -0
  233. package/src/api/events/background-tool-completed.ts +29 -0
  234. package/src/api/events/background-tool-started.test.ts +36 -0
  235. package/src/api/events/background-tool-started.ts +28 -0
  236. package/src/api/events/conversation-error.ts +0 -1
  237. package/src/api/events/open-panel.ts +33 -0
  238. package/src/api/events/subagent-status-changed.ts +4 -1
  239. package/src/api/index.ts +19 -0
  240. package/src/api/responses/conversation-message.ts +34 -0
  241. package/src/approvals/guardian-decision-primitive.ts +74 -24
  242. package/src/approvals/guardian-request-resolvers.ts +450 -122
  243. package/src/apps/app-store.ts +1 -0
  244. package/src/apps/shared-app-links-store.ts +1 -0
  245. package/src/avatar/identity-avatar.ts +3 -7
  246. package/src/background-wake/wake-intent-hooks.test.ts +1 -1
  247. package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
  248. package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
  249. package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
  250. package/src/calls/call-controller.ts +134 -48
  251. package/src/calls/call-domain.ts +18 -42
  252. package/src/calls/call-pointer-messages.ts +51 -67
  253. package/src/calls/call-setup-flow-types.ts +76 -0
  254. package/src/calls/call-setup-flow.ts +1743 -0
  255. package/src/calls/call-speech-output.ts +87 -23
  256. package/src/calls/call-store.ts +4 -0
  257. package/src/calls/call-transport.ts +22 -36
  258. package/src/calls/fish-audio-client.ts +3 -0
  259. package/src/calls/gateway-invite-reader.ts +98 -0
  260. package/src/calls/guardian-wait-controller.ts +451 -0
  261. package/src/calls/media-stream-audio-transcode.ts +82 -2
  262. package/src/calls/media-stream-output.ts +142 -36
  263. package/src/calls/media-stream-server.ts +297 -178
  264. package/src/calls/media-stream-stt-session.ts +393 -50
  265. package/src/calls/media-turn-detector.ts +4 -6
  266. package/src/calls/relay-access-wait.ts +3 -4
  267. package/src/calls/relay-setup-router.ts +22 -43
  268. package/src/calls/relay-verification.ts +17 -24
  269. package/src/calls/resolve-call-tts-provider.ts +96 -29
  270. package/src/calls/stt-hints.ts +24 -87
  271. package/src/calls/telephony-credential-preflight.ts +159 -0
  272. package/src/calls/telephony-tts-capability.ts +183 -0
  273. package/src/calls/tts-call-strategy.ts +18 -20
  274. package/src/calls/twilio-provider.ts +3 -2
  275. package/src/calls/twilio-routes.ts +47 -351
  276. package/src/calls/types.ts +5 -3
  277. package/src/calls/voice-ingress-preflight.ts +9 -0
  278. package/src/calls/voice-quality.ts +13 -18
  279. package/src/calls/voice-session-bridge.ts +25 -35
  280. package/src/channels/__tests__/types.test.ts +28 -0
  281. package/src/channels/channel-verification-sessions.ts +7 -2
  282. package/src/channels/config.ts +0 -42
  283. package/src/channels/slack-thread-store.ts +12 -0
  284. package/src/channels/types.ts +23 -0
  285. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  286. package/src/cli/commands/__tests__/schedules.test.ts +83 -34
  287. package/src/cli/commands/__tests__/status.test.ts +42 -2
  288. package/src/cli/commands/contacts.ts +2 -2
  289. package/src/cli/commands/credentials.ts +17 -0
  290. package/src/cli/commands/monitoring.ts +191 -0
  291. package/src/cli/commands/schedules-worker.ts +175 -0
  292. package/src/cli/commands/schedules.ts +169 -43
  293. package/src/cli/commands/status.ts +12 -1
  294. package/src/cli/commands/tools.ts +5 -1
  295. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
  296. package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
  297. package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
  298. package/src/cli/lib/search-plugins.ts +6 -0
  299. package/src/cli/lib/toggle-plugin.ts +21 -2
  300. package/src/cli/program.ts +3 -1
  301. package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
  302. package/src/config/acp-defaults.test.ts +0 -10
  303. package/src/config/acp-defaults.ts +0 -6
  304. package/src/config/bundled-skills/acp/SKILL.md +13 -45
  305. package/src/config/bundled-skills/acp/TOOLS.json +3 -3
  306. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  307. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
  308. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
  309. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
  310. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  311. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
  312. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
  313. package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
  314. package/src/config/call-site-defaults.ts +0 -2
  315. package/src/config/feature-flag-registry.json +16 -8
  316. package/src/config/loader.ts +11 -0
  317. package/src/config/schema.ts +9 -1
  318. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
  319. package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
  320. package/src/config/schemas/call-site-catalog.ts +0 -12
  321. package/src/config/schemas/calls.ts +8 -0
  322. package/src/config/schemas/elevenlabs.ts +1 -1
  323. package/src/config/schemas/llm.ts +0 -2
  324. package/src/config/schemas/memory-v2.ts +1 -1
  325. package/src/config/schemas/memory-v3.ts +82 -0
  326. package/src/config/schemas/monitoring.ts +61 -0
  327. package/src/config/schemas/schedules.ts +35 -0
  328. package/src/config/seed-inference-profiles.ts +2 -2
  329. package/src/config/skills-plugin-filter.test.ts +96 -0
  330. package/src/config/skills.ts +26 -0
  331. package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
  332. package/src/contacts/canonical-guardian-store.ts +5 -0
  333. package/src/contacts/contact-store.ts +81 -35
  334. package/src/contacts/contacts-write.ts +50 -25
  335. package/src/contacts/member-write-relay.ts +115 -31
  336. package/src/contacts/types.ts +0 -8
  337. package/src/context/compactor.ts +29 -15
  338. package/src/context/outbound-sanitize.ts +202 -0
  339. package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
  340. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
  341. package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
  342. package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
  343. package/src/daemon/abort-watchdog.ts +7 -0
  344. package/src/daemon/app-source-watcher.ts +9 -0
  345. package/src/daemon/assistant-attachments.ts +42 -0
  346. package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
  347. package/src/daemon/conversation-agent-loop.ts +61 -68
  348. package/src/daemon/conversation-error.ts +1 -15
  349. package/src/daemon/conversation-history.ts +32 -236
  350. package/src/daemon/conversation-initial-prompt.ts +77 -0
  351. package/src/daemon/conversation-lifecycle.ts +25 -1
  352. package/src/daemon/conversation-messaging.ts +2 -2
  353. package/src/daemon/conversation-plugin-scope.ts +38 -0
  354. package/src/daemon/conversation-process.ts +50 -14
  355. package/src/daemon/conversation-runtime-assembly.ts +36 -6
  356. package/src/daemon/conversation-skill-tools.ts +19 -3
  357. package/src/daemon/conversation-store.ts +3 -3
  358. package/src/daemon/conversation-surfaces.ts +171 -0
  359. package/src/daemon/conversation-tool-setup.test.ts +95 -0
  360. package/src/daemon/conversation-tool-setup.ts +116 -1
  361. package/src/daemon/conversation-turn-finalize.ts +211 -0
  362. package/src/daemon/conversation.ts +92 -1
  363. package/src/daemon/disk-pressure-policy.ts +2 -1
  364. package/src/daemon/event-loop-watchdog.ts +22 -6
  365. package/src/daemon/external-plugins-bootstrap.ts +47 -134
  366. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
  367. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
  368. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
  369. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
  370. package/src/daemon/handlers/config-a2a.ts +7 -14
  371. package/src/daemon/handlers/config-channels.ts +2 -10
  372. package/src/daemon/handlers/conversation-history.ts +3 -1
  373. package/src/daemon/handlers/conversations.ts +0 -52
  374. package/src/daemon/handlers/shared.ts +4 -24
  375. package/src/daemon/handlers/skills.ts +4 -1
  376. package/src/daemon/host-cu-proxy.ts +1 -1
  377. package/src/daemon/lifecycle.ts +59 -472
  378. package/src/daemon/message-protocol.ts +3 -3
  379. package/src/daemon/message-provenance.ts +12 -14
  380. package/src/daemon/message-types/background-tools.ts +28 -0
  381. package/src/daemon/message-types/contacts.ts +2 -10
  382. package/src/daemon/message-types/conversations.ts +10 -0
  383. package/src/daemon/message-types/integrations.ts +2 -0
  384. package/src/daemon/message-types/surfaces.ts +1 -0
  385. package/src/daemon/message-types/sync.ts +1 -0
  386. package/src/daemon/pointer-conversation-trust.ts +23 -9
  387. package/src/daemon/pointer-turn-runner.ts +174 -0
  388. package/src/daemon/providers-setup.ts +16 -0
  389. package/src/daemon/shutdown-handlers.ts +139 -47
  390. package/src/daemon/skill-memory-refresh.ts +2 -2
  391. package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
  392. package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
  393. package/src/daemon/tool-setup-types.ts +8 -0
  394. package/src/daemon/trust-context.ts +21 -6
  395. package/src/daemon/turn-latency-tracker.test.ts +93 -0
  396. package/src/daemon/turn-latency-tracker.ts +38 -0
  397. package/src/daemon/wake-conversation-ops.ts +10 -8
  398. package/src/documents/document-comments-store.test.ts +7 -1
  399. package/src/documents/document-comments-store.ts +8 -0
  400. package/src/documents/document-store.ts +15 -0
  401. package/src/export/__tests__/transcript-formatter.test.ts +11 -0
  402. package/src/export/transcript-formatter.ts +49 -67
  403. package/src/home/__tests__/feed-writer.test.ts +247 -1
  404. package/src/home/conversation-starter-checkpoints.ts +1 -0
  405. package/src/home/feed-writer.ts +61 -0
  406. package/src/home/job-handlers/conversation-starters.ts +1 -0
  407. package/src/hooks/hook-loader.ts +12 -0
  408. package/src/hooks/registry.ts +32 -4
  409. package/src/inbound/public-ingress-urls.ts +0 -17
  410. package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
  411. package/src/ipc/assistant-server.ts +28 -7
  412. package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
  413. package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
  414. package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
  415. package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
  416. package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
  417. package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
  418. package/src/ipc/routes/db-proxy.ts +5 -2
  419. package/src/ipc/routes/invite-ipc-routes.ts +53 -39
  420. package/src/jobs/worker.ts +9 -2
  421. package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
  422. package/src/live-voice/live-voice-archive.ts +5 -0
  423. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
  424. package/src/messaging/providers/channel-transport.ts +6 -0
  425. package/src/messaging/providers/index.ts +3 -0
  426. package/src/messaging/providers/slack/api.test.ts +49 -1
  427. package/src/messaging/providers/slack/api.ts +110 -0
  428. package/src/messaging/providers/slack/send.test.ts +85 -0
  429. package/src/messaging/providers/slack/send.ts +100 -28
  430. package/src/messaging/providers/slack/transport.ts +7 -13
  431. package/src/messaging/providers/whatsapp/send.ts +29 -9
  432. package/src/monitoring/__tests__/control.test.ts +235 -0
  433. package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
  434. package/src/monitoring/control.ts +105 -0
  435. package/src/monitoring/resource-sampler.ts +258 -0
  436. package/src/monitoring/sample-ring-buffer.ts +108 -0
  437. package/src/monitoring/worker.ts +94 -0
  438. package/src/notifications/README.md +1 -1
  439. package/src/notifications/access-request-copy.ts +94 -23
  440. package/src/notifications/adapters/slack.ts +89 -21
  441. package/src/notifications/approval-card-builder.ts +33 -18
  442. package/src/notifications/approval-card-data.ts +48 -15
  443. package/src/notifications/broadcaster.ts +25 -16
  444. package/src/notifications/decision-engine.ts +131 -32
  445. package/src/notifications/guardian-question-mode.ts +58 -10
  446. package/src/permissions/approval-policy.ts +8 -1
  447. package/src/permissions/checker.ts +2 -0
  448. package/src/permissions/gateway-threshold-reader.ts +7 -6
  449. package/src/permissions/ipc-risk-types.ts +1 -0
  450. package/src/permissions/secret-prompter.ts +11 -3
  451. package/src/permissions/types.ts +2 -2
  452. package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
  453. package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
  454. package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
  455. package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
  456. package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
  457. package/src/persistence/a2a-invite-store.ts +205 -0
  458. package/src/persistence/attachments-store.ts +94 -10
  459. package/src/persistence/checkpoints.ts +45 -0
  460. package/src/persistence/conversation-crud.ts +453 -260
  461. package/src/persistence/conversation-display-order-migration.ts +8 -2
  462. package/src/persistence/conversation-group-migration.test.ts +8 -0
  463. package/src/persistence/conversation-group-migration.ts +9 -0
  464. package/src/persistence/conversation-queries.ts +244 -125
  465. package/src/persistence/conversation-row-batch-delete.ts +9 -5
  466. package/src/persistence/conversation-search-lexical.ts +49 -0
  467. package/src/persistence/db-async-query.ts +42 -0
  468. package/src/persistence/db-connection.ts +13 -0
  469. package/src/persistence/delivery-channels.ts +0 -82
  470. package/src/persistence/delivery-crud.ts +27 -6
  471. package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
  472. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
  473. package/src/persistence/embeddings/embedding-identity.ts +17 -5
  474. package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
  475. package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
  476. package/src/persistence/embeddings/qdrant-client.ts +81 -5
  477. package/src/persistence/group-crud.ts +13 -1
  478. package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
  479. package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
  480. package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
  481. package/src/persistence/job-handlers/cleanup.ts +57 -9
  482. package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
  483. package/src/persistence/job-handlers/message-lexical.ts +251 -0
  484. package/src/persistence/jobs-store.ts +102 -8
  485. package/src/persistence/jobs-worker.ts +68 -28
  486. package/src/persistence/llm-request-log-store.ts +35 -19
  487. package/src/persistence/llm-usage-store.ts +11 -0
  488. package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
  489. package/src/persistence/memory-lifecycle-hooks.ts +55 -1
  490. package/src/persistence/migrations/028-call-session-mode.ts +1 -1
  491. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  492. package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
  493. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  494. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  495. package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
  496. package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
  497. package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
  498. package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
  499. package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
  500. package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
  501. package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
  502. package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
  503. package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
  504. package/src/persistence/raw-query.ts +50 -11
  505. package/src/persistence/schema/a2a.ts +14 -0
  506. package/src/persistence/schema/contacts.ts +0 -62
  507. package/src/persistence/schema/conversation-groups.ts +12 -0
  508. package/src/persistence/schema/conversations.ts +2 -0
  509. package/src/persistence/schema/documents.ts +65 -0
  510. package/src/persistence/schema/guardian.ts +4 -0
  511. package/src/persistence/schema/index.ts +4 -0
  512. package/src/persistence/schema/memory-injection.ts +62 -0
  513. package/src/persistence/schema/workflows.ts +55 -0
  514. package/src/persistence/slow-query-log.ts +437 -0
  515. package/src/persistence/slow-sync-log.ts +204 -0
  516. package/src/persistence/steps.ts +16 -0
  517. package/src/persistence/subagent-store.ts +125 -0
  518. package/src/persistence/worker-control.ts +45 -238
  519. package/src/plugin-api/event-hub-facade.ts +172 -0
  520. package/src/plugin-api/index.ts +9 -1
  521. package/src/plugin-api/types.ts +40 -9
  522. package/src/plugins/defaults/default-plugin-names.ts +60 -0
  523. package/src/plugins/defaults/index.ts +19 -29
  524. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
  525. package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
  526. package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
  527. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
  528. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
  530. package/src/plugins/defaults/memory/config.ts +11 -0
  531. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
  532. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
  533. package/src/plugins/defaults/memory/embeddings.ts +77 -0
  534. package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
  535. package/src/plugins/defaults/memory/frontmatter.ts +50 -0
  536. package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
  537. package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
  538. package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
  539. package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
  540. package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
  541. package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
  542. package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
  543. package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
  544. package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
  545. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  546. package/src/plugins/defaults/memory/hooks/init.ts +31 -5
  547. package/src/plugins/defaults/memory/indexer.ts +2 -3
  548. package/src/plugins/defaults/memory/injectors.ts +2 -2
  549. package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
  550. package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
  551. package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
  552. package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
  553. package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
  554. package/src/plugins/defaults/memory/job-handlers.ts +13 -12
  555. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
  556. package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
  557. package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
  558. package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
  559. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
  560. package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
  561. package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
  562. package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
  563. package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
  564. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
  565. package/src/plugins/defaults/memory/search/semantic.ts +2 -2
  566. package/src/plugins/defaults/memory/startup.ts +244 -0
  567. package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
  568. package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
  570. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
  571. package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
  572. package/src/plugins/defaults/memory/v2/activation.ts +2 -4
  573. package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
  574. package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
  575. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
  576. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
  577. package/src/plugins/defaults/memory/v2/migration.ts +2 -5
  578. package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
  579. package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
  580. package/src/plugins/defaults/memory/v2/router.ts +5 -5
  581. package/src/plugins/defaults/memory/v2/sim.ts +2 -4
  582. package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
  583. package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
  584. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
  585. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
  586. package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
  587. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
  588. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
  589. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
  590. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
  591. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
  592. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
  593. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
  594. package/src/plugins/defaults/memory/v3/card.ts +1 -4
  595. package/src/plugins/defaults/memory/v3/dense.ts +60 -25
  596. package/src/plugins/defaults/memory/v3/edge.ts +1 -1
  597. package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
  598. package/src/plugins/defaults/memory/v3/gate.ts +158 -0
  599. package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
  600. package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
  601. package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
  602. package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
  603. package/src/plugins/defaults/memory/v3/prune.ts +2 -2
  604. package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
  605. package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
  606. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
  607. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
  608. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
  609. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  610. package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
  611. package/src/plugins/injector-registry.ts +13 -2
  612. package/src/plugins/mtime-cache.ts +36 -47
  613. package/src/plugins/pipeline.ts +15 -1
  614. package/src/plugins/types.ts +14 -23
  615. package/src/prompts/__tests__/system-prompt.test.ts +170 -32
  616. package/src/prompts/persona-resolver.ts +13 -9
  617. package/src/prompts/system-prompt.ts +30 -9
  618. package/src/prompts/templates/BOOTSTRAP.md +16 -119
  619. package/src/prompts/templates/SOUL.md +43 -13
  620. package/src/prompts/templates/system-sections.ts +32 -27
  621. package/src/prompts/templates/users/default.md +19 -0
  622. package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
  623. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
  624. package/src/providers/model-intents.ts +2 -2
  625. package/src/providers/openai/chat-completions-provider.ts +14 -1
  626. package/src/providers/openai/responses-provider.ts +18 -0
  627. package/src/providers/placeholder-sentinels.ts +18 -8
  628. package/src/providers/retry.ts +1 -0
  629. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
  630. package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
  631. package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
  632. package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
  633. package/src/providers/speech-to-text/provider-catalog.ts +0 -89
  634. package/src/providers/speech-to-text/resolve.ts +55 -0
  635. package/src/providers/tool-progress-events.test.ts +50 -0
  636. package/src/providers/tool-progress-events.ts +99 -0
  637. package/src/providers/types.ts +1 -1
  638. package/src/providers/vellum-model-routing.test.ts +51 -0
  639. package/src/providers/vellum-model-routing.ts +84 -0
  640. package/src/runtime/AGENTS.md +5 -4
  641. package/src/runtime/__tests__/agent-wake.test.ts +158 -1
  642. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  643. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
  644. package/src/runtime/access-request-helper.ts +154 -3
  645. package/src/runtime/actor-trust-resolver.ts +7 -21
  646. package/src/runtime/agent-wake.ts +41 -1
  647. package/src/runtime/assistant-event.ts +86 -12
  648. package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
  649. package/src/runtime/channel-approval-types.ts +52 -2
  650. package/src/runtime/channel-approvals.ts +23 -8
  651. package/src/runtime/channel-invite-transport.ts +2 -2
  652. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  653. package/src/runtime/channel-readiness-service.ts +3 -5
  654. package/src/runtime/channel-retry-sweep.ts +18 -24
  655. package/src/runtime/channel-verification-service.ts +9 -11
  656. package/src/runtime/finalize-event-delivery.ts +30 -16
  657. package/src/runtime/guardian-action-service.ts +10 -11
  658. package/src/runtime/guardian-reply-router.ts +122 -21
  659. package/src/runtime/http-server.ts +44 -101
  660. package/src/runtime/introduction-policy.test.ts +214 -0
  661. package/src/runtime/introduction-policy.ts +284 -0
  662. package/src/runtime/invite-service.ts +104 -361
  663. package/src/runtime/local-actor-identity.test.ts +38 -0
  664. package/src/runtime/local-actor-identity.ts +14 -0
  665. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
  666. package/src/runtime/middleware/rate-limiter.ts +30 -0
  667. package/src/runtime/middleware/twilio-validation.ts +5 -7
  668. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
  669. package/src/runtime/migrations/origin-mode.ts +6 -6
  670. package/src/runtime/pre-first-message-gate.ts +1 -0
  671. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
  672. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
  673. package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
  674. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
  675. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
  676. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
  677. package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
  678. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
  679. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
  680. package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
  681. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
  682. package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
  683. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
  684. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
  685. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
  686. package/src/runtime/routes/acp-routes.ts +15 -3
  687. package/src/runtime/routes/app-management-routes.ts +4 -7
  688. package/src/runtime/routes/approval-routes.ts +4 -0
  689. package/src/runtime/routes/attachment-routes.ts +54 -3
  690. package/src/runtime/routes/avatar-routes.ts +0 -4
  691. package/src/runtime/routes/background-tool-routes.ts +34 -5
  692. package/src/runtime/routes/btw-routes.ts +32 -4
  693. package/src/runtime/routes/channel-route-shared.ts +18 -14
  694. package/src/runtime/routes/channel-verification-routes.ts +0 -3
  695. package/src/runtime/routes/contact-routes.ts +298 -117
  696. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  697. package/src/runtime/routes/conversation-management-routes.ts +74 -37
  698. package/src/runtime/routes/conversation-query-routes.ts +5 -2
  699. package/src/runtime/routes/conversation-routes.ts +222 -82
  700. package/src/runtime/routes/conversations-import-routes.ts +8 -1
  701. package/src/runtime/routes/credential-prompt-routes.ts +20 -5
  702. package/src/runtime/routes/debug-routes.ts +1 -0
  703. package/src/runtime/routes/documents-routes.ts +5 -2
  704. package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
  705. package/src/runtime/routes/global-search-routes.ts +2 -1
  706. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  707. package/src/runtime/routes/home-feed-routes.ts +60 -1
  708. package/src/runtime/routes/identity-routes.ts +4 -87
  709. package/src/runtime/routes/inbound-message-handler.ts +82 -41
  710. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
  711. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
  712. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
  713. package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
  714. package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
  715. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  716. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  717. package/src/runtime/routes/index.ts +6 -0
  718. package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
  719. package/src/runtime/routes/internal-twilio-routes.ts +0 -15
  720. package/src/runtime/routes/messages-lexical-routes.ts +75 -0
  721. package/src/runtime/routes/migration-routes.ts +69 -6
  722. package/src/runtime/routes/monitoring-routes.ts +237 -0
  723. package/src/runtime/routes/plugins-routes.ts +329 -8
  724. package/src/runtime/routes/schedule-routes.ts +49 -8
  725. package/src/runtime/routes/schedule-worker-routes.ts +210 -0
  726. package/src/runtime/routes/secret-routes.ts +1 -1
  727. package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
  728. package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
  729. package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
  730. package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
  731. package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
  732. package/src/runtime/services/analyze-conversation.ts +14 -4
  733. package/src/runtime/services/conversation-serializer.ts +5 -0
  734. package/src/runtime/slack-no-response.ts +47 -0
  735. package/src/runtime/slack-reply-session.test.ts +633 -0
  736. package/src/runtime/slack-reply-session.ts +382 -0
  737. package/src/runtime/slack-task-progress.ts +95 -0
  738. package/src/runtime/sync/resource-sync-events.ts +25 -0
  739. package/src/runtime/tool-grant-request-helper.ts +17 -1
  740. package/src/runtime/trust-class.test.ts +141 -0
  741. package/src/runtime/trust-class.ts +121 -0
  742. package/src/runtime/trust-verdict-consumer.ts +7 -6
  743. package/src/schedule/__tests__/run-script.test.ts +21 -0
  744. package/src/schedule/__tests__/worker-control.test.ts +241 -0
  745. package/src/schedule/run-script.ts +13 -6
  746. package/src/schedule/schedule-usage-store.ts +1 -0
  747. package/src/schedule/scheduler.ts +178 -127
  748. package/src/schedule/worker-control.ts +118 -0
  749. package/src/schedule/worker.ts +131 -0
  750. package/src/skills/__tests__/categories-cache.test.ts +38 -0
  751. package/src/skills/categories-cache.ts +40 -5
  752. package/src/skills/managed-store.ts +41 -6
  753. package/src/skills/validate-input.ts +47 -0
  754. package/src/stt/types.ts +5 -9
  755. package/src/subagent/manager.ts +267 -39
  756. package/src/subagent/types.ts +6 -2
  757. package/src/subagent/validate-allowlists.ts +65 -0
  758. package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
  759. package/src/telemetry/watchdog-direct-emit.ts +89 -0
  760. package/src/tools/acp/list-agents.test.ts +0 -6
  761. package/src/tools/background-tool-registry.ts +61 -0
  762. package/src/tools/document/document-comment-tool.test.ts +4 -0
  763. package/src/tools/host-terminal/host-shell.test.ts +397 -0
  764. package/src/tools/host-terminal/host-shell.ts +203 -14
  765. package/src/tools/skills/find-similar-skills.test.ts +152 -1
  766. package/src/tools/skills/find-similar-skills.ts +30 -4
  767. package/src/tools/skills/load.ts +36 -0
  768. package/src/tools/skills/scaffold-managed.ts +66 -34
  769. package/src/tools/skills/skill-script-runner.ts +17 -0
  770. package/src/tools/skills/skill-tool-factory.ts +5 -1
  771. package/src/tools/terminal/safe-env.ts +3 -0
  772. package/src/tools/terminal/shell.test.ts +177 -0
  773. package/src/tools/terminal/shell.ts +98 -4
  774. package/src/tools/tool-approval-handler.ts +106 -29
  775. package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
  776. package/src/tools/types.ts +29 -17
  777. package/src/tools/ui-surface/definitions.ts +3 -1
  778. package/src/tts/__tests__/provider-adapters.test.ts +37 -0
  779. package/src/tts/__tests__/provider-catalog.test.ts +21 -0
  780. package/src/tts/provider-catalog.ts +40 -9
  781. package/src/tts/providers/fish-audio-provider.ts +19 -10
  782. package/src/tts/types.ts +3 -4
  783. package/src/util/__tests__/process-tree.test.ts +10 -2
  784. package/src/util/__tests__/text-spacing.test.ts +53 -0
  785. package/src/util/cgroup-memory.ts +142 -0
  786. package/src/util/image-conversion.ts +281 -0
  787. package/src/util/phone.ts +1 -8
  788. package/src/util/platform.ts +33 -0
  789. package/src/util/process-tree.ts +10 -2
  790. package/src/util/text-spacing.ts +53 -0
  791. package/src/util/worker-process.ts +271 -0
  792. package/src/workflows/journal-store.ts +16 -1
  793. package/src/workflows/library.test.ts +35 -20
  794. package/src/workflows/library.ts +105 -66
  795. package/src/workspace/git-service.ts +1 -0
  796. package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
  797. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
  798. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
  799. package/src/workspace/migrations/118-seed-now-md.ts +64 -0
  800. package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
  801. package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
  802. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  803. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  804. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  805. package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
  806. package/src/workspace/migrations/registry.ts +12 -0
  807. package/drizzle/0000_dizzy_maggott.sql +0 -301
  808. package/drizzle/meta/0000_snapshot.json +0 -1933
  809. package/drizzle/meta/_journal.json +0 -13
  810. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
  811. package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
  812. package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
  813. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
  814. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
  815. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
  816. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
  817. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
  818. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
  819. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
  820. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
  821. package/src/__tests__/invite-redemption-service.test.ts +0 -1180
  822. package/src/__tests__/invite-service-ipc.test.ts +0 -350
  823. package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
  824. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
  825. package/src/__tests__/relay-server.test.ts +0 -6153
  826. package/src/__tests__/telephony-stt-routing.test.ts +0 -329
  827. package/src/__tests__/voice-invite-redemption.test.ts +0 -702
  828. package/src/calls/relay-server.ts +0 -2463
  829. package/src/calls/telephony-stt-routing.ts +0 -145
  830. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
  831. package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
  832. package/src/daemon/daemon-skill-host.ts +0 -262
  833. package/src/daemon/message-types/meet.ts +0 -143
  834. package/src/daemon/shutdown-registry.ts +0 -40
  835. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
  836. package/src/ipc/__tests__/skill-server.test.ts +0 -182
  837. package/src/ipc/skill-ipc-types.ts +0 -54
  838. package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
  839. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
  840. package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
  841. package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
  842. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
  843. package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
  844. package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
  845. package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
  846. package/src/ipc/skill-routes/config.ts +0 -47
  847. package/src/ipc/skill-routes/events.ts +0 -147
  848. package/src/ipc/skill-routes/identity.ts +0 -21
  849. package/src/ipc/skill-routes/index.ts +0 -37
  850. package/src/ipc/skill-routes/log.ts +0 -40
  851. package/src/ipc/skill-routes/memory.ts +0 -92
  852. package/src/ipc/skill-routes/platform.ts +0 -39
  853. package/src/ipc/skill-routes/providers.ts +0 -162
  854. package/src/ipc/skill-routes/registries.ts +0 -338
  855. package/src/ipc/skill-server.ts +0 -831
  856. package/src/ipc/skill-socket-path.ts +0 -20
  857. package/src/jobs/register-job-handlers.ts +0 -74
  858. package/src/persistence/invite-store.ts +0 -481
  859. package/src/plugins/job-handler-registry.ts +0 -94
  860. package/src/providers/cache-control.ts +0 -26
  861. package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
  862. package/src/runtime/invite-redemption-service.ts +0 -828
  863. package/src/runtime/invite-redemption-templates.ts +0 -46
  864. package/src/runtime/runtime-mode.ts +0 -40
  865. package/src/runtime/slack-dm-text-delivery.ts +0 -177
  866. package/src/util/voice-code.ts +0 -31
@@ -191,7 +191,6 @@ mock.module("../calls/voice-session-bridge.js", () => {
191
191
  mockStartVoiceTurn = mock(createMockVoiceTurn(["Hello", " there"]));
192
192
  return {
193
193
  startVoiceTurn: (...args: unknown[]) => mockStartVoiceTurn(...args),
194
- setVoiceBridgeDeps: () => {},
195
194
  };
196
195
  });
197
196
 
@@ -291,7 +290,6 @@ interface MockTransport extends CallTransport {
291
290
  sentPlayUrls: string[];
292
291
  endCalled: boolean;
293
292
  endReason: string | undefined;
294
- mockConnectionState: string;
295
293
  }
296
294
 
297
295
  function createMockTransport(): MockTransport {
@@ -300,7 +298,6 @@ function createMockTransport(): MockTransport {
300
298
  sentPlayUrls: [] as string[],
301
299
  _endCalled: false,
302
300
  _endReason: undefined as string | undefined,
303
- _connectionState: "connected",
304
301
  };
305
302
 
306
303
  return {
@@ -316,12 +313,6 @@ function createMockTransport(): MockTransport {
316
313
  get endReason() {
317
314
  return state._endReason;
318
315
  },
319
- get mockConnectionState() {
320
- return state._connectionState;
321
- },
322
- set mockConnectionState(v: string) {
323
- state._connectionState = v;
324
- },
325
316
  sendTextToken(token: string, last: boolean) {
326
317
  state.sentTokens.push({ token, last });
327
318
  },
@@ -332,9 +323,6 @@ function createMockTransport(): MockTransport {
332
323
  state._endCalled = true;
333
324
  state._endReason = reason;
334
325
  },
335
- getConnectionState() {
336
- return state._connectionState;
337
- },
338
326
  } as MockTransport;
339
327
  }
340
328
 
@@ -1025,6 +1013,52 @@ describe("call-controller", () => {
1025
1013
  controller.destroy();
1026
1014
  });
1027
1015
 
1016
+ test("lock-contention rejection: speaks a brief re-prompt, no technical-issue speech, returns to idle", async () => {
1017
+ mockStartVoiceTurn.mockImplementation(async () => {
1018
+ throw new Error("Conversation is already processing a message");
1019
+ });
1020
+
1021
+ const { relay, controller } = setupController();
1022
+
1023
+ await controller.handleCallerUtterance("Hello");
1024
+
1025
+ // A wedged prior turn must never surface a technical-error message.
1026
+ const errorTokens = relay.sentTokens.filter((t) =>
1027
+ t.token.includes("technical issue"),
1028
+ );
1029
+ expect(errorTokens.length).toBe(0);
1030
+
1031
+ // A brief natural re-prompt (last=true) is spoken so the caller knows to
1032
+ // repeat themselves and the relay transitions back to listening.
1033
+ const reprompts = relay.sentTokens.filter(
1034
+ (t) => t.token === "Sorry, could you say that again?" && t.last === true,
1035
+ );
1036
+ expect(reprompts.length).toBeGreaterThan(0);
1037
+
1038
+ // Controller goes idle and re-arms the silence watchdog.
1039
+ expect(controller.getState()).toBe("idle");
1040
+
1041
+ controller.destroy();
1042
+ });
1043
+
1044
+ test("genuine non-lock-contention error still speaks the technical-issue fallback", async () => {
1045
+ mockStartVoiceTurn.mockImplementation(async () => {
1046
+ throw new Error("boom");
1047
+ });
1048
+
1049
+ const { relay, controller } = setupController();
1050
+
1051
+ await controller.handleCallerUtterance("Hello");
1052
+
1053
+ const errorTokens = relay.sentTokens.filter((t) =>
1054
+ t.token.includes("technical issue"),
1055
+ );
1056
+ expect(errorTokens.length).toBeGreaterThan(0);
1057
+ expect(controller.getState()).toBe("idle");
1058
+
1059
+ controller.destroy();
1060
+ });
1061
+
1028
1062
  test("handleUserAnswer: returns false when no pending consultation exists", async () => {
1029
1063
  const { controller } = setupController();
1030
1064
 
@@ -1054,17 +1088,13 @@ describe("call-controller", () => {
1054
1088
  onComplete: () => void;
1055
1089
  }) => {
1056
1090
  return new Promise((resolve) => {
1057
- // Simulate a long-running turn that can be aborted
1058
- const timeout = setTimeout(() => {
1059
- opts.onTextDelta("This should be interrupted");
1060
- opts.onComplete();
1061
- resolve({ turnId: "run-1", abort: () => {} });
1062
- }, 1000);
1091
+ // Emit a token right away so the assistant is actively speaking
1092
+ // (state flips to `speaking`) before the interrupt arrives.
1093
+ opts.onTextDelta("This should be interrupted");
1063
1094
 
1064
1095
  opts.signal?.addEventListener(
1065
1096
  "abort",
1066
1097
  () => {
1067
- clearTimeout(timeout);
1068
1098
  // In the real system, generation_cancelled triggers
1069
1099
  // onComplete via the event sink. The AbortSignal listener
1070
1100
  // in call-controller also resolves turnComplete defensively.
@@ -2123,7 +2153,10 @@ describe("call-controller", () => {
2123
2153
  "Are you still there?",
2124
2154
  );
2125
2155
  await new Promise((r) => setTimeout(r, 10));
2126
- expect(controller.getState()).toBe("speaking");
2156
+ // The turn is paused before emitting any token, so the controller is still
2157
+ // in the pre-speech `processing` phase (it only flips to `speaking` once
2158
+ // real outbound audio/tokens begin).
2159
+ expect(controller.getState()).toBe("processing");
2127
2160
 
2128
2161
  // Answer arrives while the controller is processing/speaking
2129
2162
  const accepted = await controller.handleUserAnswer("3pm works");
@@ -2400,30 +2433,11 @@ describe("call-controller", () => {
2400
2433
 
2401
2434
  // ── Silence suppression during guardian wait ──────────────────────
2402
2435
 
2403
- test('silence timeout suppressed during guardian wait: does not say "Are you still there?"', async () => {
2404
- mockSilenceTimeoutMs = 20; // Short timeout for testing
2405
- const { relay, controller } = setupController();
2406
-
2407
- // Simulate guardian wait state on the relay
2408
- relay.mockConnectionState = "awaiting_guardian_decision";
2409
-
2410
- // Wait for the silence timeout to fire
2411
- await new Promise((r) => setTimeout(r, 30));
2412
-
2413
- // "Are you still there?" should NOT have been sent
2414
- const silenceTokens = relay.sentTokens.filter((t) =>
2415
- t.token.includes("Are you still there?"),
2416
- );
2417
- expect(silenceTokens.length).toBe(0);
2418
-
2419
- controller.destroy();
2420
- });
2421
-
2422
2436
  test("silence timeout fires normally when not in guardian wait", async () => {
2423
2437
  mockSilenceTimeoutMs = 20; // Short timeout for testing
2424
2438
  const { relay, controller } = setupController();
2425
2439
 
2426
- // Default connection state is 'connected' (not guardian wait)
2440
+ // No pending guardian consultation the nudge should fire.
2427
2441
 
2428
2442
  // Wait for the silence timeout to fire
2429
2443
  await new Promise((r) => setTimeout(r, 30));
@@ -2456,8 +2470,6 @@ describe("call-controller", () => {
2456
2470
 
2457
2471
  // Verify a guardian input request is now pending
2458
2472
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
2459
- // Relay state is still 'connected' (not 'awaiting_guardian_decision')
2460
- expect(relay.mockConnectionState).toBe("connected");
2461
2473
 
2462
2474
  // Clear any tokens from the turn itself
2463
2475
  relay.sentTokens.length = 0;
@@ -2682,6 +2694,65 @@ describe("call-controller", () => {
2682
2694
  controller.destroy();
2683
2695
  });
2684
2696
 
2697
+ test("synthesized provider: a superseded run does not emit native fallback text", async () => {
2698
+ const cfg = loadConfig();
2699
+ cfg.services.tts.provider = "fish-audio";
2700
+ cfg.services.tts.providers["fish-audio"].referenceId = "fish-ref-123";
2701
+
2702
+ _resetTtsProviderRegistry();
2703
+ // elevenlabs present so native fallback is available for fish-audio.
2704
+ const elevenlabs: TtsProvider = {
2705
+ id: "elevenlabs",
2706
+ capabilities: { supportsStreaming: false, supportedFormats: ["mp3"] },
2707
+ async synthesize() {
2708
+ return { audio: Buffer.from(""), contentType: "audio/mpeg" };
2709
+ },
2710
+ };
2711
+ registerTtsProvider(elevenlabs);
2712
+
2713
+ let releaseSynth: (() => void) | undefined;
2714
+ const synthGate = new Promise<void>((resolve) => {
2715
+ releaseSynth = resolve;
2716
+ });
2717
+ const fishAudioGatedFailing: TtsProvider = {
2718
+ id: "fish-audio",
2719
+ capabilities: {
2720
+ supportsStreaming: true,
2721
+ supportedFormats: ["mp3", "wav", "opus"],
2722
+ },
2723
+ async synthesize() {
2724
+ throw new Error("fish-audio synth failure");
2725
+ },
2726
+ async synthesizeStream() {
2727
+ // Ignore the abort signal, block, then fail with no audio — this would
2728
+ // normally trigger the native fallback text emission.
2729
+ await synthGate;
2730
+ throw new Error("fish-audio stream failure");
2731
+ },
2732
+ };
2733
+ registerTtsProvider(fishAudioGatedFailing);
2734
+
2735
+ mockStartVoiceTurn.mockImplementation(
2736
+ createMockVoiceTurn(["Stale synthesized response"]),
2737
+ );
2738
+ const { relay, controller } = setupController();
2739
+
2740
+ const turn = controller.handleCallerUtterance("Hi");
2741
+ await new Promise((r) => setTimeout(r, 20)); // reach synthesis, blocked
2742
+
2743
+ // Supersede the turn mid-synthesis, then let synthesis fail.
2744
+ controller.handleInterrupt();
2745
+ releaseSynth?.();
2746
+ await turn;
2747
+
2748
+ // The stale run must NOT leak its native fallback text into the relay.
2749
+ const emitted = relay.sentTokens.map((t) => t.token).join("");
2750
+ expect(emitted).not.toContain("Stale synthesized response");
2751
+ expect(relay.sentPlayUrls.length).toBe(0);
2752
+
2753
+ controller.destroy();
2754
+ });
2755
+
2685
2756
  test("synthesized provider: play URL uses public base URL", async () => {
2686
2757
  const cfg = loadConfig();
2687
2758
  cfg.ingress.publicBaseUrl = "https://twilio.example.com/";
@@ -2726,11 +2797,118 @@ describe("call-controller", () => {
2726
2797
  controller.destroy();
2727
2798
  });
2728
2799
 
2729
- test("Deepgram selected path resolves useSynthesizedPath to true", () => {
2800
+ test("synthesized provider: stays 'processing' during synthesis latency, so barge-in cannot abort an inaudible turn", async () => {
2801
+ const cfg = loadConfig();
2802
+ cfg.services.tts.provider = "fish-audio";
2803
+ cfg.services.tts.providers["fish-audio"].referenceId = "fish-ref-123";
2804
+
2805
+ _resetTtsProviderRegistry();
2806
+ let releaseChunk: (() => void) | undefined;
2807
+ const chunkGate = new Promise<void>((resolve) => {
2808
+ releaseChunk = resolve;
2809
+ });
2810
+ const fishAudioStreaming: TtsProvider = {
2811
+ id: "fish-audio",
2812
+ capabilities: {
2813
+ supportsStreaming: true,
2814
+ supportedFormats: ["mp3", "wav", "opus"],
2815
+ },
2816
+ async synthesize() {
2817
+ return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
2818
+ },
2819
+ async synthesizeStream(_request, onChunk) {
2820
+ // Simulate provider latency: emit no audio until the gate releases.
2821
+ await chunkGate;
2822
+ onChunk(Buffer.from("fish-audio-stream"));
2823
+ return {
2824
+ audio: Buffer.from("fish-audio-stream"),
2825
+ contentType: "audio/mpeg",
2826
+ };
2827
+ },
2828
+ };
2829
+ registerTtsProvider(fishAudioStreaming);
2830
+
2831
+ mockStartVoiceTurn.mockImplementation(
2832
+ createMockVoiceTurn(["Hello from synthesized path."]),
2833
+ );
2834
+ const { relay, controller } = setupController();
2835
+
2836
+ const turn = controller.handleCallerUtterance("Hi");
2837
+ // Let the turn generate its text and reach synthesis, blocked on the gate.
2838
+ await new Promise((r) => setTimeout(r, 20));
2839
+
2840
+ // No audio has reached the caller yet (play URL not sent), so the controller
2841
+ // must stay in `processing` — a barge-in here would abort a turn the caller
2842
+ // cannot hear. See JARVIS-1232.
2843
+ expect(relay.sentPlayUrls.length).toBe(0);
2844
+ expect(controller.getState()).toBe("processing");
2845
+ expect(controller.handleBargeIn()).toBe(false);
2846
+
2847
+ // Release audio → play URL sent → turn finishes and returns to idle.
2848
+ releaseChunk?.();
2849
+ await turn;
2850
+ expect(relay.sentPlayUrls.length).toBeGreaterThan(0);
2851
+ expect(controller.getState()).toBe("idle");
2852
+
2853
+ controller.destroy();
2854
+ });
2855
+
2856
+ test("synthesized provider: a superseded turn does not send a stale play URL", async () => {
2857
+ const cfg = loadConfig();
2858
+ cfg.services.tts.provider = "fish-audio";
2859
+ cfg.services.tts.providers["fish-audio"].referenceId = "fish-ref-123";
2860
+
2861
+ _resetTtsProviderRegistry();
2862
+ let releaseChunk: (() => void) | undefined;
2863
+ const chunkGate = new Promise<void>((resolve) => {
2864
+ releaseChunk = resolve;
2865
+ });
2866
+ const fishAudioStreaming: TtsProvider = {
2867
+ id: "fish-audio",
2868
+ capabilities: {
2869
+ supportsStreaming: true,
2870
+ supportedFormats: ["mp3", "wav", "opus"],
2871
+ },
2872
+ async synthesize() {
2873
+ return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
2874
+ },
2875
+ async synthesizeStream(_request, onChunk) {
2876
+ // Block until released so we can supersede the turn mid-synthesis.
2877
+ await chunkGate;
2878
+ onChunk(Buffer.from("stale-audio"));
2879
+ return { audio: Buffer.from("stale-audio"), contentType: "audio/mpeg" };
2880
+ },
2881
+ };
2882
+ registerTtsProvider(fishAudioStreaming);
2883
+
2884
+ mockStartVoiceTurn.mockImplementation(
2885
+ createMockVoiceTurn(["Hello from synthesized path."]),
2886
+ );
2887
+ const { relay, controller } = setupController();
2888
+
2889
+ const turn = controller.handleCallerUtterance("first");
2890
+ // Let the turn generate text and block inside provider synthesis.
2891
+ await new Promise((r) => setTimeout(r, 20));
2892
+ expect(relay.sentPlayUrls.length).toBe(0);
2893
+
2894
+ // Supersede the in-flight synthesized turn (bumps run version + aborts synthesis).
2895
+ controller.handleInterrupt();
2896
+
2897
+ // Releasing the stale synthesis must NOT send a play URL for the old run,
2898
+ // nor inject a stale end-of-turn marker into the relay stream.
2899
+ releaseChunk?.();
2900
+ await turn;
2901
+ expect(relay.sentPlayUrls.length).toBe(0);
2902
+ expect(relay.sentTokens.filter((t) => t.last).length).toBe(0);
2903
+
2904
+ controller.destroy();
2905
+ });
2906
+
2907
+ test("Deepgram selected path resolves useSynthesizedPath to true", async () => {
2730
2908
  const cfg = loadConfig();
2731
2909
  cfg.services.tts.provider = "deepgram";
2732
2910
 
2733
- const result = resolveCallTtsProvider();
2911
+ const result = await resolveCallTtsProvider();
2734
2912
  expect(result.provider).not.toBeNull();
2735
2913
  expect(result.provider!.id).toBe("deepgram");
2736
2914
  expect(result.useSynthesizedPath).toBe(true);
@@ -2841,16 +3019,13 @@ describe("call-controller", () => {
2841
3019
  onComplete: () => void;
2842
3020
  }) => {
2843
3021
  return new Promise((resolve) => {
2844
- const timeout = setTimeout(() => {
2845
- opts.onTextDelta("This should be interrupted");
2846
- opts.onComplete();
2847
- resolve({ turnId: "run-1", abort: () => {} });
2848
- }, 1000);
3022
+ // Emit a token right away so the assistant is actively speaking
3023
+ // before the interrupt arrives.
3024
+ opts.onTextDelta("This should be interrupted");
2849
3025
 
2850
3026
  opts.signal?.addEventListener(
2851
3027
  "abort",
2852
3028
  () => {
2853
- clearTimeout(timeout);
2854
3029
  opts.onComplete();
2855
3030
  resolve({ turnId: "run-1", abort: () => {} });
2856
3031
  },
@@ -2879,30 +3054,30 @@ describe("call-controller", () => {
2879
3054
  // ── Shared TTS provider resolution ──────────────────────────────────
2880
3055
 
2881
3056
  describe("resolveCallTtsProvider (shared helper)", () => {
2882
- test("returns native path with elevenlabs (non-streaming provider)", () => {
3057
+ test("returns native path with elevenlabs (non-streaming provider)", async () => {
2883
3058
  // Default config has provider: "elevenlabs" which is registered as
2884
3059
  // non-streaming in registerTestTtsProviders()
2885
- const result = resolveCallTtsProvider();
3060
+ const result = await resolveCallTtsProvider();
2886
3061
  expect(result.provider).not.toBeNull();
2887
3062
  expect(result.provider!.id).toBe("elevenlabs");
2888
3063
  expect(result.useSynthesizedPath).toBe(false);
2889
3064
  expect(result.audioFormat).toBe("mp3");
2890
3065
  });
2891
3066
 
2892
- test("returns fallback when provider registry is empty", () => {
3067
+ test("returns fallback when provider registry is empty", async () => {
2893
3068
  _resetTtsProviderRegistry();
2894
- const result = resolveCallTtsProvider();
3069
+ const result = await resolveCallTtsProvider();
2895
3070
  expect(result.provider).toBeNull();
2896
3071
  expect(result.useSynthesizedPath).toBe(false);
2897
3072
  expect(result.audioFormat).toBe("mp3");
2898
3073
  });
2899
3074
 
2900
- test("degrades fish-audio synthesized path when referenceId is missing", () => {
3075
+ test("degrades fish-audio synthesized path when referenceId is missing", async () => {
2901
3076
  const cfg = loadConfig();
2902
3077
  cfg.services.tts.provider = "fish-audio";
2903
3078
  cfg.services.tts.providers["fish-audio"].referenceId = "";
2904
3079
 
2905
- const result = resolveCallTtsProvider();
3080
+ const result = await resolveCallTtsProvider();
2906
3081
  expect(result.provider).toBeNull();
2907
3082
  expect(result.useSynthesizedPath).toBe(false);
2908
3083
  expect(result.audioFormat).toBe("mp3");
@@ -2926,18 +3101,18 @@ describe("call-controller", () => {
2926
3101
  controller.destroy();
2927
3102
  });
2928
3103
 
2929
- test("returns synthesized path with deepgram provider", () => {
3104
+ test("returns synthesized path with deepgram provider", async () => {
2930
3105
  const cfg = loadConfig();
2931
3106
  cfg.services.tts.provider = "deepgram";
2932
3107
 
2933
- const result = resolveCallTtsProvider();
3108
+ const result = await resolveCallTtsProvider();
2934
3109
  expect(result.provider).not.toBeNull();
2935
3110
  expect(result.provider!.id).toBe("deepgram");
2936
3111
  expect(result.useSynthesizedPath).toBe(true);
2937
3112
  expect(result.audioFormat).toBe("mp3");
2938
3113
  });
2939
3114
 
2940
- test("Deepgram does not apply fish-audio referenceId gate", () => {
3115
+ test("Deepgram does not apply fish-audio referenceId gate", async () => {
2941
3116
  // Deepgram has no referenceId requirement. Verify the fish-audio
2942
3117
  // config gate does not apply to deepgram resolution.
2943
3118
  const cfg = loadConfig();
@@ -2945,7 +3120,7 @@ describe("call-controller", () => {
2945
3120
  // fish-audio referenceId left empty — should not affect deepgram.
2946
3121
  cfg.services.tts.providers["fish-audio"].referenceId = "";
2947
3122
 
2948
- const result = resolveCallTtsProvider();
3123
+ const result = await resolveCallTtsProvider();
2949
3124
  expect(result.provider).not.toBeNull();
2950
3125
  expect(result.provider!.id).toBe("deepgram");
2951
3126
  expect(result.useSynthesizedPath).toBe(true);
@@ -2960,9 +3135,11 @@ describe("call-controller", () => {
2960
3135
 
2961
3136
  // Controller starts idle after construction
2962
3137
  expect(controller.getState()).toBe("idle");
2963
- const result = controller.handleBargeIn();
3138
+ const onAccepted = mock(() => {});
3139
+ const result = controller.handleBargeIn(onAccepted);
2964
3140
 
2965
3141
  expect(result).toBe(false);
3142
+ expect(onAccepted).not.toHaveBeenCalled();
2966
3143
  // No end-of-turn token should have been sent (no interruption)
2967
3144
  const endTokens = relay.sentTokens.filter(
2968
3145
  (t) => t.last === true && t.token === "",
@@ -2972,49 +3149,246 @@ describe("call-controller", () => {
2972
3149
  controller.destroy();
2973
3150
  });
2974
3151
 
2975
- test("handleBargeIn returns false when controller is processing", async () => {
2976
- // Use a slow turn that never completes so we can observe
2977
- // the processing state.
3152
+ test("handleBargeIn returns false and does not abort while still processing (no output yet)", async () => {
3153
+ // Simulate a turn stuck waiting for the processing lock: no tokens
3154
+ // emitted, no completion. The controller must stay in `processing` and
3155
+ // NOT flip to `speaking`, so barge-in can't abort a silent turn.
2978
3156
  mockStartVoiceTurn.mockImplementation(
2979
3157
  async (opts: {
2980
3158
  onTextDelta: (t: string) => void;
2981
3159
  onComplete: () => void;
2982
3160
  signal?: AbortSignal;
2983
3161
  }) => {
2984
- // Don't call onCompletekeep in processing/speaking
3162
+ // Never emit tokens or complete remain in the pre-speech phase.
2985
3163
  return { turnId: "run-slow", abort: () => opts.onComplete() };
2986
3164
  },
2987
3165
  );
2988
3166
 
2989
3167
  const { relay, controller } = setupController();
2990
- // Kick off a turn (moves to speaking state)
2991
3168
  const turnPromise = controller.handleCallerUtterance("Hello");
2992
3169
 
2993
3170
  // Wait for microtasks to settle
2994
3171
  for (let i = 0; i < 5; i++) await Promise.resolve();
2995
3172
 
2996
- // The controller transitions to "speaking" once runTurnInner starts.
2997
- // Before any onTextDelta, a barge-in should be accepted if speaking.
2998
- // But if no text has been emitted yet, the state is "speaking" per
2999
- // the implementation (state is set to speaking at the start of
3000
- // runTurnInner). So handleBargeIn should accept. Let's verify the
3001
- // state and behavior.
3002
- const bargeResult = controller.handleBargeIn();
3003
-
3004
- // Regardless of the specific state, if accepted the transport
3005
- // should see an interrupt token.
3006
- if (bargeResult) {
3007
- const endTokens = relay.sentTokens.filter(
3008
- (t) => t.last === true && t.token === "",
3009
- );
3010
- expect(endTokens.length).toBeGreaterThan(0);
3011
- }
3173
+ // No outbound audio/tokens yet still processing.
3174
+ expect(controller.getState()).toBe("processing");
3175
+
3176
+ const onAccepted = mock(() => {});
3177
+ const bargeResult = controller.handleBargeIn(onAccepted);
3178
+ expect(bargeResult).toBe(false);
3179
+ expect(onAccepted).not.toHaveBeenCalled();
3180
+ // Still processing (not aborted), and no interrupt/end-of-turn token sent.
3181
+ expect(controller.getState()).toBe("processing");
3182
+ const endTokens = relay.sentTokens.filter(
3183
+ (t) => t.last === true && t.token === "",
3184
+ );
3185
+ expect(endTokens.length).toBe(0);
3012
3186
 
3013
3187
  // Cleanup: abort the pending turn
3014
3188
  controller.destroy();
3015
3189
  await turnPromise.catch(() => {});
3016
3190
  });
3017
3191
 
3192
+ test("stays in processing until first token, then flips to speaking and barge-in is accepted", async () => {
3193
+ // Gate the first token so we can observe the pre-speech processing phase
3194
+ // (e.g. the lock-wait / generation window) before any audio is emitted.
3195
+ let emitFirstToken!: () => void;
3196
+ const firstTokenGate = new Promise<void>((r) => {
3197
+ emitFirstToken = r;
3198
+ });
3199
+ let releaseTurn!: () => void;
3200
+ const completeGate = new Promise<void>((r) => {
3201
+ releaseTurn = r;
3202
+ });
3203
+
3204
+ mockStartVoiceTurn.mockImplementation(
3205
+ async (opts: {
3206
+ onTextDelta: (t: string) => void;
3207
+ onComplete: () => void;
3208
+ signal?: AbortSignal;
3209
+ }) => {
3210
+ await firstTokenGate;
3211
+ if (opts.signal?.aborted) {
3212
+ const err = new Error("aborted");
3213
+ err.name = "AbortError";
3214
+ throw err;
3215
+ }
3216
+ opts.onTextDelta("Hello");
3217
+ // Hold the turn open so state remains `speaking` until we barge in.
3218
+ opts.signal?.addEventListener("abort", () => releaseTurn());
3219
+ await completeGate;
3220
+ opts.onComplete();
3221
+ return { turnId: "run-gate", abort: () => releaseTurn() };
3222
+ },
3223
+ );
3224
+
3225
+ const { controller } = setupController();
3226
+ const turnPromise = controller.handleCallerUtterance("Hi");
3227
+ for (let i = 0; i < 5; i++) await Promise.resolve();
3228
+
3229
+ // Before any token: processing, barge-in ignored (turn not aborted).
3230
+ expect(controller.getState()).toBe("processing");
3231
+ expect(controller.handleBargeIn()).toBe(false);
3232
+ expect(controller.getState()).toBe("processing");
3233
+
3234
+ // Release the first token → controller flips to speaking.
3235
+ emitFirstToken();
3236
+ for (let i = 0; i < 5; i++) await Promise.resolve();
3237
+ expect(controller.getState()).toBe("speaking");
3238
+
3239
+ // Now barge-in is accepted and interrupts the turn.
3240
+ expect(controller.handleBargeIn()).toBe(true);
3241
+ expect(controller.getState()).toBe("idle");
3242
+
3243
+ controller.destroy();
3244
+ await turnPromise.catch(() => {});
3245
+ });
3246
+
3247
+ test("buffering transport (media-stream): streamed tokens do not flip to speaking; barge-in is rejected and the turn still delivers", async () => {
3248
+ // Simulates the media-stream transport: sendTextToken(.., false)
3249
+ // only buffers; audio starts later. The controller must stay in
3250
+ // `processing` until the transport's audio-start signal fires, so
3251
+ // VAD speech-start during generation cannot abort a silent turn.
3252
+ let releaseTurn!: () => void;
3253
+ const completeGate = new Promise<void>((r) => {
3254
+ releaseTurn = r;
3255
+ });
3256
+
3257
+ mockStartVoiceTurn.mockImplementation(
3258
+ async (opts: {
3259
+ onTextDelta: (t: string) => void;
3260
+ onComplete: () => void;
3261
+ signal?: AbortSignal;
3262
+ }) => {
3263
+ opts.onTextDelta("Hello");
3264
+ opts.onTextDelta(" caller");
3265
+ opts.signal?.addEventListener("abort", () => releaseTurn());
3266
+ await completeGate;
3267
+ opts.onComplete();
3268
+ return { turnId: "run-buffered", abort: () => releaseTurn() };
3269
+ },
3270
+ );
3271
+
3272
+ const { relay, controller } = setupController();
3273
+ let audioStartCallback: (() => void) | null = null;
3274
+ relay.setAudioStartCallback = (cb) => {
3275
+ audioStartCallback = cb;
3276
+ };
3277
+
3278
+ const turnPromise = controller.handleCallerUtterance("Hi");
3279
+ for (let i = 0; i < 10; i++) await Promise.resolve();
3280
+
3281
+ // Tokens were emitted (buffered by the transport) but no audio has
3282
+ // started — the controller must still be processing.
3283
+ expect(
3284
+ relay.sentTokens.filter((t) => !t.last && t.token.length > 0).length,
3285
+ ).toBeGreaterThan(0);
3286
+ expect(controller.getState()).toBe("processing");
3287
+ expect(audioStartCallback).not.toBeNull();
3288
+
3289
+ // VAD speech-start mid-generation → barge-in rejected, turn intact.
3290
+ expect(controller.handleBargeIn()).toBe(false);
3291
+ expect(controller.getState()).toBe("processing");
3292
+
3293
+ // The turn completes and delivers its end-of-turn signal.
3294
+ releaseTurn();
3295
+ await turnPromise;
3296
+ const endTokens = relay.sentTokens.filter(
3297
+ (t) => t.last === true && t.token === "",
3298
+ );
3299
+ expect(endTokens.length).toBe(1);
3300
+
3301
+ controller.destroy();
3302
+ });
3303
+
3304
+ test("buffering transport: audio-start signal flips to speaking and barge-in is accepted", async () => {
3305
+ let releaseTurn!: () => void;
3306
+ const completeGate = new Promise<void>((r) => {
3307
+ releaseTurn = r;
3308
+ });
3309
+
3310
+ mockStartVoiceTurn.mockImplementation(
3311
+ async (opts: {
3312
+ onTextDelta: (t: string) => void;
3313
+ onComplete: () => void;
3314
+ signal?: AbortSignal;
3315
+ }) => {
3316
+ opts.onTextDelta("Hello");
3317
+ opts.signal?.addEventListener("abort", () => releaseTurn());
3318
+ await completeGate;
3319
+ opts.onComplete();
3320
+ return { turnId: "run-buffered-2", abort: () => releaseTurn() };
3321
+ },
3322
+ );
3323
+
3324
+ const { relay, controller } = setupController();
3325
+ let audioStartCallback: (() => void) | null = null;
3326
+ let discardedPendingText = 0;
3327
+ relay.setAudioStartCallback = (cb) => {
3328
+ audioStartCallback = cb;
3329
+ };
3330
+ relay.discardPendingText = () => {
3331
+ discardedPendingText++;
3332
+ };
3333
+
3334
+ const turnPromise = controller.handleCallerUtterance("Hi");
3335
+ for (let i = 0; i < 10; i++) await Promise.resolve();
3336
+ expect(controller.getState()).toBe("processing");
3337
+
3338
+ // Transport reports real outbound audio started → speaking.
3339
+ audioStartCallback!();
3340
+ expect(controller.getState()).toBe("speaking");
3341
+
3342
+ // Barge-in now aborts the turn, discards the transport's buffered
3343
+ // text, and returns the controller to idle.
3344
+ expect(controller.handleBargeIn()).toBe(true);
3345
+ expect(controller.getState()).toBe("idle");
3346
+ expect(discardedPendingText).toBeGreaterThan(0);
3347
+
3348
+ controller.destroy();
3349
+ await turnPromise.catch(() => {});
3350
+ });
3351
+
3352
+ test("buffering transport: stale audio-start signal from a superseded run does not flip state", async () => {
3353
+ let releaseTurn!: () => void;
3354
+ const completeGate = new Promise<void>((r) => {
3355
+ releaseTurn = r;
3356
+ });
3357
+
3358
+ mockStartVoiceTurn.mockImplementation(
3359
+ async (opts: {
3360
+ onTextDelta: (t: string) => void;
3361
+ onComplete: () => void;
3362
+ signal?: AbortSignal;
3363
+ }) => {
3364
+ opts.onTextDelta("Hello");
3365
+ opts.signal?.addEventListener("abort", () => releaseTurn());
3366
+ await completeGate;
3367
+ opts.onComplete();
3368
+ return { turnId: "run-buffered-3", abort: () => releaseTurn() };
3369
+ },
3370
+ );
3371
+
3372
+ const { relay, controller } = setupController();
3373
+ let audioStartCallback: (() => void) | null = null;
3374
+ relay.setAudioStartCallback = (cb) => {
3375
+ audioStartCallback = cb;
3376
+ };
3377
+
3378
+ const turnPromise = controller.handleCallerUtterance("Hi");
3379
+ for (let i = 0; i < 10; i++) await Promise.resolve();
3380
+ const staleCallback = audioStartCallback!;
3381
+
3382
+ // Supersede the run (hard interrupt), then fire the stale signal.
3383
+ controller.handleInterrupt();
3384
+ expect(controller.getState()).toBe("idle");
3385
+ staleCallback();
3386
+ expect(controller.getState()).toBe("idle");
3387
+
3388
+ controller.destroy();
3389
+ await turnPromise.catch(() => {});
3390
+ });
3391
+
3018
3392
  test("handleBargeIn returns true and interrupts when controller is speaking", async () => {
3019
3393
  // Create a turn that holds the speaking state long enough to test barge-in
3020
3394
  let resolveComplete: () => void;
@@ -3040,7 +3414,7 @@ describe("call-controller", () => {
3040
3414
  },
3041
3415
  );
3042
3416
 
3043
- const { controller } = setupController();
3417
+ const { relay, controller } = setupController();
3044
3418
  const turnPromise = controller.handleCallerUtterance("Hi");
3045
3419
 
3046
3420
  // Let microtasks settle so onTextDelta runs
@@ -3048,8 +3422,24 @@ describe("call-controller", () => {
3048
3422
 
3049
3423
  expect(controller.getState()).toBe("speaking");
3050
3424
 
3051
- const result = controller.handleBargeIn();
3425
+ // onAccepted must fire after the speaking gate but BEFORE the
3426
+ // end-of-turn token from handleInterrupt, so a transport's queue
3427
+ // flush cannot wipe that mark.
3428
+ const endTokensAtAccept: number[] = [];
3429
+ const onAccepted = mock(() => {
3430
+ endTokensAtAccept.push(
3431
+ relay.sentTokens.filter((t) => t.last === true && t.token === "")
3432
+ .length,
3433
+ );
3434
+ });
3435
+ const result = controller.handleBargeIn(onAccepted);
3052
3436
  expect(result).toBe(true);
3437
+ expect(onAccepted).toHaveBeenCalledTimes(1);
3438
+ expect(endTokensAtAccept).toEqual([0]);
3439
+ const endTokens = relay.sentTokens.filter(
3440
+ (t) => t.last === true && t.token === "",
3441
+ );
3442
+ expect(endTokens.length).toBe(1);
3053
3443
 
3054
3444
  // After barge-in, controller should be idle
3055
3445
  expect(controller.getState()).toBe("idle");