@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
|
@@ -12,9 +12,9 @@ This skill provides Gmail-specific tools. For cross-platform messaging (send, re
|
|
|
12
12
|
|
|
13
13
|
## Email Routing Priority
|
|
14
14
|
|
|
15
|
-
When the user mentions "email" - sending, reading, checking, decluttering, drafting, or anything else - **always default to the user's own email (Gmail)** unless they explicitly ask about the assistant's own email address (e.g., "set up your email", "send from your address", "check your inbox"). The vast majority of email requests are about the user's Gmail, not the assistant's
|
|
15
|
+
When the user mentions "email" - sending, reading, checking, decluttering, drafting, or anything else - **always default to the user's own email (Gmail)** unless they explicitly ask about the assistant's own email address (e.g., "set up your email", "send from your address", "check your inbox"). The vast majority of email requests are about the user's Gmail, not the assistant's @vellum.me address.
|
|
16
16
|
|
|
17
|
-
Do not offer
|
|
17
|
+
Do not offer the assistant's own email as an option unless the user specifically asks. If Gmail is not connected, guide them through Gmail setup.
|
|
18
18
|
|
|
19
19
|
## Communication Style
|
|
20
20
|
|
|
@@ -106,7 +106,7 @@ When searching Gmail, the query uses Gmail's search operators:
|
|
|
106
106
|
|
|
107
107
|
When a user asks to declutter, clean up, or organize their email - start scanning immediately. Don't ask what kind of cleanup they want or request permission to read their inbox. Go straight to scanning - but once results are ready, always show them via `ui_show` and let the user choose actions before archiving or unsubscribing.
|
|
108
108
|
|
|
109
|
-
**CRITICAL**: Never call `gmail_archive`, `gmail_unsubscribe`, or `messaging_archive_by_sender` unless the user has clicked an action button on the table for that specific batch. Each batch of results requires its own explicit user confirmation via the table UI. If the user says "keep going" or "keep decluttering," that means scan and present a new table - NOT auto-archive. Previous batch approvals do not carry forward, but **deselections DO carry forward**: when the user deselects senders from a cleanup table,
|
|
109
|
+
**CRITICAL**: Never call `gmail_archive`, `gmail_unsubscribe`, or `messaging_archive_by_sender` unless the user has clicked an action button on the table for that specific batch. Each batch of results requires its own explicit user confirmation via the table UI. If the user says "keep going" or "keep decluttering," that means scan and present a new table - NOT auto-archive. Previous batch approvals do not carry forward, but **deselections DO carry forward**: when the user deselects senders from a cleanup table, call `gmail_preferences` with `action: "add_safelist"` to persist those senders. Before building the next cleanup table, call `gmail_preferences` with `action: "list"` and exclude safelisted senders from the table — the user already indicated they want to keep those.
|
|
110
110
|
|
|
111
111
|
### Workflow
|
|
112
112
|
|
|
@@ -135,19 +135,65 @@ When a user asks to declutter, clean up, or organize their email - start scannin
|
|
|
135
135
|
- If yes, call `gmail_filters` with `action: "create"` for each sender with `from` set to the sender's email and `remove_label_ids: ["INBOX"]`.
|
|
136
136
|
- Then offer a recurring declutter schedule: "Want me to scan for new clutter monthly?" If yes, use `schedule_create` to set up a monthly declutter check.
|
|
137
137
|
|
|
138
|
+
### Cold Outreach Cleanup
|
|
139
|
+
|
|
140
|
+
After the newsletter/promotions pass, offer to clean up cold outreach — unsolicited emails from senders without unsubscribe links. This catches sales pitches, recruiting spam, and mass outreach that newsletter filters miss.
|
|
141
|
+
|
|
142
|
+
1. **Scan**: Call `gmail_outreach_scan` (default: last 90 days, senders without `List-Unsubscribe` headers). The scan includes a `has_prior_reply` flag per sender — true means the user has previously replied to that sender.
|
|
143
|
+
2. **Filter out known contacts**: Exclude senders where `has_prior_reply: true` — these are conversations, not cold outreach. If the `contacts` skill is loaded, also cross-reference against Google Contacts and exclude matches.
|
|
144
|
+
3. **Classify senders** using sample subjects, email domains, and message patterns. Categorize into:
|
|
145
|
+
- **Clear junk** (pre-select for archive): loan/LOC offers, generic SaaS pitches, mass marketing from unknown domains, senders with random/concatenated domain names
|
|
146
|
+
- **Sales outreach** (pre-select for archive): targeted product pitches with personalised subject lines ("Hi [name]", "for [company]"), outreach tool domains (apollo.io, outreach.io, lemlist.com, instantly.ai, etc.)
|
|
147
|
+
- **Potentially useful** (deselect / keep by default): recruiting, investor outreach, partnership proposals, vendor introductions that reference the user's specific product or role
|
|
148
|
+
- **Ambiguous** (deselect / keep by default): anything you're not confident about
|
|
149
|
+
4. **Present as a table** following the same `ui_show` pattern as the newsletter workflow. Use two visual sections:
|
|
150
|
+
- Pre-selected rows: clear junk + sales outreach
|
|
151
|
+
- Deselected rows: potentially useful + ambiguous senders (user reviews these)
|
|
152
|
+
- **Caption**: "Cold outreach from the last 90 days (senders without unsubscribe links). Pre-selected senders look like spam or sales pitches. Deselected senders may be useful — review before archiving."
|
|
153
|
+
5. **Archive on user action**: Same flow as newsletter cleanup — wait for surface action button click, then batch archive.
|
|
154
|
+
|
|
155
|
+
**Key principle**: Not all cold outreach is unwanted. Recruiting, investor, and partnership emails can be valuable. When uncertain, default to keeping the sender (deselected) and let the user decide.
|
|
156
|
+
|
|
157
|
+
### Large Inbox Handling
|
|
158
|
+
|
|
159
|
+
When a scan returns `truncated: true` or `time_budget_exceeded: true`, the inbox has more messages than a single scan pass can cover. Split subsequent scans by date range to ensure full coverage:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Pass 1: in:inbox older_than:90d (oldest backlog)
|
|
163
|
+
Pass 2: in:inbox newer_than:90d older_than:30d (recent months)
|
|
164
|
+
Pass 3: in:inbox newer_than:30d older_than:7d (recent weeks)
|
|
165
|
+
Pass 4: in:inbox newer_than:7d (this week)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Merge results from all passes before presenting the final table. Each pass covers a smaller window, reducing per-scan message count and avoiding timeouts. Only split when a scan actually reports truncation — most inboxes are handled fine in a single pass.
|
|
169
|
+
|
|
138
170
|
### Edge Cases
|
|
139
171
|
|
|
140
172
|
- **Zero results**: Tell the user "No newsletter emails found" and suggest broadening the query (e.g. removing the category filter or extending the date range)
|
|
141
173
|
- **Unsubscribe failures**: Report per-sender success/failure; the existing `gmail_unsubscribe` tool handles edge cases
|
|
142
|
-
- **Truncation handling**: The scan covers up to 5,000 messages by default (cap 10,000). If `truncated` is true, the top senders are still captured
|
|
143
|
-
- **Time budget exceeded**: If the scan returns `time_budget_exceeded: true`, present whatever results were collected.
|
|
174
|
+
- **Truncation handling**: The scan covers up to 5,000 messages by default (cap 10,000). If `truncated` is true, the top senders are still captured. Offer to run additional date-range passes to cover the remaining messages (see Large Inbox Handling above).
|
|
175
|
+
- **Time budget exceeded**: If the scan returns `time_budget_exceeded: true`, present whatever results were collected. Offer to run additional date-range passes for uncovered periods.
|
|
176
|
+
|
|
177
|
+
## Cleanup Preferences (Blocklist & Safelist)
|
|
178
|
+
|
|
179
|
+
The `gmail_preferences` tool persists sender preferences across cleanup sessions:
|
|
180
|
+
|
|
181
|
+
- **Blocklist**: Sender emails archived in previous sessions. On future cleanups, pre-pass archive all blocklisted senders before scanning (use `gmail_archive` with `query: "from:email1 OR from:email2 ... in:inbox"`).
|
|
182
|
+
- **Safelist**: Sender emails the user explicitly deselected (chose to keep). Exclude these senders from future cleanup tables entirely.
|
|
183
|
+
|
|
184
|
+
### Workflow integration
|
|
185
|
+
|
|
186
|
+
1. **Before scanning**: Call `gmail_preferences` with `action: "list"`. If blocklisted senders exist, offer to auto-archive them first ("I have N previously archived senders — want me to clean those up first?"). Remove safelisted senders from scan results before presenting the table.
|
|
187
|
+
2. **After archiving**: The blocklist is updated automatically when `gmail_archive` runs with `scan_id` + `sender_ids`.
|
|
188
|
+
3. **After user deselects**: When the user deselects senders from a cleanup table, call `gmail_preferences` with `action: "add_safelist"` and the deselected sender emails.
|
|
189
|
+
4. **User overrides**: If the user asks to stop blocking or stop keeping a sender, use `remove_blocklist` or `remove_safelist` accordingly.
|
|
144
190
|
|
|
145
191
|
## Scan ID
|
|
146
192
|
|
|
147
|
-
Scan tools (`gmail_sender_digest`, `gmail_outreach_scan`) return a `scan_id` that references message IDs stored server-side. This keeps thousands of message IDs out of the conversation context. `gmail_outreach_scan`
|
|
193
|
+
Scan tools (`gmail_sender_digest`, `gmail_outreach_scan`) return a `scan_id` that references message IDs stored server-side. This keeps thousands of message IDs out of the conversation context. `gmail_outreach_scan` finds senders without List-Unsubscribe headers (potential cold outreach) and enriches each sender with `has_prior_reply` (whether the user has ever sent an email to that address). Use this signal to filter out legitimate correspondents before classifying cold outreach.
|
|
148
194
|
|
|
149
195
|
- Pass `scan_id` + `sender_ids` to `gmail_archive` instead of `message_ids`
|
|
150
|
-
- Scan results expire after **30 minutes
|
|
196
|
+
- Scan results expire after **30 minutes**. When a scan expires (`resolved === null`), archiving automatically falls back to query-based archiving per sender. If sender IDs don't match the scan results (`resolved` is empty), the tool returns an error — re-run the scan to get fresh results.
|
|
151
197
|
- Raw `message_ids` still work as a fallback for non-scan workflows
|
|
152
198
|
|
|
153
199
|
## Batch Operations
|
|
@@ -494,15 +494,15 @@
|
|
|
494
494
|
},
|
|
495
495
|
"max_messages": {
|
|
496
496
|
"type": "number",
|
|
497
|
-
"description": "Maximum messages to scan (default
|
|
497
|
+
"description": "Maximum messages to scan (default 2000, cap 5000)"
|
|
498
498
|
},
|
|
499
499
|
"max_senders": {
|
|
500
500
|
"type": "number",
|
|
501
|
-
"description": "Maximum senders to return (default 50)"
|
|
501
|
+
"description": "Maximum senders to return (default 50, max 75)"
|
|
502
502
|
},
|
|
503
503
|
"page_token": {
|
|
504
504
|
"type": "string",
|
|
505
|
-
"description": "Resume token from a previous scan (rarely needed - scans now cover up to
|
|
505
|
+
"description": "Resume token from a previous scan (rarely needed - scans now cover up to 5,000 messages in a single call)"
|
|
506
506
|
},
|
|
507
507
|
"activity": {
|
|
508
508
|
"type": "string",
|
|
@@ -553,6 +553,36 @@
|
|
|
553
553
|
},
|
|
554
554
|
"executor": "tools/gmail-outreach-scan.ts",
|
|
555
555
|
"execution_target": "host"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
"name": "gmail_preferences",
|
|
559
|
+
"description": "Manage Gmail cleanup preferences (blocklist and safelist). The blocklist contains senders to auto-archive in future cleanups. The safelist contains senders the user wants to keep (excluded from cleanup tables).",
|
|
560
|
+
"category": "gmail",
|
|
561
|
+
"risk": "low",
|
|
562
|
+
"input_schema": {
|
|
563
|
+
"type": "object",
|
|
564
|
+
"properties": {
|
|
565
|
+
"action": {
|
|
566
|
+
"type": "string",
|
|
567
|
+
"enum": ["list", "add_blocklist", "add_safelist", "remove_blocklist", "remove_safelist"],
|
|
568
|
+
"description": "Preference action: list all preferences, or add/remove emails from blocklist or safelist"
|
|
569
|
+
},
|
|
570
|
+
"emails": {
|
|
571
|
+
"type": "array",
|
|
572
|
+
"items": {
|
|
573
|
+
"type": "string"
|
|
574
|
+
},
|
|
575
|
+
"description": "Email addresses to add or remove (required for add/remove actions)"
|
|
576
|
+
},
|
|
577
|
+
"activity": {
|
|
578
|
+
"type": "string",
|
|
579
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
"required": ["action"]
|
|
583
|
+
},
|
|
584
|
+
"executor": "tools/gmail-preferences-tool.ts",
|
|
585
|
+
"execution_target": "host"
|
|
556
586
|
}
|
|
557
587
|
]
|
|
558
588
|
}
|
|
@@ -8,12 +8,49 @@ import type {
|
|
|
8
8
|
ToolContext,
|
|
9
9
|
ToolExecutionResult,
|
|
10
10
|
} from "../../../../tools/types.js";
|
|
11
|
+
import { addToBlocklist } from "./gmail-preferences.js";
|
|
11
12
|
import { getSenderMessageIds } from "./scan-result-store.js";
|
|
12
13
|
import { err, ok } from "./shared.js";
|
|
13
14
|
|
|
14
15
|
const BATCH_MODIFY_LIMIT = 1000;
|
|
15
16
|
const MAX_MESSAGES = 5000;
|
|
16
17
|
|
|
18
|
+
function decodeSenderEmail(senderId: string): string | null {
|
|
19
|
+
try {
|
|
20
|
+
return Buffer.from(senderId, "base64url").toString("utf-8");
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Persist archived sender emails to the blocklist for future sessions.
|
|
28
|
+
* Only runs when the archive was initiated via a validated scan_id path
|
|
29
|
+
* (not bare message_ids) to prevent unverified emails from being recorded.
|
|
30
|
+
*/
|
|
31
|
+
function recordBlocklist(
|
|
32
|
+
scanId: string | undefined,
|
|
33
|
+
senderIds: string[] | undefined,
|
|
34
|
+
): void {
|
|
35
|
+
if (!scanId || !senderIds?.length) return;
|
|
36
|
+
const archivedEmails: string[] = [];
|
|
37
|
+
for (const sid of senderIds) {
|
|
38
|
+
try {
|
|
39
|
+
const email = Buffer.from(sid, "base64url").toString("utf-8");
|
|
40
|
+
if (email.includes("@")) archivedEmails.push(email);
|
|
41
|
+
} catch {
|
|
42
|
+
// Skip undecodable sender IDs
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (archivedEmails.length > 0) {
|
|
46
|
+
try {
|
|
47
|
+
addToBlocklist(archivedEmails);
|
|
48
|
+
} catch {
|
|
49
|
+
// Non-fatal — preferences are best-effort
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
17
54
|
export async function run(
|
|
18
55
|
input: Record<string, unknown>,
|
|
19
56
|
context: ToolContext,
|
|
@@ -28,9 +65,9 @@ export async function run(
|
|
|
28
65
|
// Resolve message IDs via priority: query → scan_id+sender_ids → message_ids → message_id
|
|
29
66
|
if (query) {
|
|
30
67
|
// Query path requires surface action confirmation
|
|
31
|
-
if (!context.triggeredBySurfaceAction) {
|
|
68
|
+
if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
|
|
32
69
|
return err(
|
|
33
|
-
"This tool requires
|
|
70
|
+
"This tool requires either a surface action or a scheduled task run with this tool in required_tools. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
|
|
34
71
|
);
|
|
35
72
|
}
|
|
36
73
|
|
|
@@ -83,24 +120,92 @@ export async function run(
|
|
|
83
120
|
}
|
|
84
121
|
} else if (scanId && senderIds?.length) {
|
|
85
122
|
// Scan path requires surface action confirmation
|
|
86
|
-
if (!context.triggeredBySurfaceAction) {
|
|
123
|
+
if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
|
|
87
124
|
return err(
|
|
88
|
-
"This tool requires
|
|
125
|
+
"This tool requires either a surface action or a scheduled task run with this tool in required_tools. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
|
|
89
126
|
);
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
const resolved = getSenderMessageIds(scanId, senderIds);
|
|
93
|
-
if (
|
|
130
|
+
if (resolved !== null && resolved.length > 0) {
|
|
131
|
+
messageIds = resolved;
|
|
132
|
+
} else if (resolved === null) {
|
|
133
|
+
// Scan expired or sender IDs unresolved — fall back to query-based archiving
|
|
134
|
+
const emails: string[] = [];
|
|
135
|
+
const undecodable: string[] = [];
|
|
136
|
+
for (const sid of senderIds) {
|
|
137
|
+
const email = decodeSenderEmail(sid);
|
|
138
|
+
if (email && email.includes("@")) {
|
|
139
|
+
emails.push(email);
|
|
140
|
+
} else {
|
|
141
|
+
undecodable.push(sid);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (emails.length === 0) {
|
|
146
|
+
return err(
|
|
147
|
+
"Scan results have expired and sender IDs could not be decoded. Please re-run the scan.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const connection = await resolveOAuthConnection("google", {
|
|
153
|
+
account,
|
|
154
|
+
});
|
|
155
|
+
const allMessageIds: string[] = [];
|
|
156
|
+
|
|
157
|
+
for (const email of emails) {
|
|
158
|
+
const fallbackQuery = `from:"${email.replace(/"/g, "")}" in:inbox`;
|
|
159
|
+
let pageToken: string | undefined;
|
|
160
|
+
while (allMessageIds.length < MAX_MESSAGES) {
|
|
161
|
+
const listResp = await listMessages(
|
|
162
|
+
connection,
|
|
163
|
+
fallbackQuery,
|
|
164
|
+
Math.min(500, MAX_MESSAGES - allMessageIds.length),
|
|
165
|
+
pageToken,
|
|
166
|
+
);
|
|
167
|
+
const ids = (listResp.messages ?? []).map((m) => m.id);
|
|
168
|
+
if (ids.length === 0) break;
|
|
169
|
+
allMessageIds.push(...ids);
|
|
170
|
+
pageToken = listResp.nextPageToken ?? undefined;
|
|
171
|
+
if (!pageToken) break;
|
|
172
|
+
}
|
|
173
|
+
if (allMessageIds.length >= MAX_MESSAGES) break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (allMessageIds.length === 0) {
|
|
177
|
+
return ok("No inbox messages found for the selected senders.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < allMessageIds.length; i += BATCH_MODIFY_LIMIT) {
|
|
181
|
+
const chunk = allMessageIds.slice(i, i + BATCH_MODIFY_LIMIT);
|
|
182
|
+
await batchModifyMessages(connection, chunk, {
|
|
183
|
+
removeLabelIds: ["INBOX"],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const parts = [
|
|
188
|
+
`Archived ${allMessageIds.length} message(s) via query fallback (scan results had expired).`,
|
|
189
|
+
];
|
|
190
|
+
if (undecodable.length > 0) {
|
|
191
|
+
parts.push(
|
|
192
|
+
`${undecodable.length} sender ID(s) could not be decoded and were skipped.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return ok(parts.join(" "));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
94
200
|
return err(
|
|
95
|
-
"
|
|
201
|
+
"The provided sender IDs do not match the scan results. Please re-run the scan.",
|
|
96
202
|
);
|
|
97
203
|
}
|
|
98
|
-
messageIds = resolved;
|
|
99
204
|
} else if (messageIds?.length) {
|
|
100
205
|
// Batch message_ids path requires surface action confirmation
|
|
101
|
-
if (!context.triggeredBySurfaceAction) {
|
|
206
|
+
if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
|
|
102
207
|
return err(
|
|
103
|
-
"This tool requires
|
|
208
|
+
"This tool requires either a surface action or a scheduled task run with this tool in required_tools. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
|
|
104
209
|
);
|
|
105
210
|
}
|
|
106
211
|
} else if (messageId) {
|
|
@@ -133,6 +238,7 @@ export async function run(
|
|
|
133
238
|
await modifyMessage(connection, messageIds[0], {
|
|
134
239
|
removeLabelIds: ["INBOX"],
|
|
135
240
|
});
|
|
241
|
+
recordBlocklist(scanId, senderIds);
|
|
136
242
|
return ok("Message archived.");
|
|
137
243
|
}
|
|
138
244
|
|
|
@@ -142,6 +248,7 @@ export async function run(
|
|
|
142
248
|
removeLabelIds: ["INBOX"],
|
|
143
249
|
});
|
|
144
250
|
}
|
|
251
|
+
recordBlocklist(scanId, senderIds);
|
|
145
252
|
return ok(`Archived ${messageIds.length} message(s).`);
|
|
146
253
|
} catch (e) {
|
|
147
254
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -11,6 +11,11 @@ import type {
|
|
|
11
11
|
import { storeScanResult } from "./scan-result-store.js";
|
|
12
12
|
import { err, ok } from "./shared.js";
|
|
13
13
|
|
|
14
|
+
function isRateLimitError(e: unknown): boolean {
|
|
15
|
+
if (!(e instanceof Error)) return false;
|
|
16
|
+
return /\b429\b/.test(e.message);
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
const MAX_MESSAGES_CAP = 5000;
|
|
15
20
|
const MAX_IDS_PER_SENDER = 5000;
|
|
16
21
|
const MAX_SAMPLE_SUBJECTS = 3;
|
|
@@ -69,6 +74,8 @@ export async function run(
|
|
|
69
74
|
const startTime = Date.now();
|
|
70
75
|
const TIME_BUDGET_MS = 90_000;
|
|
71
76
|
|
|
77
|
+
let rateLimited = false;
|
|
78
|
+
|
|
72
79
|
while (allMessageIds.length < maxMessages) {
|
|
73
80
|
if (Date.now() - startTime > TIME_BUDGET_MS) {
|
|
74
81
|
timeBudgetExceeded = true;
|
|
@@ -76,12 +83,17 @@ export async function run(
|
|
|
76
83
|
break;
|
|
77
84
|
}
|
|
78
85
|
const pageSize = Math.min(100, maxMessages - allMessageIds.length);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
query,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
let listResp;
|
|
87
|
+
try {
|
|
88
|
+
listResp = await listMessages(connection, query, pageSize, pageToken);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (isRateLimitError(e)) {
|
|
91
|
+
rateLimited = true;
|
|
92
|
+
truncated = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
85
97
|
const ids = (listResp.messages ?? []).map((m) => m.id);
|
|
86
98
|
if (ids.length === 0) break;
|
|
87
99
|
allMessageIds.push(...ids);
|
|
@@ -103,6 +115,17 @@ export async function run(
|
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
if (allMessageIds.length === 0) {
|
|
118
|
+
if (rateLimited) {
|
|
119
|
+
return ok(
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
senders: [],
|
|
122
|
+
total_scanned: 0,
|
|
123
|
+
rate_limited: true,
|
|
124
|
+
truncated: true,
|
|
125
|
+
note: "Rate limited before any messages could be fetched. Try again later or reduce max_messages.",
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
106
129
|
return ok(
|
|
107
130
|
JSON.stringify({
|
|
108
131
|
senders: [],
|
|
@@ -112,7 +135,35 @@ export async function run(
|
|
|
112
135
|
);
|
|
113
136
|
}
|
|
114
137
|
|
|
115
|
-
|
|
138
|
+
// Settle all fetch promises — collect successes and tolerate 429 failures.
|
|
139
|
+
const elapsedMs = Date.now() - startTime;
|
|
140
|
+
const settleDeadlineMs = Math.max(TIME_BUDGET_MS - elapsedMs, 5_000);
|
|
141
|
+
const deadlineRejection = new Promise<never>((_, reject) =>
|
|
142
|
+
setTimeout(
|
|
143
|
+
() => reject(new Error("fetch deadline exceeded")),
|
|
144
|
+
settleDeadlineMs,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
const settled = await Promise.allSettled(
|
|
148
|
+
fetchPromises.map((p) => Promise.race([p, deadlineRejection])),
|
|
149
|
+
);
|
|
150
|
+
const messages: GmailMessage[] = [];
|
|
151
|
+
for (const result of settled) {
|
|
152
|
+
if (result.status === "fulfilled") {
|
|
153
|
+
messages.push(...result.value);
|
|
154
|
+
} else if (isRateLimitError(result.reason)) {
|
|
155
|
+
rateLimited = true;
|
|
156
|
+
truncated = true;
|
|
157
|
+
} else if (
|
|
158
|
+
result.reason instanceof Error &&
|
|
159
|
+
result.reason.message === "fetch deadline exceeded"
|
|
160
|
+
) {
|
|
161
|
+
timeBudgetExceeded = true;
|
|
162
|
+
truncated = true;
|
|
163
|
+
} else {
|
|
164
|
+
throw result.reason;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
116
167
|
|
|
117
168
|
// Aggregate all fetched messages by sender
|
|
118
169
|
const senderMap = new Map<string, OutreachSenderAggregation>();
|
|
@@ -177,16 +228,91 @@ export async function run(
|
|
|
177
228
|
}
|
|
178
229
|
}
|
|
179
230
|
|
|
180
|
-
// Sort by message count desc,
|
|
231
|
+
// Sort by message count desc — over-fetch before enrichment, cap after
|
|
181
232
|
const sorted = [...senderMap.values()]
|
|
182
233
|
.sort((a, b) => b.messageCount - a.messageCount)
|
|
183
|
-
.slice(0, maxSenders);
|
|
234
|
+
.slice(0, maxSenders * 3);
|
|
235
|
+
|
|
236
|
+
// Enrich with prior-reply signal: check if user has ever sent to each sender.
|
|
237
|
+
// Uses bounded concurrency (batches of 10) and AbortController to cancel
|
|
238
|
+
// in-flight requests when the time budget expires.
|
|
239
|
+
const ENRICHMENT_CONCURRENCY = 10;
|
|
240
|
+
const priorReplyMap = new Map<string, boolean>();
|
|
241
|
+
if (!rateLimited) {
|
|
242
|
+
const enrichmentBudgetMs = Math.max(
|
|
243
|
+
TIME_BUDGET_MS - (Date.now() - startTime),
|
|
244
|
+
5_000,
|
|
245
|
+
);
|
|
246
|
+
const abortController = new AbortController();
|
|
247
|
+
const budgetTimer = setTimeout(
|
|
248
|
+
() => abortController.abort(),
|
|
249
|
+
enrichmentBudgetMs,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// Process in waves to limit concurrency and stop on budget expiry
|
|
254
|
+
for (
|
|
255
|
+
let i = 0;
|
|
256
|
+
i < sorted.length && !abortController.signal.aborted;
|
|
257
|
+
i += ENRICHMENT_CONCURRENCY
|
|
258
|
+
) {
|
|
259
|
+
const batch = sorted.slice(i, i + ENRICHMENT_CONCURRENCY);
|
|
260
|
+
const batchChecks = batch.map(async (s) => {
|
|
261
|
+
if (abortController.signal.aborted) return;
|
|
262
|
+
try {
|
|
263
|
+
const resp = await listMessages(
|
|
264
|
+
connection,
|
|
265
|
+
`from:me to:${s.email}`,
|
|
266
|
+
1,
|
|
267
|
+
);
|
|
268
|
+
priorReplyMap.set(s.email, (resp.messages?.length ?? 0) > 0);
|
|
269
|
+
} catch {
|
|
270
|
+
// Non-fatal — default to safe direction (assume prior reply exists)
|
|
271
|
+
priorReplyMap.set(s.email, true);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
await Promise.race([
|
|
275
|
+
Promise.all(batchChecks),
|
|
276
|
+
new Promise<void>((resolve) =>
|
|
277
|
+
abortController.signal.addEventListener(
|
|
278
|
+
"abort",
|
|
279
|
+
() => resolve(),
|
|
280
|
+
{
|
|
281
|
+
once: true,
|
|
282
|
+
},
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
} finally {
|
|
288
|
+
clearTimeout(budgetTimer);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Default any un-enriched senders to safe direction
|
|
292
|
+
for (const s of sorted) {
|
|
293
|
+
if (!priorReplyMap.has(s.email)) {
|
|
294
|
+
priorReplyMap.set(s.email, true);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Rate limited — default all to safe direction
|
|
299
|
+
for (const s of sorted) {
|
|
300
|
+
priorReplyMap.set(s.email, true);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
184
303
|
|
|
185
|
-
|
|
304
|
+
// Filter out senders with prior replies, then cap to maxSenders.
|
|
305
|
+
// This is the purpose of over-fetching (maxSenders * 3): enrich more
|
|
306
|
+
// candidates, discard those with existing replies, then take the top N.
|
|
307
|
+
const capped = sorted
|
|
308
|
+
.filter((s) => !priorReplyMap.get(s.email))
|
|
309
|
+
.slice(0, maxSenders);
|
|
310
|
+
const senders = capped.map((s) => ({
|
|
186
311
|
id: Buffer.from(s.email).toString("base64url"),
|
|
187
312
|
display_name: s.displayName || s.email.split("@")[0],
|
|
188
313
|
email: s.email,
|
|
189
314
|
message_count: s.messageCount,
|
|
315
|
+
has_prior_reply: priorReplyMap.get(s.email) ?? true,
|
|
190
316
|
newest_message_id: s.newestMessageId,
|
|
191
317
|
oldest_date: s.oldestDate,
|
|
192
318
|
newest_date: s.newestDate,
|
|
@@ -196,7 +322,7 @@ export async function run(
|
|
|
196
322
|
|
|
197
323
|
// Store message IDs server-side to keep them out of LLM context
|
|
198
324
|
const scanId = storeScanResult(
|
|
199
|
-
|
|
325
|
+
capped.map((s) => ({
|
|
200
326
|
id: Buffer.from(s.email).toString("base64url"),
|
|
201
327
|
messageIds: s.messageIds,
|
|
202
328
|
newestMessageId: s.newestMessageId,
|
|
@@ -211,6 +337,7 @@ export async function run(
|
|
|
211
337
|
total_scanned: allMessageIds.length,
|
|
212
338
|
...(truncated ? { truncated: true } : {}),
|
|
213
339
|
...(timeBudgetExceeded ? { time_budget_exceeded: true } : {}),
|
|
340
|
+
...(rateLimited ? { rate_limited: true } : {}),
|
|
214
341
|
note: "Scanned inbox for senders without List-Unsubscribe headers (potential cold outreach). Use gmail_archive and gmail_filters for cleanup.",
|
|
215
342
|
}),
|
|
216
343
|
);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ToolContext,
|
|
3
|
+
ToolExecutionResult,
|
|
4
|
+
} from "../../../../tools/types.js";
|
|
5
|
+
import {
|
|
6
|
+
addToBlocklist,
|
|
7
|
+
addToSafelist,
|
|
8
|
+
loadPreferences,
|
|
9
|
+
removeFromBlocklist,
|
|
10
|
+
removeFromSafelist,
|
|
11
|
+
} from "./gmail-preferences.js";
|
|
12
|
+
import { err, ok } from "./shared.js";
|
|
13
|
+
|
|
14
|
+
export async function run(
|
|
15
|
+
input: Record<string, unknown>,
|
|
16
|
+
_context: ToolContext,
|
|
17
|
+
): Promise<ToolExecutionResult> {
|
|
18
|
+
const action = input.action as string;
|
|
19
|
+
const emails = input.emails as string[] | undefined;
|
|
20
|
+
|
|
21
|
+
switch (action) {
|
|
22
|
+
case "list": {
|
|
23
|
+
const prefs = loadPreferences();
|
|
24
|
+
return ok(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
blocklist_count: prefs.blocklist.length,
|
|
27
|
+
safelist_count: prefs.safelist.length,
|
|
28
|
+
blocklist: prefs.blocklist,
|
|
29
|
+
safelist: prefs.safelist,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
case "add_blocklist": {
|
|
34
|
+
if (!emails?.length) return err("emails is required for add_blocklist");
|
|
35
|
+
addToBlocklist(emails);
|
|
36
|
+
return ok(`Added ${emails.length} sender(s) to blocklist.`);
|
|
37
|
+
}
|
|
38
|
+
case "add_safelist": {
|
|
39
|
+
if (!emails?.length) return err("emails is required for add_safelist");
|
|
40
|
+
addToSafelist(emails);
|
|
41
|
+
return ok(`Added ${emails.length} sender(s) to safelist.`);
|
|
42
|
+
}
|
|
43
|
+
case "remove_blocklist": {
|
|
44
|
+
if (!emails?.length)
|
|
45
|
+
return err("emails is required for remove_blocklist");
|
|
46
|
+
removeFromBlocklist(emails);
|
|
47
|
+
return ok(`Removed ${emails.length} sender(s) from blocklist.`);
|
|
48
|
+
}
|
|
49
|
+
case "remove_safelist": {
|
|
50
|
+
if (!emails?.length) return err("emails is required for remove_safelist");
|
|
51
|
+
removeFromSafelist(emails);
|
|
52
|
+
return ok(`Removed ${emails.length} sender(s) from safelist.`);
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return err(
|
|
56
|
+
`Unknown action "${action}". Use list, add_blocklist, add_safelist, remove_blocklist, or remove_safelist.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|