@vellumai/assistant 0.6.3 → 0.6.4
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 +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `wakeAgentForOpportunity()` — the generic internal agent-wake
|
|
3
|
+
* mechanism.
|
|
4
|
+
*
|
|
5
|
+
* Exercise strategy: the wake helper takes a `resolveTarget` dependency so
|
|
6
|
+
* these tests stub out the heavyweight `Conversation` class with a minimal
|
|
7
|
+
* `WakeTarget` that just tracks agent-event forwards, buffered messages,
|
|
8
|
+
* persisted tail messages, drain invocations, and a scripted
|
|
9
|
+
* `agentLoop.run()` response.
|
|
10
|
+
*
|
|
11
|
+
* Persistence is now delegated to `WakeTarget.persistTailMessage` (the
|
|
12
|
+
* daemon adapter is responsible for building channel/interface metadata
|
|
13
|
+
* and disk-view sync — out of scope for runtime tests), so we assert on
|
|
14
|
+
* the calls received by the mock instead of stubbing
|
|
15
|
+
* `memory/conversation-crud.js`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
import type { AgentEvent } from "../../agent/loop.js";
|
|
21
|
+
import type { Message } from "../../providers/types.js";
|
|
22
|
+
import {
|
|
23
|
+
__resetWakeChainForTests,
|
|
24
|
+
wakeAgentForOpportunity,
|
|
25
|
+
type WakeTarget,
|
|
26
|
+
} from "../agent-wake.js";
|
|
27
|
+
|
|
28
|
+
// ── Test helpers ─────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface MockTarget extends WakeTarget {
|
|
31
|
+
emittedEvents: AgentEvent[];
|
|
32
|
+
pushedMessages: Message[];
|
|
33
|
+
runCalls: Array<{ input: Message[]; requestId?: string }>;
|
|
34
|
+
processingToggles: boolean[];
|
|
35
|
+
/** Tail messages handed to `persistTailMessage`, in call order. */
|
|
36
|
+
persistedTailCalls: Message[];
|
|
37
|
+
/** Number of times `drainQueue` was invoked. */
|
|
38
|
+
drainQueueCalls: number;
|
|
39
|
+
/**
|
|
40
|
+
* Cross-hook call sequence tag. Each push/persist/drain (and the
|
|
41
|
+
* processing toggles that bracket them) appends an entry so tests can
|
|
42
|
+
* assert end-to-end ordering, not just per-hook counts.
|
|
43
|
+
*/
|
|
44
|
+
callSequence: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Snapshot of `processing` at the moment `drainQueue` was invoked.
|
|
47
|
+
* Lets tests prove drain ran AFTER markProcessing(false), rather than
|
|
48
|
+
* just inferring it from the order of recorded toggles.
|
|
49
|
+
*/
|
|
50
|
+
processingDuringDrain: boolean[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeTarget(options: {
|
|
54
|
+
conversationId?: string;
|
|
55
|
+
baseline?: Message[];
|
|
56
|
+
scriptedAssistant?: Message | null;
|
|
57
|
+
/** Extra tail messages appended *after* `scriptedAssistant` (e.g. tool_result, follow-up assistant). */
|
|
58
|
+
scriptedTail?: Message[];
|
|
59
|
+
scriptedEvents?: AgentEvent[];
|
|
60
|
+
isProcessing?: boolean;
|
|
61
|
+
/** When true, omit `drainQueue` so we can verify the wake handles its absence. */
|
|
62
|
+
omitDrainQueue?: boolean;
|
|
63
|
+
}): MockTarget {
|
|
64
|
+
const emittedEvents: AgentEvent[] = [];
|
|
65
|
+
const pushedMessages: Message[] = [];
|
|
66
|
+
const runCalls: Array<{ input: Message[]; requestId?: string }> = [];
|
|
67
|
+
const processingToggles: boolean[] = [];
|
|
68
|
+
const persistedTailCalls: Message[] = [];
|
|
69
|
+
const callSequence: string[] = [];
|
|
70
|
+
const processingDuringDrain: boolean[] = [];
|
|
71
|
+
const history: Message[] = [...(options.baseline ?? [])];
|
|
72
|
+
let processing = options.isProcessing ?? false;
|
|
73
|
+
let drainQueueCalls = 0;
|
|
74
|
+
|
|
75
|
+
const target: MockTarget = {
|
|
76
|
+
conversationId: options.conversationId ?? "conv-test",
|
|
77
|
+
emittedEvents,
|
|
78
|
+
pushedMessages,
|
|
79
|
+
runCalls,
|
|
80
|
+
processingToggles,
|
|
81
|
+
persistedTailCalls,
|
|
82
|
+
callSequence,
|
|
83
|
+
processingDuringDrain,
|
|
84
|
+
get drainQueueCalls() {
|
|
85
|
+
return drainQueueCalls;
|
|
86
|
+
},
|
|
87
|
+
agentLoop: {
|
|
88
|
+
run: async (
|
|
89
|
+
input: Message[],
|
|
90
|
+
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
91
|
+
_signal?: AbortSignal,
|
|
92
|
+
requestId?: string,
|
|
93
|
+
) => {
|
|
94
|
+
runCalls.push({ input: [...input], requestId });
|
|
95
|
+
// Emit any scripted events the test wanted us to produce.
|
|
96
|
+
for (const ev of options.scriptedEvents ?? []) {
|
|
97
|
+
await onEvent(ev);
|
|
98
|
+
}
|
|
99
|
+
// Final history = input + optional assistant message + optional tail.
|
|
100
|
+
const next = [...input];
|
|
101
|
+
if (options.scriptedAssistant) {
|
|
102
|
+
next.push(options.scriptedAssistant);
|
|
103
|
+
await onEvent({
|
|
104
|
+
type: "message_complete",
|
|
105
|
+
message: options.scriptedAssistant,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (options.scriptedTail) {
|
|
109
|
+
for (const tailMsg of options.scriptedTail) {
|
|
110
|
+
next.push(tailMsg);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
getMessages: () => history,
|
|
117
|
+
pushMessage: (msg: Message) => {
|
|
118
|
+
pushedMessages.push(msg);
|
|
119
|
+
history.push(msg);
|
|
120
|
+
callSequence.push("push");
|
|
121
|
+
},
|
|
122
|
+
emitAgentEvent: (event) => {
|
|
123
|
+
emittedEvents.push(event);
|
|
124
|
+
},
|
|
125
|
+
isProcessing: () => processing,
|
|
126
|
+
markProcessing: (on: boolean) => {
|
|
127
|
+
processing = on;
|
|
128
|
+
processingToggles.push(on);
|
|
129
|
+
callSequence.push(on ? "processing:true" : "processing:false");
|
|
130
|
+
},
|
|
131
|
+
persistTailMessage: async (msg: Message) => {
|
|
132
|
+
persistedTailCalls.push(msg);
|
|
133
|
+
callSequence.push("persist");
|
|
134
|
+
},
|
|
135
|
+
...(options.omitDrainQueue
|
|
136
|
+
? {}
|
|
137
|
+
: {
|
|
138
|
+
drainQueue: async () => {
|
|
139
|
+
drainQueueCalls++;
|
|
140
|
+
// Snapshot the live processing flag *inside* drain, not via
|
|
141
|
+
// the toggle log, so we directly observe the state visible
|
|
142
|
+
// to the dequeued message's enqueueMessage() gate.
|
|
143
|
+
processingDuringDrain.push(processing);
|
|
144
|
+
callSequence.push("drain");
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Expose processing setter via test-only side-channel for tests that
|
|
150
|
+
// simulate an external (non-wake) processing state.
|
|
151
|
+
(target as unknown as { setProcessing: (v: boolean) => void }).setProcessing =
|
|
152
|
+
(v: boolean) => {
|
|
153
|
+
processing = v;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return target;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
__resetWakeChainForTests();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
describe("wakeAgentForOpportunity", () => {
|
|
166
|
+
test("silent no-op when agent produces no tool calls and no text", async () => {
|
|
167
|
+
const target = makeTarget({
|
|
168
|
+
baseline: [
|
|
169
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
170
|
+
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
171
|
+
],
|
|
172
|
+
// Assistant replies with empty text — counts as no output.
|
|
173
|
+
scriptedAssistant: {
|
|
174
|
+
role: "assistant",
|
|
175
|
+
content: [{ type: "text", text: "" }],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await wakeAgentForOpportunity(
|
|
180
|
+
{
|
|
181
|
+
conversationId: target.conversationId,
|
|
182
|
+
hint: "someone asked a question",
|
|
183
|
+
source: "unit-test",
|
|
184
|
+
},
|
|
185
|
+
{ resolveTarget: async () => target },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
189
|
+
// Nothing emitted to client.
|
|
190
|
+
expect(target.emittedEvents).toHaveLength(0);
|
|
191
|
+
// Nothing persisted.
|
|
192
|
+
expect(target.persistedTailCalls).toHaveLength(0);
|
|
193
|
+
// Nothing pushed into live history.
|
|
194
|
+
expect(target.pushedMessages).toHaveLength(0);
|
|
195
|
+
// Hint was included in the run input, but baseline is unchanged.
|
|
196
|
+
expect(target.runCalls).toHaveLength(1);
|
|
197
|
+
const input = target.runCalls[0]!.input;
|
|
198
|
+
expect(input).toHaveLength(3); // 2 baseline + 1 hint
|
|
199
|
+
expect(input[2]).toEqual({
|
|
200
|
+
role: "user",
|
|
201
|
+
content: [
|
|
202
|
+
{ type: "text", text: "[opportunity:unit-test] someone asked a question" },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("produces tool calls when LLM emits a tool_use block", async () => {
|
|
208
|
+
const assistantMessage: Message = {
|
|
209
|
+
role: "assistant",
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "tool_use",
|
|
213
|
+
id: "tu-1",
|
|
214
|
+
name: "meet_send_chat",
|
|
215
|
+
input: { text: "Sure, here's the link" },
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
const target = makeTarget({
|
|
220
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
221
|
+
scriptedAssistant: assistantMessage,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await wakeAgentForOpportunity(
|
|
225
|
+
{
|
|
226
|
+
conversationId: target.conversationId,
|
|
227
|
+
hint: "question directed at assistant",
|
|
228
|
+
source: "meet-chat-opportunity",
|
|
229
|
+
},
|
|
230
|
+
{ resolveTarget: async () => target },
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: true });
|
|
234
|
+
// Assistant message persisted via the target hook.
|
|
235
|
+
expect(target.persistedTailCalls).toHaveLength(1);
|
|
236
|
+
expect(target.persistedTailCalls[0]).toEqual(assistantMessage);
|
|
237
|
+
// Assistant message pushed into live history.
|
|
238
|
+
expect(target.pushedMessages).toContainEqual(assistantMessage);
|
|
239
|
+
// message_complete event flushed to the client via the translator
|
|
240
|
+
// surface (raw AgentEvent — adapter is responsible for wire shape).
|
|
241
|
+
const flushed = target.emittedEvents.find(
|
|
242
|
+
(e) => e.type === "message_complete",
|
|
243
|
+
);
|
|
244
|
+
expect(flushed).toBeDefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("persists full multi-turn tail (assistant → tool_result → follow-up assistant)", async () => {
|
|
248
|
+
// Simulate a wake that produces a tool_use, an executed tool_result
|
|
249
|
+
// user message, and a follow-up assistant summary. All three must be
|
|
250
|
+
// persisted; otherwise the next rehydration loses the tool_result
|
|
251
|
+
// and the provider rejects the orphaned tool_use.
|
|
252
|
+
const firstAssistant: Message = {
|
|
253
|
+
role: "assistant",
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "tool_use",
|
|
257
|
+
id: "tu-1",
|
|
258
|
+
name: "meet_send_chat",
|
|
259
|
+
input: { text: "Sure" },
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
const toolResultUserMsg: Message = {
|
|
264
|
+
role: "user",
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "tool_result",
|
|
268
|
+
tool_use_id: "tu-1",
|
|
269
|
+
content: "sent",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
const followupAssistant: Message = {
|
|
274
|
+
role: "assistant",
|
|
275
|
+
content: [{ type: "text", text: "Done." }],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const target = makeTarget({
|
|
279
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
280
|
+
scriptedAssistant: firstAssistant,
|
|
281
|
+
scriptedTail: [toolResultUserMsg, followupAssistant],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await wakeAgentForOpportunity(
|
|
285
|
+
{
|
|
286
|
+
conversationId: target.conversationId,
|
|
287
|
+
hint: "question directed at assistant",
|
|
288
|
+
source: "meet-chat-opportunity",
|
|
289
|
+
},
|
|
290
|
+
{ resolveTarget: async () => target },
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: true });
|
|
294
|
+
|
|
295
|
+
// All three tail messages persisted in order via the target hook.
|
|
296
|
+
expect(target.persistedTailCalls).toHaveLength(3);
|
|
297
|
+
expect(target.persistedTailCalls[0]).toEqual(firstAssistant);
|
|
298
|
+
expect(target.persistedTailCalls[1]).toEqual(toolResultUserMsg);
|
|
299
|
+
expect(target.persistedTailCalls[2]).toEqual(followupAssistant);
|
|
300
|
+
|
|
301
|
+
// All three also pushed into live history so next turn sees them.
|
|
302
|
+
expect(target.pushedMessages).toHaveLength(3);
|
|
303
|
+
expect(target.pushedMessages[0]).toEqual(firstAssistant);
|
|
304
|
+
expect(target.pushedMessages[1]).toEqual(toolResultUserMsg);
|
|
305
|
+
expect(target.pushedMessages[2]).toEqual(followupAssistant);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("marks processing true during the run and false afterwards", async () => {
|
|
309
|
+
const target = makeTarget({
|
|
310
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
311
|
+
scriptedAssistant: {
|
|
312
|
+
role: "assistant",
|
|
313
|
+
content: [{ type: "text", text: "reply" }],
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Snapshot isProcessing() inside the run to prove we actually
|
|
318
|
+
// hold the processing flag while agentLoop.run executes.
|
|
319
|
+
const observedDuringRun: boolean[] = [];
|
|
320
|
+
const originalRun = target.agentLoop.run;
|
|
321
|
+
target.agentLoop.run = async (input, onEvent, signal, requestId) => {
|
|
322
|
+
observedDuringRun.push(target.isProcessing());
|
|
323
|
+
return originalRun(input, onEvent, signal, requestId);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
await wakeAgentForOpportunity(
|
|
327
|
+
{
|
|
328
|
+
conversationId: target.conversationId,
|
|
329
|
+
hint: "x",
|
|
330
|
+
source: "unit-test",
|
|
331
|
+
},
|
|
332
|
+
{ resolveTarget: async () => target },
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// markProcessing toggled on then off exactly once.
|
|
336
|
+
expect(target.processingToggles).toEqual([true, false]);
|
|
337
|
+
// And the flag was observed as true inside the run body.
|
|
338
|
+
expect(observedDuringRun).toEqual([true]);
|
|
339
|
+
// Back to idle by the time the wake returns.
|
|
340
|
+
expect(target.isProcessing()).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("marks processing false even when the agent loop throws", async () => {
|
|
344
|
+
const history: Message[] = [];
|
|
345
|
+
const toggles: boolean[] = [];
|
|
346
|
+
let processing = false;
|
|
347
|
+
const target: WakeTarget = {
|
|
348
|
+
conversationId: "conv-err-guard",
|
|
349
|
+
agentLoop: {
|
|
350
|
+
run: async () => {
|
|
351
|
+
throw new Error("LLM exploded");
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
getMessages: () => history,
|
|
355
|
+
pushMessage: () => {},
|
|
356
|
+
emitAgentEvent: () => {},
|
|
357
|
+
isProcessing: () => processing,
|
|
358
|
+
markProcessing: (on) => {
|
|
359
|
+
processing = on;
|
|
360
|
+
toggles.push(on);
|
|
361
|
+
},
|
|
362
|
+
persistTailMessage: async () => {},
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const result = await wakeAgentForOpportunity(
|
|
366
|
+
{ conversationId: "conv-err-guard", hint: "boom", source: "t" },
|
|
367
|
+
{ resolveTarget: async () => target },
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
371
|
+
// Critical: the finally block must have released the flag despite
|
|
372
|
+
// the thrown error, otherwise the next user turn would hang.
|
|
373
|
+
expect(toggles).toEqual([true, false]);
|
|
374
|
+
expect(processing).toBe(false);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("two concurrent wakes on the same conversation are serialized", async () => {
|
|
378
|
+
// Build a target whose agentLoop.run resolves only when we signal.
|
|
379
|
+
const gate1 = Promise.withResolvers<void>();
|
|
380
|
+
const gate2 = Promise.withResolvers<void>();
|
|
381
|
+
const runStartOrder: number[] = [];
|
|
382
|
+
const runCompleteOrder: number[] = [];
|
|
383
|
+
|
|
384
|
+
let callIndex = 0;
|
|
385
|
+
const history: Message[] = [];
|
|
386
|
+
let processing = false;
|
|
387
|
+
const target: WakeTarget = {
|
|
388
|
+
conversationId: "conv-serialize",
|
|
389
|
+
agentLoop: {
|
|
390
|
+
run: async (input) => {
|
|
391
|
+
const myIndex = ++callIndex;
|
|
392
|
+
runStartOrder.push(myIndex);
|
|
393
|
+
if (myIndex === 1) {
|
|
394
|
+
await gate1.promise;
|
|
395
|
+
} else {
|
|
396
|
+
await gate2.promise;
|
|
397
|
+
}
|
|
398
|
+
runCompleteOrder.push(myIndex);
|
|
399
|
+
return input; // no assistant message → silent no-op
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
getMessages: () => history,
|
|
403
|
+
pushMessage: (msg) => {
|
|
404
|
+
history.push(msg);
|
|
405
|
+
},
|
|
406
|
+
emitAgentEvent: () => {},
|
|
407
|
+
isProcessing: () => processing,
|
|
408
|
+
markProcessing: (on) => {
|
|
409
|
+
processing = on;
|
|
410
|
+
},
|
|
411
|
+
persistTailMessage: async () => {},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const deps = { resolveTarget: async () => target };
|
|
415
|
+
|
|
416
|
+
const p1 = wakeAgentForOpportunity(
|
|
417
|
+
{ conversationId: "conv-serialize", hint: "first", source: "t1" },
|
|
418
|
+
deps,
|
|
419
|
+
);
|
|
420
|
+
const p2 = wakeAgentForOpportunity(
|
|
421
|
+
{ conversationId: "conv-serialize", hint: "second", source: "t2" },
|
|
422
|
+
deps,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Let the microtask queue flush so p1 can start.
|
|
426
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
427
|
+
expect(runStartOrder).toEqual([1]);
|
|
428
|
+
|
|
429
|
+
// Releasing gate2 should NOT let p2 start — it's queued behind p1.
|
|
430
|
+
gate2.resolve();
|
|
431
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
432
|
+
expect(runStartOrder).toEqual([1]);
|
|
433
|
+
|
|
434
|
+
// Now release gate1 — p1 completes, then p2 starts and completes.
|
|
435
|
+
gate1.resolve();
|
|
436
|
+
await Promise.all([p1, p2]);
|
|
437
|
+
expect(runStartOrder).toEqual([1, 2]);
|
|
438
|
+
expect(runCompleteOrder).toEqual([1, 2]);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("waits while a concurrent user turn is in flight", async () => {
|
|
442
|
+
const history: Message[] = [];
|
|
443
|
+
let processing = true;
|
|
444
|
+
const target: WakeTarget & { setProcessing: (v: boolean) => void } = {
|
|
445
|
+
conversationId: "conv-user-turn",
|
|
446
|
+
agentLoop: {
|
|
447
|
+
run: async (input) => input,
|
|
448
|
+
},
|
|
449
|
+
getMessages: () => history,
|
|
450
|
+
pushMessage: (msg) => {
|
|
451
|
+
history.push(msg);
|
|
452
|
+
},
|
|
453
|
+
emitAgentEvent: () => {},
|
|
454
|
+
isProcessing: () => processing,
|
|
455
|
+
// The wake's own markProcessing updates track the flag too — the
|
|
456
|
+
// outer "user turn" holds it at true until setProcessing(false)
|
|
457
|
+
// is called below.
|
|
458
|
+
markProcessing: (on) => {
|
|
459
|
+
processing = on;
|
|
460
|
+
},
|
|
461
|
+
persistTailMessage: async () => {},
|
|
462
|
+
setProcessing: (v) => {
|
|
463
|
+
processing = v;
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const wakePromise = wakeAgentForOpportunity(
|
|
468
|
+
{
|
|
469
|
+
conversationId: "conv-user-turn",
|
|
470
|
+
hint: "opportunity while user typing",
|
|
471
|
+
source: "unit-test",
|
|
472
|
+
},
|
|
473
|
+
{ resolveTarget: async () => target },
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Wake should be waiting (isProcessing returns true).
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
478
|
+
// Hasn't resolved yet.
|
|
479
|
+
let settled = false;
|
|
480
|
+
void wakePromise.then(() => {
|
|
481
|
+
settled = true;
|
|
482
|
+
});
|
|
483
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
484
|
+
expect(settled).toBe(false);
|
|
485
|
+
|
|
486
|
+
// "User turn" completes — wake now proceeds.
|
|
487
|
+
target.setProcessing(false);
|
|
488
|
+
const result = await wakePromise;
|
|
489
|
+
expect(result.invoked).toBe(true);
|
|
490
|
+
expect(result.producedToolCalls).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("returns invoked: false when the conversation cannot be resolved", async () => {
|
|
494
|
+
const result = await wakeAgentForOpportunity(
|
|
495
|
+
{ conversationId: "missing", hint: "x", source: "y" },
|
|
496
|
+
{ resolveTarget: async () => null },
|
|
497
|
+
);
|
|
498
|
+
expect(result).toEqual({ invoked: false, producedToolCalls: false });
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("agent loop error is treated as a no-op", async () => {
|
|
502
|
+
const history: Message[] = [];
|
|
503
|
+
let processing = false;
|
|
504
|
+
const persisted: Message[] = [];
|
|
505
|
+
const target: WakeTarget = {
|
|
506
|
+
conversationId: "conv-err",
|
|
507
|
+
agentLoop: {
|
|
508
|
+
run: async () => {
|
|
509
|
+
throw new Error("LLM exploded");
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
getMessages: () => history,
|
|
513
|
+
pushMessage: () => {},
|
|
514
|
+
emitAgentEvent: () => {},
|
|
515
|
+
isProcessing: () => processing,
|
|
516
|
+
markProcessing: (on) => {
|
|
517
|
+
processing = on;
|
|
518
|
+
},
|
|
519
|
+
persistTailMessage: async (m) => {
|
|
520
|
+
persisted.push(m);
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const result = await wakeAgentForOpportunity(
|
|
525
|
+
{ conversationId: "conv-err", hint: "boom", source: "t" },
|
|
526
|
+
{ resolveTarget: async () => target },
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
530
|
+
expect(persisted).toHaveLength(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("drainQueue is called in finally after a successful run", async () => {
|
|
534
|
+
// Verifies Gap 1 fix: messages queued during a wake (because the
|
|
535
|
+
// wake set `processing = true`) must be picked up after the wake
|
|
536
|
+
// completes. Mirrors the canonical user-turn `finally` path which
|
|
537
|
+
// sets `processing = false` then calls `drainQueue`.
|
|
538
|
+
const target = makeTarget({
|
|
539
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
540
|
+
scriptedAssistant: {
|
|
541
|
+
role: "assistant",
|
|
542
|
+
content: [{ type: "text", text: "reply" }],
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await wakeAgentForOpportunity(
|
|
547
|
+
{
|
|
548
|
+
conversationId: target.conversationId,
|
|
549
|
+
hint: "x",
|
|
550
|
+
source: "unit-test",
|
|
551
|
+
},
|
|
552
|
+
{ resolveTarget: async () => target },
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
expect(target.drainQueueCalls).toBe(1);
|
|
556
|
+
// Critical ordering invariant: drain runs after processing=false.
|
|
557
|
+
// If drain ran while processing was still true,
|
|
558
|
+
// `enqueueMessage`'s `if (!ctx.processing) return ...` gate would
|
|
559
|
+
// see processing=true and the drained item would itself just
|
|
560
|
+
// re-enqueue — no progress. Snapshot the live flag *inside* drain
|
|
561
|
+
// (rather than inferring from toggle order) so a future regression
|
|
562
|
+
// that called drain before markProcessing(false) would fail this
|
|
563
|
+
// assertion directly.
|
|
564
|
+
expect(target.processingDuringDrain).toEqual([false]);
|
|
565
|
+
expect(target.processingToggles).toEqual([true, false]);
|
|
566
|
+
expect(target.isProcessing()).toBe(false);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("drainQueue is called in finally even when the agent loop throws", async () => {
|
|
570
|
+
// Verifies the drain is in the finally block, not just on success.
|
|
571
|
+
// A wake that crashes mid-run must still flush queued messages —
|
|
572
|
+
// otherwise a transient LLM error strands every concurrent send.
|
|
573
|
+
const drainProcessingSnapshots: boolean[] = [];
|
|
574
|
+
const toggles: boolean[] = [];
|
|
575
|
+
let processing = false;
|
|
576
|
+
const target: WakeTarget = {
|
|
577
|
+
conversationId: "conv-drain-on-throw",
|
|
578
|
+
agentLoop: {
|
|
579
|
+
run: async () => {
|
|
580
|
+
throw new Error("LLM exploded mid-wake");
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
getMessages: () => [],
|
|
584
|
+
pushMessage: () => {},
|
|
585
|
+
emitAgentEvent: () => {},
|
|
586
|
+
isProcessing: () => processing,
|
|
587
|
+
markProcessing: (on) => {
|
|
588
|
+
processing = on;
|
|
589
|
+
toggles.push(on);
|
|
590
|
+
},
|
|
591
|
+
persistTailMessage: async () => {},
|
|
592
|
+
drainQueue: async () => {
|
|
593
|
+
// Snapshot the live `processing` flag *inside* drain rather
|
|
594
|
+
// than inferring from toggle order. This directly observes the
|
|
595
|
+
// state visible to enqueueMessage's gate when a queued message
|
|
596
|
+
// is dequeued.
|
|
597
|
+
drainProcessingSnapshots.push(processing);
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const result = await wakeAgentForOpportunity(
|
|
602
|
+
{ conversationId: "conv-drain-on-throw", hint: "boom", source: "t" },
|
|
603
|
+
{ resolveTarget: async () => target },
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
607
|
+
// Drain ran AFTER markProcessing(false), satisfying the
|
|
608
|
+
// enqueueMessage gate invariant. Snapshot proves the flag was
|
|
609
|
+
// false at the moment drain ran.
|
|
610
|
+
expect(drainProcessingSnapshots).toEqual([false]);
|
|
611
|
+
expect(toggles).toEqual([true, false]);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("missing drainQueue hook is tolerated (no-op fallback)", async () => {
|
|
615
|
+
// The hook is intentionally optional so test stubs without a queue
|
|
616
|
+
// can omit it. Production daemon adapter always wires it.
|
|
617
|
+
const target = makeTarget({
|
|
618
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
619
|
+
scriptedAssistant: {
|
|
620
|
+
role: "assistant",
|
|
621
|
+
content: [{ type: "text", text: "reply" }],
|
|
622
|
+
},
|
|
623
|
+
omitDrainQueue: true,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const result = await wakeAgentForOpportunity(
|
|
627
|
+
{
|
|
628
|
+
conversationId: target.conversationId,
|
|
629
|
+
hint: "x",
|
|
630
|
+
source: "unit-test",
|
|
631
|
+
},
|
|
632
|
+
{ resolveTarget: async () => target },
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
expect(result.invoked).toBe(true);
|
|
636
|
+
// No throw, no drain attempt recorded.
|
|
637
|
+
expect(target.drainQueueCalls).toBe(0);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("drainQueue rejection does not propagate from the wake", async () => {
|
|
641
|
+
// Defense in depth: if the queue drain throws (e.g. a poisoned
|
|
642
|
+
// message), the wake itself must still resolve normally — the
|
|
643
|
+
// drain failure is logged but never surfaced.
|
|
644
|
+
const target = makeTarget({
|
|
645
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
646
|
+
scriptedAssistant: {
|
|
647
|
+
role: "assistant",
|
|
648
|
+
content: [{ type: "text", text: "reply" }],
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
target.drainQueue = async () => {
|
|
652
|
+
throw new Error("drain blew up");
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const result = await wakeAgentForOpportunity(
|
|
656
|
+
{
|
|
657
|
+
conversationId: target.conversationId,
|
|
658
|
+
hint: "x",
|
|
659
|
+
source: "unit-test",
|
|
660
|
+
},
|
|
661
|
+
{ resolveTarget: async () => target },
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
expect(result.invoked).toBe(true);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("persistTailMessage called for each tail message in order", async () => {
|
|
668
|
+
// Verifies Gap 2 fix: the wake delegates persistence to the target
|
|
669
|
+
// so the daemon adapter can build channel/interface metadata. We
|
|
670
|
+
// only check the call ordering / arguments here — the daemon
|
|
671
|
+
// adapter's metadata composition is exercised separately.
|
|
672
|
+
const firstAssistant: Message = {
|
|
673
|
+
role: "assistant",
|
|
674
|
+
content: [
|
|
675
|
+
{
|
|
676
|
+
type: "tool_use",
|
|
677
|
+
id: "tu-1",
|
|
678
|
+
name: "some_tool",
|
|
679
|
+
input: {},
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
};
|
|
683
|
+
const toolResultUserMsg: Message = {
|
|
684
|
+
role: "user",
|
|
685
|
+
content: [
|
|
686
|
+
{ type: "tool_result", tool_use_id: "tu-1", content: "ok" },
|
|
687
|
+
],
|
|
688
|
+
};
|
|
689
|
+
const followup: Message = {
|
|
690
|
+
role: "assistant",
|
|
691
|
+
content: [{ type: "text", text: "All set." }],
|
|
692
|
+
};
|
|
693
|
+
const target = makeTarget({
|
|
694
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
695
|
+
scriptedAssistant: firstAssistant,
|
|
696
|
+
scriptedTail: [toolResultUserMsg, followup],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
await wakeAgentForOpportunity(
|
|
700
|
+
{
|
|
701
|
+
conversationId: target.conversationId,
|
|
702
|
+
hint: "x",
|
|
703
|
+
source: "meet-chat-opportunity",
|
|
704
|
+
},
|
|
705
|
+
{ resolveTarget: async () => target },
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
expect(target.persistedTailCalls).toEqual([
|
|
709
|
+
firstAssistant,
|
|
710
|
+
toolResultUserMsg,
|
|
711
|
+
followup,
|
|
712
|
+
]);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test(
|
|
716
|
+
"tail messages are pushed and persisted BEFORE drainQueue runs " +
|
|
717
|
+
"(so dequeued turns see updated history)",
|
|
718
|
+
async () => {
|
|
719
|
+
// Locks in the round-3 fix: a user message queued during the wake
|
|
720
|
+
// is drained against `conversation.messages`, so the wake's tail
|
|
721
|
+
// MUST be appended (push) and persisted to DB (persist) before the
|
|
722
|
+
// queue is drained. Otherwise `drainSingleMessage` reads stale
|
|
723
|
+
// history and writes a DB row that lands out of chronological
|
|
724
|
+
// order (queued user msg before the wake's just-produced
|
|
725
|
+
// assistant outputs).
|
|
726
|
+
//
|
|
727
|
+
// Mirrors the canonical user-turn pattern in
|
|
728
|
+
// conversation-agent-loop.ts:1860,2106-2126: messages updated →
|
|
729
|
+
// processing=false → drainQueue.
|
|
730
|
+
const firstAssistant: Message = {
|
|
731
|
+
role: "assistant",
|
|
732
|
+
content: [
|
|
733
|
+
{ type: "tool_use", id: "tu-1", name: "some_tool", input: {} },
|
|
734
|
+
],
|
|
735
|
+
};
|
|
736
|
+
const toolResultUserMsg: Message = {
|
|
737
|
+
role: "user",
|
|
738
|
+
content: [
|
|
739
|
+
{ type: "tool_result", tool_use_id: "tu-1", content: "ok" },
|
|
740
|
+
],
|
|
741
|
+
};
|
|
742
|
+
const followup: Message = {
|
|
743
|
+
role: "assistant",
|
|
744
|
+
content: [{ type: "text", text: "All done." }],
|
|
745
|
+
};
|
|
746
|
+
const target = makeTarget({
|
|
747
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
748
|
+
scriptedAssistant: firstAssistant,
|
|
749
|
+
scriptedTail: [toolResultUserMsg, followup],
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
await wakeAgentForOpportunity(
|
|
753
|
+
{
|
|
754
|
+
conversationId: target.conversationId,
|
|
755
|
+
hint: "x",
|
|
756
|
+
source: "meet-chat-opportunity",
|
|
757
|
+
},
|
|
758
|
+
{ resolveTarget: async () => target },
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// Full call sequence: processing toggled true → 3 pushes →
|
|
762
|
+
// 3 persists → processing toggled false → drain. Specifically,
|
|
763
|
+
// every push and every persist must precede the single drain.
|
|
764
|
+
expect(target.callSequence).toEqual([
|
|
765
|
+
"processing:true",
|
|
766
|
+
"push",
|
|
767
|
+
"push",
|
|
768
|
+
"push",
|
|
769
|
+
"persist",
|
|
770
|
+
"persist",
|
|
771
|
+
"persist",
|
|
772
|
+
"processing:false",
|
|
773
|
+
"drain",
|
|
774
|
+
]);
|
|
775
|
+
|
|
776
|
+
// Belt-and-braces: cross-check via index lookups so the failure
|
|
777
|
+
// mode (drain before push/persist) shows up clearly even if the
|
|
778
|
+
// exact sequence ever picks up additional entries.
|
|
779
|
+
const drainIdx = target.callSequence.indexOf("drain");
|
|
780
|
+
const lastPushIdx = target.callSequence.lastIndexOf("push");
|
|
781
|
+
const lastPersistIdx = target.callSequence.lastIndexOf("persist");
|
|
782
|
+
expect(drainIdx).toBeGreaterThan(lastPushIdx);
|
|
783
|
+
expect(drainIdx).toBeGreaterThan(lastPersistIdx);
|
|
784
|
+
|
|
785
|
+
// And processing was false when drain ran.
|
|
786
|
+
expect(target.processingDuringDrain).toEqual([false]);
|
|
787
|
+
},
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
test(
|
|
791
|
+
"silent no-op: drainQueue still runs (in finally) but nothing is " +
|
|
792
|
+
"pushed, persisted, or emitted",
|
|
793
|
+
async () => {
|
|
794
|
+
// The wake's silent-no-op semantics must be preserved by the
|
|
795
|
+
// round-3 reordering: an empty assistant reply produces no
|
|
796
|
+
// visible text and no tool calls, so no push/persist/emit should
|
|
797
|
+
// happen. drainQueue must still run in the finally block so a
|
|
798
|
+
// racy queued message is not stranded.
|
|
799
|
+
const target = makeTarget({
|
|
800
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
801
|
+
scriptedAssistant: {
|
|
802
|
+
role: "assistant",
|
|
803
|
+
content: [{ type: "text", text: "" }],
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
await wakeAgentForOpportunity(
|
|
808
|
+
{
|
|
809
|
+
conversationId: target.conversationId,
|
|
810
|
+
hint: "x",
|
|
811
|
+
source: "unit-test",
|
|
812
|
+
},
|
|
813
|
+
{ resolveTarget: async () => target },
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// No push, no persist, no emit.
|
|
817
|
+
expect(target.pushedMessages).toHaveLength(0);
|
|
818
|
+
expect(target.persistedTailCalls).toHaveLength(0);
|
|
819
|
+
expect(target.emittedEvents).toHaveLength(0);
|
|
820
|
+
|
|
821
|
+
// But drain still ran exactly once, after processing flipped to
|
|
822
|
+
// false. Sequence: toggle true → toggle false → drain.
|
|
823
|
+
expect(target.callSequence).toEqual([
|
|
824
|
+
"processing:true",
|
|
825
|
+
"processing:false",
|
|
826
|
+
"drain",
|
|
827
|
+
]);
|
|
828
|
+
expect(target.processingDuringDrain).toEqual([false]);
|
|
829
|
+
},
|
|
830
|
+
);
|
|
831
|
+
});
|