@vellumai/assistant 0.11.7 → 0.11.8-staging.1

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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Ambient camera frames kept mid-call (`sight_frame`).
3
+ *
4
+ * The behaviour under test is the keep-stream rule: a kept frame persists
5
+ * immediately as its own tagged user message and dispatches no turn, so the
6
+ * transcript is the record of what the assistant saw and the model correlates
7
+ * a frame with speech by adjacency.
8
+ *
9
+ * The two neighbouring contracts are pinned by their own suites: a deliberate
10
+ * snap persists standalone and untagged (`live-voice-attach-image.test.ts`), a
11
+ * parked frame persists nothing and rides the next turn
12
+ * (`live-voice-attach-frame.test.ts`).
13
+ */
14
+
15
+ import { describe, expect, mock, test } from "bun:test";
16
+
17
+ import {
18
+ createMockProvider,
19
+ textResponse,
20
+ } from "../../__tests__/helpers/mock-provider.js";
21
+ import { setConfig } from "../../__tests__/helpers/set-config.js";
22
+ import { waitFor } from "../../__tests__/helpers/wait-for.js";
23
+
24
+ setConfig("memory", { enabled: false });
25
+
26
+ import type { VoiceTurnOptions } from "../../calls/voice-session-bridge.js";
27
+ import { Conversation } from "../../daemon/conversation.js";
28
+ import {
29
+ deleteConversation,
30
+ setConversation,
31
+ } from "../../daemon/conversation-registry.js";
32
+ import {
33
+ getAttachmentsForMessage,
34
+ uploadAttachment,
35
+ } from "../../persistence/attachments-store.js";
36
+ import {
37
+ createConversation,
38
+ getMessages,
39
+ selectSightFrameCaptureTimes,
40
+ } from "../../persistence/conversation-crud.js";
41
+ import { sightFrameAttachmentIdsFromMetadata } from "../../persistence/conversation-types.js";
42
+ import { initializeDb } from "../../persistence/db-init.js";
43
+ import type {
44
+ StreamingTranscriber,
45
+ SttStreamServerEvent,
46
+ } from "../../stt/types.js";
47
+ import {
48
+ LiveVoiceSession,
49
+ type LiveVoiceTurnStarter,
50
+ } from "../live-voice-session.js";
51
+ import type { LiveVoiceSessionFactoryContext } from "../live-voice-session-manager.js";
52
+ import {
53
+ createLiveVoiceServerFrameSequencer,
54
+ type LiveVoiceServerFrame,
55
+ validateLiveVoiceClientFrame,
56
+ } from "../protocol.js";
57
+
58
+ await initializeDb();
59
+
60
+ const IMAGE_BASE64 =
61
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk";
62
+
63
+ class MockStreamingTranscriber implements StreamingTranscriber {
64
+ readonly providerId = "deepgram" as const;
65
+ readonly boundaryId = "daemon-streaming" as const;
66
+ private onEvent: ((event: SttStreamServerEvent) => void) | null = null;
67
+
68
+ async start(onEvent: (event: SttStreamServerEvent) => void): Promise<void> {
69
+ this.onEvent = onEvent;
70
+ }
71
+
72
+ sendAudio(): void {}
73
+
74
+ stop(): void {
75
+ this.onEvent?.({ type: "closed" });
76
+ }
77
+ }
78
+
79
+ /** A real attachment row, so the arrival-time existence check has one to find. */
80
+ async function uploadFrame(): Promise<string> {
81
+ const attachment = await uploadAttachment(
82
+ "frame.png",
83
+ "image/png",
84
+ IMAGE_BASE64,
85
+ );
86
+ return attachment.id;
87
+ }
88
+
89
+ /**
90
+ * A session bound to a live conversation, so a kept frame has somewhere real
91
+ * to land. Returns a `dispose` that unregisters it.
92
+ */
93
+ function createSessionHarness(title: string) {
94
+ const conversation = createConversation(title);
95
+ const { provider } = createMockProvider([textResponse("")]);
96
+ const activeConversation = new Conversation(
97
+ conversation.id,
98
+ provider,
99
+ "system prompt",
100
+ () => {},
101
+ "/tmp",
102
+ { maxTokens: 4096 },
103
+ );
104
+ activeConversation.setTrustContext({
105
+ trustClass: "guardian",
106
+ sourceChannel: "vellum",
107
+ });
108
+ setConversation(conversation.id, activeConversation);
109
+
110
+ const sequencer = createLiveVoiceServerFrameSequencer();
111
+ const frames: LiveVoiceServerFrame[] = [];
112
+ const context: LiveVoiceSessionFactoryContext = {
113
+ sessionId: "session-sight-frame",
114
+ startFrame: {
115
+ type: "start",
116
+ conversationId: conversation.id,
117
+ audio: { mimeType: "audio/pcm", sampleRate: 24_000, channels: 1 },
118
+ },
119
+ sendFrame: mock(async (payload) => {
120
+ const frame = sequencer.next(payload);
121
+ frames.push(frame);
122
+ return frame;
123
+ }),
124
+ };
125
+
126
+ const startVoiceTurn: LiveVoiceTurnStarter = mock(
127
+ async (_options: VoiceTurnOptions) => ({
128
+ turnId: "bridge-turn-1",
129
+ abort: mock(),
130
+ }),
131
+ );
132
+
133
+ const session = new LiveVoiceSession(context, {
134
+ resolveTranscriber: mock(async () => new MockStreamingTranscriber()),
135
+ startVoiceTurn,
136
+ createTurnId: () => "live-turn-1",
137
+ emitMetrics: false,
138
+ });
139
+
140
+ return {
141
+ activeConversation,
142
+ conversationId: conversation.id,
143
+ frames,
144
+ session,
145
+ startVoiceTurn,
146
+ dispose: () => {
147
+ deleteConversation(conversation.id);
148
+ activeConversation.dispose();
149
+ },
150
+ };
151
+ }
152
+
153
+ describe("live-voice sight_frame frame", () => {
154
+ test("accepts a well-formed frame", () => {
155
+ const result = validateLiveVoiceClientFrame({
156
+ type: "sight_frame",
157
+ attachmentId: "att-1",
158
+ });
159
+ expect(result).toEqual({
160
+ ok: true,
161
+ frame: { type: "sight_frame", attachmentId: "att-1" },
162
+ });
163
+ });
164
+
165
+ test("rejects a missing attachmentId, naming the frame", () => {
166
+ const result = validateLiveVoiceClientFrame({ type: "sight_frame" });
167
+ expect(result.ok).toBe(false);
168
+ if (!result.ok) {
169
+ expect(result.error.code).toBe("missing_required_field");
170
+ expect(result.error.field).toBe("attachmentId");
171
+ expect(result.error.frameType).toBe("sight_frame");
172
+ }
173
+ });
174
+
175
+ test("rejects an empty attachmentId, distinctly from a missing one", () => {
176
+ const result = validateLiveVoiceClientFrame({
177
+ type: "sight_frame",
178
+ attachmentId: "",
179
+ });
180
+ expect(result.ok).toBe(false);
181
+ if (!result.ok) {
182
+ expect(result.error.code).toBe("invalid_field");
183
+ expect(result.error.field).toBe("attachmentId");
184
+ expect(result.error.frameType).toBe("sight_frame");
185
+ }
186
+ });
187
+
188
+ test("rejects a null attachmentId", () => {
189
+ // `attach_frame` reads null as an unpark. A keep has nothing staged to
190
+ // give up, so null is simply malformed here.
191
+ const result = validateLiveVoiceClientFrame({
192
+ type: "sight_frame",
193
+ attachmentId: null,
194
+ });
195
+ expect(result.ok).toBe(false);
196
+ if (!result.ok) {
197
+ expect(result.error.code).toBe("invalid_field");
198
+ expect(result.error.frameType).toBe("sight_frame");
199
+ }
200
+ });
201
+
202
+ test("rejects a non-string attachmentId", () => {
203
+ const result = validateLiveVoiceClientFrame({
204
+ type: "sight_frame",
205
+ attachmentId: 7,
206
+ });
207
+ expect(result.ok).toBe(false);
208
+ if (!result.ok) {
209
+ expect(result.error.code).toBe("invalid_field");
210
+ expect(result.error.frameType).toBe("sight_frame");
211
+ }
212
+ });
213
+ });
214
+
215
+ describe("live-voice camera frames kept mid-call", () => {
216
+ test("a kept frame persists as its own tagged message and runs no turn", async () => {
217
+ const harness = createSessionHarness("Sight keep stream");
218
+ try {
219
+ await harness.session.start();
220
+ const attachmentId = await uploadFrame();
221
+
222
+ await harness.session.handleClientFrame({
223
+ type: "sight_frame",
224
+ attachmentId,
225
+ });
226
+ await waitFor(() => getMessages(harness.conversationId).length > 0, {
227
+ message: "Timed out waiting for the kept frame to persist",
228
+ });
229
+
230
+ const rows = getMessages(harness.conversationId);
231
+ expect(rows).toHaveLength(1);
232
+ const [row] = rows;
233
+ expect(row.role).toBe("user");
234
+ expect(getAttachmentsForMessage(row.id)).toHaveLength(1);
235
+
236
+ // The tag names the attachment on the row that carries it, which is what
237
+ // retention reads to decide what a later turn still sends in full.
238
+ const metadata = JSON.parse(row.metadata ?? "{}") as Record<
239
+ string,
240
+ unknown
241
+ >;
242
+ expect(sightFrameAttachmentIdsFromMetadata(metadata)).toEqual([
243
+ attachmentId,
244
+ ]);
245
+ expect(
246
+ selectSightFrameCaptureTimes(harness.conversationId).has(attachmentId),
247
+ ).toBe(true);
248
+
249
+ // Nothing is dispatched for the frame itself: answering a picture the
250
+ // user never asked about would talk over whatever they are saying.
251
+ expect(harness.startVoiceTurn).not.toHaveBeenCalled();
252
+ expect(
253
+ harness.frames.filter((frame) => frame.type === "error"),
254
+ ).toHaveLength(0);
255
+ } finally {
256
+ harness.dispose();
257
+ }
258
+ });
259
+
260
+ test("an unknown attachment id is refused, recoverably and attributably", async () => {
261
+ const harness = createSessionHarness("Sight keep unknown id");
262
+ try {
263
+ await harness.session.start();
264
+ const before = harness.frames.length;
265
+
266
+ await harness.session.handleClientFrame({
267
+ type: "sight_frame",
268
+ attachmentId: "att-missing",
269
+ });
270
+ await waitFor(() => harness.frames.length > before, {
271
+ message: "Timed out waiting for the rejected camera frame's error",
272
+ });
273
+
274
+ expect(
275
+ harness.frames.slice(before).find((frame) => frame.type === "error"),
276
+ ).toMatchObject({
277
+ type: "error",
278
+ frameType: "sight_frame",
279
+ recoverable: true,
280
+ });
281
+ // Nothing landed and the call carries on.
282
+ expect(getMessages(harness.conversationId)).toHaveLength(0);
283
+ expect(harness.startVoiceTurn).not.toHaveBeenCalled();
284
+ } finally {
285
+ harness.dispose();
286
+ }
287
+ });
288
+
289
+ test("refuses an unknown id while a turn is running, without waiting it out", async () => {
290
+ // The id is checked before the persist is scheduled. Behind the persist it
291
+ // would be answered only once the turn released the lock, long after the
292
+ // client could still retract the preview it showed.
293
+ const harness = createSessionHarness("Sight keep unknown id mid-turn");
294
+ try {
295
+ await harness.session.start();
296
+ harness.activeConversation.setProcessing(true);
297
+ const before = harness.frames.length;
298
+
299
+ await harness.session.handleClientFrame({
300
+ type: "sight_frame",
301
+ attachmentId: "att-missing",
302
+ });
303
+ await waitFor(() => harness.frames.length > before, {
304
+ message: "Timed out waiting for the mid-turn rejection",
305
+ });
306
+
307
+ expect(
308
+ harness.frames.slice(before).find((frame) => frame.type === "error"),
309
+ ).toMatchObject({ type: "error", frameType: "sight_frame" });
310
+ } finally {
311
+ harness.activeConversation.setProcessing(false);
312
+ harness.dispose();
313
+ }
314
+ });
315
+
316
+ test("keeps persist one message each", async () => {
317
+ // One message per keep, no batching: the gate's rate floor separates them,
318
+ // so there is no natural batch to form.
319
+ const harness = createSessionHarness("Sight keep stream repeated");
320
+ try {
321
+ await harness.session.start();
322
+ const first = await uploadFrame();
323
+ const second = await uploadFrame();
324
+
325
+ await harness.session.handleClientFrame({
326
+ type: "sight_frame",
327
+ attachmentId: first,
328
+ });
329
+ await waitFor(() => getMessages(harness.conversationId).length === 1, {
330
+ message: "Timed out waiting for the first kept frame",
331
+ });
332
+ await harness.session.handleClientFrame({
333
+ type: "sight_frame",
334
+ attachmentId: second,
335
+ });
336
+ await waitFor(() => getMessages(harness.conversationId).length === 2, {
337
+ message: "Timed out waiting for the second kept frame",
338
+ });
339
+
340
+ const captureTimes = selectSightFrameCaptureTimes(harness.conversationId);
341
+ expect([...captureTimes.keys()].sort()).toEqual([first, second].sort());
342
+ expect(harness.startVoiceTurn).not.toHaveBeenCalled();
343
+ } finally {
344
+ harness.dispose();
345
+ }
346
+ });
347
+ });
@@ -177,6 +177,7 @@ function createHarness(options: {
177
177
  archiveAudio?: LiveVoiceSessionAudioArchiver;
178
178
  turnDetectorConfig?: TurnDetectorConfig;
179
179
  speechEnergyThreshold?: number;
180
+ noiseFloorMargin?: number;
180
181
  bargeInMinSpeechMs?: number;
181
182
  echoBargeInMargin?: number;
182
183
  echoEmaHalfLifeMs?: number;
@@ -257,6 +258,11 @@ function createHarness(options: {
257
258
  options.turnDetectorConfig ??
258
259
  (options.viaFactory ? undefined : { silenceThresholdMs: 40 }),
259
260
  speechEnergyThreshold: options.speechEnergyThreshold,
261
+ // Like echoBargeInMargin below: a directly-constructed session gets the
262
+ // fixed gate unless a test asks for the adaptation, so a case about
263
+ // something else cannot drift once it feeds ten seconds of audio.
264
+ noiseFloorMargin:
265
+ options.noiseFloorMargin ?? (options.viaFactory ? undefined : 0),
260
266
  bargeInMinSpeechMs: options.bargeInMinSpeechMs,
261
267
  echoBargeInMargin:
262
268
  options.echoBargeInMargin ?? (options.viaFactory ? undefined : 1),
@@ -3227,11 +3233,114 @@ describe("LiveVoiceSession VAD threshold configuration", () => {
3227
3233
  expect(countType(raisedGate.frames, "speech_started")).toBe(0);
3228
3234
  });
3229
3235
 
3236
+ // One second of steady room tone at 24kHz, i.e. exactly one noise-floor
3237
+ // block. Ten of these fill the estimator's window.
3238
+ const roomSecond = (amplitude: number): Uint8Array =>
3239
+ pcm(amplitude, SAMPLE_RATE);
3240
+
3241
+ async function listenToRoom(
3242
+ session: LiveVoiceSession,
3243
+ amplitude: number,
3244
+ seconds = 10,
3245
+ ): Promise<void> {
3246
+ for (let second = 0; second < seconds; second += 1) {
3247
+ await session.handleBinaryAudio(roomSecond(amplitude));
3248
+ }
3249
+ }
3250
+
3251
+ test("a noisy room raises the gate over a chunk that would be speech in a quiet one", async () => {
3252
+ const { frames, session } = createHarness({ noiseFloorMargin: 3 });
3253
+ await session.start();
3254
+
3255
+ // Ten seconds of a 500-level room: below the 800 gate, so none of it is
3256
+ // speech, but loud enough that 3x it lands above the gate.
3257
+ await listenToRoom(session, 500);
3258
+ await flushAsyncCallbacks();
3259
+ expect(countType(frames, "speech_started")).toBe(0);
3260
+
3261
+ // 1000 clears the fixed 800 gate and would be speech today. Against a
3262
+ // measured floor of 500 the gate is now 1500, so this is still the room.
3263
+ await session.handleBinaryAudio(pcm(1_000));
3264
+ await flushAsyncCallbacks();
3265
+ expect(countType(frames, "speech_started")).toBe(0);
3266
+
3267
+ // Someone actually speaking still gets through.
3268
+ await session.handleBinaryAudio(pcm(1_600));
3269
+ await waitFor(() => countType(frames, "speech_started") === 1);
3270
+ });
3271
+
3272
+ test("the same room and chunk stay speech with the adaptation disabled", async () => {
3273
+ // Control for the case above: nothing but noiseFloorMargin differs, so a
3274
+ // failure here means the room audio moved the gate some other way.
3275
+ const { frames, session } = createHarness({ noiseFloorMargin: 0 });
3276
+ await session.start();
3277
+
3278
+ await listenToRoom(session, 500);
3279
+ await flushAsyncCallbacks();
3280
+ expect(countType(frames, "speech_started")).toBe(0);
3281
+
3282
+ await session.handleBinaryAudio(pcm(1_000));
3283
+ await waitFor(() => countType(frames, "speech_started") === 1);
3284
+ });
3285
+
3286
+ test("a quiet room leaves the configured gate exactly where it was", async () => {
3287
+ // The adaptation is one-directional: it may raise the gate, never lower it.
3288
+ // A room quieter than the constant must not make barge-in easier than the
3289
+ // configured threshold, or the change could invent self-interruption.
3290
+ const { frames, session } = createHarness({ noiseFloorMargin: 3 });
3291
+ await session.start();
3292
+
3293
+ await listenToRoom(session, 20);
3294
+ await flushAsyncCallbacks();
3295
+
3296
+ await session.handleBinaryAudio(pcm(800));
3297
+ await flushAsyncCallbacks();
3298
+ expect(countType(frames, "speech_started")).toBe(0);
3299
+
3300
+ await session.handleBinaryAudio(pcm(801));
3301
+ await waitFor(() => countType(frames, "speech_started") === 1);
3302
+ });
3303
+
3304
+ test("the raised gate is capped at 4x the configured threshold", async () => {
3305
+ // An absurd margin stands in for a pathological room: whatever the floor
3306
+ // says, the gate must stay somewhere a person can still be heard over.
3307
+ const { frames, session } = createHarness({
3308
+ speechEnergyThreshold: 800,
3309
+ noiseFloorMargin: 100,
3310
+ });
3311
+ await session.start();
3312
+
3313
+ await listenToRoom(session, 300);
3314
+ await flushAsyncCallbacks();
3315
+
3316
+ // Uncapped this would be 30_000, past anything 16-bit audio can reach.
3317
+ await session.handleBinaryAudio(pcm(3_100));
3318
+ await flushAsyncCallbacks();
3319
+ expect(countType(frames, "speech_started")).toBe(0);
3320
+
3321
+ await session.handleBinaryAudio(pcm(3_300));
3322
+ await waitFor(() => countType(frames, "speech_started") === 1);
3323
+ });
3324
+
3325
+ test("a partial window has no opinion, so the gate stays configured", async () => {
3326
+ const { frames, session } = createHarness({ noiseFloorMargin: 3 });
3327
+ await session.start();
3328
+
3329
+ // Nine seconds is not yet a window, and a partial window must not report
3330
+ // the loudest thing it has heard as the room.
3331
+ await listenToRoom(session, 500, 9);
3332
+ await flushAsyncCallbacks();
3333
+
3334
+ await session.handleBinaryAudio(pcm(1_000));
3335
+ await waitFor(() => countType(frames, "speech_started") === 1);
3336
+ });
3337
+
3230
3338
  test("with no config set the factory defaults to 800 energy / 1200 ms silence / 30 s max turn / 250 ms barge-in", async () => {
3231
3339
  // The test workspace has no liveVoice config, so the factory reads the
3232
3340
  // schema defaults.
3233
3341
  expect(getConfig().liveVoice.vad).toEqual({
3234
3342
  speechEnergyThreshold: 800,
3343
+ noiseFloorMargin: 3,
3235
3344
  silenceThresholdMs: 1200,
3236
3345
  maxTurnDurationMs: 30_000,
3237
3346
  bargeInMinSpeechMs: 250,