@vellumai/assistant 0.6.4 → 0.6.5
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/ARCHITECTURE.md +32 -36
- package/Dockerfile +12 -0
- package/README.md +3 -4
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +1 -20
- package/docs/architecture/security.md +16 -16
- package/docs/error-handling.md +111 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- 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/openapi.yaml +123 -11
- 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__/approval-cascade.test.ts +31 -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__/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__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +27 -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 +428 -501
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-circuit-breaker.test.ts +336 -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-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +427 -114
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +530 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +30 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +61 -17
- package/src/__tests__/conversation-agent-loop.test.ts +412 -82
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +30 -9
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +6 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +36 -0
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +30 -16
- package/src/__tests__/conversation-process-callsite.test.ts +306 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -16
- package/src/__tests__/conversation-queue.test.ts +41 -26
- package/src/__tests__/conversation-routes-disk-view.test.ts +29 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2735 -55
- package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +34 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +30 -16
- package/src/__tests__/conversation-speed-override.test.ts +30 -11
- 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 +2 -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 +3 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
- package/src/__tests__/conversation-workspace-injection.test.ts +43 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +44 -16
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- 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__/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__/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 +26 -7
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- 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__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/intent-routing.test.ts +1 -40
- 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__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +684 -0
- package/src/__tests__/model-intents.test.ts +9 -83
- 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__/oauth-store.test.ts +10 -7
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -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__/permission-checker-host-gate.test.ts +1 -1
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +50 -3
- 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 +0 -1
- package/src/__tests__/reaction-persistence.test.ts +560 -0
- 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__/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-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 +160 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor.test.ts +60 -94
- 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__/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-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +1 -13
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +209 -17
- 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/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/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/cli/AGENTS.md +1 -1
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- 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 +666 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +451 -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 +594 -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/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +14 -1
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +300 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- 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/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 +23 -4
- package/src/cli.ts +0 -37
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +8 -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 +11 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -175
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +25 -9
- 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 +30 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/calls.ts +0 -4
- 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 +318 -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 +53 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skills.ts +2 -2
- package/src/context/__tests__/compact-prompt.test.ts +45 -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 +12 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/window-manager.ts +229 -25
- 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/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 -1
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +79 -12
- package/src/daemon/conversation-agent-loop.ts +462 -80
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-lifecycle.ts +30 -6
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-process.ts +10 -4
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +760 -29
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +10 -5
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +118 -30
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- 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 +54 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +2 -2
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +76 -14
- package/src/daemon/message-types/conversations.ts +14 -0
- package/src/daemon/message-types/messages.ts +9 -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 +117 -9
- package/src/daemon/tool-side-effects.ts +0 -9
- package/src/daemon/watch-handler.ts +4 -4
- 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/heartbeat-service.ts +76 -28
- 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 +4 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +56 -2
- package/src/home/relationship-state-writer.ts +2 -2
- 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 +73 -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 +4 -4
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +61 -0
- package/src/ipc/routes/browser.ts +96 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/index.ts +17 -1
- 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/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +100 -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 +103 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-title-service.ts +7 -4
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-backend.ts +1 -1
- 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 +173 -51
- package/src/memory/graph/graph-search.test.ts +92 -0
- package/src/memory/graph/graph-search.ts +4 -1
- 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 +230 -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/140-backfill-usage-cache-accounting.ts +1 -1
- 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/index.ts +1 -0
- package/src/memory/pkb/pkb-index.test.ts +368 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +438 -0
- package/src/memory/pkb/pkb-search.ts +137 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.ts +122 -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 +1373 -0
- package/src/messaging/providers/slack/render-transcript.ts +443 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/oauth/oauth-store.ts +1 -0
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/seed-providers.ts +4 -2
- package/src/permissions/approval-policy.test.ts +948 -0
- package/src/permissions/approval-policy.ts +257 -0
- package/src/permissions/bash-risk-classifier.test.ts +1208 -0
- package/src/permissions/bash-risk-classifier.ts +707 -0
- package/src/permissions/checker.ts +217 -708
- package/src/permissions/command-registry.test.ts +535 -0
- package/src/permissions/command-registry.ts +825 -0
- package/src/permissions/defaults.ts +26 -78
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/risk-types.ts +205 -0
- package/src/permissions/secret-prompter.ts +53 -2
- 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 +23 -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 +1 -16
- package/src/platform/client.ts +19 -1
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- 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 +501 -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 +76 -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/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 +578 -0
- package/src/providers/speech-to-text/xai-realtime.ts +796 -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 +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- 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/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 +52 -1
- package/src/runtime/http-types.ts +23 -1
- 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-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 +58 -0
- package/src/runtime/routes/approval-routes.ts +12 -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/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +133 -27
- 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 +912 -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/migration-routes.ts +720 -124
- 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 +28 -6
- package/src/schedule/scheduler.ts +8 -0
- 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 +26 -7
- 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 +45 -2
- package/src/tools/browser/browser-execution.ts +65 -38
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +4 -0
- 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/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +36 -15
- package/src/tools/policy-context.ts +25 -8
- package/src/tools/registry.ts +55 -3
- 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/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 +12 -3
- 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 +2 -2
- package/src/util/pricing.ts +15 -5
- 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/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 +57 -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/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- 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__/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__/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/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/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/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/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/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/shared/provider-env-vars.ts +0 -19
- 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
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 22 — verifies that an inbound Slack thread reply triggers a lazy
|
|
3
|
+
* backfill of the missing thread ancestors when the conversation has no
|
|
4
|
+
* record of the parent message, persists each backfilled message with a
|
|
5
|
+
* derived `slackMeta` envelope, de-dupes against rows already stored, and
|
|
6
|
+
* gates re-triggers behind a 10-minute idempotency cache so bursts of
|
|
7
|
+
* replies in the same thread do not flood the Slack API.
|
|
8
|
+
*
|
|
9
|
+
* Tests exercise the helper {@link triggerSlackThreadBackfillIfNeeded}
|
|
10
|
+
* directly against the real database (via the test-preload temp workspace).
|
|
11
|
+
* Only `backfillThread` is mocked, since the contract under test is "given
|
|
12
|
+
* what Slack returns, what does the daemon write to the DB".
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
afterAll,
|
|
16
|
+
afterEach,
|
|
17
|
+
beforeEach,
|
|
18
|
+
describe,
|
|
19
|
+
expect,
|
|
20
|
+
mock,
|
|
21
|
+
spyOn,
|
|
22
|
+
test,
|
|
23
|
+
} from "bun:test";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Mocks (must precede module imports under test). Note: backfillThread is
|
|
27
|
+
// stubbed via spyOn (below) rather than mock.module so the stub does not leak
|
|
28
|
+
// into other test files (e.g. backfill.test.ts) that import the same module.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
mock.module("../util/logger.js", () => ({
|
|
32
|
+
getLogger: () =>
|
|
33
|
+
new Proxy({} as Record<string, unknown>, {
|
|
34
|
+
get: () => () => {},
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
mock.module("../config/env.js", () => ({
|
|
39
|
+
isHttpAuthDisabled: () => true,
|
|
40
|
+
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
44
|
+
getCredentialMetadata: () => undefined,
|
|
45
|
+
upsertCredentialMetadata: () => {},
|
|
46
|
+
deleteCredentialMetadata: () => {},
|
|
47
|
+
listCredentialMetadata: () => [],
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
mock.module("../runtime/gateway-client.js", () => ({
|
|
51
|
+
deliverChannelReply: async () => {},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Imports (after mocks)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
import { v4 as uuid } from "uuid";
|
|
59
|
+
|
|
60
|
+
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
61
|
+
import { getDb, initializeDb } from "../memory/db.js";
|
|
62
|
+
import type { Message as MessagingMessage } from "../messaging/provider-types.js";
|
|
63
|
+
import * as slackBackfill from "../messaging/providers/slack/backfill.js";
|
|
64
|
+
import {
|
|
65
|
+
readSlackMetadata,
|
|
66
|
+
writeSlackMetadata,
|
|
67
|
+
} from "../messaging/providers/slack/message-metadata.js";
|
|
68
|
+
import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
|
|
69
|
+
import {
|
|
70
|
+
_backfillTriggerCache,
|
|
71
|
+
triggerSlackThreadBackfillIfNeeded,
|
|
72
|
+
} from "../runtime/routes/inbound-message-handler.js";
|
|
73
|
+
|
|
74
|
+
initializeDb();
|
|
75
|
+
|
|
76
|
+
// Spy on backfillThread so the stub is scoped to this test file only.
|
|
77
|
+
// Restoring after the file's tests run keeps cross-file leakage to zero —
|
|
78
|
+
// other tests (e.g. backfill.test.ts) keep seeing the real implementation.
|
|
79
|
+
const backfillThreadMock = spyOn(slackBackfill, "backfillThread");
|
|
80
|
+
backfillThreadMock.mockResolvedValue([]);
|
|
81
|
+
|
|
82
|
+
afterAll(() => {
|
|
83
|
+
backfillThreadMock.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Helpers
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
//
|
|
90
|
+
// These helpers go directly against the SQLite layer rather than calling into
|
|
91
|
+
// `conversation-crud.js`. The reason is test isolation: several other test
|
|
92
|
+
// files in the suite mock `conversation-crud.js` partially (only the exports
|
|
93
|
+
// they need), and Bun does not always reset such mocks between files. When
|
|
94
|
+
// our test runs in the same process after one of those, calls like
|
|
95
|
+
// `getMessages` come back as `undefined`. Going around the module entirely
|
|
96
|
+
// keeps this test resilient to any future module-level mocks elsewhere.
|
|
97
|
+
|
|
98
|
+
const SLACK_CHANNEL_ID = "C0THREAD";
|
|
99
|
+
|
|
100
|
+
function resetState(): void {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
db.$client.exec("DELETE FROM messages");
|
|
103
|
+
db.$client.exec("DELETE FROM conversations");
|
|
104
|
+
_backfillTriggerCache.clear();
|
|
105
|
+
backfillThreadMock.mockReset();
|
|
106
|
+
backfillThreadMock.mockImplementation(async () => []);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let convCounter = 0;
|
|
110
|
+
|
|
111
|
+
function makeConversationId(): string {
|
|
112
|
+
convCounter++;
|
|
113
|
+
return `conv-test-${convCounter}-${uuid()}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createTestConversation(): { id: string } {
|
|
117
|
+
const db = getDb();
|
|
118
|
+
const id = makeConversationId();
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
db.$client
|
|
121
|
+
.prepare(
|
|
122
|
+
`INSERT INTO conversations (
|
|
123
|
+
id, title, created_at, updated_at, total_input_tokens, total_output_tokens,
|
|
124
|
+
total_estimated_cost, context_compacted_message_count, conversation_type,
|
|
125
|
+
source, memory_scope_id, host_access, is_auto_title
|
|
126
|
+
) VALUES (?, NULL, ?, ?, 0, 0, 0, 0, 'standard', 'user', 'default', 0, 1)`,
|
|
127
|
+
)
|
|
128
|
+
.run(id, now, now);
|
|
129
|
+
return { id };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let messageCounter = 0;
|
|
133
|
+
|
|
134
|
+
function insertMessage(
|
|
135
|
+
conversationId: string,
|
|
136
|
+
role: string,
|
|
137
|
+
content: string,
|
|
138
|
+
metadata?: Record<string, unknown>,
|
|
139
|
+
): void {
|
|
140
|
+
const db = getDb();
|
|
141
|
+
const id = uuid();
|
|
142
|
+
// Use a strictly increasing timestamp so the ORDER BY in
|
|
143
|
+
// readMessagesByConversation is deterministic — Date.now() ties when
|
|
144
|
+
// multiple inserts happen inside the same millisecond.
|
|
145
|
+
messageCounter++;
|
|
146
|
+
const now = Date.now() + messageCounter;
|
|
147
|
+
const metadataStr = metadata ? JSON.stringify(metadata) : null;
|
|
148
|
+
db.$client
|
|
149
|
+
.prepare(
|
|
150
|
+
`INSERT INTO messages (id, conversation_id, role, content, created_at, metadata)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
152
|
+
)
|
|
153
|
+
.run(id, conversationId, role, content, now, metadataStr);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface RawMessageRow {
|
|
157
|
+
role: string;
|
|
158
|
+
content: string;
|
|
159
|
+
metadata: string | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readMessagesByConversation(conversationId: string): RawMessageRow[] {
|
|
163
|
+
const db = getDb();
|
|
164
|
+
return db.$client
|
|
165
|
+
.prepare(
|
|
166
|
+
"SELECT role, content, metadata FROM messages WHERE conversation_id = ? ORDER BY created_at ASC",
|
|
167
|
+
)
|
|
168
|
+
.all(conversationId) as RawMessageRow[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function makeBackfillMessage(
|
|
172
|
+
overrides: Partial<MessagingMessage> = {},
|
|
173
|
+
): MessagingMessage {
|
|
174
|
+
return {
|
|
175
|
+
id: "1234.0",
|
|
176
|
+
conversationId: SLACK_CHANNEL_ID,
|
|
177
|
+
sender: { id: "U_USER", name: "Alice" },
|
|
178
|
+
text: "thread parent",
|
|
179
|
+
timestamp: 1700000000_000,
|
|
180
|
+
threadId: undefined,
|
|
181
|
+
platform: "slack",
|
|
182
|
+
...overrides,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface PersistedRow {
|
|
187
|
+
role: string;
|
|
188
|
+
content: string;
|
|
189
|
+
channelTs: string | undefined;
|
|
190
|
+
threadTs: string | undefined;
|
|
191
|
+
displayName: string | undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readPersistedSlackRows(conversationId: string): PersistedRow[] {
|
|
195
|
+
const rows = readMessagesByConversation(conversationId);
|
|
196
|
+
const out: PersistedRow[] = [];
|
|
197
|
+
for (const row of rows) {
|
|
198
|
+
const blank: PersistedRow = {
|
|
199
|
+
role: row.role,
|
|
200
|
+
content: row.content,
|
|
201
|
+
channelTs: undefined,
|
|
202
|
+
threadTs: undefined,
|
|
203
|
+
displayName: undefined,
|
|
204
|
+
};
|
|
205
|
+
if (!row.metadata) {
|
|
206
|
+
out.push(blank);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
let envelope: Record<string, unknown>;
|
|
210
|
+
try {
|
|
211
|
+
const parsed = JSON.parse(row.metadata) as unknown;
|
|
212
|
+
if (
|
|
213
|
+
parsed === null ||
|
|
214
|
+
typeof parsed !== "object" ||
|
|
215
|
+
Array.isArray(parsed)
|
|
216
|
+
) {
|
|
217
|
+
out.push(blank);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
envelope = parsed as Record<string, unknown>;
|
|
221
|
+
} catch {
|
|
222
|
+
out.push(blank);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const slackMetaRaw = envelope.slackMeta;
|
|
226
|
+
if (typeof slackMetaRaw !== "string") {
|
|
227
|
+
out.push(blank);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const slackMeta = readSlackMetadata(slackMetaRaw);
|
|
231
|
+
out.push({
|
|
232
|
+
role: row.role,
|
|
233
|
+
content: row.content,
|
|
234
|
+
channelTs: slackMeta?.channelTs,
|
|
235
|
+
threadTs: slackMeta?.threadTs,
|
|
236
|
+
displayName: slackMeta?.displayName,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function seedSlackRow(
|
|
243
|
+
conversationId: string,
|
|
244
|
+
channelTs: string,
|
|
245
|
+
threadTs: string | undefined,
|
|
246
|
+
text: string,
|
|
247
|
+
): void {
|
|
248
|
+
insertMessage(conversationId, "user", text, {
|
|
249
|
+
slackMeta: writeSlackMetadata({
|
|
250
|
+
source: "slack",
|
|
251
|
+
channelId: SLACK_CHANNEL_ID,
|
|
252
|
+
channelTs,
|
|
253
|
+
eventKind: "message",
|
|
254
|
+
...(threadTs ? { threadTs } : {}),
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Tests
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence", () => {
|
|
264
|
+
beforeEach(() => {
|
|
265
|
+
resetState();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
afterEach(() => {
|
|
269
|
+
backfillThreadMock.mockReset();
|
|
270
|
+
_backfillTriggerCache.clear();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("inbound thread reply with unseen parent triggers backfill and persists ancestors with slackMeta", async () => {
|
|
274
|
+
const conv = createTestConversation();
|
|
275
|
+
|
|
276
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
277
|
+
makeBackfillMessage({
|
|
278
|
+
id: "1234.0",
|
|
279
|
+
text: "parent",
|
|
280
|
+
threadId: undefined,
|
|
281
|
+
sender: { id: "U_PARENT", name: "Parent User" },
|
|
282
|
+
}),
|
|
283
|
+
makeBackfillMessage({
|
|
284
|
+
id: "1234.1",
|
|
285
|
+
text: "first reply",
|
|
286
|
+
threadId: "1234.0",
|
|
287
|
+
sender: { id: "U_REPLY1", name: "Reply One" },
|
|
288
|
+
}),
|
|
289
|
+
makeBackfillMessage({
|
|
290
|
+
id: "1234.2",
|
|
291
|
+
text: "second reply",
|
|
292
|
+
threadId: "1234.0",
|
|
293
|
+
sender: { id: "U_REPLY2", name: "Reply Two" },
|
|
294
|
+
}),
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
298
|
+
conversationId: conv.id,
|
|
299
|
+
channelId: SLACK_CHANNEL_ID,
|
|
300
|
+
threadTs: "1234.0",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
304
|
+
const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
|
|
305
|
+
expect(calledChannel).toBe(SLACK_CHANNEL_ID);
|
|
306
|
+
expect(calledThread).toBe("1234.0");
|
|
307
|
+
|
|
308
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
309
|
+
expect(persisted.length).toBe(3);
|
|
310
|
+
|
|
311
|
+
const byChannelTs = new Map(
|
|
312
|
+
persisted.map((p) => [p.channelTs ?? "<no-ts>", p]),
|
|
313
|
+
);
|
|
314
|
+
expect(byChannelTs.get("1234.0")?.content).toBe("parent");
|
|
315
|
+
expect(byChannelTs.get("1234.0")?.displayName).toBe("Parent User");
|
|
316
|
+
expect(byChannelTs.get("1234.0")?.threadTs).toBeUndefined();
|
|
317
|
+
|
|
318
|
+
expect(byChannelTs.get("1234.1")?.content).toBe("first reply");
|
|
319
|
+
expect(byChannelTs.get("1234.1")?.threadTs).toBe("1234.0");
|
|
320
|
+
expect(byChannelTs.get("1234.1")?.displayName).toBe("Reply One");
|
|
321
|
+
|
|
322
|
+
expect(byChannelTs.get("1234.2")?.content).toBe("second reply");
|
|
323
|
+
expect(byChannelTs.get("1234.2")?.threadTs).toBe("1234.0");
|
|
324
|
+
expect(byChannelTs.get("1234.2")?.displayName).toBe("Reply Two");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("backfill is NOT triggered when the parent is already persisted", async () => {
|
|
328
|
+
const conv = createTestConversation();
|
|
329
|
+
|
|
330
|
+
// Seed the parent message before the trigger runs — simulates a
|
|
331
|
+
// conversation where the daemon has already seen the thread parent.
|
|
332
|
+
seedSlackRow(conv.id, "1234.0", undefined, "already here");
|
|
333
|
+
|
|
334
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
335
|
+
conversationId: conv.id,
|
|
336
|
+
channelId: SLACK_CHANNEL_ID,
|
|
337
|
+
threadTs: "1234.0",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(backfillThreadMock).not.toHaveBeenCalled();
|
|
341
|
+
|
|
342
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
343
|
+
expect(persisted.length).toBe(1);
|
|
344
|
+
expect(persisted[0].channelTs).toBe("1234.0");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("idempotency cache: a second call inside the TTL window does not re-fetch", async () => {
|
|
348
|
+
const conv = createTestConversation();
|
|
349
|
+
|
|
350
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
351
|
+
makeBackfillMessage({ id: "1234.0", text: "parent" }),
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
355
|
+
conversationId: conv.id,
|
|
356
|
+
channelId: SLACK_CHANNEL_ID,
|
|
357
|
+
threadTs: "1234.0",
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Second call for the same conversation+thread — must short-circuit on
|
|
361
|
+
// the in-memory cache without hitting backfillThread again.
|
|
362
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
363
|
+
conversationId: conv.id,
|
|
364
|
+
channelId: SLACK_CHANNEL_ID,
|
|
365
|
+
threadTs: "1234.0",
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
369
|
+
|
|
370
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
371
|
+
// Only one parent row (no duplicate from the second trigger).
|
|
372
|
+
expect(persisted.filter((p) => p.channelTs === "1234.0").length).toBe(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("backfill error: turn proceeds, no crash, no rows written", async () => {
|
|
376
|
+
const conv = createTestConversation();
|
|
377
|
+
|
|
378
|
+
backfillThreadMock.mockImplementation(async () => {
|
|
379
|
+
throw new Error("Slack API error: thread_not_found");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Must not throw — error handling is internal.
|
|
383
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
384
|
+
conversationId: conv.id,
|
|
385
|
+
channelId: SLACK_CHANNEL_ID,
|
|
386
|
+
threadTs: "1234.0",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
390
|
+
expect(readPersistedSlackRows(conv.id).length).toBe(0);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("backfill returns duplicates that are already stored — only new rows are inserted", async () => {
|
|
394
|
+
const conv = createTestConversation();
|
|
395
|
+
|
|
396
|
+
// Pre-seed sibling 1234.1 so the backfill response includes one row that
|
|
397
|
+
// already exists (and must not be re-inserted) plus two genuinely new
|
|
398
|
+
// ones (parent 1234.0 and sibling 1234.2).
|
|
399
|
+
seedSlackRow(conv.id, "1234.1", "1234.0", "already here");
|
|
400
|
+
|
|
401
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
402
|
+
makeBackfillMessage({
|
|
403
|
+
id: "1234.0",
|
|
404
|
+
text: "parent",
|
|
405
|
+
threadId: undefined,
|
|
406
|
+
}),
|
|
407
|
+
makeBackfillMessage({
|
|
408
|
+
id: "1234.1",
|
|
409
|
+
text: "duplicate sibling — must be skipped",
|
|
410
|
+
threadId: "1234.0",
|
|
411
|
+
}),
|
|
412
|
+
makeBackfillMessage({
|
|
413
|
+
id: "1234.2",
|
|
414
|
+
text: "new sibling",
|
|
415
|
+
threadId: "1234.0",
|
|
416
|
+
}),
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
420
|
+
conversationId: conv.id,
|
|
421
|
+
channelId: SLACK_CHANNEL_ID,
|
|
422
|
+
threadTs: "1234.0",
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
426
|
+
expect(persisted.length).toBe(3);
|
|
427
|
+
|
|
428
|
+
const oneRow = persisted.find((p) => p.channelTs === "1234.1");
|
|
429
|
+
// The pre-seeded row's content remains; the duplicate from backfill was
|
|
430
|
+
// skipped (otherwise the count would be 4 or the content would change).
|
|
431
|
+
expect(oneRow?.content).toBe("already here");
|
|
432
|
+
|
|
433
|
+
expect(persisted.find((p) => p.channelTs === "1234.0")?.content).toBe(
|
|
434
|
+
"parent",
|
|
435
|
+
);
|
|
436
|
+
expect(persisted.find((p) => p.channelTs === "1234.2")?.content).toBe(
|
|
437
|
+
"new sibling",
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("empty backfill response leaves the conversation untouched but still seeds the cache", async () => {
|
|
442
|
+
const conv = createTestConversation();
|
|
443
|
+
|
|
444
|
+
backfillThreadMock.mockImplementation(async () => []);
|
|
445
|
+
|
|
446
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
447
|
+
conversationId: conv.id,
|
|
448
|
+
channelId: SLACK_CHANNEL_ID,
|
|
449
|
+
threadTs: "1234.0",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
453
|
+
expect(readPersistedSlackRows(conv.id).length).toBe(0);
|
|
454
|
+
|
|
455
|
+
// Cache should now be populated for this conversation+thread, so an
|
|
456
|
+
// immediate retry must not re-run the API call.
|
|
457
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
458
|
+
conversationId: conv.id,
|
|
459
|
+
channelId: SLACK_CHANNEL_ID,
|
|
460
|
+
threadTs: "1234.0",
|
|
461
|
+
});
|
|
462
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("two distinct threads in the same conversation each trigger their own backfill", async () => {
|
|
466
|
+
const conv = createTestConversation();
|
|
467
|
+
|
|
468
|
+
backfillThreadMock.mockImplementation(async (_channel, threadTs) => [
|
|
469
|
+
makeBackfillMessage({
|
|
470
|
+
id: threadTs as string,
|
|
471
|
+
text: `parent of ${threadTs as string}`,
|
|
472
|
+
}),
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
476
|
+
conversationId: conv.id,
|
|
477
|
+
channelId: SLACK_CHANNEL_ID,
|
|
478
|
+
threadTs: "1234.0",
|
|
479
|
+
});
|
|
480
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
481
|
+
conversationId: conv.id,
|
|
482
|
+
channelId: SLACK_CHANNEL_ID,
|
|
483
|
+
threadTs: "5678.0",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(2);
|
|
487
|
+
|
|
488
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
489
|
+
expect(persisted.length).toBe(2);
|
|
490
|
+
expect(persisted.map((p) => p.channelTs).sort()).toEqual([
|
|
491
|
+
"1234.0",
|
|
492
|
+
"5678.0",
|
|
493
|
+
]);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("backfilled message without text is persisted with empty content", async () => {
|
|
497
|
+
const conv = createTestConversation();
|
|
498
|
+
|
|
499
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
500
|
+
makeBackfillMessage({
|
|
501
|
+
id: "1234.0",
|
|
502
|
+
text: "",
|
|
503
|
+
}),
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
507
|
+
conversationId: conv.id,
|
|
508
|
+
channelId: SLACK_CHANNEL_ID,
|
|
509
|
+
threadTs: "1234.0",
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
513
|
+
expect(persisted.length).toBe(1);
|
|
514
|
+
expect(persisted[0].content).toBe("");
|
|
515
|
+
expect(persisted[0].channelTs).toBe("1234.0");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("backfill skips messages with no id rather than crashing", async () => {
|
|
519
|
+
const conv = createTestConversation();
|
|
520
|
+
|
|
521
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
522
|
+
makeBackfillMessage({ id: "", text: "no id" }),
|
|
523
|
+
makeBackfillMessage({ id: "1234.0", text: "valid parent" }),
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
527
|
+
conversationId: conv.id,
|
|
528
|
+
channelId: SLACK_CHANNEL_ID,
|
|
529
|
+
threadTs: "1234.0",
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
533
|
+
expect(persisted.length).toBe(1);
|
|
534
|
+
expect(persisted[0].channelTs).toBe("1234.0");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("reaction row targeting the thread parent does not short-circuit ancestor backfill", async () => {
|
|
538
|
+
const conv = createTestConversation();
|
|
539
|
+
|
|
540
|
+
// A reaction on the thread parent stores the parent's ts as `channelTs`
|
|
541
|
+
// (the reaction *targets* that message). If the dedup scan includes
|
|
542
|
+
// reaction rows, ancestor backfill wrongly believes the parent is
|
|
543
|
+
// already persisted and skips the network fetch.
|
|
544
|
+
const db = getDb();
|
|
545
|
+
messageCounter++;
|
|
546
|
+
const now = Date.now() + messageCounter;
|
|
547
|
+
db.$client
|
|
548
|
+
.prepare(
|
|
549
|
+
`INSERT INTO messages (id, conversation_id, role, content, created_at, metadata)
|
|
550
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
551
|
+
)
|
|
552
|
+
.run(
|
|
553
|
+
uuid(),
|
|
554
|
+
conv.id,
|
|
555
|
+
"user",
|
|
556
|
+
"+1",
|
|
557
|
+
now,
|
|
558
|
+
JSON.stringify({
|
|
559
|
+
slackMeta: writeSlackMetadata({
|
|
560
|
+
source: "slack",
|
|
561
|
+
channelId: SLACK_CHANNEL_ID,
|
|
562
|
+
channelTs: "1234.0",
|
|
563
|
+
eventKind: "reaction",
|
|
564
|
+
reaction: {
|
|
565
|
+
emoji: "+1",
|
|
566
|
+
targetChannelTs: "1234.0",
|
|
567
|
+
op: "added",
|
|
568
|
+
},
|
|
569
|
+
}),
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
574
|
+
makeBackfillMessage({ id: "1234.0", text: "parent" }),
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
578
|
+
conversationId: conv.id,
|
|
579
|
+
channelId: SLACK_CHANNEL_ID,
|
|
580
|
+
threadTs: "1234.0",
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
584
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
585
|
+
// Reaction row + newly backfilled parent message row.
|
|
586
|
+
expect(persisted.length).toBe(2);
|
|
587
|
+
expect(
|
|
588
|
+
persisted.find((p) => p.channelTs === "1234.0" && p.content === "parent"),
|
|
589
|
+
).toBeDefined();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("excludeChannelTs pre-seeds the dedup set so the inbound message is not re-persisted", async () => {
|
|
593
|
+
const conv = createTestConversation();
|
|
594
|
+
|
|
595
|
+
// Simulate Slack's conversations.replies returning the just-received
|
|
596
|
+
// inbound message alongside the thread parent — this is the normal
|
|
597
|
+
// response shape. Without excludeChannelTs, the inbound row (persisted
|
|
598
|
+
// concurrently in the background) would race the backfill and produce
|
|
599
|
+
// a duplicate.
|
|
600
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
601
|
+
makeBackfillMessage({
|
|
602
|
+
id: "1234.0",
|
|
603
|
+
text: "parent",
|
|
604
|
+
threadId: undefined,
|
|
605
|
+
}),
|
|
606
|
+
makeBackfillMessage({
|
|
607
|
+
id: "1234.5",
|
|
608
|
+
text: "inbound reply — must be skipped",
|
|
609
|
+
threadId: "1234.0",
|
|
610
|
+
}),
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
614
|
+
conversationId: conv.id,
|
|
615
|
+
channelId: SLACK_CHANNEL_ID,
|
|
616
|
+
threadTs: "1234.0",
|
|
617
|
+
excludeChannelTs: "1234.5",
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
621
|
+
// Only the parent should be persisted by backfill; the inbound (1234.5)
|
|
622
|
+
// is owned by the concurrent inbound-processing path.
|
|
623
|
+
expect(persisted.length).toBe(1);
|
|
624
|
+
expect(persisted[0].channelTs).toBe("1234.0");
|
|
625
|
+
expect(persisted.find((p) => p.channelTs === "1234.5")).toBeUndefined();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("messages with malformed metadata in the conversation are tolerated when scanning", async () => {
|
|
629
|
+
const conv = createTestConversation();
|
|
630
|
+
|
|
631
|
+
// Insert a message with malformed (non-JSON) metadata directly. The
|
|
632
|
+
// scan must not throw on parse errors.
|
|
633
|
+
insertMessage(conv.id, "user", "malformed", { foo: "bar" });
|
|
634
|
+
const db = getDb();
|
|
635
|
+
db.$client
|
|
636
|
+
.prepare(
|
|
637
|
+
"UPDATE messages SET metadata = 'not-json' WHERE conversation_id = ?",
|
|
638
|
+
)
|
|
639
|
+
.run(conv.id);
|
|
640
|
+
|
|
641
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
642
|
+
makeBackfillMessage({ id: "1234.0", text: "parent" }),
|
|
643
|
+
]);
|
|
644
|
+
|
|
645
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
646
|
+
conversationId: conv.id,
|
|
647
|
+
channelId: SLACK_CHANNEL_ID,
|
|
648
|
+
threadTs: "1234.0",
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
652
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
653
|
+
// Two rows: the malformed row + the newly backfilled parent.
|
|
654
|
+
expect(persisted.length).toBe(2);
|
|
655
|
+
expect(persisted.find((p) => p.channelTs === "1234.0")?.content).toBe(
|
|
656
|
+
"parent",
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
// Integration through handleChannelInbound — the wiring contract
|
|
663
|
+
// ---------------------------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
const TEST_BEARER_TOKEN = "test-token";
|
|
666
|
+
const HTTP_SLACK_CHANNEL_ID = "C0HTTPTHREAD";
|
|
667
|
+
const HTTP_SLACK_USER_ID = "U_HTTP_USER";
|
|
668
|
+
const HTTP_SLACK_DISPLAY_NAME = "Charlie Threader";
|
|
669
|
+
|
|
670
|
+
function resetHttpState(): void {
|
|
671
|
+
const db = getDb();
|
|
672
|
+
db.run("DELETE FROM messages");
|
|
673
|
+
db.run("DELETE FROM channel_inbound_events");
|
|
674
|
+
db.run("DELETE FROM conversations");
|
|
675
|
+
db.run("DELETE FROM contact_channels");
|
|
676
|
+
db.run("DELETE FROM contacts");
|
|
677
|
+
_backfillTriggerCache.clear();
|
|
678
|
+
backfillThreadMock.mockReset();
|
|
679
|
+
backfillThreadMock.mockImplementation(async () => []);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function seedHttpActiveMember(): void {
|
|
683
|
+
upsertContactChannel({
|
|
684
|
+
sourceChannel: "slack",
|
|
685
|
+
externalUserId: HTTP_SLACK_USER_ID,
|
|
686
|
+
externalChatId: HTTP_SLACK_CHANNEL_ID,
|
|
687
|
+
status: "active",
|
|
688
|
+
policy: "allow",
|
|
689
|
+
displayName: HTTP_SLACK_DISPLAY_NAME,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
let httpMsgCounter = 0;
|
|
694
|
+
|
|
695
|
+
function buildThreadReplyRequest(
|
|
696
|
+
threadId: string,
|
|
697
|
+
messageId: string,
|
|
698
|
+
overrides: Record<string, unknown> = {},
|
|
699
|
+
): Request {
|
|
700
|
+
httpMsgCounter++;
|
|
701
|
+
const body: Record<string, unknown> = {
|
|
702
|
+
sourceChannel: "slack",
|
|
703
|
+
interface: "slack",
|
|
704
|
+
conversationExternalId: HTTP_SLACK_CHANNEL_ID,
|
|
705
|
+
externalMessageId: `${HTTP_SLACK_CHANNEL_ID}:${messageId}:${httpMsgCounter}`,
|
|
706
|
+
content: "thread reply text",
|
|
707
|
+
actorExternalId: HTTP_SLACK_USER_ID,
|
|
708
|
+
actorDisplayName: HTTP_SLACK_DISPLAY_NAME,
|
|
709
|
+
actorUsername: "charlie",
|
|
710
|
+
replyCallbackUrl: "http://localhost:7830/deliver/slack",
|
|
711
|
+
sourceMetadata: {
|
|
712
|
+
messageId,
|
|
713
|
+
threadId,
|
|
714
|
+
chatType: "channel",
|
|
715
|
+
},
|
|
716
|
+
...overrides,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
return new Request("http://localhost:8080/channels/inbound", {
|
|
720
|
+
method: "POST",
|
|
721
|
+
headers: {
|
|
722
|
+
"Content-Type": "application/json",
|
|
723
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
724
|
+
},
|
|
725
|
+
body: JSON.stringify(body),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
730
|
+
beforeEach(() => {
|
|
731
|
+
resetHttpState();
|
|
732
|
+
seedHttpActiveMember();
|
|
733
|
+
httpMsgCounter = 0;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
afterEach(() => {
|
|
737
|
+
backfillThreadMock.mockReset();
|
|
738
|
+
_backfillTriggerCache.clear();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("inbound thread reply with no stored parent triggers backfill from the HTTP path", async () => {
|
|
742
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
743
|
+
makeBackfillMessage({
|
|
744
|
+
id: "1234.0",
|
|
745
|
+
text: "parent",
|
|
746
|
+
threadId: undefined,
|
|
747
|
+
sender: { id: "U_PARENT", name: "Original Poster" },
|
|
748
|
+
}),
|
|
749
|
+
makeBackfillMessage({
|
|
750
|
+
id: "1234.1",
|
|
751
|
+
text: "earlier sibling",
|
|
752
|
+
threadId: "1234.0",
|
|
753
|
+
sender: { id: "U_SIB", name: "Earlier Sibling" },
|
|
754
|
+
}),
|
|
755
|
+
]);
|
|
756
|
+
|
|
757
|
+
const processMessage = async (): Promise<{ messageId: string }> => {
|
|
758
|
+
return { messageId: "agent-result-id" };
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const req = buildThreadReplyRequest("1234.0", "1234.3");
|
|
762
|
+
const resp = await handleChannelInbound(
|
|
763
|
+
req,
|
|
764
|
+
processMessage,
|
|
765
|
+
TEST_BEARER_TOKEN,
|
|
766
|
+
);
|
|
767
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
768
|
+
|
|
769
|
+
expect(json.accepted).toBe(true);
|
|
770
|
+
expect(json.duplicate).toBe(false);
|
|
771
|
+
|
|
772
|
+
// The backfill is fire-and-forget; settle the microtask queue so the
|
|
773
|
+
// void-promise has time to write to the DB before we assert.
|
|
774
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
775
|
+
|
|
776
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
777
|
+
const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
|
|
778
|
+
expect(calledChannel).toBe(HTTP_SLACK_CHANNEL_ID);
|
|
779
|
+
expect(calledThread).toBe("1234.0");
|
|
780
|
+
|
|
781
|
+
const db = getDb();
|
|
782
|
+
const rows = db.$client
|
|
783
|
+
.prepare("SELECT metadata FROM messages")
|
|
784
|
+
.all() as Array<{ metadata: string | null }>;
|
|
785
|
+
|
|
786
|
+
const channelTimestamps = new Set<string>();
|
|
787
|
+
for (const row of rows) {
|
|
788
|
+
if (!row.metadata) continue;
|
|
789
|
+
try {
|
|
790
|
+
const envelope = JSON.parse(row.metadata) as Record<string, unknown>;
|
|
791
|
+
if (typeof envelope.slackMeta === "string") {
|
|
792
|
+
const slackMeta = readSlackMetadata(envelope.slackMeta);
|
|
793
|
+
if (slackMeta) channelTimestamps.add(slackMeta.channelTs);
|
|
794
|
+
}
|
|
795
|
+
} catch {
|
|
796
|
+
// ignore
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
expect(channelTimestamps.has("1234.0")).toBe(true);
|
|
801
|
+
expect(channelTimestamps.has("1234.1")).toBe(true);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("second thread reply within the TTL window does not re-trigger backfill", async () => {
|
|
805
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
806
|
+
makeBackfillMessage({ id: "5678.0", text: "parent" }),
|
|
807
|
+
]);
|
|
808
|
+
|
|
809
|
+
const processMessage = async (): Promise<{ messageId: string }> => ({
|
|
810
|
+
messageId: "agent-result-id",
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const r1 = await handleChannelInbound(
|
|
814
|
+
buildThreadReplyRequest("5678.0", "5678.1"),
|
|
815
|
+
processMessage,
|
|
816
|
+
TEST_BEARER_TOKEN,
|
|
817
|
+
);
|
|
818
|
+
expect(r1.status).toBe(200);
|
|
819
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
820
|
+
|
|
821
|
+
const r2 = await handleChannelInbound(
|
|
822
|
+
buildThreadReplyRequest("5678.0", "5678.2"),
|
|
823
|
+
processMessage,
|
|
824
|
+
TEST_BEARER_TOKEN,
|
|
825
|
+
);
|
|
826
|
+
expect(r2.status).toBe(200);
|
|
827
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
828
|
+
|
|
829
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
test("backfill error from the HTTP path does not crash the request", async () => {
|
|
833
|
+
backfillThreadMock.mockImplementation(async () => {
|
|
834
|
+
throw new Error("Slack API offline");
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const processMessage = async (): Promise<{ messageId: string }> => ({
|
|
838
|
+
messageId: "agent-result-id",
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
const resp = await handleChannelInbound(
|
|
842
|
+
buildThreadReplyRequest("9999.0", "9999.1"),
|
|
843
|
+
processMessage,
|
|
844
|
+
TEST_BEARER_TOKEN,
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
expect(resp.status).toBe(200);
|
|
848
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
849
|
+
expect(json.accepted).toBe(true);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("backfill is awaited: parent is stored before handleChannelInbound returns", async () => {
|
|
853
|
+
// Replace the resolved promise with a manually-controlled deferred so we
|
|
854
|
+
// can prove that the inbound handler awaits the backfill rather than
|
|
855
|
+
// racing it against the agent-loop dispatch. If `await` were missing,
|
|
856
|
+
// `handleChannelInbound` would resolve before the parent row hit the
|
|
857
|
+
// database and the immediate post-response read below would miss it.
|
|
858
|
+
let resolveBackfill: (() => void) | null = null;
|
|
859
|
+
const backfillCompleted = new Promise<void>((resolve) => {
|
|
860
|
+
resolveBackfill = resolve;
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
backfillThreadMock.mockImplementation(async () => {
|
|
864
|
+
await backfillCompleted;
|
|
865
|
+
return [
|
|
866
|
+
makeBackfillMessage({
|
|
867
|
+
id: "8888.0",
|
|
868
|
+
text: "thread parent",
|
|
869
|
+
threadId: undefined,
|
|
870
|
+
sender: { id: "U_PARENT_AWAIT", name: "Parent Author" },
|
|
871
|
+
}),
|
|
872
|
+
];
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
let agentLoopFired = false;
|
|
876
|
+
const processMessage = async (): Promise<{ messageId: string }> => {
|
|
877
|
+
// The agent loop runs *after* backfill. Confirm the parent row is
|
|
878
|
+
// already visible at this point — that proves the backfill landed
|
|
879
|
+
// before dispatch.
|
|
880
|
+
agentLoopFired = true;
|
|
881
|
+
return { messageId: "agent-result-id" };
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const inboundPromise = handleChannelInbound(
|
|
885
|
+
buildThreadReplyRequest("8888.0", "8888.1"),
|
|
886
|
+
processMessage,
|
|
887
|
+
TEST_BEARER_TOKEN,
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
// Give the handler enough microtasks to reach the awaited backfill.
|
|
891
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
892
|
+
|
|
893
|
+
// Backfill is suspended at the awaited deferred — the parent row should
|
|
894
|
+
// not yet be persisted, and the agent loop must not have fired.
|
|
895
|
+
const db = getDb();
|
|
896
|
+
const rowsBeforeResolve = db.$client
|
|
897
|
+
.prepare("SELECT metadata FROM messages")
|
|
898
|
+
.all() as Array<{ metadata: string | null }>;
|
|
899
|
+
const tsBefore = rowsBeforeResolve
|
|
900
|
+
.map((row) => {
|
|
901
|
+
if (!row.metadata) return undefined;
|
|
902
|
+
try {
|
|
903
|
+
const env = JSON.parse(row.metadata) as Record<string, unknown>;
|
|
904
|
+
if (typeof env.slackMeta !== "string") return undefined;
|
|
905
|
+
const meta = readSlackMetadata(env.slackMeta);
|
|
906
|
+
return meta?.channelTs;
|
|
907
|
+
} catch {
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
.filter((ts): ts is string => ts !== undefined);
|
|
912
|
+
expect(tsBefore.includes("8888.0")).toBe(false);
|
|
913
|
+
expect(agentLoopFired).toBe(false);
|
|
914
|
+
|
|
915
|
+
// Release the backfill mock; the awaited handler should now finish.
|
|
916
|
+
resolveBackfill!();
|
|
917
|
+
const resp = await inboundPromise;
|
|
918
|
+
expect(resp.status).toBe(200);
|
|
919
|
+
|
|
920
|
+
const rowsAfter = db.$client
|
|
921
|
+
.prepare("SELECT metadata FROM messages")
|
|
922
|
+
.all() as Array<{ metadata: string | null }>;
|
|
923
|
+
const tsAfter = rowsAfter
|
|
924
|
+
.map((row) => {
|
|
925
|
+
if (!row.metadata) return undefined;
|
|
926
|
+
try {
|
|
927
|
+
const env = JSON.parse(row.metadata) as Record<string, unknown>;
|
|
928
|
+
if (typeof env.slackMeta !== "string") return undefined;
|
|
929
|
+
const meta = readSlackMetadata(env.slackMeta);
|
|
930
|
+
return meta?.channelTs;
|
|
931
|
+
} catch {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
})
|
|
935
|
+
.filter((ts): ts is string => ts !== undefined);
|
|
936
|
+
|
|
937
|
+
// The parent row is present before the response is delivered, so the
|
|
938
|
+
// agent loop dispatched after this point sees it.
|
|
939
|
+
expect(tsAfter.includes("8888.0")).toBe(true);
|
|
940
|
+
});
|
|
941
|
+
});
|