@vellumai/assistant 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -7
- package/Dockerfile +1 -0
- package/README.md +2 -2
- package/__tests__/permissions/gateway-threshold-reader.test.ts +79 -139
- package/bun.lock +3 -0
- package/docs/architecture/security.md +18 -16
- package/knip.json +1 -0
- package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +1 -5
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -5
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -16
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +1 -9
- package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +12 -12
- package/node_modules/@vellumai/slack-text/bun.lock +24 -0
- package/node_modules/@vellumai/slack-text/package.json +18 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +153 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +235 -0
- package/node_modules/@vellumai/slack-text/tsconfig.json +20 -0
- package/openapi.yaml +294 -107
- package/package.json +4 -2
- package/scripts/generate-openapi.ts +16 -111
- package/src/__tests__/agent-wake-override-profile.test.ts +23 -1
- package/src/__tests__/anthropic-provider.test.ts +56 -13
- package/src/__tests__/app-conversation-ids-backfill.test.ts +278 -0
- package/src/__tests__/app-conversation-ids.test.ts +151 -0
- package/src/__tests__/approval-cascade.test.ts +0 -15
- package/src/__tests__/approval-routes-http.test.ts +6 -17
- package/src/__tests__/assistant-event-hub.test.ts +126 -77
- package/src/__tests__/assistant-event.test.ts +0 -5
- package/src/__tests__/assistant-events-sse-hardening.test.ts +37 -15
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -29
- package/src/__tests__/background-shell-host-bash.test.ts +34 -43
- package/src/__tests__/call-controller.test.ts +1 -1
- package/src/__tests__/call-site-routing-provider.test.ts +193 -0
- package/src/__tests__/channel-approval-routes.test.ts +10 -296
- package/src/__tests__/channel-approvals.test.ts +25 -17
- package/src/__tests__/channel-guardian.test.ts +100 -146
- package/src/__tests__/checker.test.ts +20 -34
- package/src/__tests__/compact-event-conversation-id-guard.test.ts +50 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +6 -48
- package/src/__tests__/config-watcher.test.ts +12 -0
- package/src/__tests__/connection-policy.test.ts +1 -52
- package/src/__tests__/contacts-write.test.ts +2 -64
- package/src/__tests__/context-image-dimensions.test.ts +1 -1
- package/src/__tests__/context-search-memory-source.test.ts +120 -1
- package/src/__tests__/context-search-memory-v2-source.test.ts +383 -0
- package/src/__tests__/context-search-pkb-source.test.ts +49 -0
- package/src/__tests__/context-search-workspace-source.test.ts +9 -22
- package/src/__tests__/context-window-manager.test.ts +46 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +102 -29
- package/src/__tests__/conversation-agent-loop.test.ts +980 -13
- package/src/__tests__/conversation-analysis-routes.test.ts +12 -10
- package/src/__tests__/conversation-attention-telegram.test.ts +11 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -291
- package/src/__tests__/conversation-history-web-search.test.ts +4 -3
- package/src/__tests__/conversation-inference-profile-route.test.ts +12 -23
- package/src/__tests__/conversation-lifecycle.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +79 -2
- package/src/__tests__/conversation-queue.test.ts +3 -8
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -161
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -32
- package/src/__tests__/conversation-routes-slash-commands.test.ts +75 -66
- package/src/__tests__/conversation-runtime-assembly.test.ts +257 -3
- package/src/__tests__/conversation-slash-commands.test.ts +24 -4
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-starter-routes.test.ts +79 -2
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -5
- package/src/__tests__/conversation-surfaces-standalone.test.ts +18 -14
- package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -2
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +8 -46
- package/src/__tests__/conversation-usage.test.ts +253 -3
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +0 -39
- package/src/__tests__/credential-health-service.test.ts +68 -0
- package/src/__tests__/credential-security-e2e.test.ts +4 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -5
- package/src/__tests__/credential-token-resolver.test.ts +180 -0
- package/src/__tests__/cu-unified-flow.test.ts +33 -16
- package/src/__tests__/daemon-assistant-events.test.ts +34 -21
- package/src/__tests__/daemon-credential-client.test.ts +4 -1
- package/src/__tests__/db-connection-isolation.test.ts +125 -0
- package/src/__tests__/db-migration-rollback.test.ts +101 -0
- package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +169 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +7 -80
- package/src/__tests__/document-conversations.test.ts +332 -0
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
- package/src/__tests__/emit-event-signal.test.ts +4 -6
- package/src/__tests__/events-client-registration.test.ts +193 -49
- package/src/__tests__/filing-service.test.ts +58 -7
- package/src/__tests__/first-greeting.test.ts +156 -150
- package/src/__tests__/fixtures/mock-chrome-extension.ts +108 -66
- package/src/__tests__/get-skill-detail-audit.test.ts +3 -8
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +7 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +7 -2
- package/src/__tests__/guardian-routing-state.test.ts +1 -1
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +32 -11
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -83
- package/src/__tests__/headless-browser-mode.test.ts +4 -9
- package/src/__tests__/headless-browser-navigate.test.ts +21 -20
- package/src/__tests__/heartbeat-service.test.ts +289 -7
- package/src/__tests__/helpers/channel-test-adapter.ts +2 -2
- package/src/__tests__/helpers/create-guardian-binding.ts +91 -0
- package/src/__tests__/host-bash-proxy.test.ts +46 -122
- package/src/__tests__/host-browser-e2e-cloud.test.ts +36 -497
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +26 -96
- package/src/__tests__/host-browser-proxy.test.ts +111 -185
- package/src/__tests__/host-browser-routes.test.ts +45 -75
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +26 -30
- package/src/__tests__/host-cu-proxy.test.ts +56 -111
- package/src/__tests__/host-file-proxy.test.ts +44 -98
- package/src/__tests__/host-file-read-tool.test.ts +42 -21
- package/src/__tests__/host-shell-tool.test.ts +33 -68
- package/src/__tests__/host-transfer-pending-interactions.test.ts +2 -18
- package/src/__tests__/host-transfer-proxy.test.ts +43 -53
- package/src/__tests__/http-user-message-parity.test.ts +0 -6
- package/src/__tests__/inbound-slack-persistence.test.ts +31 -0
- package/src/__tests__/injector-chain.test.ts +10 -5
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +124 -0
- package/src/__tests__/inline-command-runner.test.ts +0 -66
- package/src/__tests__/inline-skill-load-permissions.test.ts +0 -2
- package/src/__tests__/install-skill-routing.test.ts +1 -13
- package/src/__tests__/llm-callsite-catalog.test.ts +34 -0
- package/src/__tests__/llm-catalog-parity.test.ts +90 -0
- package/src/__tests__/llm-context-resolution.test.ts +180 -0
- package/src/__tests__/llm-resolver.test.ts +80 -12
- package/src/__tests__/llm-usage-store.test.ts +269 -4
- package/src/__tests__/log-export-routes.test.ts +89 -0
- package/src/__tests__/managed-profile-guard.test.ts +225 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -10
- package/src/__tests__/manual-token-reconciliation.test.ts +334 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +95 -0
- package/src/__tests__/migration-cross-version-compatibility.test.ts +197 -291
- package/src/__tests__/migration-export-http.test.ts +33 -26
- package/src/__tests__/migration-export-streaming.test.ts +18 -10
- package/src/__tests__/migration-export-to-gcs.test.ts +49 -9
- package/src/__tests__/migration-import-commit-http.test.ts +66 -21
- package/src/__tests__/migration-import-from-gcs.test.ts +50 -9
- package/src/__tests__/migration-import-from-url.test.ts +20 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +95 -95
- package/src/__tests__/migration-parity-persistence.test.ts +62 -25
- package/src/__tests__/migration-transport.test.ts +115 -23
- package/src/__tests__/migration-validate-http.test.ts +105 -80
- package/src/__tests__/migration-wizard.test.ts +133 -27
- package/src/__tests__/non-member-access-request.test.ts +1 -1
- package/src/__tests__/notification-guardian-path.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +19 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +21 -12
- package/src/__tests__/prechat-onboarding-contract.test.ts +31 -7
- package/src/__tests__/pricing.test.ts +68 -4
- package/src/__tests__/process-message-background-slack.test.ts +331 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +153 -17
- package/src/__tests__/provider-send-message-override-profile.test.ts +50 -0
- package/src/__tests__/provider-usage-tracking.test.ts +208 -0
- package/src/__tests__/reaction-persistence.test.ts +9 -6
- package/src/__tests__/rebind-secrets-screen.test.ts +53 -16
- package/src/__tests__/recording-handler.test.ts +64 -81
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +4 -3
- package/src/__tests__/relay-server.test.ts +18 -13
- package/src/__tests__/require-fresh-approval.test.ts +13 -22
- package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
- package/src/__tests__/runtime-events-sse.test.ts +3 -12
- package/src/__tests__/search-skills-unified.test.ts +9 -15
- package/src/__tests__/secret-ingress-cli.test.ts +2 -5
- package/src/__tests__/secret-ingress-http.test.ts +0 -4
- package/src/__tests__/secret-onetime-send.test.ts +4 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +24 -7
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +42 -47
- package/src/__tests__/secret-response-routing.test.ts +29 -15
- package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -1
- package/src/__tests__/secret-scanner.test.ts +2 -545
- package/src/__tests__/send-endpoint-busy.test.ts +9 -24
- package/src/__tests__/settings-routes.test.ts +1 -1
- package/src/__tests__/shell-credential-ref.test.ts +0 -8
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -56
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -11
- package/src/__tests__/skill-tool-factory.test.ts +97 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -30
- package/src/__tests__/skills-files-catalog-fallback.test.ts +11 -17
- package/src/__tests__/slack-inbound-verification.test.ts +1 -62
- package/src/__tests__/subagent-fork-notifications.test.ts +57 -47
- package/src/__tests__/subagent-manager-notify.test.ts +70 -70
- package/src/__tests__/subagent-notify-parent.test.ts +80 -83
- package/src/__tests__/system-prompt.test.ts +115 -13
- package/src/__tests__/terminal-tools.test.ts +0 -89
- package/src/__tests__/thread-backfill.test.ts +945 -31
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -36
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -6
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -16
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +9 -19
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -7
- package/src/__tests__/tool-executor.test.ts +12 -19
- package/src/__tests__/tool-metrics-listener.test.ts +0 -35
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/tool-trace-listener.test.ts +0 -17
- package/src/__tests__/transfer-progress-screen.test.ts +63 -26
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -149
- package/src/__tests__/trusted-contact-multichannel.test.ts +2 -4
- package/src/__tests__/trusted-contact-verification.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +16 -5
- package/src/__tests__/usage-attribution.test.ts +247 -0
- package/src/__tests__/usage-cli.test.ts +143 -0
- package/src/__tests__/usage-grouped-buckets.test.ts +155 -0
- package/src/__tests__/usage-routes.test.ts +150 -0
- package/src/__tests__/validation-results-screen.test.ts +39 -16
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +12 -3
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +49 -137
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -7
- package/src/__tests__/voice-session-bridge.test.ts +5 -5
- package/src/__tests__/workspace-migration-062-drop-memory-v2-edges-json.test.ts +103 -0
- package/src/__tests__/workspace-migration-063-release-notes-dynamic-model-context.test.ts +77 -0
- package/src/__tests__/workspace-migration-064-unwind-main-agent-opus-seed.test.ts +225 -0
- package/src/__tests__/workspace-migration-memory-v2-init.test.ts +8 -30
- package/src/acp/index.ts +0 -15
- package/src/acp/session-manager.ts +37 -34
- package/src/agent/loop.ts +16 -1
- package/src/approvals/AGENTS.md +4 -0
- package/src/approvals/__tests__/guardian-feed-event.test.ts +10 -3
- package/src/approvals/guardian-request-resolvers.ts +10 -2
- package/src/backup/__tests__/backup-worker.test.ts +36 -8
- package/src/backup/__tests__/paths.test.ts +2 -2
- package/src/backup/__tests__/restore.test.ts +45 -28
- package/src/backup/backup-worker.ts +36 -2
- package/src/backup/paths.ts +9 -6
- package/src/browser-session/events.ts +0 -9
- package/src/calls/call-store.ts +1 -34
- package/src/calls/guardian-question-copy.ts +0 -108
- package/src/calls/relay-server.ts +0 -24
- package/src/calls/twilio-rest.ts +0 -38
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/voice-session-bridge.ts +7 -38
- package/src/channels/types.ts +1 -36
- package/src/cli/commands/__tests__/cache.test.ts +152 -5
- package/src/cli/commands/__tests__/memory-v2.test.ts +14 -28
- package/src/cli/commands/__tests__/trust.test.ts +21 -387
- package/src/cli/commands/backup.ts +4 -4
- package/src/cli/commands/cache-fs.ts +8 -0
- package/src/cli/commands/cache.ts +153 -82
- package/src/cli/commands/clients.ts +63 -5
- package/src/cli/commands/completions.ts +3 -3
- package/src/cli/commands/contacts.ts +231 -76
- package/src/cli/commands/keys.ts +4 -1
- package/src/cli/commands/memory-v2.ts +24 -52
- package/src/cli/commands/oauth/shared.ts +2 -29
- package/src/cli/commands/pending.ts +102 -0
- package/src/cli/commands/skills.ts +77 -35
- package/src/cli/commands/trust.ts +70 -430
- package/src/cli/commands/usage.ts +25 -16
- package/src/cli/lib/daemon-credential-client.ts +14 -0
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +0 -21
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -4
- package/src/config/env-registry.ts +12 -2
- package/src/config/env.ts +3 -14
- package/src/config/feature-flag-registry.json +30 -30
- package/src/config/llm-callsite-catalog.ts +12 -0
- package/src/config/llm-context-resolution.ts +80 -0
- package/src/config/llm-resolver.ts +58 -22
- package/src/config/loader.ts +3 -3
- package/src/config/schema.ts +2 -158
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +271 -0
- package/src/config/schemas/calls.ts +5 -5
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/ingress.ts +1 -1
- package/src/config/schemas/llm.ts +31 -3
- package/src/config/schemas/memory-retrieval.ts +2 -2
- package/src/config/schemas/memory-v2.ts +9 -0
- package/src/config/schemas/security.ts +1 -42
- package/src/config/schemas/services.ts +6 -6
- package/src/config/schemas/skills.ts +5 -5
- package/src/config/schemas/tts.ts +1 -1
- package/src/config/seed-inference-profiles.ts +117 -0
- package/src/config/skills.ts +0 -90
- package/src/config/types.ts +3 -6
- package/src/contacts/contact-store.ts +0 -17
- package/src/contacts/contacts-write.ts +1 -105
- package/src/context/window-manager.ts +44 -5
- package/src/credential-execution/process-manager.ts +34 -10
- package/src/credential-health/credential-health-service.ts +21 -16
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +75 -82
- package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -9
- package/src/daemon/connection-policy.ts +1 -26
- package/src/daemon/conversation-agent-loop-handlers.ts +53 -4
- package/src/daemon/conversation-agent-loop.ts +277 -36
- package/src/daemon/conversation-history.ts +8 -8
- package/src/daemon/conversation-launch.ts +20 -135
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-messaging.ts +1 -0
- package/src/daemon/conversation-process.ts +83 -163
- package/src/daemon/conversation-runtime-assembly.ts +219 -76
- package/src/daemon/conversation-slash.ts +47 -5
- package/src/daemon/conversation-store.ts +7 -31
- package/src/daemon/conversation-surfaces.ts +22 -28
- package/src/daemon/conversation-tool-setup.ts +3 -33
- package/src/daemon/conversation-usage.ts +36 -0
- package/src/daemon/conversation.ts +117 -233
- package/src/daemon/daemon-control.ts +3 -71
- package/src/daemon/daemon-skill-host.ts +8 -11
- package/src/daemon/dictation-profile-store.ts +2 -26
- package/src/daemon/first-greeting.ts +44 -156
- package/src/daemon/handlers/config-channels.ts +12 -12
- package/src/daemon/handlers/config-ingress.ts +4 -165
- package/src/daemon/handlers/config-model.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +0 -42
- package/src/daemon/handlers/conversations.ts +11 -190
- package/src/daemon/handlers/recording.ts +26 -158
- package/src/daemon/handlers/shared.ts +23 -71
- package/src/daemon/handlers/skills.ts +42 -93
- package/src/daemon/host-bash-proxy.ts +67 -45
- package/src/daemon/host-browser-proxy.ts +65 -27
- package/src/daemon/host-cu-proxy.ts +40 -39
- package/src/daemon/host-file-proxy.ts +58 -37
- package/src/daemon/host-transfer-proxy.ts +84 -46
- package/src/daemon/lifecycle.ts +49 -15
- package/src/daemon/message-types/conversations.ts +7 -0
- package/src/daemon/message-types/host-bash.ts +1 -0
- package/src/daemon/message-types/host-cu.ts +1 -0
- package/src/daemon/message-types/host-file.ts +1 -0
- package/src/daemon/message-types/host-transfer.ts +1 -0
- package/src/daemon/message-types/messages.ts +10 -9
- package/src/daemon/message-types/workspace.ts +1 -1
- package/src/daemon/process-message.ts +102 -239
- package/src/daemon/server.ts +13 -462
- package/src/daemon/shutdown-handlers.ts +2 -2
- package/src/daemon/tool-side-effects.ts +125 -107
- package/src/daemon/trust-context.ts +13 -0
- package/src/daemon/wake-target-adapter.ts +4 -9
- package/src/events/domain-events.ts +0 -8
- package/src/events/tool-audit-listener.ts +3 -1
- package/src/events/tool-domain-event-publisher.ts +0 -10
- package/src/events/tool-metrics-listener.ts +0 -17
- package/src/events/tool-trace-listener.ts +0 -14
- package/src/filing/filing-service.ts +13 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +6 -2
- package/src/heartbeat/heartbeat-service.ts +23 -5
- package/src/home/__tests__/feed-writer.test.ts +0 -4
- package/src/home/__tests__/relationship-state-writer.test.ts +30 -0
- package/src/home/feed-writer.ts +1 -2
- package/src/home/relationship-state-writer.ts +16 -3
- package/src/ipc/__tests__/browser-ipc.test.ts +2 -12
- package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -1
- package/src/ipc/assistant-server.ts +3 -10
- package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +39 -20
- package/src/ipc/routes/route-adapter.ts +1 -1
- package/src/ipc/routes/trust-rules.test.ts +0 -95
- package/src/ipc/skill-ipc-types.ts +41 -0
- package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +13 -27
- package/src/ipc/skill-routes/__tests__/identity.test.ts +4 -23
- package/src/ipc/skill-routes/events.ts +12 -23
- package/src/ipc/skill-routes/identity.ts +4 -17
- package/src/ipc/skill-routes/index.ts +1 -1
- package/src/ipc/skill-server.ts +6 -39
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +0 -8
- package/src/live-voice/protocol.ts +4 -13
- package/src/mcp/manager.ts +0 -5
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +55 -0
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +127 -0
- package/src/memory/app-git-service.ts +0 -32
- package/src/memory/app-store.ts +154 -0
- package/src/memory/attachments-store.ts +6 -0
- package/src/memory/context-search/sources/memory-v2.ts +578 -0
- package/src/memory/context-search/sources/memory.ts +5 -0
- package/src/memory/context-search/sources/pkb.ts +10 -1
- package/src/memory/context-search/sources/workspace.ts +3 -2
- package/src/memory/conversation-crud.ts +29 -4
- package/src/memory/conversation-disk-view.ts +1 -5
- package/src/memory/conversation-starter-checkpoints.ts +63 -0
- package/src/memory/db-connection.ts +62 -0
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.ts +3 -21
- package/src/memory/embedding-gemini.ts +0 -2
- package/src/memory/embedding-local.ts +6 -6
- package/src/memory/embedding-ollama.ts +6 -6
- package/src/memory/embedding-openai.ts +6 -6
- package/src/memory/embedding-types.ts +21 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +3 -7
- package/src/memory/graph/conversation-graph-memory.ts +35 -13
- package/src/memory/graph/injection.test.ts +2 -2
- package/src/memory/graph/injection.ts +1 -1
- package/src/memory/guardian-action-store.ts +0 -83
- package/src/memory/guardian-approvals.ts +0 -48
- package/src/memory/indexer.ts +1 -15
- package/src/memory/job-handlers/conversation-starters.ts +36 -53
- package/src/memory/job-utils.ts +0 -6
- package/src/memory/jobs-store.ts +0 -1
- package/src/memory/jobs-worker.ts +2 -16
- package/src/memory/llm-request-log-store.ts +0 -41
- package/src/memory/llm-usage-store.ts +129 -43
- package/src/memory/memory-v2-activation-log-store.ts +115 -0
- package/src/memory/migrations/233-document-conversations.ts +54 -0
- package/src/memory/migrations/234-memory-v2-activation-logs.ts +55 -0
- package/src/memory/migrations/235-llm-usage-attribution.ts +31 -0
- package/src/memory/migrations/235-slack-compaction-watermark.ts +44 -0
- package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +26 -0
- package/src/memory/migrations/__tests__/234-memory-v2-activation-logs.test.ts +182 -0
- package/src/memory/migrations/index.ts +14 -0
- package/src/memory/migrations/registry.ts +24 -0
- package/src/memory/raw-query.ts +2 -68
- package/src/memory/schema/conversations.ts +7 -0
- package/src/memory/schema/infrastructure.ts +25 -0
- package/src/memory/search/semantic.ts +5 -16
- package/src/memory/tool-usage-store.ts +2 -0
- package/src/memory/usage-buckets.ts +40 -1
- package/src/memory/usage-grouped-buckets.ts +127 -0
- package/src/memory/v2/__tests__/activation.test.ts +289 -90
- package/src/memory/v2/__tests__/backfill-jobs.test.ts +2 -129
- package/src/memory/v2/__tests__/consolidation-job.test.ts +28 -11
- package/src/memory/v2/__tests__/edge-index.test.ts +278 -0
- package/src/memory/v2/__tests__/injection.test.ts +384 -15
- package/src/memory/v2/__tests__/migration.test.ts +64 -36
- package/src/memory/v2/__tests__/page-store.test.ts +191 -8
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +181 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +115 -3
- package/src/memory/v2/__tests__/static-context.test.ts +153 -0
- package/src/memory/v2/activation.ts +168 -97
- package/src/memory/v2/backfill-jobs.ts +15 -100
- package/src/memory/v2/consolidation-job.ts +14 -12
- package/src/memory/v2/edge-index.ts +191 -0
- package/src/memory/v2/injection.ts +182 -58
- package/src/memory/v2/migration.ts +57 -64
- package/src/memory/v2/now-text.ts +2 -3
- package/src/memory/v2/page-store.ts +168 -31
- package/src/memory/v2/prompts/consolidation.ts +118 -42
- package/src/memory/v2/prompts/sweep.ts +3 -3
- package/src/memory/v2/skill-store.ts +55 -7
- package/src/memory/v2/static-context.ts +62 -0
- package/src/memory/v2/types.ts +10 -20
- package/src/memory/validation.ts +0 -11
- package/src/messaging/draft-store.ts +0 -6
- package/src/messaging/provider-types.ts +8 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/client.ts +1 -121
- package/src/messaging/providers/outlook/client.ts +0 -73
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +226 -0
- package/src/messaging/providers/slack/adapter.ts +122 -21
- package/src/messaging/providers/slack/backfill.test.ts +95 -6
- package/src/messaging/providers/slack/backfill.ts +89 -11
- package/src/messaging/providers/slack/client.ts +10 -124
- package/src/messaging/providers/slack/message-metadata.ts +12 -2
- package/src/messaging/providers/slack/render-transcript.test.ts +56 -0
- package/src/messaging/providers/slack/render-transcript.ts +126 -25
- package/src/messaging/providers/slack/types.ts +1 -0
- package/src/oauth/connection-resolver.test.ts +8 -0
- package/src/oauth/connection-resolver.ts +8 -16
- package/src/oauth/credential-token-resolver.ts +97 -0
- package/src/oauth/manual-token-connection.ts +30 -34
- package/src/oauth/oauth-store.ts +6 -4
- package/src/outbound-proxy/certs.ts +0 -7
- package/src/outbound-proxy/config.ts +0 -74
- package/src/outbound-proxy/health.ts +0 -44
- package/src/outbound-proxy/index.ts +0 -22
- package/src/permissions/approval-provenance.test.ts +184 -0
- package/src/permissions/approval-provenance.ts +70 -0
- package/src/permissions/checker.ts +4 -1
- package/src/permissions/gateway-threshold-reader.ts +4 -1
- package/src/permissions/prompter.ts +9 -2
- package/src/permissions/secret-prompter.ts +21 -48
- package/src/permissions/types.ts +33 -0
- package/src/permissions/workspace-policy.ts +0 -5
- package/src/platform/sync-identity.ts +0 -8
- package/src/plugins/defaults/injectors.ts +69 -2
- package/src/plugins/defaults/overflow-reduce.ts +3 -2
- package/src/plugins/types.ts +8 -0
- package/src/prompts/system-prompt.ts +34 -70
- package/src/prompts/templates/BOOTSTRAP.md +52 -6
- package/src/prompts/update-bulletin-job.ts +2 -0
- package/src/providers/__tests__/retry-callsite.test.ts +138 -1
- package/src/providers/anthropic/client.ts +72 -33
- package/src/providers/call-site-routing.ts +42 -3
- package/src/providers/gemini/client.ts +18 -2
- package/src/providers/managed-proxy/context.ts +0 -5
- package/src/providers/model-catalog.ts +105 -19
- package/src/providers/openai/chat-completions-provider.ts +6 -0
- package/src/providers/openai/responses-provider.ts +7 -1
- package/src/providers/provider-send-message.ts +45 -2
- package/src/providers/ratelimit.ts +7 -2
- package/src/providers/registry.ts +14 -9
- package/src/providers/retry.ts +96 -8
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +96 -0
- package/src/runtime/AGENTS.md +10 -6
- package/src/runtime/__tests__/agent-wake.test.ts +89 -0
- package/src/runtime/agent-wake.ts +39 -2
- package/src/runtime/assistant-event-hub.ts +541 -45
- package/src/runtime/assistant-event.ts +1 -6
- package/src/runtime/auth/context.ts +0 -9
- package/src/runtime/auth/middleware.ts +1 -1
- package/src/runtime/auth/route-policy.ts +11 -9
- package/src/runtime/auth/token-service.ts +0 -11
- package/src/runtime/channel-approvals.ts +6 -2
- package/src/runtime/channel-verification-service.ts +3 -5
- package/src/runtime/http-errors.ts +0 -34
- package/src/runtime/http-router.ts +6 -3
- package/src/runtime/http-server.ts +22 -82
- package/src/runtime/http-types.ts +5 -0
- package/src/runtime/interactive-ui.ts +0 -1
- package/src/runtime/middleware/auth.ts +0 -20
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +112 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +11 -4
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +253 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +19 -6
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +71 -27
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +41 -2
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +143 -79
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +143 -23
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +371 -0
- package/src/runtime/migrations/migration-transport.ts +46 -13
- package/src/runtime/migrations/migration-wizard.ts +2 -2
- package/src/runtime/migrations/origin-mode.ts +40 -0
- package/src/runtime/migrations/vbundle-builder.ts +133 -79
- package/src/runtime/migrations/vbundle-import-analyzer.ts +9 -7
- package/src/runtime/migrations/vbundle-importer.ts +7 -7
- package/src/runtime/migrations/vbundle-metadata-merge.ts +1 -1
- package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
- package/src/runtime/migrations/vbundle-streaming-validator.ts +48 -26
- package/src/runtime/migrations/vbundle-validator.ts +214 -41
- package/src/runtime/pending-interactions.ts +13 -4
- package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -1
- package/src/runtime/routes/__tests__/backup-routes.test.ts +28 -19
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +235 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +58 -0
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +54 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +19 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +7 -7
- package/src/runtime/routes/acp-routes.test.ts +0 -3
- package/src/runtime/routes/acp-routes.ts +3 -7
- package/src/runtime/routes/app-management-routes.ts +18 -9
- package/src/runtime/routes/approval-routes.ts +55 -14
- package/src/runtime/routes/avatar-routes.ts +3 -5
- package/src/runtime/routes/browser-routes.ts +1 -15
- package/src/runtime/routes/channel-guardian-routes.ts +1 -5
- package/src/runtime/routes/channel-readiness-routes.ts +3 -7
- package/src/runtime/routes/channel-route-shared.ts +2 -28
- package/src/runtime/routes/client-routes.ts +45 -12
- package/src/runtime/routes/consolidation-routes.ts +115 -0
- package/src/runtime/routes/conversation-list-routes.ts +12 -29
- package/src/runtime/routes/conversation-management-routes.ts +14 -51
- package/src/runtime/routes/conversation-query-routes.ts +120 -8
- package/src/runtime/routes/conversation-routes.ts +44 -528
- package/src/runtime/routes/conversation-starter-routes.ts +19 -40
- package/src/runtime/routes/documents-routes.ts +53 -18
- package/src/runtime/routes/events-routes.ts +59 -91
- package/src/runtime/routes/filing-routes.ts +18 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -9
- package/src/runtime/routes/host-bash-routes.ts +3 -2
- package/src/runtime/routes/host-browser-routes.ts +9 -33
- package/src/runtime/routes/host-cu-routes.ts +6 -1
- package/src/runtime/routes/host-file-routes.ts +3 -2
- package/src/runtime/routes/host-transfer-routes.ts +11 -15
- package/src/runtime/routes/identity-routes.ts +78 -6
- package/src/runtime/routes/inbound-message-handler.ts +580 -137
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -88
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +3 -0
- package/src/runtime/routes/index.ts +4 -0
- package/src/runtime/routes/integrations/slack/channel.ts +0 -24
- package/src/runtime/routes/llm-call-sites-routes.ts +22 -0
- package/src/runtime/routes/memory-v2-routes.ts +10 -15
- package/src/runtime/routes/migration-routes.ts +188 -31
- package/src/runtime/routes/playground/guard.ts +1 -1
- package/src/runtime/routes/playground/index.ts +0 -2
- package/src/runtime/routes/recording-routes.ts +4 -24
- package/src/runtime/routes/rename-conversation-routes.ts +2 -6
- package/src/runtime/routes/schedule-routes.ts +3 -6
- package/src/runtime/routes/secret-routes.ts +87 -18
- package/src/runtime/routes/settings-routes.ts +29 -28
- package/src/runtime/routes/skills-routes.ts +12 -31
- package/src/runtime/routes/suggest-trust-rule-routes.ts +32 -1
- package/src/runtime/routes/task-routes.ts +6 -6
- package/src/runtime/routes/trust-rules-routes.ts +3 -94
- package/src/runtime/routes/types.ts +4 -4
- package/src/runtime/routes/upgrade-broadcast-routes.ts +3 -10
- package/src/runtime/routes/usage-routes.ts +87 -10
- package/src/runtime/routes/user-routes.ts +17 -31
- package/src/runtime/routes/work-items-routes.ts +1 -4
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
- package/src/runtime/services/analyze-conversation.ts +7 -17
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/runtime/verification-outbound-actions.ts +1 -1
- package/src/runtime/verification-rate-limiter.ts +1 -1
- package/src/schedule/schedule-store.ts +0 -16
- package/src/security/secret-scanner.ts +14 -547
- package/src/security/secure-keys.ts +31 -11
- package/src/security/token-manager.ts +7 -3
- package/src/signals/cancel.ts +16 -25
- package/src/signals/conversation-undo.ts +2 -27
- package/src/signals/emit-event.ts +1 -2
- package/src/signals/user-message.ts +108 -22
- package/src/skills/catalog-install.ts +1 -0
- package/src/skills/clawhub.ts +2 -2
- package/src/skills/inline-command-runner.ts +1 -7
- package/src/subagent/manager.ts +67 -84
- package/src/tasks/task-store.ts +1 -28
- package/src/telemetry/types.ts +6 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +38 -15
- package/src/telemetry/usage-telemetry-reporter.ts +3 -5
- package/src/tools/acp/spawn.test.ts +1 -2
- package/src/tools/acp/steer.test.ts +1 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +44 -127
- package/src/tools/browser/browser-execution.ts +31 -147
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +92 -68
- package/src/tools/browser/cdp-client/factory.ts +48 -76
- package/src/tools/browser/cdp-client/index.ts +1 -14
- package/src/tools/executor.ts +44 -31
- package/src/tools/host-filesystem/edit.ts +3 -2
- package/src/tools/host-filesystem/read.ts +3 -2
- package/src/tools/host-filesystem/transfer.test.ts +45 -42
- package/src/tools/host-filesystem/transfer.ts +4 -3
- package/src/tools/host-filesystem/write.ts +3 -2
- package/src/tools/host-terminal/host-shell.ts +4 -3
- package/src/tools/network/script-proxy/index.ts +1 -10
- package/src/tools/permission-checker.ts +66 -1
- package/src/tools/skills/sandbox-runner.ts +1 -6
- package/src/tools/skills/skill-tool-factory.ts +32 -0
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/terminal/shell.ts +2 -78
- package/src/tools/types.ts +12 -39
- package/src/tts/__tests__/provider-catalog.test.ts +2 -2
- package/src/tts/provider-catalog.ts +1 -1
- package/src/usage/actors.ts +2 -1
- package/src/usage/attribution.ts +185 -0
- package/src/usage/pricing.ts +166 -0
- package/src/usage/types.ts +14 -0
- package/src/util/json.ts +13 -0
- package/src/util/logger.ts +3 -3
- package/src/util/pricing.ts +50 -3
- package/src/work-items/work-item-runner.ts +15 -42
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +4 -3
- package/src/workspace/migrations/052-seed-default-inference-profiles.ts +3 -3
- package/src/workspace/migrations/060-memory-v2-init.ts +2 -18
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +59 -0
- package/src/workspace/migrations/062-drop-memory-v2-edges-json.ts +27 -0
- package/src/workspace/migrations/063-release-notes-dynamic-model-context.ts +70 -0
- package/src/workspace/migrations/064-unwind-main-agent-opus-seed.ts +64 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +3 -3
- package/src/__tests__/sandbox-diagnostics.test.ts +0 -138
- package/src/__tests__/sandbox-host-parity.test.ts +0 -1024
- package/src/__tests__/secret-detection-handler.test.ts +0 -67
- package/src/__tests__/secret-scanner-executor.test.ts +0 -450
- package/src/__tests__/tcc-sandbox-deny.test.ts +0 -198
- package/src/__tests__/terminal-sandbox.test.ts +0 -374
- package/src/__tests__/tool-notification-listener.test.ts +0 -65
- package/src/context/__tests__/microcompact.test.ts +0 -805
- package/src/context/microcompact.ts +0 -443
- package/src/daemon/handlers/slack-channel-oauth-install.ts +0 -197
- package/src/events/tool-notification-listener.ts +0 -17
- package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +0 -219
- package/src/memory/v2/__tests__/edges.test.ts +0 -435
- package/src/memory/v2/edges.ts +0 -217
- package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +0 -197
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +0 -518
- package/src/runtime/__tests__/client-registry.test.ts +0 -271
- package/src/runtime/chrome-extension-registry.ts +0 -368
- package/src/runtime/client-registry.ts +0 -254
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +0 -329
- package/src/tools/secret-detection-handler.ts +0 -269
- package/src/tools/terminal/backends/native.ts +0 -327
- package/src/tools/terminal/backends/types.ts +0 -37
- package/src/tools/terminal/sandbox-diagnostics.ts +0 -87
- package/src/tools/terminal/sandbox.ts +0 -40
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* backfill of the missing thread ancestors when the conversation has no
|
|
4
4
|
* record of the parent message, persists each backfilled message with a
|
|
5
5
|
* derived `slackMeta` envelope, de-dupes against rows already stored, and
|
|
6
|
-
* gates re-triggers behind a 10-minute idempotency cache so
|
|
7
|
-
*
|
|
6
|
+
* gates exact-window re-triggers behind a 10-minute idempotency cache so
|
|
7
|
+
* bursts of retries for the same gap do not flood the Slack API.
|
|
8
8
|
*
|
|
9
9
|
* Tests exercise the helper {@link triggerSlackThreadBackfillIfNeeded}
|
|
10
10
|
* directly against the real database (via the test-preload temp workspace).
|
|
11
|
-
* Only
|
|
12
|
-
* what Slack returns, what does the daemon write to the DB".
|
|
11
|
+
* Only the Slack backfill read is mocked, since the contract under test is
|
|
12
|
+
* "given what Slack returns, what does the daemon write to the DB".
|
|
13
13
|
*/
|
|
14
14
|
import {
|
|
15
15
|
afterAll,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "bun:test";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
|
-
// Mocks (must precede module imports under test). Note:
|
|
26
|
+
// Mocks (must precede module imports under test). Note: backfillThreadWindow is
|
|
27
27
|
// stubbed via spyOn (below) rather than mock.module so the stub does not leak
|
|
28
28
|
// into other test files (e.g. backfill.test.ts) that import the same module.
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -58,6 +58,11 @@ mock.module("../runtime/gateway-client.js", () => ({
|
|
|
58
58
|
import { v4 as uuid } from "uuid";
|
|
59
59
|
|
|
60
60
|
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
61
|
+
import {
|
|
62
|
+
type ChannelCapabilities,
|
|
63
|
+
loadSlackChronologicalContext,
|
|
64
|
+
} from "../daemon/conversation-runtime-assembly.js";
|
|
65
|
+
import type { MessageRow } from "../memory/conversation-crud.js";
|
|
61
66
|
import { getDb } from "../memory/db-connection.js";
|
|
62
67
|
import { initializeDb } from "../memory/db-init.js";
|
|
63
68
|
import type { Message as MessagingMessage } from "../messaging/provider-types.js";
|
|
@@ -66,22 +71,39 @@ import {
|
|
|
66
71
|
readSlackMetadata,
|
|
67
72
|
writeSlackMetadata,
|
|
68
73
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
74
|
+
import type { Message } from "../providers/types.js";
|
|
69
75
|
import {
|
|
70
76
|
_backfillTriggerCache,
|
|
71
77
|
triggerSlackThreadBackfillIfNeeded,
|
|
72
78
|
} from "../runtime/routes/inbound-message-handler.js";
|
|
73
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
handleChannelInbound,
|
|
81
|
+
setAdapterProcessMessage,
|
|
82
|
+
} from "./helpers/channel-test-adapter.js";
|
|
74
83
|
|
|
75
84
|
initializeDb();
|
|
76
85
|
|
|
77
|
-
// Spy on
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
const backfillThreadMock =
|
|
86
|
+
// Spy on backfillThreadWindowPage so the stub is scoped to this test file
|
|
87
|
+
// only. Existing tests drive the message array through `backfillThreadMock`;
|
|
88
|
+
// page metadata defaults to "complete" unless a test overrides the page spy.
|
|
89
|
+
const backfillThreadMock = mock<typeof slackBackfill.backfillThreadWindow>(
|
|
90
|
+
async () => [],
|
|
91
|
+
);
|
|
92
|
+
const backfillThreadPageMock = spyOn(slackBackfill, "backfillThreadWindowPage");
|
|
93
|
+
function installDefaultThreadPageMock(): void {
|
|
94
|
+
backfillThreadPageMock.mockImplementation(async (...args) => ({
|
|
95
|
+
messages: await backfillThreadMock(...args),
|
|
96
|
+
hasMore: false,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
installDefaultThreadPageMock();
|
|
81
100
|
backfillThreadMock.mockResolvedValue([]);
|
|
101
|
+
const backfillDmMock = spyOn(slackBackfill, "backfillDm");
|
|
102
|
+
backfillDmMock.mockResolvedValue([]);
|
|
82
103
|
|
|
83
104
|
afterAll(() => {
|
|
84
|
-
|
|
105
|
+
backfillThreadPageMock.mockRestore();
|
|
106
|
+
backfillDmMock.mockRestore();
|
|
85
107
|
});
|
|
86
108
|
|
|
87
109
|
// ---------------------------------------------------------------------------
|
|
@@ -105,6 +127,8 @@ function resetState(): void {
|
|
|
105
127
|
_backfillTriggerCache.clear();
|
|
106
128
|
backfillThreadMock.mockReset();
|
|
107
129
|
backfillThreadMock.mockImplementation(async () => []);
|
|
130
|
+
backfillDmMock.mockReset();
|
|
131
|
+
backfillDmMock.mockImplementation(async () => []);
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
let convCounter = 0;
|
|
@@ -137,7 +161,7 @@ function insertMessage(
|
|
|
137
161
|
role: string,
|
|
138
162
|
content: string,
|
|
139
163
|
metadata?: Record<string, unknown>,
|
|
140
|
-
):
|
|
164
|
+
): string {
|
|
141
165
|
const db = getDb();
|
|
142
166
|
const id = uuid();
|
|
143
167
|
// Use a strictly increasing timestamp so the ORDER BY in
|
|
@@ -152,6 +176,7 @@ function insertMessage(
|
|
|
152
176
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
153
177
|
)
|
|
154
178
|
.run(id, conversationId, role, content, now, metadataStr);
|
|
179
|
+
return id;
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
interface RawMessageRow {
|
|
@@ -169,6 +194,19 @@ function readMessagesByConversation(conversationId: string): RawMessageRow[] {
|
|
|
169
194
|
.all(conversationId) as RawMessageRow[];
|
|
170
195
|
}
|
|
171
196
|
|
|
197
|
+
function readMessageRowsByConversation(conversationId: string): MessageRow[] {
|
|
198
|
+
const db = getDb();
|
|
199
|
+
return db.$client
|
|
200
|
+
.prepare(
|
|
201
|
+
`SELECT id, conversation_id AS conversationId, role, content,
|
|
202
|
+
created_at AS createdAt, metadata
|
|
203
|
+
FROM messages
|
|
204
|
+
WHERE conversation_id = ?
|
|
205
|
+
ORDER BY created_at ASC`,
|
|
206
|
+
)
|
|
207
|
+
.all(conversationId) as MessageRow[];
|
|
208
|
+
}
|
|
209
|
+
|
|
172
210
|
function makeBackfillMessage(
|
|
173
211
|
overrides: Partial<MessagingMessage> = {},
|
|
174
212
|
): MessagingMessage {
|
|
@@ -190,6 +228,7 @@ interface PersistedRow {
|
|
|
190
228
|
channelTs: string | undefined;
|
|
191
229
|
threadTs: string | undefined;
|
|
192
230
|
displayName: string | undefined;
|
|
231
|
+
slackFiles: Array<{ name: string; mimetype?: string }> | undefined;
|
|
193
232
|
}
|
|
194
233
|
|
|
195
234
|
function readPersistedSlackRows(conversationId: string): PersistedRow[] {
|
|
@@ -202,6 +241,7 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
|
|
|
202
241
|
channelTs: undefined,
|
|
203
242
|
threadTs: undefined,
|
|
204
243
|
displayName: undefined,
|
|
244
|
+
slackFiles: undefined,
|
|
205
245
|
};
|
|
206
246
|
if (!row.metadata) {
|
|
207
247
|
out.push(blank);
|
|
@@ -235,6 +275,10 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
|
|
|
235
275
|
channelTs: slackMeta?.channelTs,
|
|
236
276
|
threadTs: slackMeta?.threadTs,
|
|
237
277
|
displayName: slackMeta?.displayName,
|
|
278
|
+
slackFiles: slackMeta?.slackFiles?.map((file) => ({
|
|
279
|
+
name: file.name,
|
|
280
|
+
...(file.mimetype ? { mimetype: file.mimetype } : {}),
|
|
281
|
+
})),
|
|
238
282
|
});
|
|
239
283
|
}
|
|
240
284
|
return out;
|
|
@@ -268,6 +312,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
268
312
|
|
|
269
313
|
afterEach(() => {
|
|
270
314
|
backfillThreadMock.mockReset();
|
|
315
|
+
installDefaultThreadPageMock();
|
|
271
316
|
_backfillTriggerCache.clear();
|
|
272
317
|
});
|
|
273
318
|
|
|
@@ -325,11 +370,297 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
325
370
|
expect(byChannelTs.get("1234.2")?.displayName).toBe("Reply Two");
|
|
326
371
|
});
|
|
327
372
|
|
|
328
|
-
test("backfill
|
|
373
|
+
test("initial late-join backfill keeps the newest bounded page before the inbound mention", async () => {
|
|
374
|
+
const conv = createTestConversation();
|
|
375
|
+
const ts = (n: number) => `1700000000.${String(n).padStart(6, "0")}`;
|
|
376
|
+
const inboundTs = ts(500000);
|
|
377
|
+
|
|
378
|
+
backfillThreadPageMock.mockImplementation(async (...args) => {
|
|
379
|
+
const messages = await backfillThreadMock(...args);
|
|
380
|
+
const opts = args[2];
|
|
381
|
+
if (opts?.limit === 25) {
|
|
382
|
+
return { messages, hasMore: true, nextCursor: "early-page-2" };
|
|
383
|
+
}
|
|
384
|
+
return { messages, hasMore: false };
|
|
385
|
+
});
|
|
386
|
+
backfillThreadMock.mockImplementation(async (_channel, _thread, opts) => {
|
|
387
|
+
if (opts?.limit === 25) {
|
|
388
|
+
return Array.from({ length: 25 }, (_, i) =>
|
|
389
|
+
makeBackfillMessage({
|
|
390
|
+
id: ts(i),
|
|
391
|
+
text: i === 0 ? "root context" : `early ${i}`,
|
|
392
|
+
threadId: i === 0 ? undefined : ts(0),
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (opts?.before === inboundTs && opts.after !== undefined) {
|
|
397
|
+
return [
|
|
398
|
+
...Array.from({ length: 50 }, (_, i) => {
|
|
399
|
+
const n = 499950 + i;
|
|
400
|
+
return makeBackfillMessage({
|
|
401
|
+
id: ts(n),
|
|
402
|
+
text: n === 499999 ? "newest file share" : `recent ${n}`,
|
|
403
|
+
threadId: ts(0),
|
|
404
|
+
...(n === 499999
|
|
405
|
+
? {
|
|
406
|
+
metadata: {
|
|
407
|
+
slackFiles: [
|
|
408
|
+
{
|
|
409
|
+
id: "F123",
|
|
410
|
+
name: "requirements.txt",
|
|
411
|
+
mimetype: "text/plain",
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
: {}),
|
|
417
|
+
});
|
|
418
|
+
}),
|
|
419
|
+
makeBackfillMessage({
|
|
420
|
+
id: ts(499960),
|
|
421
|
+
text: "duplicate recent row",
|
|
422
|
+
threadId: ts(0),
|
|
423
|
+
}),
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
return [];
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const result = await triggerSlackThreadBackfillIfNeeded({
|
|
430
|
+
conversationId: conv.id,
|
|
431
|
+
channelId: SLACK_CHANNEL_ID,
|
|
432
|
+
threadTs: ts(0),
|
|
433
|
+
excludeChannelTs: inboundTs,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(2);
|
|
437
|
+
expect(backfillThreadMock.mock.calls[0][2]?.limit).toBe(25);
|
|
438
|
+
expect(backfillThreadMock.mock.calls[0][2]?.before).toBeUndefined();
|
|
439
|
+
expect(backfillThreadMock.mock.calls[1][2]?.limit).toBe(50);
|
|
440
|
+
expect(backfillThreadMock.mock.calls[1][2]?.before).toBe(inboundTs);
|
|
441
|
+
expect(backfillThreadMock.mock.calls[1][2]?.after).toBeDefined();
|
|
442
|
+
|
|
443
|
+
expect(result.reason).toBe("thread_late_join");
|
|
444
|
+
expect(result.omittedMiddle).toBe(true);
|
|
445
|
+
|
|
446
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
447
|
+
expect(persisted.length).toBe(75);
|
|
448
|
+
expect(persisted.find((p) => p.channelTs === ts(0))?.content).toBe(
|
|
449
|
+
"root context",
|
|
450
|
+
);
|
|
451
|
+
expect(persisted.find((p) => p.channelTs === ts(250000))).toBeUndefined();
|
|
452
|
+
expect(persisted.find((p) => p.channelTs === ts(499999))?.content).toBe(
|
|
453
|
+
"newest file share",
|
|
454
|
+
);
|
|
455
|
+
expect(
|
|
456
|
+
persisted.filter((p) => p.channelTs === ts(499960)).map((p) => p.content),
|
|
457
|
+
).toEqual(["recent 499960"]);
|
|
458
|
+
expect(
|
|
459
|
+
persisted.find((p) => p.channelTs === ts(499999))?.slackFiles,
|
|
460
|
+
).toEqual([{ name: "requirements.txt", mimetype: "text/plain" }]);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("high-throughput initial backfill keeps shrinking after a truncated probe and persists newest pre-mention rows", async () => {
|
|
464
|
+
const conv = createTestConversation();
|
|
465
|
+
const ts = (seconds: number, micros = 0) =>
|
|
466
|
+
`${seconds}.${String(micros).padStart(6, "0")}`;
|
|
467
|
+
const threadTs = ts(1700000000);
|
|
468
|
+
const inboundTs = ts(1700001000);
|
|
469
|
+
const fiveMinuteAfter = ts(1700000700);
|
|
470
|
+
const sixtySecondAfter = ts(1700000940);
|
|
471
|
+
const tenSecondAfter = ts(1700000990);
|
|
472
|
+
const newestPreMention = [
|
|
473
|
+
makeBackfillMessage({
|
|
474
|
+
id: ts(1700000997, 100000),
|
|
475
|
+
text: "newest context 1",
|
|
476
|
+
threadId: threadTs,
|
|
477
|
+
}),
|
|
478
|
+
makeBackfillMessage({
|
|
479
|
+
id: ts(1700000998, 200000),
|
|
480
|
+
text: "newest context 2",
|
|
481
|
+
threadId: threadTs,
|
|
482
|
+
}),
|
|
483
|
+
makeBackfillMessage({
|
|
484
|
+
id: ts(1700000999, 300000),
|
|
485
|
+
text: "newest context 3",
|
|
486
|
+
threadId: threadTs,
|
|
487
|
+
}),
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
backfillThreadPageMock.mockImplementation(
|
|
491
|
+
async (_channel, _thread, opts) => {
|
|
492
|
+
if (opts?.limit === 25 && opts.before === undefined) {
|
|
493
|
+
return {
|
|
494
|
+
messages: [
|
|
495
|
+
makeBackfillMessage({
|
|
496
|
+
id: threadTs,
|
|
497
|
+
text: "thread parent",
|
|
498
|
+
threadId: undefined,
|
|
499
|
+
}),
|
|
500
|
+
],
|
|
501
|
+
hasMore: true,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (opts?.limit === 50 && opts.before === inboundTs) {
|
|
506
|
+
if (
|
|
507
|
+
opts.after === fiveMinuteAfter ||
|
|
508
|
+
opts.after === sixtySecondAfter
|
|
509
|
+
) {
|
|
510
|
+
return {
|
|
511
|
+
messages: Array.from({ length: 50 }, (_, i) =>
|
|
512
|
+
makeBackfillMessage({
|
|
513
|
+
id: ts(1700000940 + i, i),
|
|
514
|
+
text: `truncated high-throughput ${i}`,
|
|
515
|
+
threadId: threadTs,
|
|
516
|
+
}),
|
|
517
|
+
),
|
|
518
|
+
hasMore: true,
|
|
519
|
+
nextCursor: "still-truncated",
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (opts.after === tenSecondAfter) {
|
|
524
|
+
return { messages: newestPreMention, hasMore: false };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return { messages: [], hasMore: false };
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const pageCallOffset = backfillThreadPageMock.mock.calls.length;
|
|
533
|
+
|
|
534
|
+
const result = await triggerSlackThreadBackfillIfNeeded({
|
|
535
|
+
conversationId: conv.id,
|
|
536
|
+
channelId: SLACK_CHANNEL_ID,
|
|
537
|
+
threadTs,
|
|
538
|
+
excludeChannelTs: inboundTs,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const afterAttempts = backfillThreadPageMock.mock.calls
|
|
542
|
+
.slice(pageCallOffset)
|
|
543
|
+
.map((call) => call[2]?.after)
|
|
544
|
+
.filter((after): after is string => after !== undefined);
|
|
545
|
+
expect(afterAttempts).toContain(sixtySecondAfter);
|
|
546
|
+
expect(afterAttempts).toContain(tenSecondAfter);
|
|
547
|
+
expect(afterAttempts.indexOf(tenSecondAfter)).toBeGreaterThan(
|
|
548
|
+
afterAttempts.indexOf(sixtySecondAfter),
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
expect(result.reason).toBe("thread_late_join");
|
|
552
|
+
expect(result.omittedMiddle).toBe(true);
|
|
553
|
+
|
|
554
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
555
|
+
expect(
|
|
556
|
+
persisted.filter((p) => p.threadTs === threadTs).map((p) => p.content),
|
|
557
|
+
).toEqual(["newest context 1", "newest context 2", "newest context 3"]);
|
|
558
|
+
expect(
|
|
559
|
+
persisted.some((p) => p.content.startsWith("truncated high-throughput")),
|
|
560
|
+
).toBe(false);
|
|
561
|
+
expect(persisted.find((p) => p.channelTs === inboundTs)).toBeUndefined();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("high-throughput initial backfill still runs near-upper fallback after shrinking attempts are exhausted", async () => {
|
|
565
|
+
const conv = createTestConversation();
|
|
566
|
+
const ts = (seconds: number, micros = 0) =>
|
|
567
|
+
`${seconds}.${String(micros).padStart(6, "0")}`;
|
|
568
|
+
const threadTs = ts(1700000000);
|
|
569
|
+
const inboundTs = ts(1700001000);
|
|
570
|
+
const fiveMinuteAfter = ts(1700000700);
|
|
571
|
+
const sixtySecondAfter = ts(1700000940);
|
|
572
|
+
const tenSecondAfter = ts(1700000990);
|
|
573
|
+
const oneSecondAfter = ts(1700000999);
|
|
574
|
+
const hundredMillisecondAfter = ts(1700000999, 900000);
|
|
575
|
+
const nearUpperFallbackAfter = ts(1700000999, 999998);
|
|
576
|
+
|
|
577
|
+
backfillThreadPageMock.mockImplementation(
|
|
578
|
+
async (_channel, _thread, opts) => {
|
|
579
|
+
if (opts?.limit === 25 && opts.before === undefined) {
|
|
580
|
+
return {
|
|
581
|
+
messages: [
|
|
582
|
+
makeBackfillMessage({
|
|
583
|
+
id: threadTs,
|
|
584
|
+
text: "thread parent",
|
|
585
|
+
threadId: undefined,
|
|
586
|
+
}),
|
|
587
|
+
],
|
|
588
|
+
hasMore: true,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (opts?.limit === 50 && opts.before === inboundTs) {
|
|
593
|
+
if (opts.after === nearUpperFallbackAfter) {
|
|
594
|
+
return {
|
|
595
|
+
messages: [
|
|
596
|
+
makeBackfillMessage({
|
|
597
|
+
id: ts(1700000999, 999999),
|
|
598
|
+
text: "newest context after exhausted probes",
|
|
599
|
+
threadId: threadTs,
|
|
600
|
+
}),
|
|
601
|
+
],
|
|
602
|
+
hasMore: false,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
messages: Array.from({ length: 50 }, (_, i) =>
|
|
608
|
+
makeBackfillMessage({
|
|
609
|
+
id: ts(1700000999, 900000 + i),
|
|
610
|
+
text: `truncated exhausted probe ${i}`,
|
|
611
|
+
threadId: threadTs,
|
|
612
|
+
}),
|
|
613
|
+
),
|
|
614
|
+
hasMore: true,
|
|
615
|
+
nextCursor: "still-truncated",
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { messages: [], hasMore: false };
|
|
620
|
+
},
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const exhaustedPageCallOffset = backfillThreadPageMock.mock.calls.length;
|
|
624
|
+
|
|
625
|
+
const result = await triggerSlackThreadBackfillIfNeeded({
|
|
626
|
+
conversationId: conv.id,
|
|
627
|
+
channelId: SLACK_CHANNEL_ID,
|
|
628
|
+
threadTs,
|
|
629
|
+
excludeChannelTs: inboundTs,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const afterAttempts = backfillThreadPageMock.mock.calls
|
|
633
|
+
.slice(exhaustedPageCallOffset)
|
|
634
|
+
.map((call) => call[2]?.after)
|
|
635
|
+
.filter((after): after is string => after !== undefined);
|
|
636
|
+
expect(afterAttempts).toEqual([
|
|
637
|
+
fiveMinuteAfter,
|
|
638
|
+
sixtySecondAfter,
|
|
639
|
+
tenSecondAfter,
|
|
640
|
+
oneSecondAfter,
|
|
641
|
+
hundredMillisecondAfter,
|
|
642
|
+
nearUpperFallbackAfter,
|
|
643
|
+
]);
|
|
644
|
+
|
|
645
|
+
expect(result.reason).toBe("thread_late_join");
|
|
646
|
+
expect(result.omittedMiddle).toBe(true);
|
|
647
|
+
|
|
648
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
649
|
+
expect(
|
|
650
|
+
persisted.find((p) => p.channelTs === ts(1700000999, 999999))?.content,
|
|
651
|
+
).toBe("newest context after exhausted probes");
|
|
652
|
+
expect(
|
|
653
|
+
persisted.some((p) => p.content.startsWith("truncated exhausted probe")),
|
|
654
|
+
).toBe(false);
|
|
655
|
+
expect(persisted.find((p) => p.channelTs === inboundTs)).toBeUndefined();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("backfill is NOT triggered when the parent is already persisted and no upper-bound gap is known", async () => {
|
|
329
659
|
const conv = createTestConversation();
|
|
330
660
|
|
|
331
661
|
// Seed the parent message before the trigger runs — simulates a
|
|
332
|
-
// conversation where the daemon has already seen the thread parent
|
|
662
|
+
// conversation where the daemon has already seen the thread parent but
|
|
663
|
+
// the caller did not provide the inbound Slack ts needed to bound a gap.
|
|
333
664
|
seedSlackRow(conv.id, "1234.0", undefined, "already here");
|
|
334
665
|
|
|
335
666
|
await triggerSlackThreadBackfillIfNeeded({
|
|
@@ -345,6 +676,175 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
345
676
|
expect(persisted[0].channelTs).toBe("1234.0");
|
|
346
677
|
});
|
|
347
678
|
|
|
679
|
+
test("parent already persisted but later replies are missing triggers a bounded delta backfill", async () => {
|
|
680
|
+
const conv = createTestConversation();
|
|
681
|
+
|
|
682
|
+
seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
|
|
683
|
+
|
|
684
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
685
|
+
makeBackfillMessage({
|
|
686
|
+
id: "1234.0",
|
|
687
|
+
text: "duplicate parent",
|
|
688
|
+
threadId: undefined,
|
|
689
|
+
}),
|
|
690
|
+
makeBackfillMessage({
|
|
691
|
+
id: "1234.1",
|
|
692
|
+
text: "unseen earlier reply",
|
|
693
|
+
threadId: "1234.0",
|
|
694
|
+
}),
|
|
695
|
+
makeBackfillMessage({
|
|
696
|
+
id: "1234.5",
|
|
697
|
+
text: "live inbound reply",
|
|
698
|
+
threadId: "1234.0",
|
|
699
|
+
}),
|
|
700
|
+
]);
|
|
701
|
+
|
|
702
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
703
|
+
conversationId: conv.id,
|
|
704
|
+
channelId: SLACK_CHANNEL_ID,
|
|
705
|
+
threadTs: "1234.0",
|
|
706
|
+
excludeChannelTs: "1234.5",
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
710
|
+
const [, , opts] = backfillThreadMock.mock.calls[0];
|
|
711
|
+
expect(opts?.after).toBe("1234.0");
|
|
712
|
+
expect(opts?.before).toBe("1234.5");
|
|
713
|
+
|
|
714
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
715
|
+
expect(persisted.length).toBe(2);
|
|
716
|
+
expect(persisted.find((p) => p.channelTs === "1234.0")?.content).toBe(
|
|
717
|
+
"parent already here",
|
|
718
|
+
);
|
|
719
|
+
expect(persisted.find((p) => p.channelTs === "1234.1")?.content).toBe(
|
|
720
|
+
"unseen earlier reply",
|
|
721
|
+
);
|
|
722
|
+
expect(persisted.find((p) => p.channelTs === "1234.5")).toBeUndefined();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("multi-page delta backfill keeps the newest rows before the inbound mention", async () => {
|
|
726
|
+
const conv = createTestConversation();
|
|
727
|
+
const parentTs = "1699990000.000000";
|
|
728
|
+
const inboundTs = "1700000000.500000";
|
|
729
|
+
const ts = (n: number) => `1700000000.${String(n).padStart(6, "0")}`;
|
|
730
|
+
|
|
731
|
+
seedSlackRow(conv.id, parentTs, undefined, "parent already here");
|
|
732
|
+
|
|
733
|
+
backfillThreadPageMock.mockImplementation(async (...args) => {
|
|
734
|
+
const messages = await backfillThreadMock(...args);
|
|
735
|
+
const opts = args[2];
|
|
736
|
+
if (opts?.limit === 1) {
|
|
737
|
+
return { messages, hasMore: messages.length > 0 };
|
|
738
|
+
}
|
|
739
|
+
return { messages, hasMore: false };
|
|
740
|
+
});
|
|
741
|
+
backfillThreadMock.mockImplementation(async (_channel, _thread, opts) => {
|
|
742
|
+
if (opts?.limit === 1) {
|
|
743
|
+
return [
|
|
744
|
+
makeBackfillMessage({
|
|
745
|
+
id: ts(100000),
|
|
746
|
+
text: "omitted earlier delta",
|
|
747
|
+
threadId: parentTs,
|
|
748
|
+
}),
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
if (opts?.before === inboundTs && opts.after !== parentTs) {
|
|
752
|
+
return Array.from({ length: 50 }, (_, i) => {
|
|
753
|
+
const n = 499950 + i;
|
|
754
|
+
return makeBackfillMessage({
|
|
755
|
+
id: ts(n),
|
|
756
|
+
text: `newest delta ${n}`,
|
|
757
|
+
threadId: parentTs,
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return [];
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const result = await triggerSlackThreadBackfillIfNeeded({
|
|
765
|
+
conversationId: conv.id,
|
|
766
|
+
channelId: SLACK_CHANNEL_ID,
|
|
767
|
+
threadTs: parentTs,
|
|
768
|
+
excludeChannelTs: inboundTs,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
expect(result.reason).toBe("thread_delta");
|
|
772
|
+
expect(result.omittedMiddle).toBe(true);
|
|
773
|
+
expect(backfillThreadMock.mock.calls[0][2]?.before).toBe(inboundTs);
|
|
774
|
+
expect(backfillThreadMock.mock.calls[0][2]?.after).not.toBe(parentTs);
|
|
775
|
+
|
|
776
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
777
|
+
expect(persisted.find((p) => p.channelTs === parentTs)?.content).toBe(
|
|
778
|
+
"parent already here",
|
|
779
|
+
);
|
|
780
|
+
expect(persisted.find((p) => p.channelTs === ts(100000))).toBeUndefined();
|
|
781
|
+
expect(persisted.find((p) => p.channelTs === ts(499999))?.content).toBe(
|
|
782
|
+
"newest delta 499999",
|
|
783
|
+
);
|
|
784
|
+
expect(persisted.find((p) => p.channelTs === inboundTs)).toBeUndefined();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
test("file-bearing backfill renders a Slack file marker without binary hydration", async () => {
|
|
788
|
+
const conv = createTestConversation();
|
|
789
|
+
|
|
790
|
+
seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
|
|
791
|
+
|
|
792
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
793
|
+
makeBackfillMessage({
|
|
794
|
+
id: "1234.1",
|
|
795
|
+
text: "uploaded the draft",
|
|
796
|
+
threadId: "1234.0",
|
|
797
|
+
sender: { id: "U_FILE", name: "File Sharer" },
|
|
798
|
+
metadata: {
|
|
799
|
+
slackFiles: [
|
|
800
|
+
{
|
|
801
|
+
id: "F-DRAFT",
|
|
802
|
+
name: "project-plan.pdf",
|
|
803
|
+
mimetype: "application/pdf",
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
},
|
|
807
|
+
}),
|
|
808
|
+
]);
|
|
809
|
+
|
|
810
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
811
|
+
conversationId: conv.id,
|
|
812
|
+
channelId: SLACK_CHANNEL_ID,
|
|
813
|
+
threadTs: "1234.0",
|
|
814
|
+
excludeChannelTs: "1234.2",
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const context = loadSlackChronologicalContext(conv.id, SLACK_CHANNEL_CAPS, {
|
|
818
|
+
loader: readMessageRowsByConversation,
|
|
819
|
+
trustClass: "guardian",
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
expect(context).not.toBeNull();
|
|
823
|
+
const rendered = flattenText(context!.messages);
|
|
824
|
+
expect(rendered).toContain("uploaded the draft");
|
|
825
|
+
expect(rendered).toContain(
|
|
826
|
+
"[attached file: project-plan.pdf, application/pdf]",
|
|
827
|
+
);
|
|
828
|
+
expect(rendered).not.toContain("F-DRAFT");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("latest stored thread message at or after inbound ts skips backfill using parsed Slack timestamps", async () => {
|
|
832
|
+
const conv = createTestConversation();
|
|
833
|
+
|
|
834
|
+
seedSlackRow(conv.id, "1234.0", undefined, "parent");
|
|
835
|
+
seedSlackRow(conv.id, "1234.10", "1234.0", "newer stored reply");
|
|
836
|
+
|
|
837
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
838
|
+
conversationId: conv.id,
|
|
839
|
+
channelId: SLACK_CHANNEL_ID,
|
|
840
|
+
threadTs: "1234.0",
|
|
841
|
+
excludeChannelTs: "1234.2",
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
expect(backfillThreadMock).not.toHaveBeenCalled();
|
|
845
|
+
expect(readPersistedSlackRows(conv.id).length).toBe(2);
|
|
846
|
+
});
|
|
847
|
+
|
|
348
848
|
test("idempotency cache: a second call inside the TTL window does not re-fetch", async () => {
|
|
349
849
|
const conv = createTestConversation();
|
|
350
850
|
|
|
@@ -358,8 +858,8 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
358
858
|
threadTs: "1234.0",
|
|
359
859
|
});
|
|
360
860
|
|
|
361
|
-
// Second call for the same
|
|
362
|
-
//
|
|
861
|
+
// Second call for the same unbounded window — must short-circuit on the
|
|
862
|
+
// in-memory cache without hitting backfillThreadWindow again.
|
|
363
863
|
await triggerSlackThreadBackfillIfNeeded({
|
|
364
864
|
conversationId: conv.id,
|
|
365
865
|
channelId: SLACK_CHANNEL_ID,
|
|
@@ -394,17 +894,13 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
394
894
|
test("backfill returns duplicates that are already stored — only new rows are inserted", async () => {
|
|
395
895
|
const conv = createTestConversation();
|
|
396
896
|
|
|
397
|
-
// Pre-seed sibling 1234.1 so the
|
|
398
|
-
// already exists (and must not be re-inserted)
|
|
399
|
-
//
|
|
897
|
+
// Pre-seed parent and sibling 1234.1 so the bounded delta response
|
|
898
|
+
// includes one row that already exists (and must not be re-inserted)
|
|
899
|
+
// plus one genuinely new sibling.
|
|
900
|
+
seedSlackRow(conv.id, "1234.0", undefined, "parent");
|
|
400
901
|
seedSlackRow(conv.id, "1234.1", "1234.0", "already here");
|
|
401
902
|
|
|
402
903
|
backfillThreadMock.mockImplementation(async () => [
|
|
403
|
-
makeBackfillMessage({
|
|
404
|
-
id: "1234.0",
|
|
405
|
-
text: "parent",
|
|
406
|
-
threadId: undefined,
|
|
407
|
-
}),
|
|
408
904
|
makeBackfillMessage({
|
|
409
905
|
id: "1234.1",
|
|
410
906
|
text: "duplicate sibling — must be skipped",
|
|
@@ -421,8 +917,13 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
421
917
|
conversationId: conv.id,
|
|
422
918
|
channelId: SLACK_CHANNEL_ID,
|
|
423
919
|
threadTs: "1234.0",
|
|
920
|
+
excludeChannelTs: "1234.3",
|
|
424
921
|
});
|
|
425
922
|
|
|
923
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
924
|
+
expect(backfillThreadMock.mock.calls[0][2]?.after).toBe("1234.1");
|
|
925
|
+
expect(backfillThreadMock.mock.calls[0][2]?.before).toBe("1234.3");
|
|
926
|
+
|
|
426
927
|
const persisted = readPersistedSlackRows(conv.id);
|
|
427
928
|
expect(persisted.length).toBe(3);
|
|
428
929
|
|
|
@@ -453,7 +954,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
453
954
|
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
454
955
|
expect(readPersistedSlackRows(conv.id).length).toBe(0);
|
|
455
956
|
|
|
456
|
-
// Cache should now be populated for this
|
|
957
|
+
// Cache should now be populated for this exact unbounded window, so an
|
|
457
958
|
// immediate retry must not re-run the API call.
|
|
458
959
|
await triggerSlackThreadBackfillIfNeeded({
|
|
459
960
|
conversationId: conv.id,
|
|
@@ -463,6 +964,124 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
463
964
|
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
464
965
|
});
|
|
465
966
|
|
|
967
|
+
test("TTL cache suppresses the same bounded window but not a newer upper-bound window", async () => {
|
|
968
|
+
const conv = createTestConversation();
|
|
969
|
+
|
|
970
|
+
backfillThreadMock.mockImplementation(async () => []);
|
|
971
|
+
|
|
972
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
973
|
+
conversationId: conv.id,
|
|
974
|
+
channelId: SLACK_CHANNEL_ID,
|
|
975
|
+
threadTs: "1234.0",
|
|
976
|
+
excludeChannelTs: "1234.5",
|
|
977
|
+
});
|
|
978
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
979
|
+
conversationId: conv.id,
|
|
980
|
+
channelId: SLACK_CHANNEL_ID,
|
|
981
|
+
threadTs: "1234.0",
|
|
982
|
+
excludeChannelTs: "1234.5",
|
|
983
|
+
});
|
|
984
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
985
|
+
conversationId: conv.id,
|
|
986
|
+
channelId: SLACK_CHANNEL_ID,
|
|
987
|
+
threadTs: "1234.0",
|
|
988
|
+
excludeChannelTs: "1234.6",
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
expect(
|
|
992
|
+
backfillThreadMock.mock.calls.some(
|
|
993
|
+
(call) => call[2]?.before === "1234.5",
|
|
994
|
+
),
|
|
995
|
+
).toBe(true);
|
|
996
|
+
expect(
|
|
997
|
+
backfillThreadMock.mock.calls.some(
|
|
998
|
+
(call) => call[2]?.before === "1234.6",
|
|
999
|
+
),
|
|
1000
|
+
).toBe(true);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("rapid consecutive replies can fetch a newer gap even when the prior inbound reply was only excluded", async () => {
|
|
1004
|
+
const conv = createTestConversation();
|
|
1005
|
+
|
|
1006
|
+
backfillThreadMock.mockImplementation(async (_channel, _thread, opts) => {
|
|
1007
|
+
if (opts?.limit === 25) {
|
|
1008
|
+
return [
|
|
1009
|
+
makeBackfillMessage({
|
|
1010
|
+
id: "1234.0",
|
|
1011
|
+
text: "parent",
|
|
1012
|
+
threadId: undefined,
|
|
1013
|
+
}),
|
|
1014
|
+
];
|
|
1015
|
+
}
|
|
1016
|
+
if (opts?.before === "1234.5") {
|
|
1017
|
+
return [
|
|
1018
|
+
makeBackfillMessage({
|
|
1019
|
+
id: "1234.0",
|
|
1020
|
+
text: "parent",
|
|
1021
|
+
threadId: undefined,
|
|
1022
|
+
}),
|
|
1023
|
+
makeBackfillMessage({
|
|
1024
|
+
id: "1234.4",
|
|
1025
|
+
text: "reply before first live inbound",
|
|
1026
|
+
threadId: "1234.0",
|
|
1027
|
+
}),
|
|
1028
|
+
makeBackfillMessage({
|
|
1029
|
+
id: "1234.5",
|
|
1030
|
+
text: "first live inbound",
|
|
1031
|
+
threadId: "1234.0",
|
|
1032
|
+
}),
|
|
1033
|
+
];
|
|
1034
|
+
}
|
|
1035
|
+
return [
|
|
1036
|
+
makeBackfillMessage({
|
|
1037
|
+
id: "1234.5",
|
|
1038
|
+
text: "first live inbound recovered by newer window",
|
|
1039
|
+
threadId: "1234.0",
|
|
1040
|
+
}),
|
|
1041
|
+
makeBackfillMessage({
|
|
1042
|
+
id: "1234.6",
|
|
1043
|
+
text: "second live inbound",
|
|
1044
|
+
threadId: "1234.0",
|
|
1045
|
+
}),
|
|
1046
|
+
];
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
1050
|
+
conversationId: conv.id,
|
|
1051
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1052
|
+
threadTs: "1234.0",
|
|
1053
|
+
excludeChannelTs: "1234.5",
|
|
1054
|
+
});
|
|
1055
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
1056
|
+
conversationId: conv.id,
|
|
1057
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1058
|
+
threadTs: "1234.0",
|
|
1059
|
+
excludeChannelTs: "1234.6",
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
expect(backfillThreadMock.mock.calls.length).toBeGreaterThanOrEqual(3);
|
|
1063
|
+
expect(backfillThreadMock.mock.calls[0][2]?.after).toBeUndefined();
|
|
1064
|
+
expect(backfillThreadMock.mock.calls[0][2]?.before).toBeUndefined();
|
|
1065
|
+
expect(
|
|
1066
|
+
backfillThreadMock.mock.calls.some(
|
|
1067
|
+
(call) => call[2]?.before === "1234.5",
|
|
1068
|
+
),
|
|
1069
|
+
).toBe(true);
|
|
1070
|
+
expect(
|
|
1071
|
+
backfillThreadMock.mock.calls.some(
|
|
1072
|
+
(call) => call[2]?.before === "1234.6",
|
|
1073
|
+
),
|
|
1074
|
+
).toBe(true);
|
|
1075
|
+
|
|
1076
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
1077
|
+
expect(persisted.map((p) => p.channelTs).sort()).toEqual([
|
|
1078
|
+
"1234.0",
|
|
1079
|
+
"1234.4",
|
|
1080
|
+
"1234.5",
|
|
1081
|
+
]);
|
|
1082
|
+
expect(persisted.find((p) => p.channelTs === "1234.6")).toBeUndefined();
|
|
1083
|
+
});
|
|
1084
|
+
|
|
466
1085
|
test("two distinct threads in the same conversation each trigger their own backfill", async () => {
|
|
467
1086
|
const conv = createTestConversation();
|
|
468
1087
|
|
|
@@ -678,13 +1297,16 @@ function resetHttpState(): void {
|
|
|
678
1297
|
_backfillTriggerCache.clear();
|
|
679
1298
|
backfillThreadMock.mockReset();
|
|
680
1299
|
backfillThreadMock.mockImplementation(async () => []);
|
|
1300
|
+
backfillDmMock.mockReset();
|
|
1301
|
+
backfillDmMock.mockImplementation(async () => []);
|
|
1302
|
+
setAdapterProcessMessage(undefined);
|
|
681
1303
|
}
|
|
682
1304
|
|
|
683
|
-
function seedHttpActiveMember(): void {
|
|
1305
|
+
function seedHttpActiveMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
|
|
684
1306
|
upsertContactChannel({
|
|
685
1307
|
sourceChannel: "slack",
|
|
686
1308
|
externalUserId: HTTP_SLACK_USER_ID,
|
|
687
|
-
externalChatId:
|
|
1309
|
+
externalChatId: chatId,
|
|
688
1310
|
status: "active",
|
|
689
1311
|
policy: "allow",
|
|
690
1312
|
displayName: HTTP_SLACK_DISPLAY_NAME,
|
|
@@ -727,6 +1349,93 @@ function buildThreadReplyRequest(
|
|
|
727
1349
|
});
|
|
728
1350
|
}
|
|
729
1351
|
|
|
1352
|
+
function buildSlackDmRequest(
|
|
1353
|
+
channelId: string,
|
|
1354
|
+
messageId: string,
|
|
1355
|
+
overrides: Record<string, unknown> = {},
|
|
1356
|
+
): Request {
|
|
1357
|
+
httpMsgCounter++;
|
|
1358
|
+
const body: Record<string, unknown> = {
|
|
1359
|
+
sourceChannel: "slack",
|
|
1360
|
+
interface: "slack",
|
|
1361
|
+
conversationExternalId: channelId,
|
|
1362
|
+
externalMessageId: `${channelId}:${messageId}:${httpMsgCounter}`,
|
|
1363
|
+
content: "DM text",
|
|
1364
|
+
actorExternalId: HTTP_SLACK_USER_ID,
|
|
1365
|
+
actorDisplayName: HTTP_SLACK_DISPLAY_NAME,
|
|
1366
|
+
actorUsername: "charlie",
|
|
1367
|
+
replyCallbackUrl: "http://localhost:7830/deliver/slack",
|
|
1368
|
+
sourceMetadata: {
|
|
1369
|
+
messageId,
|
|
1370
|
+
chatType: "im",
|
|
1371
|
+
},
|
|
1372
|
+
...overrides,
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
return new Request("http://localhost:8080/channels/inbound", {
|
|
1376
|
+
method: "POST",
|
|
1377
|
+
headers: {
|
|
1378
|
+
"Content-Type": "application/json",
|
|
1379
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
1380
|
+
},
|
|
1381
|
+
body: JSON.stringify(body),
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
interface SlackInboundProcessOptions {
|
|
1386
|
+
slackRuntimeContextNotice?: string;
|
|
1387
|
+
slackInbound?: {
|
|
1388
|
+
channelId: string;
|
|
1389
|
+
channelTs: string;
|
|
1390
|
+
threadTs?: string;
|
|
1391
|
+
displayName?: string;
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function persistSlackInboundFromProcessMessage(
|
|
1396
|
+
conversationId: string,
|
|
1397
|
+
content: string,
|
|
1398
|
+
options?: SlackInboundProcessOptions,
|
|
1399
|
+
): string {
|
|
1400
|
+
const slackInbound = options?.slackInbound;
|
|
1401
|
+
return insertMessage(conversationId, "user", content, {
|
|
1402
|
+
...(slackInbound
|
|
1403
|
+
? {
|
|
1404
|
+
slackMeta: writeSlackMetadata({
|
|
1405
|
+
source: "slack",
|
|
1406
|
+
channelId: slackInbound.channelId,
|
|
1407
|
+
channelTs: slackInbound.channelTs,
|
|
1408
|
+
...(slackInbound.threadTs
|
|
1409
|
+
? { threadTs: slackInbound.threadTs }
|
|
1410
|
+
: {}),
|
|
1411
|
+
...(slackInbound.displayName
|
|
1412
|
+
? { displayName: slackInbound.displayName }
|
|
1413
|
+
: {}),
|
|
1414
|
+
eventKind: "message",
|
|
1415
|
+
}),
|
|
1416
|
+
}
|
|
1417
|
+
: {}),
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const SLACK_CHANNEL_CAPS: ChannelCapabilities = {
|
|
1422
|
+
channel: "slack",
|
|
1423
|
+
dashboardCapable: false,
|
|
1424
|
+
supportsDynamicUi: false,
|
|
1425
|
+
supportsVoiceInput: false,
|
|
1426
|
+
chatType: "channel",
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
function flattenText(messages: Message[]): string {
|
|
1430
|
+
return messages
|
|
1431
|
+
.flatMap((message) => message.content)
|
|
1432
|
+
.filter((block): block is { type: "text"; text: string } => {
|
|
1433
|
+
return block.type === "text";
|
|
1434
|
+
})
|
|
1435
|
+
.map((block) => block.text)
|
|
1436
|
+
.join("\n");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
730
1439
|
describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
731
1440
|
beforeEach(() => {
|
|
732
1441
|
resetHttpState();
|
|
@@ -736,6 +1445,7 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
736
1445
|
|
|
737
1446
|
afterEach(() => {
|
|
738
1447
|
backfillThreadMock.mockReset();
|
|
1448
|
+
installDefaultThreadPageMock();
|
|
739
1449
|
_backfillTriggerCache.clear();
|
|
740
1450
|
});
|
|
741
1451
|
|
|
@@ -755,9 +1465,22 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
755
1465
|
}),
|
|
756
1466
|
]);
|
|
757
1467
|
|
|
758
|
-
|
|
1468
|
+
let capturedHints: string[] | undefined;
|
|
1469
|
+
let capturedSlackNotice: string | undefined;
|
|
1470
|
+
const processMessage = async (
|
|
1471
|
+
_conversationId: string,
|
|
1472
|
+
_content: string,
|
|
1473
|
+
_attachmentIds?: string[],
|
|
1474
|
+
options?: {
|
|
1475
|
+
transport?: { hints?: string[] };
|
|
1476
|
+
slackRuntimeContextNotice?: string;
|
|
1477
|
+
},
|
|
1478
|
+
): Promise<{ messageId: string }> => {
|
|
1479
|
+
capturedHints = options?.transport?.hints;
|
|
1480
|
+
capturedSlackNotice = options?.slackRuntimeContextNotice;
|
|
759
1481
|
return { messageId: "agent-result-id" };
|
|
760
1482
|
};
|
|
1483
|
+
setAdapterProcessMessage(processMessage);
|
|
761
1484
|
|
|
762
1485
|
const req = buildThreadReplyRequest("1234.0", "1234.3");
|
|
763
1486
|
const resp = await handleChannelInbound(
|
|
@@ -774,7 +1497,7 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
774
1497
|
// void-promise has time to write to the DB before we assert.
|
|
775
1498
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
776
1499
|
|
|
777
|
-
expect(backfillThreadMock).
|
|
1500
|
+
expect(backfillThreadMock.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
778
1501
|
const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
|
|
779
1502
|
expect(calledChannel).toBe(HTTP_SLACK_CHANNEL_ID);
|
|
780
1503
|
expect(calledThread).toBe("1234.0");
|
|
@@ -800,9 +1523,190 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
800
1523
|
|
|
801
1524
|
expect(channelTimestamps.has("1234.0")).toBe(true);
|
|
802
1525
|
expect(channelTimestamps.has("1234.1")).toBe(true);
|
|
1526
|
+
|
|
1527
|
+
expect(
|
|
1528
|
+
capturedHints?.some((hint) => hint.includes("joined an existing thread")),
|
|
1529
|
+
).not.toBe(true);
|
|
1530
|
+
expect(capturedSlackNotice).toContain("joined an existing thread");
|
|
1531
|
+
const contents = db.$client
|
|
1532
|
+
.prepare("SELECT content FROM messages")
|
|
1533
|
+
.all() as Array<{ content: string }>;
|
|
1534
|
+
expect(
|
|
1535
|
+
contents.some((row) => row.content.includes("Slack context note")),
|
|
1536
|
+
).toBe(false);
|
|
803
1537
|
});
|
|
804
1538
|
|
|
805
|
-
test("
|
|
1539
|
+
test("late app mention sees unseen backfilled replies before the mention", async () => {
|
|
1540
|
+
let capturedTranscript = "";
|
|
1541
|
+
let parentTurnSeen = false;
|
|
1542
|
+
let resolveParentTurn: (() => void) | undefined;
|
|
1543
|
+
let secondTurnSeen = false;
|
|
1544
|
+
const parentTurnProcessed = new Promise<void>((resolve) => {
|
|
1545
|
+
resolveParentTurn = resolve;
|
|
1546
|
+
});
|
|
1547
|
+
let resolveSecondTurn: (() => void) | undefined;
|
|
1548
|
+
const secondTurnProcessed = new Promise<void>((resolve) => {
|
|
1549
|
+
resolveSecondTurn = resolve;
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
const processMessage = async (
|
|
1553
|
+
conversationId: string,
|
|
1554
|
+
content: string,
|
|
1555
|
+
_attachmentIds?: string[],
|
|
1556
|
+
options?: SlackInboundProcessOptions,
|
|
1557
|
+
): Promise<{ messageId: string }> => {
|
|
1558
|
+
const messageId = persistSlackInboundFromProcessMessage(
|
|
1559
|
+
conversationId,
|
|
1560
|
+
content,
|
|
1561
|
+
options,
|
|
1562
|
+
);
|
|
1563
|
+
if (options?.slackInbound?.channelTs === "1234.0") {
|
|
1564
|
+
parentTurnSeen = true;
|
|
1565
|
+
resolveParentTurn?.();
|
|
1566
|
+
}
|
|
1567
|
+
if (options?.slackInbound?.channelTs === "1234.5") {
|
|
1568
|
+
const context = loadSlackChronologicalContext(
|
|
1569
|
+
conversationId,
|
|
1570
|
+
SLACK_CHANNEL_CAPS,
|
|
1571
|
+
{
|
|
1572
|
+
loader: readMessageRowsByConversation,
|
|
1573
|
+
trustClass: "guardian",
|
|
1574
|
+
},
|
|
1575
|
+
);
|
|
1576
|
+
capturedTranscript = context ? flattenText(context.messages) : "";
|
|
1577
|
+
secondTurnSeen = true;
|
|
1578
|
+
resolveSecondTurn?.();
|
|
1579
|
+
}
|
|
1580
|
+
return { messageId };
|
|
1581
|
+
};
|
|
1582
|
+
setAdapterProcessMessage(processMessage);
|
|
1583
|
+
|
|
1584
|
+
const parentResp = await handleChannelInbound(
|
|
1585
|
+
buildThreadReplyRequest("1234.0", "1234.0", {
|
|
1586
|
+
content: "parent already stored",
|
|
1587
|
+
sourceMetadata: {
|
|
1588
|
+
messageId: "1234.0",
|
|
1589
|
+
chatType: "channel",
|
|
1590
|
+
},
|
|
1591
|
+
}),
|
|
1592
|
+
processMessage,
|
|
1593
|
+
TEST_BEARER_TOKEN,
|
|
1594
|
+
);
|
|
1595
|
+
expect(parentResp.status).toBe(200);
|
|
1596
|
+
await Promise.race([
|
|
1597
|
+
parentTurnProcessed,
|
|
1598
|
+
new Promise((resolve) => setTimeout(resolve, 250)),
|
|
1599
|
+
]);
|
|
1600
|
+
expect(parentTurnSeen).toBe(true);
|
|
1601
|
+
|
|
1602
|
+
backfillThreadMock.mockReset();
|
|
1603
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
1604
|
+
makeBackfillMessage({
|
|
1605
|
+
id: "1234.1",
|
|
1606
|
+
text: "unseen first reply",
|
|
1607
|
+
threadId: "1234.0",
|
|
1608
|
+
sender: { id: "U_ONE", name: "Reply One" },
|
|
1609
|
+
}),
|
|
1610
|
+
makeBackfillMessage({
|
|
1611
|
+
id: "1234.2",
|
|
1612
|
+
text: "unseen second reply",
|
|
1613
|
+
threadId: "1234.0",
|
|
1614
|
+
sender: { id: "U_TWO", name: "Reply Two" },
|
|
1615
|
+
}),
|
|
1616
|
+
makeBackfillMessage({
|
|
1617
|
+
id: "1234.5",
|
|
1618
|
+
text: "live app mention should not be duplicated by backfill",
|
|
1619
|
+
threadId: "1234.0",
|
|
1620
|
+
sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
|
|
1621
|
+
}),
|
|
1622
|
+
]);
|
|
1623
|
+
|
|
1624
|
+
const mentionResp = await handleChannelInbound(
|
|
1625
|
+
buildThreadReplyRequest("1234.0", "1234.5", {
|
|
1626
|
+
content: "<@U_ASSISTANT> please answer with the missing context",
|
|
1627
|
+
sourceMetadata: {
|
|
1628
|
+
messageId: "1234.5",
|
|
1629
|
+
threadId: "1234.0",
|
|
1630
|
+
chatType: "channel",
|
|
1631
|
+
eventType: "app_mention",
|
|
1632
|
+
},
|
|
1633
|
+
}),
|
|
1634
|
+
processMessage,
|
|
1635
|
+
TEST_BEARER_TOKEN,
|
|
1636
|
+
);
|
|
1637
|
+
expect(mentionResp.status).toBe(200);
|
|
1638
|
+
|
|
1639
|
+
await Promise.race([
|
|
1640
|
+
secondTurnProcessed,
|
|
1641
|
+
new Promise((resolve) => setTimeout(resolve, 250)),
|
|
1642
|
+
]);
|
|
1643
|
+
|
|
1644
|
+
expect(secondTurnSeen).toBe(true);
|
|
1645
|
+
expect(backfillThreadMock).toHaveBeenCalledTimes(1);
|
|
1646
|
+
expect(backfillThreadMock.mock.calls[0][2]?.after).toBe("1234.0");
|
|
1647
|
+
expect(backfillThreadMock.mock.calls[0][2]?.before).toBe("1234.5");
|
|
1648
|
+
|
|
1649
|
+
const parentIndex = capturedTranscript.indexOf("parent already stored");
|
|
1650
|
+
const firstReplyIndex = capturedTranscript.indexOf("unseen first reply");
|
|
1651
|
+
const secondReplyIndex = capturedTranscript.indexOf("unseen second reply");
|
|
1652
|
+
const mentionIndex = capturedTranscript.indexOf(
|
|
1653
|
+
"please answer with the missing context",
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
expect(parentIndex).toBeGreaterThanOrEqual(0);
|
|
1657
|
+
expect(firstReplyIndex).toBeGreaterThan(parentIndex);
|
|
1658
|
+
expect(secondReplyIndex).toBeGreaterThan(firstReplyIndex);
|
|
1659
|
+
expect(mentionIndex).toBeGreaterThan(secondReplyIndex);
|
|
1660
|
+
expect(
|
|
1661
|
+
capturedTranscript.match(/live app mention should not be duplicated/g),
|
|
1662
|
+
).toBeNull();
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
test("cold-start Slack DMs still use backfillDm without thread backfill", async () => {
|
|
1666
|
+
const dmChannelId = "D0HTTPDM";
|
|
1667
|
+
seedHttpActiveMember(dmChannelId);
|
|
1668
|
+
backfillDmMock.mockImplementation(async () => [
|
|
1669
|
+
makeBackfillMessage({
|
|
1670
|
+
id: "1700000000.000100",
|
|
1671
|
+
conversationId: dmChannelId,
|
|
1672
|
+
text: "earlier DM context",
|
|
1673
|
+
sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
|
|
1674
|
+
}),
|
|
1675
|
+
]);
|
|
1676
|
+
|
|
1677
|
+
const processMessage = async (
|
|
1678
|
+
conversationId: string,
|
|
1679
|
+
content: string,
|
|
1680
|
+
_attachmentIds?: string[],
|
|
1681
|
+
options?: SlackInboundProcessOptions,
|
|
1682
|
+
): Promise<{ messageId: string }> => ({
|
|
1683
|
+
messageId: persistSlackInboundFromProcessMessage(
|
|
1684
|
+
conversationId,
|
|
1685
|
+
content,
|
|
1686
|
+
options,
|
|
1687
|
+
),
|
|
1688
|
+
});
|
|
1689
|
+
setAdapterProcessMessage(processMessage);
|
|
1690
|
+
|
|
1691
|
+
const resp = await handleChannelInbound(
|
|
1692
|
+
buildSlackDmRequest(dmChannelId, "1700000000.000200"),
|
|
1693
|
+
processMessage,
|
|
1694
|
+
TEST_BEARER_TOKEN,
|
|
1695
|
+
);
|
|
1696
|
+
|
|
1697
|
+
expect(resp.status).toBe(200);
|
|
1698
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1699
|
+
|
|
1700
|
+
expect(backfillDmMock).toHaveBeenCalledTimes(1);
|
|
1701
|
+
expect(backfillDmMock.mock.calls[0][0]).toBe(dmChannelId);
|
|
1702
|
+
expect(backfillDmMock.mock.calls[0][1]).toMatchObject({
|
|
1703
|
+
limit: 50,
|
|
1704
|
+
before: "1700000000.000200",
|
|
1705
|
+
});
|
|
1706
|
+
expect(backfillThreadMock).not.toHaveBeenCalled();
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
test("second thread reply within the TTL window can fetch a newer bounded gap", async () => {
|
|
806
1710
|
backfillThreadMock.mockImplementation(async () => [
|
|
807
1711
|
makeBackfillMessage({ id: "5678.0", text: "parent" }),
|
|
808
1712
|
]);
|
|
@@ -827,7 +1731,17 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
827
1731
|
expect(r2.status).toBe(200);
|
|
828
1732
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
829
1733
|
|
|
830
|
-
expect(backfillThreadMock).
|
|
1734
|
+
expect(backfillThreadMock.mock.calls[0][2]?.before).toBeUndefined();
|
|
1735
|
+
expect(
|
|
1736
|
+
backfillThreadMock.mock.calls.some(
|
|
1737
|
+
(call) => call[2]?.before === "5678.1",
|
|
1738
|
+
),
|
|
1739
|
+
).toBe(true);
|
|
1740
|
+
expect(
|
|
1741
|
+
backfillThreadMock.mock.calls.some(
|
|
1742
|
+
(call) => call[2]?.before === "5678.2",
|
|
1743
|
+
),
|
|
1744
|
+
).toBe(true);
|
|
831
1745
|
});
|
|
832
1746
|
|
|
833
1747
|
test("backfill error from the HTTP path does not crash the request", async () => {
|