@vellumai/assistant 0.12.1 → 0.12.2-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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +20 -21
- package/docs/architecture/turn-actor.md +15 -8
- package/docs/guardian-request-flow.md +16 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +360 -51
- package/package.json +3 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/anthropic-provider.test.ts +0 -63
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
- package/src/__tests__/assistant-event-hub.test.ts +44 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +19 -7
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +250 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/cu-unified-flow.test.ts +14 -4
- package/src/__tests__/document-sync-tags.test.ts +25 -0
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/heartbeat-service.test.ts +56 -40
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-cu-proxy.test.ts +596 -0
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
- package/src/__tests__/oauth-commands-routes.test.ts +168 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/openai-provider.test.ts +1 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-callback-registration.test.ts +8 -16
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
- package/src/__tests__/qdrant-manager.test.ts +132 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/service-account-token.test.ts +247 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/unicode.test.ts +13 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +96 -10
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/agent/loop.ts +1 -0
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/events/notification-intent.ts +2 -0
- package/src/api/events/user-message-echo.ts +7 -1
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/avatar/identity-avatar.ts +7 -2
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +155 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +92 -72
- package/src/calls/voice-triage-escalate.ts +178 -59
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/bash.test.ts +53 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/bash.help.ts +8 -13
- package/src/cli/commands/bash.ts +7 -25
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +255 -0
- package/src/config/__tests__/platform-rehydration.test.ts +99 -9
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
- package/src/config/bundled-skills/schedule/SKILL.md +3 -4
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +33 -13
- package/src/config/loader.ts +2 -0
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +201 -0
- package/src/config/platform-rehydration.ts +11 -27
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/credential-health/credential-health-service.ts +11 -17
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-error.ts +3 -2
- package/src/daemon/conversation-media-retry.ts +2 -1
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-runtime-assembly.ts +20 -7
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +21 -10
- package/src/daemon/conversation-tool-setup.ts +45 -59
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/dictation-profile-store.ts +2 -1
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +6 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-cu-proxy.ts +224 -19
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-proxy-preactivation.ts +2 -1
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/orphan-reaper.ts +1 -1
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/reaction-history-render.ts +2 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +122 -0
- package/src/desktop/desktop-chrome-policy.test.ts +79 -0
- package/src/desktop/desktop-chrome-policy.ts +47 -0
- package/src/desktop/desktop-chrome-session.ts +37 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +308 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +730 -0
- package/src/desktop/desktop-session-manager.ts +850 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-drag.ts +177 -0
- package/src/desktop/desktop-window-manager-config.test.ts +230 -0
- package/src/desktop/desktop-window-manager-config.ts +238 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/heartbeat/heartbeat-service.ts +21 -3
- package/src/home/__tests__/feed-writer.test.ts +22 -0
- package/src/home/feed-writer.ts +11 -2
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/protocol.test.ts +39 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/__tests__/session-controls.test.ts +79 -0
- package/src/live-voice/live-voice-photo.ts +25 -6
- package/src/live-voice/live-voice-session.ts +297 -699
- package/src/live-voice/protocol.ts +60 -2
- package/src/live-voice/session-controls.ts +111 -0
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/guardian-feed-projection.ts +125 -5
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/notifications/schedule-result-producer.ts +66 -22
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
- package/src/oauth/byo-connection.test.ts +105 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +100 -0
- package/src/oauth/connection-resolver.ts +120 -3
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/scope-utils.ts +24 -0
- package/src/oauth/seed-providers.ts +314 -27
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-queries.ts +2 -2
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/embeddings/qdrant-manager.ts +163 -18
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +4 -2
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
- package/src/plugins/defaults/memory/context-search/format.ts +3 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +28 -2
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
- package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
- package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
- package/src/plugins/defaults/memory/v3/card.ts +3 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
- package/src/plugins/defaults/memory/v3/sections.ts +9 -3
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/__tests__/retry-callsite.test.ts +41 -0
- package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
- package/src/providers/anthropic/client.ts +0 -47
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
- package/src/providers/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/endpoint-probe.ts +10 -1
- package/src/providers/inference/resolve-auth.ts +22 -4
- package/src/providers/inference/service-account-token.ts +248 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +212 -79
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/providers/opencode/client.test.ts +73 -4
- package/src/providers/opencode/client.ts +60 -3
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/outbound-request-sanitize.ts +96 -0
- package/src/providers/retry.ts +30 -19
- package/src/providers/unparseable-tool-args.ts +5 -1
- package/src/runtime/AGENTS.md +3 -1
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/host-observe.test.ts +40 -1
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/agent-wake.ts +2 -1
- package/src/runtime/assistant-event-hub.ts +21 -3
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/host-observe.ts +11 -1
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +90 -2
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/conversation-routes.ts +16 -3
- package/src/runtime/routes/credential-routes.ts +20 -14
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +61 -4
- package/src/runtime/routes/events-routes.ts +4 -0
- package/src/runtime/routes/host-cu-routes.ts +6 -0
- package/src/runtime/routes/index.ts +4 -2
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +6 -20
- package/src/runtime/routes/oauth-commands-routes.ts +50 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/routes/workspace-routes.test.ts +55 -26
- package/src/runtime/routes/workspace-routes.ts +46 -63
- package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
- package/src/runtime/routes/workspace-tree-walk.ts +240 -0
- package/src/runtime/routes/workspace-utils.ts +17 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/run-script.ts +2 -1
- package/src/schedule/schedule-store.ts +9 -2
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/security/untrusted-content.ts +3 -1
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
- package/src/tools/browser/network-recorder.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +121 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/filesystem/search.ts +2 -1
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/network/web-search.ts +6 -1
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/skills/scaffold-managed.ts +61 -3
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/types/host-capabilities.ts +5 -3
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/util/unicode.ts +15 -1
- package/src/watch/watch-retro.ts +6 -8
- package/src/watch/watch-timeline.ts +2 -2
- package/src/workflows/run-manager.ts +2 -1
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +6 -55
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/runtime/routes/debug-bash-routes.ts +0 -173
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
package/src/providers/retry.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
resolveCallSiteConfig,
|
|
5
3
|
selectWinningProfile,
|
|
@@ -10,7 +8,6 @@ import {
|
|
|
10
8
|
sanitizeUsageMetadataValue,
|
|
11
9
|
} from "../usage/attribution.js";
|
|
12
10
|
import { resolveSubagentAttribution } from "../usage/subagent-attribution.js";
|
|
13
|
-
import { getExistingDeviceId } from "../util/device-id.js";
|
|
14
11
|
import {
|
|
15
12
|
type ProviderCredentialSource,
|
|
16
13
|
ProviderError,
|
|
@@ -43,7 +40,8 @@ import {
|
|
|
43
40
|
isAdaptiveThinkingOnlyModel,
|
|
44
41
|
isAdaptiveThinkingUnsupportedModel,
|
|
45
42
|
} from "./model-catalog.js";
|
|
46
|
-
import {
|
|
43
|
+
import { resolveOpenCodeRequestHeaders } from "./opencode/client.js";
|
|
44
|
+
import { sanitizeOutboundRequest } from "./outbound-request-sanitize.js";
|
|
47
45
|
import { dispatchProviderResolvable } from "./provider-resolvability.js";
|
|
48
46
|
import {
|
|
49
47
|
isThinkingConfigAdaptive,
|
|
@@ -646,18 +644,11 @@ function normalizeSendMessageOptions(
|
|
|
646
644
|
typeof config.conversationId === "string"
|
|
647
645
|
? config.conversationId
|
|
648
646
|
: undefined;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
// session header instead of persisting a new ID for this purpose.
|
|
655
|
-
fallbackSessionId: getExistingDeviceId() ?? undefined,
|
|
656
|
-
requestId: randomUUID(),
|
|
657
|
-
});
|
|
658
|
-
if (Object.keys(requestHeaders).length > 0) {
|
|
659
|
-
nextConfig.requestHeaders = requestHeaders;
|
|
660
|
-
}
|
|
647
|
+
// Profile probes and background call paths (memory/commit-message
|
|
648
|
+
// enrichment, proactivity, workflow runs) have no conversationId; the
|
|
649
|
+
// fallback keeps a session header on every request since zen/go rejects
|
|
650
|
+
// requests without one.
|
|
651
|
+
nextConfig.requestHeaders = resolveOpenCodeRequestHeaders(conversationId);
|
|
661
652
|
}
|
|
662
653
|
|
|
663
654
|
// `overrideProfile`, `forceOverrideProfile`, `selectionSeed`,
|
|
@@ -1142,8 +1133,9 @@ export class RetryProvider implements Provider {
|
|
|
1142
1133
|
|
|
1143
1134
|
// Forward the optional token-counting endpoint so the capability survives
|
|
1144
1135
|
// the wrapper chain (callers gate on its presence). Bound straight to the
|
|
1145
|
-
// inner provider
|
|
1146
|
-
//
|
|
1136
|
+
// inner provider behind the same surrogate sanitizer as `sendMessage` —
|
|
1137
|
+
// count_tokens is a cheap separate endpoint and its caller already falls
|
|
1138
|
+
// back on error, so it needs no retry wrapping.
|
|
1147
1139
|
// Deliberately not re-bound when a credential refresh swaps `inner`: every
|
|
1148
1140
|
// outer wrapper snapshots this the same way at construction, so a re-bind
|
|
1149
1141
|
// here would never reach callers. count_tokens on the pre-refresh credential
|
|
@@ -1188,7 +1180,18 @@ export class RetryProvider implements Provider {
|
|
|
1188
1180
|
this.inner = inner;
|
|
1189
1181
|
this.name = inner.name;
|
|
1190
1182
|
if (inner.countInputTokens) {
|
|
1191
|
-
|
|
1183
|
+
const countInputTokens = inner.countInputTokens.bind(inner);
|
|
1184
|
+
this.countInputTokens = (messages, systemPrompt, tools) => {
|
|
1185
|
+
const clean = sanitizeOutboundRequest(this.name, {
|
|
1186
|
+
messages,
|
|
1187
|
+
options: { systemPrompt, tools },
|
|
1188
|
+
});
|
|
1189
|
+
return countInputTokens(
|
|
1190
|
+
clean.messages,
|
|
1191
|
+
clean.options?.systemPrompt ?? systemPrompt,
|
|
1192
|
+
clean.options?.tools,
|
|
1193
|
+
);
|
|
1194
|
+
};
|
|
1192
1195
|
}
|
|
1193
1196
|
}
|
|
1194
1197
|
|
|
@@ -1262,6 +1265,14 @@ export class RetryProvider implements Provider {
|
|
|
1262
1265
|
let credentialRefreshAttempted = false;
|
|
1263
1266
|
let correctiveResendAttempted = false;
|
|
1264
1267
|
let fallbackAttempted = false;
|
|
1268
|
+
|
|
1269
|
+
// Every attempt below, the backup route included, sends what this
|
|
1270
|
+
// wrapper was handed, so an orphaned UTF-16 surrogate is stripped here
|
|
1271
|
+
// once rather than rejected by the upstream parser on every resend.
|
|
1272
|
+
({ messages, options } = sanitizeOutboundRequest(this.name, {
|
|
1273
|
+
messages,
|
|
1274
|
+
options,
|
|
1275
|
+
}));
|
|
1265
1276
|
let messagesForAttempt = messages;
|
|
1266
1277
|
|
|
1267
1278
|
const normalizedOptions = normalizeSendMessageOptions(this.name, options, {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* an error tool result, so the model sees the failure and can retry, instead
|
|
13
13
|
* of a tool executing with garbage input.
|
|
14
14
|
*/
|
|
15
|
+
|
|
16
|
+
import { safeStringSlice } from "../util/unicode.js";
|
|
15
17
|
const UNPARSEABLE_TOOL_ARGS_KEY = "_raw";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -56,7 +58,9 @@ export function unparseableToolArgsMessage(
|
|
|
56
58
|
): string {
|
|
57
59
|
const PREVIEW_LIMIT = 200;
|
|
58
60
|
const preview =
|
|
59
|
-
raw.length > PREVIEW_LIMIT
|
|
61
|
+
raw.length > PREVIEW_LIMIT
|
|
62
|
+
? `${safeStringSlice(raw, 0, PREVIEW_LIMIT)}…`
|
|
63
|
+
: raw;
|
|
60
64
|
return (
|
|
61
65
|
`Error: the arguments for "${toolName}" were not valid JSON — the argument stream was malformed or truncated, so the tool was NOT executed. ` +
|
|
62
66
|
`Received: ${preview || "(empty)"}\n` +
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -12,6 +12,8 @@ The single HTTP send endpoint is `POST /v1/messages`. Key behaviors:
|
|
|
12
12
|
|
|
13
13
|
Do NOT add new send endpoints. All message ingress should go through `POST /v1/messages` (HTTP).
|
|
14
14
|
|
|
15
|
+
`POST /v1/channels/send` is not ingress: it is outbound delivery, the assistant posting text to a channel chat. It runs `sendChannelText` (`runtime/channel-send.ts`), the one send implementation for every channel whose transport declares proactive addressing (`ChannelTransport.addressFor`), and the messaging tool's channel branch runs the same function in-process, as the CLI's channel send door does over the route. A model-directed channel send goes through that function or it is not recorded; do not add a provider-side send for a channel that has a transport. The route names no sending turn: only the in-process tool call can claim a send landed in the turn's own chat, so everything sent through the route is recorded.
|
|
16
|
+
|
|
15
17
|
**`interrupt-on-send`: a send may stop the running turn instead of queueing behind it.** With the flag on, a busy conversation is handed to `interruptRunningTurn` (`daemon/conversation-interrupt.ts`), which aborts the turn with `preempted_by_new_message`, waits for it to release the processing lock AND for its turn-boundary commit to finish, repairs the abandoned `tool_use` blocks with a persisted synthetic `tool_result`, and answers `released`. The route then falls through to the code an idle conversation already uses, so there is one turn dispatch rather than a second one for interrupts. The queue is not discarded: it may hold another actor's messages.
|
|
16
18
|
|
|
17
19
|
**The handover runs off the response, not on it.** Fire-and-forget is not suspended for an interrupt. The eligibility decision is synchronous (`classifyInterruptEligibility`), so the route answers `202 { accepted, requestId }` the moment it accepts the message, then runs the abort, the release wait, the commit wait, the repair, the persist and the dispatch in a detached task. Holding the request would cost the abort budget (`ABORT_RELEASE_WAIT_MS`) plus the turn-boundary commit wait, long enough for a client to time out and retry a message the daemon is still placing. That 202 carries `messageId` as well as `requestId`, and they are the same value: a user turn persists its row under its `requestId` (`persistUserMessage` passes `id: requestId`), which is what lets the id be answered before the row exists. Both are required, not decorative: `postChatMessage` rejects an accepted, non-queued response without a string `messageId` and the client drops the optimistic row. Clients reconcile that row on `user_message_echo`. Two rules for anything added to that task: it must not be able to drop the message (every failure path falls back to `queueSend`, which never takes the enqueue's idle fast path, and a queue that refuses the fallback is reported to the sender as a `QUEUE_FULL` error keyed on the `requestId` the 202 carried, since the 429 itself reaches nobody), and a duplicate `clientMessageId` is answered from the existing row BEFORE the abort, since the insert's own dedup settles too late to un-abort a turn.
|
|
@@ -95,7 +97,7 @@ Host CU allows the assistant to proxy computer-use actions (screenshots, mouse/k
|
|
|
95
97
|
|
|
96
98
|
- **Discovery**: Clients discover pending host CU requests via SSE events (`host_cu_request`) which include a `requestId`.
|
|
97
99
|
- **Resolution**: Clients execute the CU action on the host and respond via:
|
|
98
|
-
- `POST /v1/host-cu-result
|
|
100
|
+
- `POST /v1/host-cu-result`: `{ requestId, axTree?, axDiff?, screenshot?, screenshotWidthPx?, screenshotHeightPx?, screenWidthPt?, screenHeightPt?, executionResult?, executionError?, secondaryWindows?, userGuidance?, timings? }`, where `timings` is optional per-phase helper timings in milliseconds
|
|
99
101
|
- **Tracking**: Uses the same `pending-interactions` tracker as the other host proxy types, with `kind: "host_cu"`. Registration happens in `conversation-routes.ts` and the route handler is in `host-cu-routes.ts`.
|
|
100
102
|
- **Conversation-agnostic observation**: `observeHostScreen()` in `host-observe.ts` issues a `computer_use_observe` request with no conversation, for callers outside an agent turn (`HostCuProxy` only exists inside one). It takes the initiating actor's principal id and reaches only that actor's own `host_cu` clients: `pickSameUserAutoResolve` picks the default target and `enforceSameActorOrErrorResult` gates an explicitly named one, both before the request is registered or broadcast. Its pending interaction carries no `conversationId`, so `host-cu-routes.ts` hands the raw observation fields straight to the waiting caller instead of routing through a conversation's CU proxy. On the desktop side the ordinary `host_cu` executor services the request.
|
|
101
103
|
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ChannelTransport,
|
|
5
|
+
ProactiveTarget,
|
|
6
|
+
} from "../../messaging/providers/channel-transport.js";
|
|
7
|
+
|
|
8
|
+
// The seam under test is the send function: it must ask the transport for
|
|
9
|
+
// the address, deliver through it, and record only what the channel
|
|
10
|
+
// acknowledged. The transport is a fake registered by channel id; the
|
|
11
|
+
// recording dependencies are stubbed so the tests read what was recorded.
|
|
12
|
+
|
|
13
|
+
const deliverCalls: Array<{
|
|
14
|
+
ctx: { callbackUrl: string; params: Record<string, string> };
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
}> = [];
|
|
17
|
+
let deliverResult: Record<string, unknown> = {
|
|
18
|
+
ok: true,
|
|
19
|
+
ts: "1700000000.000100",
|
|
20
|
+
messageIds: ["1700000000.000100"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function fakeTransport(
|
|
24
|
+
channel: "slack" | "telegram",
|
|
25
|
+
opts: { binds?: boolean; addressable?: boolean; refuses?: boolean } = {},
|
|
26
|
+
): ChannelTransport {
|
|
27
|
+
const transport: ChannelTransport = {
|
|
28
|
+
channel,
|
|
29
|
+
async deliver(ctx, payload) {
|
|
30
|
+
deliverCalls.push({
|
|
31
|
+
ctx: { callbackUrl: ctx.callbackUrl, params: { ...ctx.params } },
|
|
32
|
+
payload: payload as unknown as Record<string, unknown>,
|
|
33
|
+
});
|
|
34
|
+
return deliverResult as unknown as Awaited<
|
|
35
|
+
ReturnType<ChannelTransport["deliver"]>
|
|
36
|
+
>;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
if (opts.addressable !== false) {
|
|
40
|
+
transport.addressFor = (target: ProactiveTarget) => {
|
|
41
|
+
if (opts.refuses) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const threadId = target.threadId?.trim();
|
|
45
|
+
const params: Record<string, string> = threadId ? { threadId } : {};
|
|
46
|
+
return {
|
|
47
|
+
ctx: { callbackUrl: `/deliver/${channel}`, params },
|
|
48
|
+
chatId: target.chatId,
|
|
49
|
+
...(threadId ? { threadId } : {}),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (opts.binds) {
|
|
54
|
+
(
|
|
55
|
+
transport as { bindsChatOnProactiveSend?: boolean }
|
|
56
|
+
).bindsChatOnProactiveSend = true;
|
|
57
|
+
}
|
|
58
|
+
return transport;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let transports: Record<string, ChannelTransport> = {};
|
|
62
|
+
mock.module("../../messaging/providers/index.js", () => ({
|
|
63
|
+
getTransportForChannel: (channel: string | undefined) =>
|
|
64
|
+
channel ? transports[channel] : undefined,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const resolveHomeMock = mock(async (_params: Record<string, unknown>) => ({
|
|
68
|
+
conversationId: "home-1",
|
|
69
|
+
createdNewConversation: false,
|
|
70
|
+
}));
|
|
71
|
+
const recordMock = mock(async (_post: Record<string, unknown>) => ({
|
|
72
|
+
messageId: "row-1",
|
|
73
|
+
}));
|
|
74
|
+
const bindCalls: Array<Record<string, unknown>> = [];
|
|
75
|
+
const getOrCreateCalls: string[] = [];
|
|
76
|
+
|
|
77
|
+
mock.module("../../notifications/conversation-pairing.js", () => ({
|
|
78
|
+
resolveProactiveHomeConversation: resolveHomeMock,
|
|
79
|
+
}));
|
|
80
|
+
mock.module("../../notifications/delivered-post-record.js", () => ({
|
|
81
|
+
recordDeliveredChannelPost: recordMock,
|
|
82
|
+
}));
|
|
83
|
+
mock.module("../../persistence/conversation-crud.js", () => ({
|
|
84
|
+
getConversation: (id: string) => ({ id, createdAt: 1700000000000 }),
|
|
85
|
+
}));
|
|
86
|
+
mock.module("../../persistence/conversation-disk-view.js", () => ({
|
|
87
|
+
syncMessageToDisk: () => {},
|
|
88
|
+
}));
|
|
89
|
+
let bindCreates = true;
|
|
90
|
+
let bindThrows = false;
|
|
91
|
+
mock.module("../../persistence/conversation-key-store.js", () => ({
|
|
92
|
+
getOrCreateConversation: (key: string) => {
|
|
93
|
+
getOrCreateCalls.push(key);
|
|
94
|
+
return { conversationId: `conv-for-${key}`, created: bindCreates };
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
const listChangedMock = mock((_reason: string) => {});
|
|
98
|
+
const actualSync = await import("../sync/resource-sync-events.js");
|
|
99
|
+
mock.module("../sync/resource-sync-events.js", () => ({
|
|
100
|
+
...actualSync,
|
|
101
|
+
publishConversationListChanged: listChangedMock,
|
|
102
|
+
}));
|
|
103
|
+
mock.module("../../persistence/delivery-crud.js", () => ({
|
|
104
|
+
buildScopedConversationKey: (
|
|
105
|
+
channel: string,
|
|
106
|
+
chatId: string,
|
|
107
|
+
threadId?: string | null,
|
|
108
|
+
) =>
|
|
109
|
+
threadId
|
|
110
|
+
? `asst:self:${channel}:${chatId}:thread:${threadId}`
|
|
111
|
+
: `asst:self:${channel}:${chatId}`,
|
|
112
|
+
}));
|
|
113
|
+
mock.module("../../persistence/external-conversation-store.js", () => ({
|
|
114
|
+
normalizeExternalThreadId: (value: string | null | undefined) => {
|
|
115
|
+
const trimmed = value?.trim();
|
|
116
|
+
return trimmed ? trimmed : null;
|
|
117
|
+
},
|
|
118
|
+
upsertOutboundBinding: (input: Record<string, unknown>) => {
|
|
119
|
+
if (bindThrows) {
|
|
120
|
+
throw new Error("binding store unavailable");
|
|
121
|
+
}
|
|
122
|
+
bindCalls.push(input);
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const {
|
|
127
|
+
ChannelNotAddressableError,
|
|
128
|
+
ChannelSendFailedError,
|
|
129
|
+
isProactivelyAddressable,
|
|
130
|
+
sendChannelText,
|
|
131
|
+
} = await import("../channel-send.js");
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
deliverCalls.length = 0;
|
|
135
|
+
bindCalls.length = 0;
|
|
136
|
+
getOrCreateCalls.length = 0;
|
|
137
|
+
bindCreates = true;
|
|
138
|
+
bindThrows = false;
|
|
139
|
+
listChangedMock.mockClear();
|
|
140
|
+
resolveHomeMock.mockClear();
|
|
141
|
+
recordMock.mockClear();
|
|
142
|
+
deliverResult = {
|
|
143
|
+
ok: true,
|
|
144
|
+
ts: "1700000000.000100",
|
|
145
|
+
messageIds: ["1700000000.000100"],
|
|
146
|
+
};
|
|
147
|
+
transports = {
|
|
148
|
+
slack: fakeTransport("slack"),
|
|
149
|
+
telegram: fakeTransport("telegram", { binds: true }),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const sender = {
|
|
154
|
+
conversationId: "conv-A",
|
|
155
|
+
executionChannel: "slack",
|
|
156
|
+
requesterChatId: "C123",
|
|
157
|
+
sourceThreadId: "1690000000.000001",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
describe("addressability", () => {
|
|
161
|
+
test("a channel is addressable exactly when its transport declares it", () => {
|
|
162
|
+
transports.a2a = fakeTransport("slack", { addressable: false });
|
|
163
|
+
expect(isProactivelyAddressable("slack")).toBe(true);
|
|
164
|
+
expect(isProactivelyAddressable("a2a")).toBe(false);
|
|
165
|
+
expect(isProactivelyAddressable("email")).toBe(false);
|
|
166
|
+
expect(isProactivelyAddressable("not-a-channel")).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("refuses before delivery when the channel has no addressing, or refuses the chat", async () => {
|
|
170
|
+
await expect(
|
|
171
|
+
sendChannelText({
|
|
172
|
+
channel: "email",
|
|
173
|
+
target: { kind: "chat", chatId: "x" },
|
|
174
|
+
text: "hi",
|
|
175
|
+
}),
|
|
176
|
+
).rejects.toBeInstanceOf(ChannelNotAddressableError);
|
|
177
|
+
|
|
178
|
+
transports.slack = fakeTransport("slack", { refuses: true });
|
|
179
|
+
await expect(
|
|
180
|
+
sendChannelText({
|
|
181
|
+
channel: "slack",
|
|
182
|
+
target: { kind: "chat", chatId: "C-unreachable" },
|
|
183
|
+
text: "hi",
|
|
184
|
+
}),
|
|
185
|
+
).rejects.toBeInstanceOf(ChannelNotAddressableError);
|
|
186
|
+
expect(deliverCalls).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("delivery through the transport", () => {
|
|
191
|
+
test("delivers to the address the transport resolves, with the rich render asked for", async () => {
|
|
192
|
+
const result = await sendChannelText({
|
|
193
|
+
channel: "slack",
|
|
194
|
+
target: { kind: "chat", chatId: "C123", threadId: "1690000000.000001" },
|
|
195
|
+
text: "hello",
|
|
196
|
+
renderRichly: true,
|
|
197
|
+
assistantId: "self",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(deliverCalls).toEqual([
|
|
201
|
+
{
|
|
202
|
+
ctx: {
|
|
203
|
+
callbackUrl: "/deliver/slack",
|
|
204
|
+
params: { threadId: "1690000000.000001" },
|
|
205
|
+
},
|
|
206
|
+
payload: {
|
|
207
|
+
chatId: "C123",
|
|
208
|
+
text: "hello",
|
|
209
|
+
renderRichly: true,
|
|
210
|
+
assistantId: "self",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
]);
|
|
214
|
+
expect(result).toMatchObject({
|
|
215
|
+
channel: "slack",
|
|
216
|
+
chatId: "C123",
|
|
217
|
+
threadId: "1690000000.000001",
|
|
218
|
+
messageIds: ["1700000000.000100"],
|
|
219
|
+
lastMessageId: "1700000000.000100",
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a send the channel does not acknowledge fails and records nothing", async () => {
|
|
224
|
+
deliverResult = { ok: false };
|
|
225
|
+
await expect(
|
|
226
|
+
sendChannelText({
|
|
227
|
+
channel: "slack",
|
|
228
|
+
target: { kind: "chat", chatId: "C123" },
|
|
229
|
+
text: "hello",
|
|
230
|
+
}),
|
|
231
|
+
).rejects.toBeInstanceOf(ChannelSendFailedError);
|
|
232
|
+
expect(recordMock).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("binds the chat's inbound conversation after acknowledgement, only where the transport declares it", async () => {
|
|
236
|
+
await sendChannelText({
|
|
237
|
+
channel: "telegram",
|
|
238
|
+
target: { kind: "chat", chatId: "123456789" },
|
|
239
|
+
text: "hello",
|
|
240
|
+
});
|
|
241
|
+
expect(getOrCreateCalls).toEqual(["asst:self:telegram:123456789"]);
|
|
242
|
+
expect(bindCalls).toEqual([
|
|
243
|
+
{
|
|
244
|
+
conversationId: "conv-for-asst:self:telegram:123456789",
|
|
245
|
+
sourceChannel: "telegram",
|
|
246
|
+
externalChatId: "123456789",
|
|
247
|
+
externalThreadId: null,
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
bindCalls.length = 0;
|
|
252
|
+
await sendChannelText({
|
|
253
|
+
channel: "slack",
|
|
254
|
+
target: { kind: "chat", chatId: "C123" },
|
|
255
|
+
text: "hello",
|
|
256
|
+
});
|
|
257
|
+
expect(bindCalls).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a topic send binds the topic's own conversation, the one its replies resolve to", async () => {
|
|
261
|
+
await sendChannelText({
|
|
262
|
+
channel: "telegram",
|
|
263
|
+
target: { kind: "chat", chatId: "123456789", threadId: "42" },
|
|
264
|
+
text: "hello",
|
|
265
|
+
});
|
|
266
|
+
expect(getOrCreateCalls).toEqual([
|
|
267
|
+
"asst:self:telegram:123456789:thread:42",
|
|
268
|
+
]);
|
|
269
|
+
expect(bindCalls).toEqual([
|
|
270
|
+
{
|
|
271
|
+
conversationId: "conv-for-asst:self:telegram:123456789:thread:42",
|
|
272
|
+
sourceChannel: "telegram",
|
|
273
|
+
externalChatId: "123456789",
|
|
274
|
+
externalThreadId: "42",
|
|
275
|
+
},
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("a conversation the binding mints reaches the list; one it finds does not", async () => {
|
|
280
|
+
await sendChannelText({
|
|
281
|
+
channel: "telegram",
|
|
282
|
+
target: { kind: "chat", chatId: "123456789", threadId: "42" },
|
|
283
|
+
text: "hello",
|
|
284
|
+
});
|
|
285
|
+
expect(listChangedMock).toHaveBeenCalledTimes(1);
|
|
286
|
+
expect(listChangedMock).toHaveBeenCalledWith("created");
|
|
287
|
+
|
|
288
|
+
listChangedMock.mockClear();
|
|
289
|
+
bindCreates = false;
|
|
290
|
+
await sendChannelText({
|
|
291
|
+
channel: "telegram",
|
|
292
|
+
target: { kind: "chat", chatId: "123456789", threadId: "42" },
|
|
293
|
+
text: "again",
|
|
294
|
+
});
|
|
295
|
+
expect(listChangedMock).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a binding that throws still leaves its minted conversation in the list", async () => {
|
|
299
|
+
bindThrows = true;
|
|
300
|
+
const result = await sendChannelText({
|
|
301
|
+
channel: "telegram",
|
|
302
|
+
target: { kind: "chat", chatId: "123456789", threadId: "42" },
|
|
303
|
+
text: "hello",
|
|
304
|
+
});
|
|
305
|
+
expect(bindCalls).toEqual([]);
|
|
306
|
+
expect(listChangedMock).toHaveBeenCalledWith("created");
|
|
307
|
+
expect(result.messageIds).toEqual(["1700000000.000100"]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("a failed send binds nothing, so it does not move where the next inbound lands", async () => {
|
|
311
|
+
deliverResult = { ok: false };
|
|
312
|
+
await expect(
|
|
313
|
+
sendChannelText({
|
|
314
|
+
channel: "telegram",
|
|
315
|
+
target: { kind: "chat", chatId: "123456789" },
|
|
316
|
+
text: "hello",
|
|
317
|
+
}),
|
|
318
|
+
).rejects.toBeInstanceOf(ChannelSendFailedError);
|
|
319
|
+
expect(getOrCreateCalls).toEqual([]);
|
|
320
|
+
expect(bindCalls).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("does not bind a chat when acting for another assistant", async () => {
|
|
324
|
+
await sendChannelText({
|
|
325
|
+
channel: "telegram",
|
|
326
|
+
target: { kind: "chat", chatId: "123456789" },
|
|
327
|
+
text: "hello",
|
|
328
|
+
assistantId: "asst-other",
|
|
329
|
+
});
|
|
330
|
+
expect(bindCalls).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("recording after acknowledgement", () => {
|
|
335
|
+
test("records every acknowledged id in the chat's home, cross-posted from the sender", async () => {
|
|
336
|
+
deliverResult = { ok: true, messageIds: ["10", "11", "12"] };
|
|
337
|
+
const result = await sendChannelText({
|
|
338
|
+
channel: "telegram",
|
|
339
|
+
target: { kind: "chat", chatId: "123456789" },
|
|
340
|
+
text: "a long message",
|
|
341
|
+
sender: { conversationId: "conv-A" },
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(resolveHomeMock.mock.calls[0]![0]).toMatchObject({
|
|
345
|
+
sourceChannel: "telegram",
|
|
346
|
+
externalChatId: "123456789",
|
|
347
|
+
});
|
|
348
|
+
expect(recordMock).toHaveBeenCalledTimes(1);
|
|
349
|
+
expect(recordMock.mock.calls[0]![0]).toEqual({
|
|
350
|
+
conversationId: "home-1",
|
|
351
|
+
channel: "telegram",
|
|
352
|
+
externalChatId: "123456789",
|
|
353
|
+
text: "a long message",
|
|
354
|
+
providerMessageId: "10",
|
|
355
|
+
additionalProviderMessageIds: ["11", "12"],
|
|
356
|
+
crossPostedFrom: "conv-A",
|
|
357
|
+
});
|
|
358
|
+
expect(result).toMatchObject({
|
|
359
|
+
messageIds: ["10", "11", "12"],
|
|
360
|
+
lastMessageId: "12",
|
|
361
|
+
recordedIn: "home-1",
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("a success that names no message id is not an acknowledgement", async () => {
|
|
366
|
+
deliverResult = { ok: true, messageIds: [] };
|
|
367
|
+
await expect(
|
|
368
|
+
sendChannelText({
|
|
369
|
+
channel: "slack",
|
|
370
|
+
target: { kind: "chat", chatId: "C123" },
|
|
371
|
+
text: "hello",
|
|
372
|
+
}),
|
|
373
|
+
).rejects.toThrow("acknowledged no message id");
|
|
374
|
+
expect(recordMock).not.toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("records nothing when the post landed in the turn's own chat and thread", async () => {
|
|
378
|
+
await sendChannelText({
|
|
379
|
+
channel: "slack",
|
|
380
|
+
target: { kind: "chat", chatId: "C123", threadId: "1690000000.000001" },
|
|
381
|
+
text: "hello",
|
|
382
|
+
sender,
|
|
383
|
+
});
|
|
384
|
+
expect(resolveHomeMock).not.toHaveBeenCalled();
|
|
385
|
+
expect(recordMock).not.toHaveBeenCalled();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("a send into the turn's chat but a different thread is a cross-post, recorded in that thread", async () => {
|
|
389
|
+
await sendChannelText({
|
|
390
|
+
channel: "slack",
|
|
391
|
+
target: { kind: "chat", chatId: "C123", threadId: "1690000000.000009" },
|
|
392
|
+
text: "hello",
|
|
393
|
+
sender,
|
|
394
|
+
});
|
|
395
|
+
expect(resolveHomeMock.mock.calls[0]![0]).toMatchObject({
|
|
396
|
+
sourceChannel: "slack",
|
|
397
|
+
externalChatId: "C123",
|
|
398
|
+
threadId: "1690000000.000009",
|
|
399
|
+
});
|
|
400
|
+
expect(recordMock).toHaveBeenCalledTimes(1);
|
|
401
|
+
expect(recordMock.mock.calls[0]![0]).toMatchObject({
|
|
402
|
+
externalChatId: "C123",
|
|
403
|
+
threadId: "1690000000.000009",
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a thread-less delivery never matches a turn that arrived in a thread", async () => {
|
|
408
|
+
await sendChannelText({
|
|
409
|
+
channel: "slack",
|
|
410
|
+
target: { kind: "chat", chatId: "C123" },
|
|
411
|
+
text: "hello",
|
|
412
|
+
sender,
|
|
413
|
+
});
|
|
414
|
+
expect(recordMock).toHaveBeenCalledTimes(1);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("a sender that arrived through no channel falls back to the home comparison", async () => {
|
|
418
|
+
resolveHomeMock.mockImplementationOnce(async () => ({
|
|
419
|
+
conversationId: "conv-A",
|
|
420
|
+
createdNewConversation: false,
|
|
421
|
+
}));
|
|
422
|
+
await sendChannelText({
|
|
423
|
+
channel: "slack",
|
|
424
|
+
target: { kind: "chat", chatId: "C123" },
|
|
425
|
+
text: "hello",
|
|
426
|
+
sender: { conversationId: "conv-A" },
|
|
427
|
+
});
|
|
428
|
+
expect(recordMock).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("a recording failure does not fail the send", async () => {
|
|
432
|
+
recordMock.mockImplementationOnce(async () => {
|
|
433
|
+
throw new Error("db unavailable");
|
|
434
|
+
});
|
|
435
|
+
const result = await sendChannelText({
|
|
436
|
+
channel: "slack",
|
|
437
|
+
target: { kind: "chat", chatId: "C123" },
|
|
438
|
+
text: "hello",
|
|
439
|
+
});
|
|
440
|
+
expect(result.messageIds).toEqual(["1700000000.000100"]);
|
|
441
|
+
expect(result.recordedIn).toBeUndefined();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
|
|
4
|
+
import { RuntimeHttpServer } from "../http-server.js";
|
|
5
|
+
import {
|
|
6
|
+
mintActorToken,
|
|
7
|
+
mintGatewayToken,
|
|
8
|
+
requireHttpAuth,
|
|
9
|
+
upgradeHeaders,
|
|
10
|
+
waitForClose,
|
|
11
|
+
} from "./runtime-ws-test-utils.js";
|
|
12
|
+
|
|
13
|
+
describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
14
|
+
let server: RuntimeHttpServer;
|
|
15
|
+
let baseUrl: string;
|
|
16
|
+
let restoreAuthEnv: () => void;
|
|
17
|
+
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
restoreAuthEnv = requireHttpAuth();
|
|
21
|
+
const port = 21400 + Math.floor(Math.random() * 300);
|
|
22
|
+
server = new RuntimeHttpServer({ port, hostname: "127.0.0.1" });
|
|
23
|
+
await server.start();
|
|
24
|
+
baseUrl = `127.0.0.1:${server.actualPort}`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await server.stop();
|
|
29
|
+
restoreAuthEnv();
|
|
30
|
+
if (originalContainerized === undefined) {
|
|
31
|
+
delete process.env.IS_CONTAINERIZED;
|
|
32
|
+
} else {
|
|
33
|
+
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
34
|
+
}
|
|
35
|
+
setOverridesForTesting({});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("refuses a non-private origin with 403", async () => {
|
|
39
|
+
const res = await fetch(
|
|
40
|
+
`http://${baseUrl}/v1/desktop/stream?token=${mintGatewayToken()}`,
|
|
41
|
+
{
|
|
42
|
+
headers: { ...upgradeHeaders, Origin: "https://external.example.com" },
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
expect(res.status).toBe(403);
|
|
46
|
+
const body = (await res.json()) as { error: { message: string } };
|
|
47
|
+
expect(body.error.message).toContain(
|
|
48
|
+
"Direct desktop stream access disabled",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("refuses a missing or non-gateway token with 401", async () => {
|
|
53
|
+
const missing = await fetch(`http://${baseUrl}/v1/desktop/stream`, {
|
|
54
|
+
headers: upgradeHeaders,
|
|
55
|
+
});
|
|
56
|
+
expect(missing.status).toBe(401);
|
|
57
|
+
|
|
58
|
+
const actor = await fetch(
|
|
59
|
+
`http://${baseUrl}/v1/desktop/stream?token=${mintActorToken()}`,
|
|
60
|
+
{ headers: upgradeHeaders },
|
|
61
|
+
);
|
|
62
|
+
expect(actor.status).toBe(401);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("upgrades a gateway request and reports the feature gate as close code 4008", async () => {
|
|
66
|
+
// The test daemon is not containerized, so the socket opens and is then
|
|
67
|
+
// closed by the desktop-stream open handler rather than refused pre-upgrade.
|
|
68
|
+
const ws = new WebSocket(
|
|
69
|
+
`ws://${baseUrl}/v1/desktop/stream?token=${encodeURIComponent(mintGatewayToken())}`,
|
|
70
|
+
);
|
|
71
|
+
const closed = await waitForClose(ws);
|
|
72
|
+
expect(closed.code).toBe(4008);
|
|
73
|
+
expect(closed.reason).toBe("Desktop is not available on this assistant");
|
|
74
|
+
});
|
|
75
|
+
for (const flag of [false, undefined]) {
|
|
76
|
+
test(`refuses a containerized stream with a disabled or missing flag (${flag})`, async () => {
|
|
77
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
78
|
+
setOverridesForTesting(
|
|
79
|
+
flag === undefined ? {} : { "assistant-desktop": flag },
|
|
80
|
+
);
|
|
81
|
+
const ws = new WebSocket(
|
|
82
|
+
`ws://${baseUrl}/v1/desktop/stream?token=${encodeURIComponent(mintGatewayToken())}`,
|
|
83
|
+
);
|
|
84
|
+
const closed = await waitForClose(ws);
|
|
85
|
+
expect(closed.code).toBe(4008);
|
|
86
|
+
expect(closed.reason).toBe("Desktop is not available on this assistant");
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|