@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.
- package/ARCHITECTURE.md +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- 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,
|