@vellumai/assistant 0.11.6 → 0.11.7-staging.2
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 +31 -19
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +3 -3
- package/docs/flux-turn-detection-spike.md +8 -7
- package/docs/runbook-trusted-contacts.md +14 -0
- package/docs/stt-provider-onboarding.md +8 -4
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
- package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
- package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
- package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
- package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
- package/node_modules/@vellumai/environments/src/shell.ts +65 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/openapi.yaml +419 -55
- package/package.json +4 -1
- package/scripts/sync-llm-catalog.ts +6 -0
- package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
- package/src/__tests__/approval-card-edit.test.ts +153 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/avatar-identity-sync.test.ts +5 -1
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-policy.test.ts +1 -3
- package/src/__tests__/channel-readiness-discord.test.ts +9 -70
- package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
- package/src/__tests__/config-loader-backfill.test.ts +69 -0
- package/src/__tests__/config-schema.test.ts +2 -3
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +44 -0
- package/src/__tests__/conversation-evictor.test.ts +19 -0
- package/src/__tests__/conversation-fork-referential.test.ts +73 -63
- package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +0 -218
- package/src/__tests__/credential-broker.test.ts +43 -420
- package/src/__tests__/credential-record-write-through.test.ts +78 -0
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -9
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/delete-propagation.test.ts +92 -2
- package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
- package/src/__tests__/discord-channel-config.test.ts +237 -0
- package/src/__tests__/edit-propagation.test.ts +42 -2
- package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
- package/src/__tests__/image-conversion.test.ts +38 -4
- package/src/__tests__/install-skill-routing.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver.test.ts +74 -0
- package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
- package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
- package/src/__tests__/managed-profile-guard.test.ts +28 -0
- package/src/__tests__/managed-speech-defaults.test.ts +196 -1
- package/src/__tests__/mcp-cli.test.ts +23 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
- package/src/__tests__/media-stream-stt-session.test.ts +2 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-discord-adapter.test.ts +162 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/platform.test.ts +19 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/require-fresh-approval.test.ts +5 -2
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/schedule-retry.test.ts +183 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
- package/src/__tests__/skill-load-tool.test.ts +34 -0
- package/src/__tests__/skill-tool-factory.test.ts +6 -1
- package/src/__tests__/skill-tool-manifest.test.ts +20 -0
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/subagent-manager-notify.test.ts +38 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +146 -2
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/tool-audit.test.ts +111 -40
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
- package/src/__tests__/ui-shape-teaching.test.ts +1 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
- package/src/__tests__/voice-config-update.test.ts +43 -0
- package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
- package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
- package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
- package/src/agent/loop.ts +31 -2
- package/src/approvals/guardian-card-withdrawal.ts +22 -0
- package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
- package/src/avatar/avatar-manifest.ts +48 -100
- package/src/avatar/avatar-store.ts +7 -7
- package/src/avatar/ensure-raster.ts +146 -0
- package/src/avatar/resvg-lazy.test.ts +0 -1
- package/src/avatar/traits-png-sync.ts +9 -10
- package/src/background-wake/wake-intent-hooks.test.ts +0 -1
- package/src/backup/__tests__/paths.test.ts +6 -2
- package/src/backup/paths.ts +6 -4
- package/src/bundler/app-compiler.ts +2 -2
- package/src/bundler/compiler-tools.test.ts +36 -0
- package/src/bundler/compiler-tools.ts +54 -18
- package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
- package/src/calls/media-stream-stt-session.ts +2 -1
- package/src/calls/telephony-synthesis-language.ts +13 -5
- package/src/calls/voice-session-bridge.ts +19 -4
- package/src/channels/config.ts +23 -12
- package/src/cli/AGENTS.md +3 -1
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/contacts.help.ts +16 -4
- package/src/cli/commands/contacts.ts +5 -1
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-providers.ts +18 -53
- package/src/cli/commands/mcp.help.ts +6 -5
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +11 -5
- package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
- package/src/config/default-profile-catalog.ts +195 -10
- package/src/config/default-profile-names.ts +41 -0
- package/src/config/feature-flag-registry.json +34 -2
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +25 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +160 -10
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
- package/src/config/schemas/__tests__/stt.test.ts +94 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/channels.ts +32 -0
- package/src/config/schemas/live-voice.ts +13 -4
- package/src/config/schemas/llm.ts +263 -15
- package/src/config/schemas/mcp.ts +10 -2
- package/src/config/schemas/memory-retrospective.ts +1 -19
- package/src/config/schemas/services.ts +24 -0
- package/src/config/schemas/stt.ts +128 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +21 -0
- package/src/contacts/notify-contacts-changed.ts +5 -2
- package/src/context/compactor.ts +74 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
- package/src/daemon/config-watcher.ts +2 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
- package/src/daemon/conversation-agent-loop.ts +86 -10
- package/src/daemon/conversation-error.ts +25 -0
- package/src/daemon/conversation-evictor.ts +2 -4
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-store.ts +8 -1
- package/src/daemon/conversation-tool-setup.ts +37 -0
- package/src/daemon/conversation.ts +0 -4
- package/src/daemon/handlers/channel-config-result.ts +30 -0
- package/src/daemon/handlers/config-discord-channel.ts +301 -0
- package/src/daemon/handlers/config-ingress.ts +9 -0
- package/src/daemon/handlers/config-slack-channel.ts +13 -15
- package/src/daemon/handlers/config-telegram.ts +2 -5
- package/src/daemon/handlers/conversations.ts +0 -2
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +3 -1
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/tool-setup-types.ts +5 -3
- package/src/daemon/trust-context-types.test.ts +33 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
- package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
- package/src/live-voice/__tests__/protocol.test.ts +224 -0
- package/src/live-voice/live-voice-credential-preflight.ts +5 -1
- package/src/live-voice/live-voice-session-manager.ts +39 -2
- package/src/live-voice/live-voice-session.ts +224 -9
- package/src/live-voice/protocol.ts +187 -1
- package/src/messaging/providers/discord/api.ts +2 -1
- package/src/messaging/providers/discord/withdraw.ts +62 -0
- package/src/messaging/read-provider-metadata.ts +32 -0
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/db-integrity-sample.ts +1 -0
- package/src/monitoring/file-descriptors.ts +39 -6
- package/src/monitoring/process-memory.ts +37 -5
- package/src/monitoring/resource-sampler.ts +25 -8
- package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
- package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
- package/src/notifications/__tests__/connected-channels.test.ts +12 -0
- package/src/notifications/__tests__/copy-composer.test.ts +83 -0
- package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
- package/src/notifications/activity-failed-dedupe.ts +95 -0
- package/src/notifications/adapters/discord.ts +132 -0
- package/src/notifications/adapters/shared.ts +16 -0
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +5 -8
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/background-failure-signal.ts +116 -0
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/copy-composer.ts +41 -8
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/destination-resolver.ts +45 -5
- package/src/notifications/emit-signal.ts +19 -5
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/notifications/types.ts +2 -8
- package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
- package/src/oauth/connect-orchestrator.ts +16 -0
- package/src/oauth/seed-providers.ts +262 -0
- package/src/permissions/checker.test.ts +21 -0
- package/src/permissions/checker.ts +6 -0
- package/src/permissions/gateway-threshold-reader.test.ts +184 -0
- package/src/permissions/gateway-threshold-reader.ts +144 -10
- package/src/permissions/prompter.ts +0 -6
- package/src/permissions/types.ts +5 -4
- package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
- package/src/persistence/bulk-write-gate.ts +9 -10
- package/src/persistence/conversation-crud.ts +88 -134
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/embeddings/embedding-local.ts +14 -85
- package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
- package/src/persistence/lifecycle-events-store.test.ts +50 -0
- package/src/persistence/lifecycle-events-store.ts +75 -5
- package/src/platform/platform-patch-queue.test.ts +368 -0
- package/src/platform/platform-patch-queue.ts +190 -0
- package/src/platform/sync-avatar.test.ts +527 -0
- package/src/platform/sync-avatar.ts +209 -0
- package/src/platform/sync-identity.test.ts +75 -0
- package/src/platform/sync-identity.ts +17 -110
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugin-api/transcription-session.ts +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +4 -0
- package/src/plugins/defaults/image-recovery/recover.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
- package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
- package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +55 -0
- package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
- package/src/providers/connection-resolution.ts +2 -17
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
- package/src/providers/inference/adapter-factory.ts +246 -3
- package/src/providers/inference/auth.ts +15 -14
- package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
- package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
- package/src/providers/inference/resolve-auth.ts +1 -1
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +100 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/api-error-normalization.ts +13 -0
- package/src/providers/openai/chat-completions-provider.ts +84 -0
- package/src/providers/opencode/client.test.ts +150 -0
- package/src/providers/opencode/client.ts +61 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/provider-secret-catalog.ts +3 -2
- package/src/providers/retry.ts +956 -68
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +191 -3
- package/src/providers/speech-to-text/resolve.ts +132 -24
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +39 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/providers/vellum/client.ts +29 -0
- package/src/providers/vellum-model-routing.test.ts +2 -0
- package/src/providers/vellum-model-routing.ts +3 -3
- package/src/runtime/AGENTS.md +1 -2
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/background-job-runner.ts +68 -50
- package/src/runtime/channel-readiness-service.ts +0 -39
- package/src/runtime/http-server.ts +12 -4
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
- package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
- package/src/runtime/routes/approval-routes.ts +0 -12
- package/src/runtime/routes/avatar-routes.ts +17 -30
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
- package/src/runtime/routes/contact-prompt-routes.ts +65 -6
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +98 -10
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +12 -9
- package/src/runtime/routes/inbound-message-handler.ts +47 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
- package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profiles-routes.ts +12 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +2 -2
- package/src/runtime/routes/integrations/discord.ts +119 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/mcp-auth-routes.ts +12 -6
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/oauth-pending-flows.ts +136 -0
- package/src/runtime/routes/secret-routes.ts +7 -0
- package/src/runtime/routes/stt-routes.ts +38 -6
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +9 -5
- package/src/runtime/sync/resource-sync-events.ts +2 -0
- package/src/runtime/trust-verdict-consumer.ts +3 -3
- package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/schedule-recovery.ts +0 -1
- package/src/schedule/scheduler.ts +89 -51
- package/src/schedule/worker.ts +57 -0
- package/src/security/__tests__/openai-device-auth.test.ts +355 -0
- package/src/security/ces-rpc-record-backend.ts +123 -0
- package/src/security/oauth2.ts +7 -0
- package/src/security/openai-device-auth.ts +364 -0
- package/src/security/secure-keys.ts +15 -0
- package/src/skills/available-skills.ts +10 -1
- package/src/skills/catalog-install.ts +19 -1
- package/src/skills/inline-command-runner.ts +6 -2
- package/src/skills/platform-compatibility.test.ts +49 -0
- package/src/skills/platform-compatibility.ts +65 -0
- package/src/skills/tool-manifest.ts +20 -0
- package/src/stt/__tests__/roles.test.ts +152 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/roles.ts +180 -0
- package/src/stt/stt-stream-session.ts +3 -2
- package/src/stt/types.ts +33 -3
- package/src/subagent/manager.ts +12 -0
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
- package/src/telemetry/live-voice-funnel.ts +12 -2
- package/src/telemetry/telemetry-wire.generated.ts +6 -0
- package/src/telemetry/tool-audit.ts +83 -30
- package/src/telemetry/tool-executed-events-store.test.ts +0 -1
- package/src/telemetry/turn-outcome.ts +75 -4
- package/src/tools/client-os.ts +115 -0
- package/src/tools/computer-use/definitions.ts +11 -7
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
- package/src/tools/credentials/broker-types.ts +0 -71
- package/src/tools/credentials/broker.ts +6 -201
- package/src/tools/credentials/metadata-store.ts +65 -16
- package/src/tools/credentials/store.ts +2 -0
- package/src/tools/executor.ts +2 -8
- package/src/tools/host-shell.test.ts +75 -0
- package/src/tools/host-shell.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/mcp/mcp-tool-factory.ts +44 -9
- package/src/tools/permission-checker.ts +73 -21
- package/src/tools/policy-context.ts +1 -0
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/find-similar-skills.ts +10 -2
- package/src/tools/skills/load.ts +41 -6
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
- package/src/tools/terminal/safe-env.ts +39 -9
- package/src/tools/terminal/shell.ts +12 -31
- package/src/tools/tool-approval-handler.ts +64 -10
- package/src/tools/tool-types.ts +10 -0
- package/src/tools/types.ts +19 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
- package/src/tools/workflows/manage-workflows.ts +2 -2
- package/src/tools/workflows/run-workflow.test.ts +2 -1
- package/src/util/__tests__/file-use.test.ts +61 -0
- package/src/util/__tests__/process-table.test.ts +66 -0
- package/src/util/__tests__/process-tree.test.ts +46 -0
- package/src/util/errors.ts +1 -0
- package/src/util/file-use.ts +91 -0
- package/src/util/host-process.test.ts +17 -0
- package/src/util/host-process.ts +75 -0
- package/src/util/image-conversion.ts +39 -7
- package/src/util/platform.ts +32 -13
- package/src/util/process-table.ts +249 -0
- package/src/util/process-tree.ts +85 -129
- package/src/util/provider-error-patterns.ts +20 -0
- package/src/util/spawn.ts +9 -2
- package/src/watch/__tests__/watch-retro.test.ts +48 -1
- package/src/watch/watch-retro.ts +46 -4
- package/src/workspace/git-service.ts +4 -14
- package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
- package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
- package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
- package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/permission-types.test.ts +0 -17
- package/src/channels/gateway-discord-admission.ts +0 -32
- package/src/persistence/fork-message-copy.ts +0 -239
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
- package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
- package/src/security/oauth2-device-code.ts +0 -312
package/ARCHITECTURE.md
CHANGED
|
@@ -84,7 +84,7 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
|
|
|
84
84
|
|
|
85
85
|
**Identity lifecycle:**
|
|
86
86
|
|
|
87
|
-
1. **Bootstrap (loopback-only,
|
|
87
|
+
1. **Bootstrap (loopback-only, desktop)**: On first launch, the desktop client calls `POST /v1/guardian/init` with `{ platform, deviceId }`. The endpoint is loopback-only and mints a JWT access token + refresh token pair. Returns `{ guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`. The CLI obtains its bearer token during `hatch` and does not perform a separate bootstrap step.
|
|
88
88
|
|
|
89
89
|
2. **Refresh** — `POST /v1/guardian/refresh` accepts `{ refreshToken }` and returns a new access/refresh token pair. Single-use rotation with replay detection and family-based revocation.
|
|
90
90
|
|
|
@@ -577,17 +577,29 @@ All guardian decisions for voice access requests flow through:
|
|
|
577
577
|
|
|
578
578
|
Audio-to-text conversion occurs in six distinct runtime boundaries, each with its own provider model and adapter layer. The `services.stt` config block is the single source of truth for STT provider selection across assistant, client, live voice, and telephony boundaries.
|
|
579
579
|
|
|
580
|
+
**Per-consumer roles:** `services.stt.provider` (plus that provider's `services.stt.providers.<id>.model` family) is the global selection. `services.stt.roles.<role>` overrides it for one consumer, naming a `{provider, model?}` pair; an unset role falls back to the global. Roles exist because capability is per boundary while the global is not: the Flux family streams but has no batch endpoint, so a global set to it breaks file transcription and telephony at the same moment it improves live voice. A role names the consumer rather than the boundary, because one telephony call resolves a streaming transcriber and a batch fallback within the same session.
|
|
581
|
+
|
|
582
|
+
| Role | Consumers | Boundaries required |
|
|
583
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
584
|
+
| `liveVoice` | `src/live-voice/live-voice-session.ts`, `src/live-voice/live-voice-credential-preflight.ts` | streaming |
|
|
585
|
+
| `telephony` | `src/calls/media-stream-stt-session.ts` (streaming dial and batch fallback) | streaming + batch + telephony |
|
|
586
|
+
| `dictation` | `/v1/stt/stream` (`src/runtime/http-server.ts`), `POST /v1/stt/transcribe`, `src/plugin-api/transcription-session.ts` | streaming + batch |
|
|
587
|
+
| `watch` | `/v1/watch/stream` (`src/runtime/routes/watch-routes.ts`) | streaming |
|
|
588
|
+
| `batch` | inbound audio (`routes/inbound-stages/transcribe-audio.ts`), `POST /v1/stt/transcribe-file`, media-processing/transcribe skills | batch |
|
|
589
|
+
|
|
590
|
+
Roles are defined in `src/stt/roles.ts`: `STT_ROLE_REQUIREMENTS` declares each role's boundaries, `sttSelectionForRole()`/`sttCatalogKeyForRole()` perform the lookup, and `sttRoleCapabilityGap()` states why a pair cannot serve a role. Selection is resolved inside `resolveBatchTranscriber()`/`resolveStreamingTranscriber()`, so a caller passes `role` and nothing else. Capability is checked against the **resolved catalog row** (`sttCatalogKeyFor(provider, model)`), not the bare provider id, since `deepgram` batches and `deepgram` running `flux` does not. Validation is fail-closed: `services.stt.roles` rejects an incapable pair at config parse time rather than substituting a working provider at dial time. Telephony and live-voice preflight, the dictation session label, and the telephony language pin all read their own role, so what preflight judges is what the session dials.
|
|
591
|
+
|
|
580
592
|
**Provider catalog model:** The daemon's canonical provider catalog (`src/providers/speech-to-text/provider-catalog.ts`) is the single source of truth for all STT provider metadata — credential mappings, supported boundaries, telephony mode, conversation streaming mode, and client-facing display metadata (names, hints, setup mode, credentials guide). Native clients fetch provider metadata at launch via `GET /v1/stt/providers`. To add a new provider, follow the checklist in `docs/stt-provider-onboarding.md`.
|
|
581
593
|
|
|
582
594
|
**Boundary overview:**
|
|
583
595
|
|
|
584
|
-
| Boundary | Runtime | Provider (current)
|
|
585
|
-
| ---------------------------- | --------------------------------------------------- |
|
|
586
|
-
| **Telephony (media-stream)** | Daemon process (Twilio Media Streams WebSocket) |
|
|
587
|
-
| **Daemon batch** | Daemon process (REST API to provider) |
|
|
588
|
-
| **Conversation streaming** | Daemon process (WebSocket-based) |
|
|
589
|
-
| **Live voice channel** | Assistant process (gateway-authenticated WebSocket) |
|
|
590
|
-
| **Client service-first** | Web/Electron via gateway → daemon |
|
|
596
|
+
| Boundary | Runtime | Provider (current) | Adapter module | Caller |
|
|
597
|
+
| ---------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
598
|
+
| **Telephony (media-stream)** | Daemon process (Twilio Media Streams WebSocket) | `telephony` role, else `services.stt.provider` | `src/calls/media-stream-stt-session.ts`, `src/providers/speech-to-text/resolve.ts`, streaming provider adapters | `src/calls/media-stream-server.ts` |
|
|
599
|
+
| **Daemon batch** | Daemon process (REST API to provider) | `batch` role, else `services.stt.provider` | `src/stt/daemon-batch-transcriber.ts` | `src/runtime/routes/inbound-stages/transcribe-audio.ts` |
|
|
600
|
+
| **Conversation streaming** | Daemon process (WebSocket-based) | `dictation` role, else `services.stt.provider` | `src/stt/stt-stream-session.ts`, `src/providers/speech-to-text/deepgram-realtime.ts`, `src/providers/speech-to-text/google-gemini-live-stream.ts`, `src/providers/speech-to-text/openai-whisper-stream.ts`, `src/providers/speech-to-text/xai-realtime.ts` | Web/Electron dictation client via gateway WS proxy |
|
|
601
|
+
| **Live voice channel** | Assistant process (gateway-authenticated WebSocket) | `liveVoice` role, else `services.stt.provider` | `src/runtime/http-server.ts`, `src/live-voice/live-voice-session-manager.ts`, `src/live-voice/live-voice-session.ts`, `src/providers/speech-to-text/resolve.ts`, streaming provider adapters | Web/Electron live voice client via `/v1/live-voice` |
|
|
602
|
+
| **Client service-first** | Web/Electron via gateway → daemon | `dictation` (transcribe) / `batch` (transcribe-file) role, else `services.stt.provider` | `src/runtime/routes/stt-routes.ts` | Web/Electron dictation and voice clients |
|
|
591
603
|
|
|
592
604
|
**Telephony boundary (media-stream):**
|
|
593
605
|
|
|
@@ -595,8 +607,8 @@ Every phone call connects over Twilio Media Streams: the voice webhook emits `<C
|
|
|
595
607
|
|
|
596
608
|
Transcription mode is selected once per session in `media-stream-stt-session.ts`:
|
|
597
609
|
|
|
598
|
-
- **Streaming** (default)
|
|
599
|
-
- **Batch fallback
|
|
610
|
+
- **Streaming** (default): when `calls.voice.telephonyStreaming` is enabled and the `telephony` role resolves a streaming transcriber (`resolveStreamingTranscriber({ role: "telephony" })`), inbound audio is decoded (mu-law → PCM16, resampled 8 kHz → 16 kHz) and fed to the provider's realtime adapter. Replies trigger only on utterance-boundary finals (for Deepgram, `speech_final`/`UtteranceEnd`, never mid-sentence `is_final` segments), and barge-in fires from local energy VAD, never from transcriber partials.
|
|
611
|
+
- **Batch fallback**: otherwise the session segments turns with the energy-based `MediaTurnDetector` and transcribes each completed turn via the same role's batch API. Both halves of a call read the `telephony` role, which is why a role names its consumer rather than a boundary.
|
|
600
612
|
|
|
601
613
|
A credential preflight (`resolveTelephonyCredentialReadiness()` in `src/calls/telephony-credential-preflight.ts`) gates every call: it requires a credentialed, telephony-capable STT provider **and** a media-stream-playable TTS provider (the configured one or a credentialed playable fallback). Inbound calls that fail the preflight receive `<Say>` setup-required copy plus `<Hangup/>` instead of a doomed stream; outbound placement fails before dialing via `preflightVoiceIngress()` with the same user-facing message.
|
|
602
614
|
|
|
@@ -632,7 +644,7 @@ To add a new daemon batch STT provider, follow the full checklist in `docs/stt-p
|
|
|
632
644
|
|
|
633
645
|
**Conversation streaming boundary:**
|
|
634
646
|
|
|
635
|
-
Real-time conversation chat message capture on macOS uses a WebSocket-based streaming STT path. When the
|
|
647
|
+
Real-time conversation chat message capture on macOS uses a WebSocket-based streaming STT path. When the provider the `dictation` role resolves to supports conversation streaming (determined by the `conversationStreamingMode` field in the provider catalog), native clients open a WebSocket session through the gateway to the daemon's `/v1/stt/stream` endpoint. The daemon resolves a `StreamingTranscriber` for that provider and streams partial/final transcript events back to the client in real time.
|
|
636
648
|
|
|
637
649
|
Each provider that advertises a conversation streaming mode in the catalog ships an adapter implementing the `StreamingTranscriber` interface from `src/stt/types.ts`. The catalog (`src/providers/speech-to-text/provider-catalog.ts`) is the source of truth for which providers are streaming-capable; `resolveStreamingTranscriber()` maps each to its adapter:
|
|
638
650
|
|
|
@@ -653,8 +665,8 @@ Each provider that advertises a conversation streaming mode in the catalog ships
|
|
|
653
665
|
|
|
654
666
|
**Session lifecycle (daemon side):**
|
|
655
667
|
|
|
656
|
-
1. Client opens a WebSocket to `/v1/stt/stream` with required query parameter `mimeType` and optional `provider` and `sampleRate`. The `provider` parameter is optional compatibility metadata
|
|
657
|
-
2. `SttStreamSession` (in `src/stt/stt-stream-session.ts`) resolves a `StreamingTranscriber` via `resolveStreamingTranscriber()` from `src/providers/speech-to-text/resolve.ts`, using the
|
|
668
|
+
1. Client opens a WebSocket to `/v1/stt/stream` with required query parameter `mimeType` and optional `provider` and `sampleRate`. The `provider` parameter is optional compatibility metadata: the runtime is config-authoritative and always resolves the streaming transcriber from the `dictation` role (`services.stt.roles.dictation`, else `services.stt.provider`). When a requested provider disagrees with the one that role resolves to, the runtime logs a mismatch warning.
|
|
669
|
+
2. `SttStreamSession` (in `src/stt/stt-stream-session.ts`) resolves a `StreamingTranscriber` via `resolveStreamingTranscriber()` from `src/providers/speech-to-text/resolve.ts`, using the dictation role's provider (not the requested one).
|
|
658
670
|
3. The transcriber's `start()` method opens the provider session.
|
|
659
671
|
4. A `ready` event (with `provider` field) is sent to the client, signaling that audio frames are accepted.
|
|
660
672
|
5. Client sends `audio` frames (binary WebSocket frames or base64-encoded JSON) and a `stop` event when recording ends.
|
|
@@ -725,7 +737,7 @@ V1 is local/gateway-scoped. Managed/cloud WebSocket proxy support, cross-region
|
|
|
725
737
|
|
|
726
738
|
**Client service-first boundary:**
|
|
727
739
|
|
|
728
|
-
All product-facing dictation and voice-streaming paths use a service-first STT strategy. Clients record audio, encode it to WAV, and POST it through the gateway to the daemon's `POST /v1/stt/transcribe` endpoint. The daemon resolves the
|
|
740
|
+
All product-facing dictation and voice-streaming paths use a service-first STT strategy. Clients record audio, encode it to WAV, and POST it through the gateway to the daemon's `POST /v1/stt/transcribe` endpoint. The daemon resolves the `dictation` role's provider through `resolveBatchTranscriber()` and returns the transcribed text. `POST /v1/stt/transcribe-file` resolves the `batch` role the same way.
|
|
729
741
|
|
|
730
742
|
- The client receives a typed result distinguishing success from `notConfigured`, `serviceUnavailable`, and `error`, so callers can deterministically trigger their fallback path.
|
|
731
743
|
- The gateway proxies the request via assistant-scoped path rewriting: `/v1/assistants/:id/stt/transcribe` is rewritten to `/v1/stt/transcribe` on the daemon.
|
|
@@ -735,7 +747,7 @@ The web client implements these flows in `clients/web/src/domains/chat/voice/`:
|
|
|
735
747
|
|
|
736
748
|
**Cross-boundary notes:**
|
|
737
749
|
|
|
738
|
-
- The `services.stt` config block is the single source of truth for STT provider selection across the daemon batch boundary, the conversation streaming boundary, the client service-first boundary, and the telephony boundary. The batch and streaming resolvers (`resolveBatchTranscriber()`, `resolveStreamingTranscriber()`) both read
|
|
750
|
+
- The `services.stt` config block is the single source of truth for STT provider selection across the daemon batch boundary, the conversation streaming boundary, the client service-first boundary, and the telephony boundary. The batch and streaming resolvers (`resolveBatchTranscriber()`, `resolveStreamingTranscriber()`) both read it, taking the caller's `role` override (`services.stt.roles.<role>`) when one is set and the global `services.stt.provider` otherwise, and resolve credentials through the same catalog; the telephony boundary's media-stream STT session uses these same resolvers, selecting streaming vs batch transcription via `resolveStreamingTranscriber()`/`resolveBatchTranscriber()` under the `calls.voice.telephonyStreaming` flag, gated by `resolveTelephonySttCapability()`, while call placement/TwiML is gated by `resolveTelephonyCredentialReadiness()`. The daemon provider catalog (`src/providers/speech-to-text/provider-catalog.ts`) is the authoritative registry of supported providers. Native clients fetch display metadata via `GET /v1/stt/providers`.
|
|
739
751
|
- **Spoken language** is a second axis on the same block: `services.stt.language` is read by `resolveStreamingTranscriber()` and `resolveBatchTranscriber()` and forwarded to the adapters that accept one (Deepgram, xAI, and the managed relay, which passes it to Deepgram server-side). Callers may override it per streaming session, but none need to: reading it in the resolvers means every boundary inherits the setting. Gemini and Whisper take no language option and auto-detect natively, so they ignore it. Leaving it unset is **not** auto-detection on Deepgram or the managed relay: Deepgram's default is English, so non-English speech transcribes as English-sounding nonsense. `"multi"` selects nova-3 code-switching across English, Spanish, French, German, Hindi, Russian, Portuguese, Japanese, Italian, and Dutch, the mode to use when a speaker mixes languages inside one utterance. BYOK Deepgram pins `model=nova-3` for **any** explicitly configured language, not just `"multi"`: the curated roster is verified against nova-3 (the default nova-2 supports only a subset, and rejects `"multi"` outright), and `deepgramLanguageOptions()` owns the model+language pairing so every call site inherits the rule; leaving the language unset keeps the adapter default model with no language param. `"multi"` is never forwarded to xAI (it is not a BCP-47 code; xAI auto-handles multilingual audio). The managed relay pins nova-3 and allowlists `language`, so streaming needs no platform-side change; managed **batch** goes through the platform speech proxy, which accepts no language parameter, so forwarding it there is deferred pending a platform-side change.
|
|
740
752
|
- Conversation streaming does not replace the client service-first batch path. When streaming is available, it runs concurrently during recording and provides real-time partials and finals. The batch path remains the fallback for providers that do not support streaming, when streaming fails mid-session, or when streaming produces no final transcript.
|
|
741
753
|
- Credential mapping is catalog-driven: `provider-secret-catalog.ts` derives STT API-key provider names from the daemon catalog via `listCredentialProviderNames()`, deduplicating against the LLM/search provider list. Adding a provider to the catalog automatically includes its credential name in `API_KEY_PROVIDERS`.
|
|
@@ -1182,7 +1194,7 @@ The text_qa system prompt includes an action execution hierarchy that guides too
|
|
|
1182
1194
|
| **GOOD** | Headless browser | `browser_*` (bundled `browser` skill) | Web automation, form filling, scraping (background) |
|
|
1183
1195
|
| **LAST RESORT** | Foreground computer use | `computer_use_*` (bundled `computer-use` skill) | Only on explicit user request ("go ahead", "take over") |
|
|
1184
1196
|
|
|
1185
|
-
Computer-use tools are proxy tools provided by the bundled `computer-use` skill, preactivated via `preactivatedSkillIds` in desktop sessions. Each tool forwards actions to
|
|
1197
|
+
Computer-use tools are proxy tools provided by the bundled `computer-use` skill, preactivated via `preactivatedSkillIds` in desktop sessions. Each tool forwards actions to a compatible connected desktop client via `HostCuProxy`, which handles request/resolve proxying, step counting, loop detection, and observation formatting within the unified agent loop. Skill projection filters platform-specific actions from unsupported clients. Drag, app launching, and AppleScript remain macOS-only. These tools are not core-registered at daemon startup; they exist only through skill projection.
|
|
1186
1198
|
|
|
1187
1199
|
### Sandbox Filesystem and Host Access
|
|
1188
1200
|
|
|
@@ -1397,7 +1409,7 @@ The following capabilities ship as bundled skills in `assistant/src/config/bundl
|
|
|
1397
1409
|
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1398
1410
|
| `browser` | `browser_navigate`, `browser_snapshot`, `browser_screenshot`, `browser_close`, `browser_click`, `browser_type`, `browser_press_key`, `browser_wait_for`, `browser_extract`, `browser_fill_credential` | Headless browser automation — web scraping, form filling, interaction (previously core-registered as `headless-browser`; now skill-provided with default allow rules) |
|
|
1399
1411
|
| `gmail` | Gmail search, archive, send, etc. | Email management via OAuth2 integration |
|
|
1400
|
-
| `computer-use` | `computer_use_observe`, `computer_use_click`, `computer_use_type_text`, `computer_use_key`, `computer_use_scroll`, `computer_use_drag`, `computer_use_wait`, `computer_use_open_app`, `computer_use_run_applescript`, `computer_use_done`, `computer_use_respond` | Computer-use proxy tools
|
|
1412
|
+
| `computer-use` | `computer_use_observe`, `computer_use_click`, `computer_use_type_text`, `computer_use_key`, `computer_use_scroll`, `computer_use_drag`, `computer_use_wait`, `computer_use_open_app`, `computer_use_run_applescript`, `computer_use_done`, `computer_use_respond` | Computer-use proxy tools preactivated via `preactivatedSkillIds` in desktop sessions. Each tool forwards actions to a compatible connected desktop host via `HostCuProxy`, which handles request/resolve proxying, step counting, loop detection, and observation formatting in the same agent loop. |
|
|
1401
1413
|
| `weather` | `get-weather` | Fetch current weather data |
|
|
1402
1414
|
| `app-builder` | `app_create`, `app_delete`, `app_refresh`, `app_generate_icon` | Dynamic app authoring — create and manage persistent apps; file editing uses generic file tools plus `app_refresh` (activated via `skill_load app-builder`; `app_open` remains a core proxy tool) |
|
|
1403
1415
|
| `self-upgrade` | (instruction-only) | Self-improvement workflow |
|
|
@@ -1443,7 +1455,7 @@ graph TB
|
|
|
1443
1455
|
RESOLVE --> PROVIDER
|
|
1444
1456
|
```
|
|
1445
1457
|
|
|
1446
|
-
**Internal preactivation**: Some bundled skills are preactivated programmatically rather than by model discovery. For example, desktop sessions set `preactivatedSkillIds: ['computer-use']`, causing `projectSkillTools()` to load the
|
|
1458
|
+
**Internal preactivation**: Some bundled skills are preactivated programmatically rather than by model discovery. For example, desktop sessions set `preactivatedSkillIds: ['computer-use']`, causing `projectSkillTools()` to load the eligible `computer_use_*` tool definitions from the bundled skill's `TOOLS.json` on the first turn. These proxy tools forward actions to a compatible connected desktop client via `HostCuProxy`.
|
|
1447
1459
|
|
|
1448
1460
|
### Skill Tool Execution
|
|
1449
1461
|
|
|
@@ -1647,7 +1659,7 @@ graph TB
|
|
|
1647
1659
|
|
|
1648
1660
|
### Auto-Approve Threshold
|
|
1649
1661
|
|
|
1650
|
-
Auto-approve thresholds are **gateway-owned
|
|
1662
|
+
Auto-approve thresholds are **gateway-owned**: they live in the gateway's SQLite database and are read by the assistant via IPC (`get_global_thresholds`, `get_conversation_threshold`, `get_contact_threshold`). A contact ceiling is written via `set_contact_threshold` (`gateway contacts set-risk-threshold` or `POST /v1/contacts`). Users also control thresholds via the **Settings UI** (Permissions & Privacy tab) or the **per-conversation risk tolerance picker**. When the gateway is unreachable, the assistant defaults to `"none"` (Strict): fail-closed with no local fallback.
|
|
1651
1663
|
|
|
1652
1664
|
| `autoApproveUpTo` | Low-risk tools | Medium-risk tools | High-risk tools |
|
|
1653
1665
|
| ----------------- | -------------- | ----------------- | --------------- |
|
package/Dockerfile
CHANGED
|
@@ -117,6 +117,7 @@ RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
|
|
|
117
117
|
COPY --from=builder /app /app
|
|
118
118
|
|
|
119
119
|
# Copy source after installing dependencies for better layer caching.
|
|
120
|
+
COPY packages/avatar-manifest /app/packages/avatar-manifest
|
|
120
121
|
COPY packages/ces-client /app/packages/ces-client
|
|
121
122
|
COPY packages/service-contracts /app/packages/service-contracts
|
|
122
123
|
COPY packages/credential-storage /app/packages/credential-storage
|
|
@@ -34,7 +34,7 @@ The order inside `check()`: the memory-retrospective skill-authoring grant, then
|
|
|
34
34
|
|
|
35
35
|
### Auto-Approve Threshold
|
|
36
36
|
|
|
37
|
-
Thresholds are **gateway-owned**: stored in the gateway's SQLite database, read by the assistant over IPC (`get_global_thresholds`, `get_conversation_threshold`), and set
|
|
37
|
+
Thresholds are **gateway-owned**: stored in the gateway's SQLite database, read by the assistant over IPC (`get_global_thresholds`, `get_conversation_threshold`, `get_contact_threshold`), and written via `set_contact_threshold` (`gateway contacts set-risk-threshold` or `POST /v1/contacts`) as well as the Settings UI (Permissions & Privacy) or the per-conversation risk tolerance picker. When the gateway is unreachable the assistant resolves `"none"` (Strict), fail-closed with no local fallback.
|
|
38
38
|
|
|
39
39
|
Gateway defaults per execution context (`gateway/src/ipc/threshold-handlers.ts`): `interactive` (a conversation with a client) `medium`, `autonomous` (background/scheduled) `low`, `headless` `none`. A per-conversation override wins over the global value; for non-guardian actors a channel-permission cell can only lower the effective threshold.
|
|
40
40
|
|
|
@@ -203,7 +203,7 @@ sequenceDiagram
|
|
|
203
203
|
|
|
204
204
|
```mermaid
|
|
205
205
|
graph TB
|
|
206
|
-
TOOL["Tool (e.g. browser_fill_credential)"] --> BROKER["CredentialBroker.
|
|
206
|
+
TOOL["Tool (e.g. browser_fill_credential)"] --> BROKER["CredentialBroker.browserFill / serverUse"]
|
|
207
207
|
BROKER --> POLICY{"Check policy:<br/>allowedTools + allowedDomains"}
|
|
208
208
|
POLICY -->|denied| REJECT["PolicyDenied error"]
|
|
209
209
|
POLICY -->|allowed| FETCH["getSecureKeyAsync(credential/svc/field)"]
|
|
@@ -214,7 +214,7 @@ graph TB
|
|
|
214
214
|
|
|
215
215
|
The `allowOneTimeSend` config gate (default: `false`) enables a secondary "Send Once" button in the secret prompt UI. When used:
|
|
216
216
|
|
|
217
|
-
- The secret value is handed to the `CredentialBroker`, which holds it in memory for the next `
|
|
217
|
+
- The secret value is handed to the `CredentialBroker`, which holds it in memory for the next `browserFill` or `serverUse` call
|
|
218
218
|
- The value is **not** persisted to the credential store
|
|
219
219
|
- The broker discards the value after a single use
|
|
220
220
|
- The credentials prompt route output confirms delivery without including the secret value — the value is never returned to the model
|
|
@@ -59,7 +59,7 @@ If it does not, set the Deepgram key the ordinary way (client Settings, Speech-t
|
|
|
59
59
|
|
|
60
60
|
## 2. Enable Flux
|
|
61
61
|
|
|
62
|
-
> **
|
|
62
|
+
> **Putting a provider on the `flux` model family turns off batch transcription for the whole workspace, not just live voice.** `services.stt.provider` plus `services.stt.providers.<provider>.model` is the single source of truth for every STT route, and Flux is the only family in the catalog with no `daemon-batch` boundary. For as long as it is set, these all stop working: voice-message and inbound-attachment transcription, the `transcribe` skill, the `media-processing` skill's audio segments, `POST /v1/stt/transcribe`, and phone-call transcription. Each reports a message naming Flux as the cause rather than failing silently, but they do not fall back to `deepgram` on their own. **Set `services.stt.providers.deepgram.model` back to `nova-3` when the spike is over**, and do not run the spike on an assistant that is also taking calls or handling voice messages.
|
|
63
63
|
|
|
64
64
|
Two keys in `config.json`, which lives at `$VELLUM_WORKSPACE_DIR/config.json` (default `~/.vellum/workspace/config.json`):
|
|
65
65
|
|
|
@@ -67,7 +67,8 @@ Two keys in `config.json`, which lives at `$VELLUM_WORKSPACE_DIR/config.json` (d
|
|
|
67
67
|
{
|
|
68
68
|
"services": {
|
|
69
69
|
"stt": {
|
|
70
|
-
"provider": "deepgram
|
|
70
|
+
"provider": "deepgram",
|
|
71
|
+
"providers": { "deepgram": { "model": "flux" } }
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
74
|
"liveVoice": {
|
|
@@ -93,12 +94,12 @@ The rest of `liveVoice.flux` is optional and defaulted (`config/schemas/live-voi
|
|
|
93
94
|
|
|
94
95
|
## 3. Run the A/B
|
|
95
96
|
|
|
96
|
-
**Flip `turnEnd.enabled` between runs and leave
|
|
97
|
+
**Flip `turnEnd.enabled` between runs and leave the `flux` model family selected in both arms.** That holds the STT engine, the model, the socket, and the transcriber lifecycle constant, so the only thing that changes is which signal commits the turn.
|
|
97
98
|
|
|
98
|
-
- **Arm A (control):** `
|
|
99
|
-
- **Arm B (treatment):** `
|
|
99
|
+
- **Arm A (control):** `providers.deepgram.model: "flux"`, `turnEnd.enabled: false`. Flux transcribes; the four turn-detection events are ignored; the local silence boundary and the front-door hold path run exactly as they do today.
|
|
100
|
+
- **Arm B (treatment):** `providers.deepgram.model: "flux"`, `turnEnd.enabled: true`.
|
|
100
101
|
|
|
101
|
-
**Do not A/B by switching the
|
|
102
|
+
**Do not A/B by switching the model family between `nova-3` and `flux`.** That confounds two changes at once:
|
|
102
103
|
|
|
103
104
|
1. It swaps the STT model. `deepgram` runs `nova-2` (`DEFAULT_MODEL`, `deepgram-realtime.ts`), so any transcript-quality or first-partial difference lands in your latency numbers.
|
|
104
105
|
2. It swaps the transcriber lifecycle. `deepgram` implements `finalizeUtterance`, so the session adopts it as a persistent stream shared across the whole session (`sharedTranscriber`) and never tears it down between turns. Flux implements no `finalizeUtterance`, so every utterance owns its own `/v2/listen` socket and every release closes it. That per-turn socket churn is inside `roundTripMs` and `totalMs` on the Flux side and absent on the `deepgram` side, so a naive provider-swap A/B attributes it to turn detection. The next section is what it costs and why it cannot be removed.
|
|
@@ -230,7 +231,7 @@ Check these if you port the spike anywhere else, because a run that violates eit
|
|
|
230
231
|
|
|
231
232
|
## 8. Falling back
|
|
232
233
|
|
|
233
|
-
Set `liveVoice.flux.turnEnd.enabled` back to `false` (or delete the key) and restart. That is the whole rollback: the latch goes down, the turn-detection events become no-ops, and the silence-boundary path runs unchanged. You can leave
|
|
234
|
+
Set `liveVoice.flux.turnEnd.enabled` back to `false` (or delete the key) and restart. That is the whole rollback: the latch goes down, the turn-detection events become no-ops, and the silence-boundary path runs unchanged. You can leave the `flux` family selected or move back to `nova-3`; either is a working configuration.
|
|
234
235
|
|
|
235
236
|
Runtime fallback needs no action. A Flux stream that never emits `turn-end` is caught by the fail-open deadline and the utterance replays onto the silence path, so an outage degrades to today's behavior rather than to a hung turn.
|
|
236
237
|
|
|
@@ -37,6 +37,8 @@ assistant contacts list --role contact
|
|
|
37
37
|
assistant contacts list --role guardian
|
|
38
38
|
assistant contacts list --channel-type telegram
|
|
39
39
|
assistant contacts get <contactId>
|
|
40
|
+
gateway contacts list
|
|
41
|
+
gateway contacts get <contactId>
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
### Via HTTP API
|
|
@@ -142,6 +144,18 @@ sqlite3 "file:$GW_DB?mode=ro" \
|
|
|
142
144
|
|
|
143
145
|
`verification_purpose` is `guardian` for guardian binding flows and `trusted_contact` for the access-request handshake.
|
|
144
146
|
|
|
147
|
+
## 3b. Contact writes
|
|
148
|
+
|
|
149
|
+
All write operations on contacts are gateway-owned. Use the gateway CLI, not
|
|
150
|
+
`assistant contacts`. From the host, run the same command via
|
|
151
|
+
`vellum exec --service gateway --`.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
gateway contacts set-risk-threshold <contactId> --threshold high
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`inherit` clears the risk ceiling so the contact follows room and trust-class settings. Stored vocabulary is `none`, `low`, `medium`, or `high`.
|
|
158
|
+
|
|
145
159
|
## 4. Force-Revoke a Trusted Contact
|
|
146
160
|
|
|
147
161
|
### Via CLI (preferred)
|
|
@@ -29,7 +29,7 @@ This ensures the exhaustive switch in `daemon-batch-transcriber.ts` produces a c
|
|
|
29
29
|
|
|
30
30
|
- Append the new provider ID string to the `VALID_STT_PROVIDERS` tuple.
|
|
31
31
|
|
|
32
|
-
The `services.stt.providers` map uses a sparse `z.record(z.string(), ...)` schema, so adding a new provider does **not** require a workspace migration to seed a `services.stt.providers.<id>` entry. Users only need to set `services.stt.provider` to the new ID and supply credentials.
|
|
32
|
+
The `services.stt.providers` map uses a sparse `z.record(z.string(), ...)` schema, so adding a new provider does **not** require a workspace migration to seed a `services.stt.providers.<id>` entry. Users only need to set `services.stt.provider` (or a single consumer's `services.stt.roles.<role>`, see step 7) to the new ID and supply credentials.
|
|
33
33
|
|
|
34
34
|
**Language handling.** `services.stt.language` is resolved centrally in `resolveStreamingTranscriber()` (and in `resolveBatchTranscriber()` for the daemon-batch boundary), so a new adapter inherits it for free: accept a `language` option in the adapter's constructor and forward it to the provider. If the provider auto-detects natively and has no language parameter (as Gemini and Whisper do), accept nothing and let the resolver's value be ignored; document that choice in the adapter, because "no language param" means auto-detect for some providers and _English_ for others (Deepgram), and that difference is an easy source of silent wrong-language transcription. A provider that supports both batch and streaming must forward the language on **both** paths, not just the streaming one.
|
|
35
35
|
|
|
@@ -84,12 +84,16 @@ Clients derive shared-vs-exclusive key behavior from the catalog automatically:
|
|
|
84
84
|
|
|
85
85
|
## 7. Verify unified STT architecture
|
|
86
86
|
|
|
87
|
-
`services.stt
|
|
87
|
+
The `services.stt` block is the single source of truth for all STT routing, including telephony. There is no separate telephony STT config path.
|
|
88
|
+
|
|
89
|
+
Routing has two levels. `services.stt.provider`, plus that provider's `services.stt.providers.<id>.model` family, is the global selection every consumer falls back to. `services.stt.roles.<role>` overrides it for one consumer, naming a `{provider, model?}` pair. The roles are `liveVoice`, `telephony`, `dictation`, `watch` and `batch`; `src/stt/roles.ts` maps each to the boundaries its call sites resolve on, and `ARCHITECTURE.md` lists the consumers per role. A new provider needs no per-role registration: a role may name any provider in the catalog whose row covers the boundaries that role requires.
|
|
88
90
|
|
|
89
91
|
Before submitting the PR, verify that:
|
|
90
92
|
|
|
91
|
-
1. **No stale config references
|
|
93
|
+
1. **No stale config references**: grep for any references to a separate telephony transcription config. Telephony transcription runs daemon-side over the Twilio media-stream transport (`src/calls/media-stream-stt-session.ts`), which resolves through `resolveStreamingTranscriber()`/`resolveBatchTranscriber()` under the `telephony` role like every other boundary resolves under its own.
|
|
92
94
|
|
|
93
95
|
2. **Provider catalog telephony metadata** — the new provider's catalog entry (step 1) declares `telephonyMode` and `supportedBoundaries`; these are the single source of truth for the telephony capability check (`resolveTelephonySttCapability()`) and for streaming-vs-batch mode selection in the media-stream STT session. No per-provider routing maps exist.
|
|
94
96
|
|
|
95
|
-
3. **
|
|
97
|
+
3. **Role capability follows the catalog row**: `sttRoleCapabilityGap()` reads `supportedBoundaries` and `telephonyMode` from the row the pair resolves to (`sttCatalogKeyFor(provider, model)`), so a model family that drops a boundary must be its own catalog row. Get the row right and config validation rejects incapable role pairs on its own; nothing per-role needs writing.
|
|
98
|
+
|
|
99
|
+
4. **No duplicate wiring**: a provider should appear only once in `services.stt`. The telephony layer consumes the same catalog, and a role selects from it; there is no second registration step for telephony or for any role.
|
package/knip.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vellumai/avatar-manifest",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "bunx tsc --noEmit",
|
|
12
|
+
"test": "bun test src/"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.11",
|
|
16
|
+
"typescript": "5.9.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AVATAR_FIELD_MAX_LENGTH,
|
|
5
|
+
deriveAvatarFromLegacyFiles,
|
|
6
|
+
parseAvatarManifest,
|
|
7
|
+
resolveAvatarFromFiles,
|
|
8
|
+
} from "../manifest.js";
|
|
9
|
+
|
|
10
|
+
const overlong = "x".repeat(AVATAR_FIELD_MAX_LENGTH + 1);
|
|
11
|
+
|
|
12
|
+
const traits = { bodyShape: "round", eyeStyle: "dot", color: "#abc" };
|
|
13
|
+
const image = { updatedAt: "2026-01-01T00:00:00.000Z", etag: "abc" };
|
|
14
|
+
|
|
15
|
+
describe("parseAvatarManifest", () => {
|
|
16
|
+
test("accepts a character manifest", () => {
|
|
17
|
+
expect(
|
|
18
|
+
parseAvatarManifest({ kind: "character", traits, source: "builder" }),
|
|
19
|
+
).toEqual({ kind: "character", traits, source: "builder", image: null });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("accepts an image manifest", () => {
|
|
23
|
+
expect(parseAvatarManifest({ kind: "image", image })).toEqual({
|
|
24
|
+
kind: "image",
|
|
25
|
+
traits: null,
|
|
26
|
+
source: null,
|
|
27
|
+
image,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("accepts kind none without a payload", () => {
|
|
32
|
+
expect(parseAvatarManifest({ kind: "none" })).toEqual({
|
|
33
|
+
kind: "none",
|
|
34
|
+
traits: null,
|
|
35
|
+
source: null,
|
|
36
|
+
image: null,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("rejects an overlong trait or image field", () => {
|
|
41
|
+
expect(
|
|
42
|
+
parseAvatarManifest({
|
|
43
|
+
kind: "character",
|
|
44
|
+
traits: { ...traits, color: overlong },
|
|
45
|
+
}),
|
|
46
|
+
).toBeNull();
|
|
47
|
+
expect(
|
|
48
|
+
parseAvatarManifest({
|
|
49
|
+
kind: "image",
|
|
50
|
+
image: { ...image, etag: overlong },
|
|
51
|
+
}),
|
|
52
|
+
).toBeNull();
|
|
53
|
+
expect(
|
|
54
|
+
parseAvatarManifest({
|
|
55
|
+
kind: "character",
|
|
56
|
+
traits: { ...traits, color: "x".repeat(AVATAR_FIELD_MAX_LENGTH) },
|
|
57
|
+
}),
|
|
58
|
+
).not.toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("normalizes an unknown source to null", () => {
|
|
62
|
+
expect(
|
|
63
|
+
parseAvatarManifest({ kind: "image", image, source: "unknown" }),
|
|
64
|
+
).toEqual({ kind: "image", traits: null, source: null, image });
|
|
65
|
+
expect(parseAvatarManifest({ kind: "none", source: 7 })).toEqual({
|
|
66
|
+
kind: "none",
|
|
67
|
+
traits: null,
|
|
68
|
+
source: null,
|
|
69
|
+
image: null,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("drops payload irrelevant to the kind", () => {
|
|
74
|
+
expect(
|
|
75
|
+
parseAvatarManifest({ kind: "image", image, traits: { bodyShape: 1 } }),
|
|
76
|
+
).toEqual({ kind: "image", traits: null, source: null, image });
|
|
77
|
+
expect(
|
|
78
|
+
parseAvatarManifest({ kind: "character", traits, image: "stale" }),
|
|
79
|
+
).toEqual({ kind: "character", traits, source: null, image: null });
|
|
80
|
+
expect(parseAvatarManifest({ kind: "none", traits, image })).toEqual({
|
|
81
|
+
kind: "none",
|
|
82
|
+
traits: null,
|
|
83
|
+
source: null,
|
|
84
|
+
image: null,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test.each([
|
|
89
|
+
["non-object", "nope"],
|
|
90
|
+
["missing kind", {}],
|
|
91
|
+
["invalid kind", { kind: "sprite", traits }],
|
|
92
|
+
["character without traits", { kind: "character" }],
|
|
93
|
+
[
|
|
94
|
+
"character with incomplete traits",
|
|
95
|
+
{ kind: "character", traits: { bodyShape: "round" } },
|
|
96
|
+
],
|
|
97
|
+
["image without meta", { kind: "image" }],
|
|
98
|
+
["image with empty etag", { kind: "image", image: { ...image, etag: "" } }],
|
|
99
|
+
])("rejects %s", (_label, value) => {
|
|
100
|
+
expect(parseAvatarManifest(value)).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("deriveAvatarFromLegacyFiles", () => {
|
|
105
|
+
test("overlong traits are invalid", () => {
|
|
106
|
+
expect(
|
|
107
|
+
deriveAvatarFromLegacyFiles({
|
|
108
|
+
traitsJson: { ...traits, bodyShape: overlong },
|
|
109
|
+
hasImage: true,
|
|
110
|
+
}),
|
|
111
|
+
).toEqual({ kind: "image" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("valid traits win over a present image", () => {
|
|
115
|
+
expect(
|
|
116
|
+
deriveAvatarFromLegacyFiles({ traitsJson: traits, hasImage: true }),
|
|
117
|
+
).toEqual({ kind: "character", traits });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("invalid traits fall back to the image", () => {
|
|
121
|
+
expect(
|
|
122
|
+
deriveAvatarFromLegacyFiles({
|
|
123
|
+
traitsJson: { bodyShape: 1 },
|
|
124
|
+
hasImage: true,
|
|
125
|
+
}),
|
|
126
|
+
).toEqual({ kind: "image" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("nothing usable yields none", () => {
|
|
130
|
+
expect(
|
|
131
|
+
deriveAvatarFromLegacyFiles({ traitsJson: undefined, hasImage: false }),
|
|
132
|
+
).toEqual({ kind: "none" });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("resolveAvatarFromFiles", () => {
|
|
137
|
+
test("a valid manifest decides regardless of sidecars", () => {
|
|
138
|
+
expect(
|
|
139
|
+
resolveAvatarFromFiles({
|
|
140
|
+
manifestJson: { kind: "none" },
|
|
141
|
+
traitsJson: traits,
|
|
142
|
+
hasImage: true,
|
|
143
|
+
}),
|
|
144
|
+
).toEqual({ kind: "none" });
|
|
145
|
+
expect(
|
|
146
|
+
resolveAvatarFromFiles({
|
|
147
|
+
manifestJson: { kind: "image", image },
|
|
148
|
+
traitsJson: traits,
|
|
149
|
+
hasImage: true,
|
|
150
|
+
}),
|
|
151
|
+
).toEqual({ kind: "image", image });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("a partial image manifest falls back to traits-first derivation", () => {
|
|
155
|
+
expect(
|
|
156
|
+
resolveAvatarFromFiles({
|
|
157
|
+
manifestJson: { kind: "image" },
|
|
158
|
+
traitsJson: traits,
|
|
159
|
+
hasImage: true,
|
|
160
|
+
}),
|
|
161
|
+
).toEqual({ kind: "character", traits });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a legacy image carries no manifest metadata", () => {
|
|
165
|
+
expect(
|
|
166
|
+
resolveAvatarFromFiles({
|
|
167
|
+
manifestJson: undefined,
|
|
168
|
+
traitsJson: undefined,
|
|
169
|
+
hasImage: true,
|
|
170
|
+
}),
|
|
171
|
+
).toEqual({ kind: "image", image: null });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
symlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { AVATAR_SIDECAR_MAX_BYTES, readWorkspaceAvatar } from "../read.js";
|
|
13
|
+
|
|
14
|
+
const traits = { bodyShape: "round", eyeStyle: "dot", color: "#abc" };
|
|
15
|
+
|
|
16
|
+
describe("readWorkspaceAvatar", () => {
|
|
17
|
+
let workspaceDir: string;
|
|
18
|
+
let avatarDir: string;
|
|
19
|
+
|
|
20
|
+
const write = (name: string, contents: string): void => {
|
|
21
|
+
mkdirSync(avatarDir, { recursive: true });
|
|
22
|
+
writeFileSync(join(avatarDir, name), contents);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "avatar-manifest-"));
|
|
27
|
+
avatarDir = join(workspaceDir, "data", "avatar");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("missing avatar dir yields none", () => {
|
|
35
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({ kind: "none" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("manifest character returns its traits", () => {
|
|
39
|
+
write("avatar.json", JSON.stringify({ kind: "character", traits }));
|
|
40
|
+
write("avatar-image.png", "png");
|
|
41
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({
|
|
42
|
+
kind: "character",
|
|
43
|
+
traits,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("image resolves to the PNG path", () => {
|
|
48
|
+
write("avatar-image.png", "png");
|
|
49
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({
|
|
50
|
+
kind: "image",
|
|
51
|
+
imagePath: join(avatarDir, "avatar-image.png"),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("corrupt manifest falls back to the traits sidecar", () => {
|
|
56
|
+
write("avatar.json", "{ not json");
|
|
57
|
+
write("character-traits.json", JSON.stringify(traits));
|
|
58
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({
|
|
59
|
+
kind: "character",
|
|
60
|
+
traits,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("oversized sidecars are treated as absent", () => {
|
|
65
|
+
const padded = JSON.stringify({
|
|
66
|
+
kind: "character",
|
|
67
|
+
traits,
|
|
68
|
+
pad: "x".repeat(AVATAR_SIDECAR_MAX_BYTES),
|
|
69
|
+
});
|
|
70
|
+
write("avatar.json", padded);
|
|
71
|
+
write("character-traits.json", JSON.stringify({ ...traits, pad: padded }));
|
|
72
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({ kind: "none" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a sidecar at the cap still reads", () => {
|
|
76
|
+
const body = JSON.stringify({ kind: "character", traits, pad: "" });
|
|
77
|
+
write(
|
|
78
|
+
"avatar.json",
|
|
79
|
+
body.replace(
|
|
80
|
+
'"pad":""',
|
|
81
|
+
`"pad":"${"x".repeat(AVATAR_SIDECAR_MAX_BYTES - body.length)}"`,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({
|
|
85
|
+
kind: "character",
|
|
86
|
+
traits,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a symlinked avatar dir is treated as absent", () => {
|
|
91
|
+
const foreign = mkdtempSync(join(tmpdir(), "foreign-avatar-"));
|
|
92
|
+
writeFileSync(
|
|
93
|
+
join(foreign, "character-traits.json"),
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
bodyShape: "blob",
|
|
96
|
+
eyeStyle: "curious",
|
|
97
|
+
color: "green",
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
mkdirSync(join(workspaceDir, "data"), { recursive: true });
|
|
101
|
+
symlinkSync(foreign, avatarDir);
|
|
102
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({ kind: "none" });
|
|
103
|
+
rmSync(foreign, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("symlinked sidecars are treated as absent", () => {
|
|
107
|
+
const outside = join(workspaceDir, "outside.json");
|
|
108
|
+
writeFileSync(outside, JSON.stringify({ kind: "character", traits }));
|
|
109
|
+
mkdirSync(avatarDir, { recursive: true });
|
|
110
|
+
symlinkSync(outside, join(avatarDir, "avatar.json"));
|
|
111
|
+
symlinkSync(outside, join(avatarDir, "character-traits.json"));
|
|
112
|
+
expect(readWorkspaceAvatar(workspaceDir)).toEqual({ kind: "none" });
|
|
113
|
+
});
|
|
114
|
+
});
|