@vellumai/assistant 0.6.4 → 0.6.6
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/.prettierignore +5 -0
- package/AGENTS.md +9 -1
- package/ARCHITECTURE.md +43 -49
- package/Dockerfile +17 -3
- package/README.md +3 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +33 -59
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +19 -18
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/error-handling.md +111 -0
- package/docs/plugins.md +761 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/knip.json +9 -2
- package/node_modules/@vellumai/ces-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +334 -78
- package/package.json +6 -3
- package/scripts/generate-openapi.ts +50 -11
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +112 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +171 -2
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +36 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +51 -182
- package/src/__tests__/btw-routes.test.ts +47 -1
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +96 -4
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +870 -655
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +11 -28
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +440 -114
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +883 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
- package/src/__tests__/conversation-agent-loop.test.ts +435 -216
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
- package/src/__tests__/conversation-process-callsite.test.ts +309 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
- package/src/__tests__/conversation-queue.test.ts +68 -38
- package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
- package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +39 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
- package/src/__tests__/conversation-speed-override.test.ts +36 -12
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +118 -2
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +4 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
- package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +5 -2
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +135 -19
- package/src/__tests__/credentials-cli.test.ts +1 -9
- package/src/__tests__/cross-provider-web-search.test.ts +84 -0
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/gemini-provider.test.ts +0 -3
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +1 -66
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +121 -0
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +621 -0
- package/src/__tests__/model-intents.test.ts +11 -83
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +46 -78
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/openai-provider.test.ts +7 -0
- package/src/__tests__/openai-responses-provider.test.ts +396 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +93 -14
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +69 -9
- package/src/__tests__/reaction-persistence.test.ts +561 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/server-history-render.test.ts +31 -0
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skills.test.ts +19 -33
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +259 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +201 -94
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/trust-store.test.ts +442 -109
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +22 -16
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +545 -115
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/telephony-stt-routing.ts +1 -1
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/email-list.test.ts +6 -0
- package/src/cli/commands/__tests__/email-send.test.ts +93 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -12
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +69 -8
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +299 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +39 -24
- package/src/cli.ts +0 -37
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -190
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +42 -10
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +194 -10
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +49 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/calls.ts +0 -4
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/filing.ts +2 -7
- package/src/config/schemas/heartbeat.ts +0 -5
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +317 -0
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +9 -4
- package/src/config/schemas/stt.ts +1 -0
- package/src/config/schemas/tts.ts +64 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +96 -7
- package/src/context/__tests__/compact-prompt.test.ts +63 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/prompts/compact.md +26 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +417 -39
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +19 -8
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +65 -2
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +0 -3
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
- package/src/daemon/conversation-agent-loop.ts +1282 -599
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +59 -17
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +24 -11
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +1063 -211
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +51 -9
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +197 -64
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +65 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +7 -3
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +109 -82
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +63 -0
- package/src/daemon/message-types/messages.ts +21 -1
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +122 -12
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -65
- package/src/daemon/web-search-history.ts +126 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +99 -28
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/__tests__/feed-scheduler.test.ts +39 -11
- package/src/home/__tests__/rollup-producer.test.ts +44 -0
- package/src/home/assistant-feed-authoring.ts +4 -0
- package/src/home/emit-feed-event.ts +11 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +97 -4
- package/src/home/relationship-state-writer.ts +2 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/home/rollup-producer.ts +34 -5
- package/src/home/suggested-prompts.ts +101 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/socket-path.test.ts +34 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +2 -1
- package/src/ipc/cli-server.ts +26 -8
- package/src/ipc/gateway-client.ts +6 -3
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +63 -0
- package/src/ipc/routes/browser.ts +97 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +31 -1
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +76 -0
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/conversation-analyze-job.ts +14 -13
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +133 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +32 -4
- package/src/memory/db-init.ts +10 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +89 -29
- package/src/memory/graph/extraction.test.ts +272 -2
- package/src/memory/graph/extraction.ts +183 -53
- package/src/memory/graph/graph-search.test.ts +93 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +237 -48
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/indexer.ts +5 -5
- package/src/memory/job-handlers/conversation-starters.ts +23 -20
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +44 -3
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/pkb/pkb-index.test.ts +369 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +499 -0
- package/src/memory/pkb/pkb-search.ts +159 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +147 -1
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +22 -0
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/adapter.ts +14 -2
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
- package/src/messaging/providers/slack/render-transcript.ts +501 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +31 -14
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -106
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +1223 -0
- package/src/permissions/approval-policy.ts +309 -0
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +1620 -0
- package/src/permissions/bash-risk-classifier.ts +950 -0
- package/src/permissions/checker.ts +348 -711
- package/src/permissions/command-registry.test.ts +774 -0
- package/src/permissions/command-registry.ts +1005 -0
- package/src/permissions/defaults.ts +28 -79
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +262 -0
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +161 -62
- package/src/permissions/types.ts +25 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +9 -19
- package/src/platform/client.ts +19 -1
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/prompts/templates/SOUL.md +2 -2
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +183 -14
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/gemini/client.ts +65 -2
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +524 -33
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +57 -1
- package/src/providers/openai/responses-provider.ts +86 -9
- package/src/providers/openrouter/client.ts +80 -9
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +19 -8
- package/src/providers/retry.ts +174 -39
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
- package/src/providers/speech-to-text/provider-catalog.ts +17 -0
- package/src/providers/speech-to-text/resolve.ts +7 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
- package/src/providers/speech-to-text/xai-realtime.ts +821 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +93 -3
- package/src/runtime/AGENTS.md +27 -18
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/agent-wake.ts +63 -22
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/btw-sidechain.ts +13 -3
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +129 -9
- package/src/runtime/http-types.ts +23 -3
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/migrations/vbundle-importer.ts +154 -9
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
- package/src/runtime/routes/approval-routes.ts +29 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +351 -138
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +120 -2
- package/src/runtime/routes/inbound-message-handler.ts +987 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/integrations/slack/channel.ts +25 -3
- package/src/runtime/routes/llm-context-normalization.ts +23 -1
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +720 -127
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/routes/settings-routes.ts +4 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +3 -2
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
- package/src/runtime/services/analyze-conversation.ts +12 -16
- package/src/runtime/skill-route-registry.ts +97 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +56 -8
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +98 -35
- package/src/security/secure-keys.ts +7 -8
- package/src/security/token-manager.ts +27 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/skills/catalog-cache.ts +35 -9
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
- package/src/stt/daemon-batch-transcriber.ts +33 -0
- package/src/stt/stt-stream-session.ts +8 -1
- package/src/stt/types.ts +5 -1
- package/src/subagent/manager.ts +41 -13
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
- package/src/tools/browser/browser-execution.ts +150 -54
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +129 -73
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/network/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +116 -46
- package/src/tools/policy-context.ts +29 -8
- package/src/tools/registry.ts +195 -6
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/side-effects.ts +0 -11
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +1 -0
- package/src/tools/tool-manifest.ts +6 -21
- package/src/tools/types.ts +40 -5
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +240 -13
- package/src/tts/provider-catalog.ts +18 -0
- package/src/tts/providers/index.ts +2 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/types.ts +46 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +9 -4
- package/src/util/pricing.ts +41 -8
- package/src/watcher/engine.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +28 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
- package/src/__tests__/gmail-archive-gate.test.ts +0 -246
- package/src/__tests__/gmail-preferences.test.ts +0 -117
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/__tests__/update-bulletin-format.test.ts +0 -181
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -478
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/doctor.ts +0 -341
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/browser/SKILL.md +0 -88
- package/src/config/bundled-skills/browser/TOOLS.json +0 -516
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
- package/src/config/bundled-skills/gmail/SKILL.md +0 -221
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/config/bundled-skills/slack/SKILL.md +0 -108
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/prompts/templates/UPDATES.md +0 -50
- package/src/prompts/update-bulletin-format.ts +0 -85
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -139
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -19,10 +19,29 @@ import {
|
|
|
19
19
|
recordConversationSeenSignal,
|
|
20
20
|
type SignalType,
|
|
21
21
|
} from "../../memory/conversation-attention-store.js";
|
|
22
|
+
import {
|
|
23
|
+
addMessage,
|
|
24
|
+
getMessageById,
|
|
25
|
+
getMessages,
|
|
26
|
+
selectSlackMetaCandidateMetadata,
|
|
27
|
+
updateMessageMetadata,
|
|
28
|
+
} from "../../memory/conversation-crud.js";
|
|
22
29
|
import * as deliveryChannels from "../../memory/delivery-channels.js";
|
|
23
30
|
import * as deliveryCrud from "../../memory/delivery-crud.js";
|
|
24
31
|
import * as deliveryStatus from "../../memory/delivery-status.js";
|
|
25
32
|
import * as externalConversationStore from "../../memory/external-conversation-store.js";
|
|
33
|
+
import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
|
|
34
|
+
import {
|
|
35
|
+
backfillDm,
|
|
36
|
+
backfillThread,
|
|
37
|
+
} from "../../messaging/providers/slack/backfill.js";
|
|
38
|
+
import {
|
|
39
|
+
mergeSlackMetadata,
|
|
40
|
+
readSlackMetadata,
|
|
41
|
+
type SlackMessageMetadata,
|
|
42
|
+
writeSlackMetadata,
|
|
43
|
+
} from "../../messaging/providers/slack/message-metadata.js";
|
|
44
|
+
import { wrapUntrustedContent } from "../../security/untrusted-content.js";
|
|
26
45
|
import { canonicalizeInboundIdentity } from "../../util/canonicalize-identity.js";
|
|
27
46
|
import { getLogger } from "../../util/logger.js";
|
|
28
47
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
@@ -52,6 +71,28 @@ import { handleVerificationIntercept } from "./inbound-stages/verification-inter
|
|
|
52
71
|
|
|
53
72
|
const log = getLogger("runtime-http");
|
|
54
73
|
|
|
74
|
+
// Delete-lookup retry configuration. Delete webhooks can race ahead of
|
|
75
|
+
// the inbound handler's `linkMessage` call when the original message's
|
|
76
|
+
// agent loop is still running. Retrying buys time for the link to land
|
|
77
|
+
// before we drop the deletion signal. Mirrors the edit-intercept path's
|
|
78
|
+
// EDIT_LOOKUP_RETRIES / EDIT_LOOKUP_DELAY_MS constants.
|
|
79
|
+
let deleteLookupRetries = 5;
|
|
80
|
+
let deleteLookupDelayMs = 2000;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Test-only override for the delete-lookup retry timings. Used by
|
|
84
|
+
* tests that exercise the "no such message" path without waiting
|
|
85
|
+
* through the full production backoff. Not exported from any barrel
|
|
86
|
+
* file — only the test file imports it directly.
|
|
87
|
+
*/
|
|
88
|
+
export function _setDeleteLookupConfigForTests(
|
|
89
|
+
retries: number,
|
|
90
|
+
delayMs: number,
|
|
91
|
+
): void {
|
|
92
|
+
deleteLookupRetries = retries;
|
|
93
|
+
deleteLookupDelayMs = delayMs;
|
|
94
|
+
}
|
|
95
|
+
|
|
55
96
|
export async function handleChannelInbound(
|
|
56
97
|
req: Request,
|
|
57
98
|
processMessage?: MessageProcessor,
|
|
@@ -238,6 +279,152 @@ export async function handleChannelInbound(
|
|
|
238
279
|
if (aclResult.earlyResponse) return aclResult.earlyResponse;
|
|
239
280
|
const { resolvedMember, guardianVerifyCode } = aclResult;
|
|
240
281
|
|
|
282
|
+
// ── Slack delete propagation ──
|
|
283
|
+
// Slack message_deleted events are forwarded by the gateway with the
|
|
284
|
+
// sentinel `callbackData = "message_deleted"` and `sourceMetadata.messageId`
|
|
285
|
+
// set to the original (deleted) message's ts. Short-circuit the rest of
|
|
286
|
+
// the pipeline: the agent loop should not run for delete notifications,
|
|
287
|
+
// and routing the event through approval / agent paths would be incorrect.
|
|
288
|
+
// We mark the stored row as deleted in slackMeta but leave `content`
|
|
289
|
+
// untouched for audit purposes — rendering elides based on the deletedAt
|
|
290
|
+
// marker. Gated behind ingress ACL so non-members cannot drive deletes
|
|
291
|
+
// (matches the edit-intercept policy).
|
|
292
|
+
if (sourceChannel === "slack" && body.callbackData === "message_deleted") {
|
|
293
|
+
const deletedMessageTs =
|
|
294
|
+
typeof sourceMetadata?.messageId === "string"
|
|
295
|
+
? sourceMetadata.messageId
|
|
296
|
+
: undefined;
|
|
297
|
+
|
|
298
|
+
if (!deletedMessageTs) {
|
|
299
|
+
log.debug(
|
|
300
|
+
{ conversationExternalId },
|
|
301
|
+
"Slack message_deleted event missing sourceMetadata.messageId; ignoring",
|
|
302
|
+
);
|
|
303
|
+
return Response.json({ accepted: true, deleted: false });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Look up the stored message via the existing channel-event lookup.
|
|
307
|
+
// The original message's externalMessageId may differ from its ts
|
|
308
|
+
// (Slack populates client_msg_id when present), so we join via the
|
|
309
|
+
// sourceMessageId column which records the ts explicitly.
|
|
310
|
+
//
|
|
311
|
+
// Retry with backoff mirrors the edit-intercept path: delete webhooks
|
|
312
|
+
// can race ahead of `linkMessage` when the original message's agent
|
|
313
|
+
// loop is still running. Without retries a delete that arrives in
|
|
314
|
+
// that window is silently dropped and the deletion signal is lost.
|
|
315
|
+
let original: { messageId: string; conversationId: string } | null = null;
|
|
316
|
+
for (let attempt = 0; attempt <= deleteLookupRetries; attempt++) {
|
|
317
|
+
original = deliveryCrud.findMessageBySourceId(
|
|
318
|
+
sourceChannel,
|
|
319
|
+
conversationExternalId,
|
|
320
|
+
deletedMessageTs,
|
|
321
|
+
);
|
|
322
|
+
if (original) break;
|
|
323
|
+
if (attempt < deleteLookupRetries) {
|
|
324
|
+
log.info(
|
|
325
|
+
{
|
|
326
|
+
conversationExternalId,
|
|
327
|
+
deletedMessageTs,
|
|
328
|
+
attempt: attempt + 1,
|
|
329
|
+
maxAttempts: deleteLookupRetries,
|
|
330
|
+
},
|
|
331
|
+
"Original message not linked yet, retrying delete lookup",
|
|
332
|
+
);
|
|
333
|
+
await new Promise((resolve) =>
|
|
334
|
+
setTimeout(resolve, deleteLookupDelayMs),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!original) {
|
|
340
|
+
log.debug(
|
|
341
|
+
{ conversationExternalId, deletedMessageTs },
|
|
342
|
+
"No stored message found for Slack delete after retries; ignoring",
|
|
343
|
+
);
|
|
344
|
+
return Response.json({ accepted: true, deleted: false });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Merge deletedAt into the existing slackMeta sub-key. If the row has
|
|
348
|
+
// no slackMeta (legacy pre-upgrade row), skip — the renderer's flat
|
|
349
|
+
// fallback ignores deletedAt for those rows anyway, and synthesizing
|
|
350
|
+
// a partial slackMeta here would produce metadata that fails
|
|
351
|
+
// readSlackMetadata validation.
|
|
352
|
+
const row = getMessageById(original.messageId);
|
|
353
|
+
if (!row?.metadata) {
|
|
354
|
+
log.debug(
|
|
355
|
+
{
|
|
356
|
+
conversationExternalId,
|
|
357
|
+
deletedMessageTs,
|
|
358
|
+
messageId: original.messageId,
|
|
359
|
+
},
|
|
360
|
+
"Stored Slack message has no metadata; skipping delete marker",
|
|
361
|
+
);
|
|
362
|
+
return Response.json({ accepted: true, deleted: false });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let parentMetadata: Record<string, unknown>;
|
|
366
|
+
try {
|
|
367
|
+
const parsed = JSON.parse(row.metadata) as unknown;
|
|
368
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
369
|
+
parentMetadata = parsed as Record<string, unknown>;
|
|
370
|
+
} else {
|
|
371
|
+
parentMetadata = {};
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
log.debug(
|
|
375
|
+
{
|
|
376
|
+
conversationExternalId,
|
|
377
|
+
deletedMessageTs,
|
|
378
|
+
messageId: original.messageId,
|
|
379
|
+
},
|
|
380
|
+
"Failed to parse stored metadata; skipping delete marker",
|
|
381
|
+
);
|
|
382
|
+
return Response.json({ accepted: true, deleted: false });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const existingSlackMeta =
|
|
386
|
+
typeof parentMetadata.slackMeta === "string"
|
|
387
|
+
? parentMetadata.slackMeta
|
|
388
|
+
: null;
|
|
389
|
+
|
|
390
|
+
if (!existingSlackMeta) {
|
|
391
|
+
log.debug(
|
|
392
|
+
{
|
|
393
|
+
conversationExternalId,
|
|
394
|
+
deletedMessageTs,
|
|
395
|
+
messageId: original.messageId,
|
|
396
|
+
},
|
|
397
|
+
"Stored Slack message has no slackMeta; skipping delete marker",
|
|
398
|
+
);
|
|
399
|
+
return Response.json({ accepted: true, deleted: false });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const updatedSlackMeta = mergeSlackMetadata(existingSlackMeta, {
|
|
403
|
+
deletedAt: Date.now(),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// updateMessageMetadata performs a shallow merge over the parent
|
|
407
|
+
// metadata, replacing only `slackMeta` and leaving sibling keys
|
|
408
|
+
// (channel, interface, provenance, etc.) untouched. Content column
|
|
409
|
+
// is intentionally not updated.
|
|
410
|
+
updateMessageMetadata(original.messageId, { slackMeta: updatedSlackMeta });
|
|
411
|
+
|
|
412
|
+
log.info(
|
|
413
|
+
{
|
|
414
|
+
conversationExternalId,
|
|
415
|
+
deletedMessageTs,
|
|
416
|
+
messageId: original.messageId,
|
|
417
|
+
},
|
|
418
|
+
"Marked Slack message as deleted",
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
return Response.json({
|
|
422
|
+
accepted: true,
|
|
423
|
+
deleted: true,
|
|
424
|
+
messageId: original.messageId,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
241
428
|
if (hasAttachments) {
|
|
242
429
|
const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
|
|
243
430
|
if (resolved.length !== attachmentIds.length) {
|
|
@@ -369,6 +556,119 @@ export async function handleChannelInbound(
|
|
|
369
556
|
});
|
|
370
557
|
}
|
|
371
558
|
|
|
559
|
+
// ── Slack reaction handling ──
|
|
560
|
+
// Reactions arrive as regular `SlackInboundEvent`s with `callbackData`
|
|
561
|
+
// prefixed `reaction:` (added) or `reaction_removed:` (removed).
|
|
562
|
+
//
|
|
563
|
+
// Two paths from here:
|
|
564
|
+
// 1. Guardian approval-by-reaction. A `reaction:` (added) event from
|
|
565
|
+
// the guardian on an active approval prompt is consumed by
|
|
566
|
+
// `handleApprovalInterception` to apply the decision. In that case
|
|
567
|
+
// we do NOT persist the reaction as a transcript line — resolved
|
|
568
|
+
// guardian approval reactions have no transcript representation.
|
|
569
|
+
// 2. All other reactions (non-guardian, no pending approval, stale,
|
|
570
|
+
// and any `reaction_removed:` event regardless of actor) fall
|
|
571
|
+
// through to `persistSlackReactionAsMessage` so the chronological
|
|
572
|
+
// renderer (PR 18) can surface them inline. Reactions never
|
|
573
|
+
// trigger an agent response, so we short-circuit before
|
|
574
|
+
// escalation and agent-loop dispatch in both cases.
|
|
575
|
+
if (isSlackReactionEvent(body)) {
|
|
576
|
+
// Approval interception runs only for reactions (added) — `reaction_removed`
|
|
577
|
+
// never expresses an approval intent, so un-reacting is left as a pure
|
|
578
|
+
// transcript signal. Gated by the same `replyCallbackUrl && !duplicate`
|
|
579
|
+
// preconditions used by the standard approval interception call below.
|
|
580
|
+
const isReactionAdded = body.callbackData?.startsWith("reaction:") === true;
|
|
581
|
+
if (isReactionAdded && replyCallbackUrl && !result.duplicate) {
|
|
582
|
+
const trustCtxForReaction: TrustContext = resolveTrustContext({
|
|
583
|
+
assistantId: canonicalAssistantId,
|
|
584
|
+
sourceChannel,
|
|
585
|
+
conversationExternalId,
|
|
586
|
+
actorExternalId: rawSenderId,
|
|
587
|
+
actorUsername: body.actorUsername,
|
|
588
|
+
actorDisplayName: body.actorDisplayName,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const approvalMessageTs =
|
|
592
|
+
typeof sourceMetadata?.messageId === "string"
|
|
593
|
+
? sourceMetadata.messageId
|
|
594
|
+
: undefined;
|
|
595
|
+
|
|
596
|
+
const reactionApprovalResult = await handleApprovalInterception({
|
|
597
|
+
conversationId: result.conversationId,
|
|
598
|
+
callbackData: body.callbackData,
|
|
599
|
+
content: trimmedContent,
|
|
600
|
+
conversationExternalId,
|
|
601
|
+
sourceChannel,
|
|
602
|
+
actorExternalId: canonicalSenderId ?? rawSenderId,
|
|
603
|
+
replyCallbackUrl,
|
|
604
|
+
bearerToken: mintBearerToken(),
|
|
605
|
+
trustCtx: trustCtxForReaction,
|
|
606
|
+
assistantId: canonicalAssistantId,
|
|
607
|
+
approvalCopyGenerator,
|
|
608
|
+
approvalConversationGenerator,
|
|
609
|
+
approvalMessageTs,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// A real guardian decision was applied — short-circuit and skip the
|
|
613
|
+
// reaction-persistence path so we do not double-record it as a
|
|
614
|
+
// transcript line. All other interception outcomes (stale_ignored,
|
|
615
|
+
// non-guardian, no pending approval) fall through to persistence.
|
|
616
|
+
if (reactionApprovalResult.type === "guardian_decision_applied") {
|
|
617
|
+
return Response.json({
|
|
618
|
+
accepted: true,
|
|
619
|
+
duplicate: false,
|
|
620
|
+
eventId: result.eventId,
|
|
621
|
+
approval: reactionApprovalResult.type,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const reactedMessageTs =
|
|
627
|
+
typeof sourceMetadata?.messageId === "string"
|
|
628
|
+
? sourceMetadata.messageId
|
|
629
|
+
: undefined;
|
|
630
|
+
if (!reactedMessageTs) {
|
|
631
|
+
log.debug(
|
|
632
|
+
{ conversationId: result.conversationId, eventId: result.eventId },
|
|
633
|
+
"Skipping reaction persistence: missing sourceMetadata.messageId",
|
|
634
|
+
);
|
|
635
|
+
return Response.json({
|
|
636
|
+
accepted: result.accepted,
|
|
637
|
+
duplicate: result.duplicate,
|
|
638
|
+
eventId: result.eventId,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const threadTs =
|
|
643
|
+
typeof sourceMetadata?.threadId === "string"
|
|
644
|
+
? sourceMetadata.threadId
|
|
645
|
+
: undefined;
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
await persistSlackReactionAsMessage({
|
|
649
|
+
conversationId: result.conversationId,
|
|
650
|
+
conversationExternalId,
|
|
651
|
+
eventId: result.eventId,
|
|
652
|
+
callbackData: body.callbackData!,
|
|
653
|
+
actorDisplayName: body.actorDisplayName,
|
|
654
|
+
threadTs,
|
|
655
|
+
reactedMessageTs,
|
|
656
|
+
duplicate: result.duplicate,
|
|
657
|
+
});
|
|
658
|
+
} catch (err) {
|
|
659
|
+
log.error(
|
|
660
|
+
{ err, conversationId: result.conversationId, eventId: result.eventId },
|
|
661
|
+
"Failed to persist Slack reaction event",
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return Response.json({
|
|
666
|
+
accepted: result.accepted,
|
|
667
|
+
duplicate: result.duplicate,
|
|
668
|
+
eventId: result.eventId,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
372
672
|
// ── Ingress escalation ──
|
|
373
673
|
const escalationResponse = handleEscalationIntercept({
|
|
374
674
|
resolvedMember,
|
|
@@ -606,7 +906,13 @@ export async function handleChannelInbound(
|
|
|
606
906
|
// of whether content/attachments are present — callback payloads always
|
|
607
907
|
// have non-empty content (normalize.ts sets message.content to cbq.data),
|
|
608
908
|
// so checking for empty content alone would miss stale callbacks.
|
|
609
|
-
|
|
909
|
+
//
|
|
910
|
+
// Reaction events (`reaction:` / `reaction_removed:`) are persisted by
|
|
911
|
+
// the earlier `isSlackReactionEvent` branch and never reach here; guard
|
|
912
|
+
// explicitly so a future refactor can't let a reaction ts drive a
|
|
913
|
+
// "This approval request has been resolved." edit that would clobber
|
|
914
|
+
// the user's reacted-to message.
|
|
915
|
+
if (hasCallbackData && !isSlackReactionEvent(body)) {
|
|
610
916
|
// Record seen signal even for stale callbacks — the user still interacted
|
|
611
917
|
if (sourceChannel === "telegram" || sourceChannel === "slack") {
|
|
612
918
|
try {
|
|
@@ -698,6 +1004,100 @@ export async function handleChannelInbound(
|
|
|
698
1004
|
heartbeatService?.resetTimer();
|
|
699
1005
|
}
|
|
700
1006
|
|
|
1007
|
+
// Slack inbound metadata captured for thread-aware persistence. The
|
|
1008
|
+
// gateway forwards `thread_ts` under `sourceMetadata.threadId` and the
|
|
1009
|
+
// message's own ts under `sourceMetadata.messageId`. Persistence turns
|
|
1010
|
+
// this into a `slackMeta` sub-object in the row's metadata column so
|
|
1011
|
+
// the chronological renderer can reconstruct thread structure without
|
|
1012
|
+
// re-fetching from Slack.
|
|
1013
|
+
const slackThreadTs =
|
|
1014
|
+
sourceChannel === "slack" &&
|
|
1015
|
+
typeof sourceMetadata?.threadId === "string"
|
|
1016
|
+
? sourceMetadata.threadId
|
|
1017
|
+
: undefined;
|
|
1018
|
+
const slackInbound =
|
|
1019
|
+
sourceChannel === "slack"
|
|
1020
|
+
? {
|
|
1021
|
+
channelId: conversationExternalId,
|
|
1022
|
+
channelTs: sourceMessageId ?? externalMessageId,
|
|
1023
|
+
...(slackThreadTs ? { threadTs: slackThreadTs } : {}),
|
|
1024
|
+
...((body.actorDisplayName ?? body.actorUsername)
|
|
1025
|
+
? {
|
|
1026
|
+
displayName: body.actorDisplayName ?? body.actorUsername!,
|
|
1027
|
+
}
|
|
1028
|
+
: {}),
|
|
1029
|
+
}
|
|
1030
|
+
: undefined;
|
|
1031
|
+
|
|
1032
|
+
// Account identifier threaded into backfill so `resolveConnection()`
|
|
1033
|
+
// can pick the right workspace in multi-account setups. Best-effort:
|
|
1034
|
+
// the gateway forwards `sourceMetadata.account` when it knows which
|
|
1035
|
+
// Slack workspace the event came from; when absent, both helpers
|
|
1036
|
+
// fall back to the default-active connection.
|
|
1037
|
+
const slackAccount =
|
|
1038
|
+
sourceChannel === "slack" &&
|
|
1039
|
+
typeof sourceMetadata?.account === "string" &&
|
|
1040
|
+
sourceMetadata.account.length > 0
|
|
1041
|
+
? sourceMetadata.account
|
|
1042
|
+
: undefined;
|
|
1043
|
+
|
|
1044
|
+
// ── DM cold-start backfill ──
|
|
1045
|
+
// First time a Slack DM lands in a conversation that has fewer than
|
|
1046
|
+
// SLACK_DM_BACKFILL_WARM_THRESHOLD stored slackMeta messages, fetch a
|
|
1047
|
+
// window of recent history so the agent sees prior context. One-shot:
|
|
1048
|
+
// once persistence warms up past the threshold, subsequent DMs no
|
|
1049
|
+
// longer trigger backfill. Failures are non-fatal — the new message
|
|
1050
|
+
// proceeds without backfilled history.
|
|
1051
|
+
if (sourceChannel === "slack" && sourceChatType === "im") {
|
|
1052
|
+
// Exclude the just-arrived webhook message from the history window —
|
|
1053
|
+
// the normal inbound persistence path writes it separately, so
|
|
1054
|
+
// including it here would produce duplicate user turns. Only pass a
|
|
1055
|
+
// bound when we actually have a Slack ts (`<secs>.<micros>`): the
|
|
1056
|
+
// fallback path writes `externalMessageId` into `channelTs`, but that
|
|
1057
|
+
// identifier is not guaranteed to be a Slack ts, and Slack's
|
|
1058
|
+
// `conversations.history` rejects anything that isn't a ts string.
|
|
1059
|
+
const boundingTs = isSlackTs(sourceMessageId)
|
|
1060
|
+
? sourceMessageId
|
|
1061
|
+
: undefined;
|
|
1062
|
+
await tryBackfillSlackDmIfCold({
|
|
1063
|
+
conversationId: result.conversationId,
|
|
1064
|
+
channelId: conversationExternalId,
|
|
1065
|
+
account: slackAccount,
|
|
1066
|
+
latestTs: boundingTs,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ── Thread-ancestor backfill ──
|
|
1071
|
+
// When a Slack reply arrives for a thread the daemon never saw the
|
|
1072
|
+
// parent of, fetch the thread's recent history from Slack and persist
|
|
1073
|
+
// the missing messages so the chronological renderer (PR 18) has the
|
|
1074
|
+
// full conversation. Awaited (mirrors the DM cold-start path above)
|
|
1075
|
+
// so the agent loop dispatched immediately afterwards observes the
|
|
1076
|
+
// backfilled parent — without this, `loadSlackChronologicalMessages`
|
|
1077
|
+
// can race the persist and miss thread context. Backfill is bounded
|
|
1078
|
+
// (parent + ~50 messages) and the agent latency is dominated by the
|
|
1079
|
+
// LLM call, so the added latency is negligible. Failures are
|
|
1080
|
+
// swallowed inside the helper so they never block dispatch.
|
|
1081
|
+
if (slackThreadTs) {
|
|
1082
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
1083
|
+
conversationId: result.conversationId,
|
|
1084
|
+
channelId: conversationExternalId,
|
|
1085
|
+
threadTs: slackThreadTs,
|
|
1086
|
+
excludeChannelTs: slackInbound?.channelTs,
|
|
1087
|
+
account: slackAccount,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Wrap non-guardian inbound content in external_content boundaries so
|
|
1092
|
+
// the model can distinguish external channel messages from instructions.
|
|
1093
|
+
const contentForProcessing =
|
|
1094
|
+
trustCtx.trustClass !== "guardian"
|
|
1095
|
+
? wrapUntrustedContent(trimmedContent, {
|
|
1096
|
+
source: "webhook",
|
|
1097
|
+
sourceDetail: trustCtx.requesterIdentifier,
|
|
1098
|
+
})
|
|
1099
|
+
: trimmedContent;
|
|
1100
|
+
|
|
701
1101
|
// Fire-and-forget: process the message and deliver the reply in the background.
|
|
702
1102
|
// The HTTP response returns immediately so the gateway webhook is not blocked.
|
|
703
1103
|
// The onEvent callback in processMessage registers pending interactions, and
|
|
@@ -706,7 +1106,7 @@ export async function handleChannelInbound(
|
|
|
706
1106
|
processMessage,
|
|
707
1107
|
conversationId: result.conversationId,
|
|
708
1108
|
eventId: result.eventId,
|
|
709
|
-
content:
|
|
1109
|
+
content: contentForProcessing,
|
|
710
1110
|
attachmentIds: hasAttachments ? attachmentIds : undefined,
|
|
711
1111
|
sourceChannel,
|
|
712
1112
|
sourceInterface,
|
|
@@ -721,6 +1121,7 @@ export async function handleChannelInbound(
|
|
|
721
1121
|
assistantId: canonicalAssistantId,
|
|
722
1122
|
approvalCopyGenerator,
|
|
723
1123
|
chatType: sourceChatType,
|
|
1124
|
+
slackInbound,
|
|
724
1125
|
});
|
|
725
1126
|
}
|
|
726
1127
|
}
|
|
@@ -731,3 +1132,587 @@ export async function handleChannelInbound(
|
|
|
731
1132
|
eventId: result.eventId,
|
|
732
1133
|
});
|
|
733
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Detect a Slack reaction event by inspecting the inbound payload's
|
|
1138
|
+
* `callbackData` prefix. The gateway encodes reactions as a unified
|
|
1139
|
+
* `SlackInboundEvent` with `callbackData` of the form
|
|
1140
|
+
* `reaction:<emoji>` (added) or `reaction_removed:<emoji>` (removed) —
|
|
1141
|
+
* see `gateway/src/slack/normalize.ts`. This helper centralizes that
|
|
1142
|
+
* convention so the daemon can route reactions to a dedicated persistence
|
|
1143
|
+
* branch instead of the agent-response pipeline.
|
|
1144
|
+
*/
|
|
1145
|
+
export function isSlackReactionEvent(body: {
|
|
1146
|
+
sourceChannel?: string;
|
|
1147
|
+
callbackData?: string;
|
|
1148
|
+
}): boolean {
|
|
1149
|
+
if (body.sourceChannel !== "slack") return false;
|
|
1150
|
+
const cb = body.callbackData;
|
|
1151
|
+
if (typeof cb !== "string") return false;
|
|
1152
|
+
return cb.startsWith("reaction:") || cb.startsWith("reaction_removed:");
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Parse a reaction `callbackData` string into its op (added/removed) and
|
|
1157
|
+
* emoji name. Returns `null` when the input is not a reaction prefix or
|
|
1158
|
+
* when the emoji portion is empty.
|
|
1159
|
+
*/
|
|
1160
|
+
export function parseSlackReactionCallbackData(
|
|
1161
|
+
callbackData: string,
|
|
1162
|
+
): { op: "added" | "removed"; emoji: string } | null {
|
|
1163
|
+
let op: "added" | "removed";
|
|
1164
|
+
let emoji: string;
|
|
1165
|
+
if (callbackData.startsWith("reaction_removed:")) {
|
|
1166
|
+
op = "removed";
|
|
1167
|
+
emoji = callbackData.slice("reaction_removed:".length);
|
|
1168
|
+
} else if (callbackData.startsWith("reaction:")) {
|
|
1169
|
+
op = "added";
|
|
1170
|
+
emoji = callbackData.slice("reaction:".length);
|
|
1171
|
+
} else {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
if (emoji.length === 0) return null;
|
|
1175
|
+
return { op, emoji };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Persist a Slack reaction event as a `messages` row with `slackMeta`
|
|
1180
|
+
* envelope so the renderer can surface it inline in the chronological
|
|
1181
|
+
* transcript. Reactions do not trigger an agent response — the row is
|
|
1182
|
+
* written and the inbound event is linked, but the agent loop is not
|
|
1183
|
+
* dispatched.
|
|
1184
|
+
*
|
|
1185
|
+
* The caller is expected to have run `recordInbound` already so that
|
|
1186
|
+
* deduplication and conversation resolution have happened. Duplicate
|
|
1187
|
+
* inbound events are skipped here to keep persistence idempotent.
|
|
1188
|
+
*/
|
|
1189
|
+
async function persistSlackReactionAsMessage(params: {
|
|
1190
|
+
conversationId: string;
|
|
1191
|
+
conversationExternalId: string;
|
|
1192
|
+
eventId: string;
|
|
1193
|
+
callbackData: string;
|
|
1194
|
+
actorDisplayName?: string;
|
|
1195
|
+
threadTs?: string;
|
|
1196
|
+
reactedMessageTs: string;
|
|
1197
|
+
duplicate: boolean;
|
|
1198
|
+
}): Promise<void> {
|
|
1199
|
+
if (params.duplicate) return;
|
|
1200
|
+
|
|
1201
|
+
const parsed = parseSlackReactionCallbackData(params.callbackData);
|
|
1202
|
+
if (!parsed) {
|
|
1203
|
+
log.debug(
|
|
1204
|
+
{
|
|
1205
|
+
conversationId: params.conversationId,
|
|
1206
|
+
callbackData: params.callbackData,
|
|
1207
|
+
},
|
|
1208
|
+
"Skipping reaction persistence: unparseable callbackData",
|
|
1209
|
+
);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const slackMeta: SlackMessageMetadata = {
|
|
1214
|
+
source: "slack",
|
|
1215
|
+
channelId: params.conversationExternalId,
|
|
1216
|
+
channelTs: params.reactedMessageTs,
|
|
1217
|
+
eventKind: "reaction",
|
|
1218
|
+
...(params.threadTs ? { threadTs: params.threadTs } : {}),
|
|
1219
|
+
...(params.actorDisplayName
|
|
1220
|
+
? { displayName: params.actorDisplayName }
|
|
1221
|
+
: {}),
|
|
1222
|
+
reaction: {
|
|
1223
|
+
emoji: parsed.emoji,
|
|
1224
|
+
targetChannelTs: params.reactedMessageTs,
|
|
1225
|
+
op: parsed.op,
|
|
1226
|
+
...(params.actorDisplayName
|
|
1227
|
+
? { actorDisplayName: params.actorDisplayName }
|
|
1228
|
+
: {}),
|
|
1229
|
+
},
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// Sentinel content — renderers (PR 18) read `slackMeta` to format the
|
|
1233
|
+
// reaction line; the literal text is never displayed to the model.
|
|
1234
|
+
const persisted = await addMessage(
|
|
1235
|
+
params.conversationId,
|
|
1236
|
+
"user",
|
|
1237
|
+
"[reaction]",
|
|
1238
|
+
{ slackMeta: writeSlackMetadata(slackMeta) },
|
|
1239
|
+
{ skipIndexing: true },
|
|
1240
|
+
);
|
|
1241
|
+
deliveryCrud.linkMessage(params.eventId, persisted.id);
|
|
1242
|
+
deliveryStatus.markProcessed(params.eventId);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Threshold of stored Slack-tagged messages below which a conversation is
|
|
1247
|
+
* considered "cold" and eligible for one-shot backfill. The number is
|
|
1248
|
+
* deliberately small but greater than 1 so a single sentinel row (e.g. a
|
|
1249
|
+
* stale reaction) does not disqualify a conversation that has no real
|
|
1250
|
+
* message history yet.
|
|
1251
|
+
*/
|
|
1252
|
+
const SLACK_DM_BACKFILL_WARM_THRESHOLD = 3;
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Shape-check for a Slack `ts` value. Slack IDs messages by `<seconds>.<micros>`
|
|
1256
|
+
* strings (e.g. `"1700000000.000100"`). The daemon also stores an
|
|
1257
|
+
* `externalMessageId` derived from the gateway's dedupe key which follows a
|
|
1258
|
+
* different format, so any path that feeds a ts to Slack's API
|
|
1259
|
+
* (`conversations.history`'s `latest`, etc.) must shape-check first — Slack
|
|
1260
|
+
* rejects non-ts arguments with `invalid_arguments`, and passing a malformed
|
|
1261
|
+
* bound silently disables the intended history window.
|
|
1262
|
+
*/
|
|
1263
|
+
function isSlackTs(value: string | null | undefined): value is string {
|
|
1264
|
+
return typeof value === "string" && /^\d+\.\d+$/.test(value);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Batch size used when pulling candidate rows from SQL. A bare
|
|
1269
|
+
* `LIKE '%"slackMeta"%'` match can include rows whose metadata JSON is
|
|
1270
|
+
* malformed or carries the literal under an unrelated key, so we fetch in
|
|
1271
|
+
* batches and re-validate each candidate with Zod. The threshold is tiny
|
|
1272
|
+
* (see `SLACK_DM_BACKFILL_WARM_THRESHOLD`), so a 10× batch is a trivial
|
|
1273
|
+
* scan while letting a handful of bad rows not starve the count.
|
|
1274
|
+
*/
|
|
1275
|
+
const SLACK_DM_CANDIDATE_BATCH_SIZE = SLACK_DM_BACKFILL_WARM_THRESHOLD * 10;
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Absolute cap on candidate rows inspected per webhook to classify a DM as
|
|
1279
|
+
* warm. If this many substring matches have been examined without reaching
|
|
1280
|
+
* the valid-row threshold, treat the conversation as cold — a scan this
|
|
1281
|
+
* deep already dominates the critical-path budget and the cold-start
|
|
1282
|
+
* backfill path is itself idempotent against re-runs.
|
|
1283
|
+
*/
|
|
1284
|
+
const SLACK_DM_CANDIDATE_MAX_SCAN = SLACK_DM_BACKFILL_WARM_THRESHOLD * 20;
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Count messages in a conversation whose `metadata` carries a well-formed
|
|
1288
|
+
* `slackMeta` envelope, capped at the warm threshold. SQL prefilters with
|
|
1289
|
+
* `LIKE` + `LIMIT`/`OFFSET` so warm DM conversations never scan the full
|
|
1290
|
+
* table on the webhook critical path, and each candidate is re-validated
|
|
1291
|
+
* through `readSlackMetadata` — a bare substring match would otherwise
|
|
1292
|
+
* wrongly count rows whose metadata is truncated, parses but fails schema
|
|
1293
|
+
* validation, or happens to contain the literal `"slackMeta"` under an
|
|
1294
|
+
* unrelated key. Pulls candidates in batches, continuing until either the
|
|
1295
|
+
* threshold of *valid* rows is reached or the per-call scan cap is hit, so
|
|
1296
|
+
* a cluster of malformed rows at the head of the scan cannot starve the
|
|
1297
|
+
* count and misclassify a warm conversation as cold.
|
|
1298
|
+
*/
|
|
1299
|
+
function countSlackMetaMessages(conversationId: string): number {
|
|
1300
|
+
let count = 0;
|
|
1301
|
+
let offset = 0;
|
|
1302
|
+
while (offset < SLACK_DM_CANDIDATE_MAX_SCAN) {
|
|
1303
|
+
const remaining = SLACK_DM_CANDIDATE_MAX_SCAN - offset;
|
|
1304
|
+
const batchLimit = Math.min(SLACK_DM_CANDIDATE_BATCH_SIZE, remaining);
|
|
1305
|
+
const candidates = selectSlackMetaCandidateMetadata(
|
|
1306
|
+
conversationId,
|
|
1307
|
+
batchLimit,
|
|
1308
|
+
offset,
|
|
1309
|
+
);
|
|
1310
|
+
if (candidates.length === 0) return count;
|
|
1311
|
+
for (const raw of candidates) {
|
|
1312
|
+
let parent: Record<string, unknown> | null = null;
|
|
1313
|
+
try {
|
|
1314
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
1315
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1316
|
+
parent = parsed as Record<string, unknown>;
|
|
1317
|
+
}
|
|
1318
|
+
} catch {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (!parent) continue;
|
|
1322
|
+
const inner = parent.slackMeta;
|
|
1323
|
+
if (typeof inner !== "string") continue;
|
|
1324
|
+
if (readSlackMetadata(inner)) {
|
|
1325
|
+
count++;
|
|
1326
|
+
if (count >= SLACK_DM_BACKFILL_WARM_THRESHOLD) return count;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (candidates.length < batchLimit) return count;
|
|
1330
|
+
offset += candidates.length;
|
|
1331
|
+
}
|
|
1332
|
+
return count;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Build the set of `slackMeta.channelTs` values already stored on a
|
|
1337
|
+
* conversation. Used by both DM cold-start backfill and thread-ancestor
|
|
1338
|
+
* backfill to dedupe rows so a partial prior backfill (or a single message
|
|
1339
|
+
* that was already persisted via the live ingress path) does not double-write.
|
|
1340
|
+
*/
|
|
1341
|
+
function readStoredSlackChannelTs(conversationId: string): Set<string> {
|
|
1342
|
+
const seen = new Set<string>();
|
|
1343
|
+
for (const row of getMessages(conversationId)) {
|
|
1344
|
+
if (!row.metadata) continue;
|
|
1345
|
+
let parent: Record<string, unknown> | null = null;
|
|
1346
|
+
try {
|
|
1347
|
+
const parsed = JSON.parse(row.metadata) as unknown;
|
|
1348
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1349
|
+
parent = parsed as Record<string, unknown>;
|
|
1350
|
+
}
|
|
1351
|
+
} catch {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (!parent) continue;
|
|
1355
|
+
const raw = parent.slackMeta;
|
|
1356
|
+
if (typeof raw !== "string") continue;
|
|
1357
|
+
const meta = readSlackMetadata(raw);
|
|
1358
|
+
// Only message rows represent stored Slack messages. Reaction rows carry
|
|
1359
|
+
// `channelTs` equal to the target message's ts, so including them would
|
|
1360
|
+
// make a reaction on a thread parent wrongly short-circuit ancestor
|
|
1361
|
+
// backfill (the parent itself may still be unseen).
|
|
1362
|
+
if (meta && meta.eventKind === "message") seen.add(meta.channelTs);
|
|
1363
|
+
}
|
|
1364
|
+
return seen;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Persist a single backfilled Slack message as a `messages` row with a
|
|
1369
|
+
* `slackMeta` envelope.
|
|
1370
|
+
*
|
|
1371
|
+
* Shared insertion point for any path that hydrates Slack history lazily
|
|
1372
|
+
* (DM cold-start backfill, thread-ancestor backfill, etc.). Role is derived
|
|
1373
|
+
* from `message.metadata.isBot` — bot-authored rows map to `"assistant"` so
|
|
1374
|
+
* our own prior replies (and any other bot traffic) are not rehydrated as
|
|
1375
|
+
* user turns, which would otherwise corrupt speaker attribution and make
|
|
1376
|
+
* the assistant treat its own outputs as new user input on later turns.
|
|
1377
|
+
* Caller is responsible for dedup checks before invoking; this helper
|
|
1378
|
+
* performs no idempotency check itself.
|
|
1379
|
+
*/
|
|
1380
|
+
async function persistBackfilledSlackMessage(params: {
|
|
1381
|
+
conversationId: string;
|
|
1382
|
+
channelId: string;
|
|
1383
|
+
message: ProviderMessage;
|
|
1384
|
+
}): Promise<void> {
|
|
1385
|
+
const { message } = params;
|
|
1386
|
+
const slackMeta: SlackMessageMetadata = {
|
|
1387
|
+
source: "slack",
|
|
1388
|
+
channelId: params.channelId,
|
|
1389
|
+
channelTs: message.id,
|
|
1390
|
+
eventKind: "message",
|
|
1391
|
+
...(message.threadId ? { threadTs: message.threadId } : {}),
|
|
1392
|
+
...(message.sender?.name ? { displayName: message.sender.name } : {}),
|
|
1393
|
+
};
|
|
1394
|
+
const role = message.metadata?.isBot === true ? "assistant" : "user";
|
|
1395
|
+
await addMessage(params.conversationId, role, message.text ?? "", {
|
|
1396
|
+
slackMeta: writeSlackMetadata(slackMeta),
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* In-memory map of in-flight DM cold-start backfills keyed by conversationId.
|
|
1402
|
+
* Concurrent inbound DMs to the same cold conversation share a single
|
|
1403
|
+
* backfill promise instead of each issuing their own Slack history fetch and
|
|
1404
|
+
* write — without this, two near-simultaneous DMs would both observe a cold
|
|
1405
|
+
* count, both fetch the same history window, and both insert duplicate rows
|
|
1406
|
+
* (channelTs lives inside a JSON metadata blob, so the DB has no uniqueness
|
|
1407
|
+
* constraint to fall back on).
|
|
1408
|
+
*/
|
|
1409
|
+
const _dmBackfillInFlight = new Map<string, Promise<void>>();
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* One-shot DM cold-start backfill. When a Slack DM lands in a conversation
|
|
1413
|
+
* with fewer than `SLACK_DM_BACKFILL_WARM_THRESHOLD` stored Slack-tagged
|
|
1414
|
+
* messages, fetch a window of recent history via `backfillDm` and persist
|
|
1415
|
+
* each returned message with a `slackMeta` envelope. Already-stored
|
|
1416
|
+
* messages (matched by `slackMeta.channelTs`) are skipped to keep the
|
|
1417
|
+
* operation idempotent across retries.
|
|
1418
|
+
*
|
|
1419
|
+
* Failure semantics: any error is logged at WARN and swallowed. The caller
|
|
1420
|
+
* proceeds with only the new message.
|
|
1421
|
+
*/
|
|
1422
|
+
async function tryBackfillSlackDmIfCold(params: {
|
|
1423
|
+
conversationId: string;
|
|
1424
|
+
channelId: string;
|
|
1425
|
+
account?: string;
|
|
1426
|
+
latestTs?: string;
|
|
1427
|
+
}): Promise<void> {
|
|
1428
|
+
const existing = _dmBackfillInFlight.get(params.conversationId);
|
|
1429
|
+
if (existing) {
|
|
1430
|
+
await existing;
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const promise = runBackfillSlackDmIfCold(params).finally(() => {
|
|
1434
|
+
_dmBackfillInFlight.delete(params.conversationId);
|
|
1435
|
+
});
|
|
1436
|
+
_dmBackfillInFlight.set(params.conversationId, promise);
|
|
1437
|
+
await promise;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function runBackfillSlackDmIfCold(params: {
|
|
1441
|
+
conversationId: string;
|
|
1442
|
+
channelId: string;
|
|
1443
|
+
account?: string;
|
|
1444
|
+
latestTs?: string;
|
|
1445
|
+
}): Promise<void> {
|
|
1446
|
+
try {
|
|
1447
|
+
const storedCount = countSlackMetaMessages(params.conversationId);
|
|
1448
|
+
if (storedCount >= SLACK_DM_BACKFILL_WARM_THRESHOLD) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Pass the webhook message's ts as `before` (Slack's `latest`,
|
|
1453
|
+
// exclusive) so history never contains the message that's about to be
|
|
1454
|
+
// persisted by the live inbound path. Without this bound the just-arrived
|
|
1455
|
+
// DM would be written twice — once here and once via normal persistence —
|
|
1456
|
+
// producing duplicate user turns.
|
|
1457
|
+
const fetched = await backfillDm(params.channelId, {
|
|
1458
|
+
limit: 50,
|
|
1459
|
+
account: params.account,
|
|
1460
|
+
before: params.latestTs,
|
|
1461
|
+
});
|
|
1462
|
+
if (fetched.length === 0) {
|
|
1463
|
+
log.debug(
|
|
1464
|
+
{ conversationId: params.conversationId, channelId: params.channelId },
|
|
1465
|
+
"DM backfill returned no messages",
|
|
1466
|
+
);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const seen = readStoredSlackChannelTs(params.conversationId);
|
|
1471
|
+
let written = 0;
|
|
1472
|
+
// Slack's conversation.history returns most-recent first. Reverse so
|
|
1473
|
+
// rows insert in chronological order, giving stable createdAt ordering
|
|
1474
|
+
// and a transcript that reads correctly when the renderer joins on
|
|
1475
|
+
// monotonic createdAt.
|
|
1476
|
+
const ordered = [...fetched].reverse();
|
|
1477
|
+
for (const message of ordered) {
|
|
1478
|
+
if (seen.has(message.id)) continue;
|
|
1479
|
+
try {
|
|
1480
|
+
await persistBackfilledSlackMessage({
|
|
1481
|
+
conversationId: params.conversationId,
|
|
1482
|
+
channelId: params.channelId,
|
|
1483
|
+
message,
|
|
1484
|
+
});
|
|
1485
|
+
seen.add(message.id);
|
|
1486
|
+
written++;
|
|
1487
|
+
} catch (perRowErr) {
|
|
1488
|
+
log.warn(
|
|
1489
|
+
{
|
|
1490
|
+
err: perRowErr,
|
|
1491
|
+
conversationId: params.conversationId,
|
|
1492
|
+
channelId: params.channelId,
|
|
1493
|
+
channelTs: message.id,
|
|
1494
|
+
},
|
|
1495
|
+
"Failed to persist backfilled DM row; continuing",
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
log.info(
|
|
1501
|
+
{
|
|
1502
|
+
conversationId: params.conversationId,
|
|
1503
|
+
channelId: params.channelId,
|
|
1504
|
+
fetched: fetched.length,
|
|
1505
|
+
written,
|
|
1506
|
+
},
|
|
1507
|
+
"DM cold-start backfill complete",
|
|
1508
|
+
);
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
// `channel_not_found` almost always means the resolved connection is
|
|
1511
|
+
// pointing at the wrong Slack workspace (a real config bug), so log it
|
|
1512
|
+
// at ERROR to match backfill's rethrow contract. Other failures
|
|
1513
|
+
// (timeout, auth, ratelimited, …) stay at WARN — they're expected
|
|
1514
|
+
// transient blips and the caller proceeds without backfilled history.
|
|
1515
|
+
const channelNotFound =
|
|
1516
|
+
err instanceof Error && /channel_not_found/i.test(err.message);
|
|
1517
|
+
const payload = {
|
|
1518
|
+
err,
|
|
1519
|
+
conversationId: params.conversationId,
|
|
1520
|
+
channelId: params.channelId,
|
|
1521
|
+
account: params.account,
|
|
1522
|
+
};
|
|
1523
|
+
if (channelNotFound) {
|
|
1524
|
+
log.error(
|
|
1525
|
+
payload,
|
|
1526
|
+
"DM cold-start backfill hit channel_not_found — connection likely points at the wrong Slack workspace",
|
|
1527
|
+
);
|
|
1528
|
+
} else {
|
|
1529
|
+
log.warn(
|
|
1530
|
+
payload,
|
|
1531
|
+
"DM cold-start backfill failed; proceeding without history",
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// ---------------------------------------------------------------------------
|
|
1538
|
+
// Slack thread backfill on gap detection
|
|
1539
|
+
// ---------------------------------------------------------------------------
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* In-memory TTL cache keyed by `<conversationId>:<threadTs>`. Tracks recent
|
|
1543
|
+
* thread-backfill triggers so a burst of replies inside the same Slack
|
|
1544
|
+
* thread (e.g. a guardian rapidly typing several lines) does not re-fetch
|
|
1545
|
+
* the same parent messages from Slack repeatedly. Entries naturally fall
|
|
1546
|
+
* out after the TTL — if the thread is still active later, a fresh
|
|
1547
|
+
* backfill becomes a cheap "are the parents already stored?" DB lookup
|
|
1548
|
+
* that short-circuits before the Slack API is touched.
|
|
1549
|
+
*
|
|
1550
|
+
* Exported only for tests; production callers should use
|
|
1551
|
+
* {@link triggerSlackThreadBackfillIfNeeded}.
|
|
1552
|
+
*/
|
|
1553
|
+
export const _backfillTriggerCache = new Map<string, number>();
|
|
1554
|
+
|
|
1555
|
+
const BACKFILL_TRIGGER_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
1556
|
+
const BACKFILL_TRIGGER_CACHE_MAX = 1_000;
|
|
1557
|
+
|
|
1558
|
+
function pruneBackfillCacheIfNeeded(): void {
|
|
1559
|
+
if (_backfillTriggerCache.size < BACKFILL_TRIGGER_CACHE_MAX) return;
|
|
1560
|
+
const now = Date.now();
|
|
1561
|
+
for (const [key, ts] of _backfillTriggerCache) {
|
|
1562
|
+
if (now - ts >= BACKFILL_TRIGGER_TTL_MS) {
|
|
1563
|
+
_backfillTriggerCache.delete(key);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
// If still over the cap after TTL sweep, drop the oldest entries (LRU-ish).
|
|
1567
|
+
if (_backfillTriggerCache.size >= BACKFILL_TRIGGER_CACHE_MAX) {
|
|
1568
|
+
const entries = [..._backfillTriggerCache.entries()].sort(
|
|
1569
|
+
(a, b) => a[1] - b[1],
|
|
1570
|
+
);
|
|
1571
|
+
const toRemove = entries.slice(
|
|
1572
|
+
0,
|
|
1573
|
+
entries.length - BACKFILL_TRIGGER_CACHE_MAX + 1,
|
|
1574
|
+
);
|
|
1575
|
+
for (const [key] of toRemove) {
|
|
1576
|
+
_backfillTriggerCache.delete(key);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function isBackfillRecentlyTriggered(cacheKey: string): boolean {
|
|
1582
|
+
const ts = _backfillTriggerCache.get(cacheKey);
|
|
1583
|
+
if (ts === undefined) return false;
|
|
1584
|
+
if (Date.now() - ts >= BACKFILL_TRIGGER_TTL_MS) {
|
|
1585
|
+
_backfillTriggerCache.delete(cacheKey);
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Lazily backfill missing Slack thread ancestors for an inbound thread reply.
|
|
1593
|
+
*
|
|
1594
|
+
* When a reply arrives for a thread the daemon has never seen (e.g. the bot
|
|
1595
|
+
* was just added to the channel, or the parent message pre-dates the
|
|
1596
|
+
* conversation), the daemon fetches the thread's recent history via
|
|
1597
|
+
* {@link backfillThread}, persists each unseen message as a `messages` row
|
|
1598
|
+
* with a `slackMeta` envelope, and skips duplicates whose `ts` already
|
|
1599
|
+
* appears in the conversation.
|
|
1600
|
+
*
|
|
1601
|
+
* Behavior contracts:
|
|
1602
|
+
* - **No-op when the parent is already stored.** Looks up the conversation's
|
|
1603
|
+
* messages and short-circuits if any row has `slackMeta.channelTs ===
|
|
1604
|
+
* threadTs`. This keeps subsequent replies in the same thread cheap.
|
|
1605
|
+
* - **TTL idempotency cache.** A 10-minute in-memory cache prevents bursts
|
|
1606
|
+
* of replies in the same thread from re-running the DB lookup or the
|
|
1607
|
+
* Slack API call.
|
|
1608
|
+
* - **Failure-tolerant.** Any error (Slack API failure, DB error, malformed
|
|
1609
|
+
* payload) is logged at `warn` and swallowed — the inbound turn must
|
|
1610
|
+
* never block on backfill.
|
|
1611
|
+
*/
|
|
1612
|
+
export async function triggerSlackThreadBackfillIfNeeded(params: {
|
|
1613
|
+
conversationId: string;
|
|
1614
|
+
channelId: string;
|
|
1615
|
+
threadTs: string;
|
|
1616
|
+
/**
|
|
1617
|
+
* The inbound message's own `channelTs`. Pre-seeded into the dedup set so
|
|
1618
|
+
* this helper does not re-persist the just-received message when Slack's
|
|
1619
|
+
* `conversations.replies` returns it in the thread window. Necessary
|
|
1620
|
+
* because thread backfill runs concurrently with
|
|
1621
|
+
* `processChannelMessageInBackground`, so the inbound row may not yet be
|
|
1622
|
+
* in the DB when `readStoredSlackChannelTs` snapshots the conversation.
|
|
1623
|
+
*/
|
|
1624
|
+
excludeChannelTs?: string;
|
|
1625
|
+
/**
|
|
1626
|
+
* OAuth account identifier used to disambiguate which Slack workspace the
|
|
1627
|
+
* backfill should read from in multi-account setups. Passed through to
|
|
1628
|
+
* `backfillThread` → `resolveConnection`. Best-effort: if omitted, the
|
|
1629
|
+
* resolver falls back to the default-active connection.
|
|
1630
|
+
*/
|
|
1631
|
+
account?: string;
|
|
1632
|
+
}): Promise<void> {
|
|
1633
|
+
const { conversationId, channelId, threadTs, excludeChannelTs, account } =
|
|
1634
|
+
params;
|
|
1635
|
+
const cacheKey = `${conversationId}:${threadTs}`;
|
|
1636
|
+
|
|
1637
|
+
try {
|
|
1638
|
+
if (isBackfillRecentlyTriggered(cacheKey)) {
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const storedChannelTs = readStoredSlackChannelTs(conversationId);
|
|
1643
|
+
if (excludeChannelTs) storedChannelTs.add(excludeChannelTs);
|
|
1644
|
+
if (storedChannelTs.has(threadTs)) {
|
|
1645
|
+
// Parent is already in the conversation; mark the cache so a burst of
|
|
1646
|
+
// replies in this thread does not redo the DB scan for each one.
|
|
1647
|
+
_backfillTriggerCache.set(cacheKey, Date.now());
|
|
1648
|
+
pruneBackfillCacheIfNeeded();
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Mark the trigger before issuing the network call. Doing this first
|
|
1653
|
+
// means a second concurrent reply in the same thread short-circuits
|
|
1654
|
+
// immediately even while the first call is still awaiting the Slack
|
|
1655
|
+
// API. The cost is a slightly larger window where a transient Slack
|
|
1656
|
+
// failure suppresses a retry, which the next reply outside the TTL
|
|
1657
|
+
// (or a daemon restart) will re-attempt anyway.
|
|
1658
|
+
_backfillTriggerCache.set(cacheKey, Date.now());
|
|
1659
|
+
pruneBackfillCacheIfNeeded();
|
|
1660
|
+
|
|
1661
|
+
const fetched = await backfillThread(channelId, threadTs, { account });
|
|
1662
|
+
if (fetched.length === 0) {
|
|
1663
|
+
log.debug(
|
|
1664
|
+
{ conversationId, channelId, threadTs },
|
|
1665
|
+
"Slack thread backfill returned no messages",
|
|
1666
|
+
);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
let persisted = 0;
|
|
1671
|
+
for (const message of fetched) {
|
|
1672
|
+
if (!message.id) continue;
|
|
1673
|
+
if (storedChannelTs.has(message.id)) continue;
|
|
1674
|
+
try {
|
|
1675
|
+
await persistBackfilledSlackMessage({
|
|
1676
|
+
conversationId,
|
|
1677
|
+
channelId,
|
|
1678
|
+
message,
|
|
1679
|
+
});
|
|
1680
|
+
storedChannelTs.add(message.id);
|
|
1681
|
+
persisted++;
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
log.warn(
|
|
1684
|
+
{ err, conversationId, channelId, threadTs, channelTs: message.id },
|
|
1685
|
+
"Failed to persist backfilled Slack thread message",
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
log.info(
|
|
1691
|
+
{
|
|
1692
|
+
conversationId,
|
|
1693
|
+
channelId,
|
|
1694
|
+
threadTs,
|
|
1695
|
+
persisted,
|
|
1696
|
+
fetched: fetched.length,
|
|
1697
|
+
},
|
|
1698
|
+
"Slack thread backfill persisted ancestor messages",
|
|
1699
|
+
);
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
// `channel_not_found` almost always means the resolved connection is
|
|
1702
|
+
// pointing at the wrong Slack workspace (a real config bug), so log it
|
|
1703
|
+
// at ERROR to match backfill's rethrow contract. Other failures
|
|
1704
|
+
// (timeout, auth, ratelimited, …) stay at WARN — they're expected
|
|
1705
|
+
// transient blips and dispatch proceeds without the ancestors.
|
|
1706
|
+
const channelNotFound =
|
|
1707
|
+
err instanceof Error && /channel_not_found/i.test(err.message);
|
|
1708
|
+
const payload = { err, conversationId, channelId, threadTs, account };
|
|
1709
|
+
if (channelNotFound) {
|
|
1710
|
+
log.error(
|
|
1711
|
+
payload,
|
|
1712
|
+
"Slack thread backfill hit channel_not_found — connection likely points at the wrong Slack workspace",
|
|
1713
|
+
);
|
|
1714
|
+
} else {
|
|
1715
|
+
log.warn(payload, "Slack thread backfill failed; proceeding without it");
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|