@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
|
@@ -45,11 +45,13 @@ import { upsertBinding } from "../../memory/external-conversation-store.js";
|
|
|
45
45
|
import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
|
|
46
46
|
import {
|
|
47
47
|
backfillDm,
|
|
48
|
-
|
|
48
|
+
backfillThreadWindowPage,
|
|
49
|
+
type SlackBackfillWindowPage,
|
|
49
50
|
} from "../../messaging/providers/slack/backfill.js";
|
|
50
51
|
import {
|
|
51
52
|
mergeSlackMetadata,
|
|
52
53
|
readSlackMetadata,
|
|
54
|
+
type SlackFileMetadata,
|
|
53
55
|
type SlackMessageMetadata,
|
|
54
56
|
writeSlackMetadata,
|
|
55
57
|
} from "../../messaging/providers/slack/message-metadata.js";
|
|
@@ -71,7 +73,6 @@ import { handleGuardianActivationIntercept } from "./inbound-stages/guardian-act
|
|
|
71
73
|
import { handleGuardianReplyIntercept } from "./inbound-stages/guardian-reply-intercept.js";
|
|
72
74
|
import { runSecretIngressCheck } from "./inbound-stages/secret-ingress-check.js";
|
|
73
75
|
import { tryTranscribeAudioAttachments } from "./inbound-stages/transcribe-audio.js";
|
|
74
|
-
import { handleVerificationIntercept } from "./inbound-stages/verification-intercept.js";
|
|
75
76
|
import type { RouteHandlerArgs } from "./types.js";
|
|
76
77
|
|
|
77
78
|
const log = getLogger("runtime-http");
|
|
@@ -263,7 +264,7 @@ export async function handleChannelInbound({
|
|
|
263
264
|
externalMessageId,
|
|
264
265
|
});
|
|
265
266
|
if (aclResult.earlyResponse) return aclResult.earlyResponse;
|
|
266
|
-
const { resolvedMember
|
|
267
|
+
const { resolvedMember } = aclResult;
|
|
267
268
|
|
|
268
269
|
// ── Slack delete propagation ──
|
|
269
270
|
// Slack message_deleted events are forwarded by the gateway with the
|
|
@@ -286,7 +287,7 @@ export async function handleChannelInbound({
|
|
|
286
287
|
{ conversationExternalId },
|
|
287
288
|
"Slack message_deleted event missing sourceMetadata.messageId; ignoring",
|
|
288
289
|
);
|
|
289
|
-
return
|
|
290
|
+
return { accepted: true, deleted: false };
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
// Look up the stored message via the existing channel-event lookup.
|
|
@@ -327,7 +328,7 @@ export async function handleChannelInbound({
|
|
|
327
328
|
{ conversationExternalId, deletedMessageTs },
|
|
328
329
|
"No stored message found for Slack delete after retries; ignoring",
|
|
329
330
|
);
|
|
330
|
-
return
|
|
331
|
+
return { accepted: true, deleted: false };
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
// Merge deletedAt into the existing slackMeta sub-key. If the row has
|
|
@@ -345,7 +346,7 @@ export async function handleChannelInbound({
|
|
|
345
346
|
},
|
|
346
347
|
"Stored Slack message has no metadata; skipping delete marker",
|
|
347
348
|
);
|
|
348
|
-
return
|
|
349
|
+
return { accepted: true, deleted: false };
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
let parentMetadata: Record<string, unknown>;
|
|
@@ -365,7 +366,7 @@ export async function handleChannelInbound({
|
|
|
365
366
|
},
|
|
366
367
|
"Failed to parse stored metadata; skipping delete marker",
|
|
367
368
|
);
|
|
368
|
-
return
|
|
369
|
+
return { accepted: true, deleted: false };
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
const existingSlackMeta =
|
|
@@ -382,7 +383,7 @@ export async function handleChannelInbound({
|
|
|
382
383
|
},
|
|
383
384
|
"Stored Slack message has no slackMeta; skipping delete marker",
|
|
384
385
|
);
|
|
385
|
-
return
|
|
386
|
+
return { accepted: true, deleted: false };
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
const updatedSlackMeta = mergeSlackMetadata(existingSlackMeta, {
|
|
@@ -404,11 +405,11 @@ export async function handleChannelInbound({
|
|
|
404
405
|
"Marked Slack message as deleted",
|
|
405
406
|
);
|
|
406
407
|
|
|
407
|
-
return
|
|
408
|
+
return {
|
|
408
409
|
accepted: true,
|
|
409
410
|
deleted: true,
|
|
410
411
|
messageId: original.messageId,
|
|
411
|
-
}
|
|
412
|
+
};
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
if (hasAttachments) {
|
|
@@ -416,7 +417,9 @@ export async function handleChannelInbound({
|
|
|
416
417
|
if (resolved.length !== attachmentIds.length) {
|
|
417
418
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
418
419
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
419
|
-
throw new BadRequestError(
|
|
420
|
+
throw new BadRequestError(
|
|
421
|
+
`Attachment IDs not found: ${missing.join(", ")}`,
|
|
422
|
+
);
|
|
420
423
|
}
|
|
421
424
|
}
|
|
422
425
|
|
|
@@ -500,11 +503,11 @@ export async function handleChannelInbound({
|
|
|
500
503
|
"Retry of pending verification reply failed; will retry on next duplicate",
|
|
501
504
|
);
|
|
502
505
|
}
|
|
503
|
-
return
|
|
506
|
+
return {
|
|
504
507
|
accepted: true,
|
|
505
508
|
duplicate: true,
|
|
506
509
|
eventId: result.eventId,
|
|
507
|
-
}
|
|
510
|
+
};
|
|
508
511
|
}
|
|
509
512
|
}
|
|
510
513
|
|
|
@@ -541,10 +544,10 @@ export async function handleChannelInbound({
|
|
|
541
544
|
// guardian approval reactions have no transcript representation.
|
|
542
545
|
// 2. All other reactions (non-guardian, no pending approval, stale,
|
|
543
546
|
// and any `reaction_removed:` event regardless of actor) fall
|
|
544
|
-
// through to `persistSlackReactionAsMessage` so
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
547
|
+
// through to `persistSlackReactionAsMessage` so Slack transcript
|
|
548
|
+
// rendering can surface them inline. Reactions never trigger an
|
|
549
|
+
// agent response, so we short-circuit before escalation and
|
|
550
|
+
// agent-loop dispatch in both cases.
|
|
548
551
|
if (isSlackReactionEvent(body)) {
|
|
549
552
|
// Approval interception runs only for reactions (added) — `reaction_removed`
|
|
550
553
|
// never expresses an approval intent, so un-reacting is left as a pure
|
|
@@ -586,12 +589,12 @@ export async function handleChannelInbound({
|
|
|
586
589
|
// transcript line. All other interception outcomes (stale_ignored,
|
|
587
590
|
// non-guardian, no pending approval) fall through to persistence.
|
|
588
591
|
if (reactionApprovalResult.type === "guardian_decision_applied") {
|
|
589
|
-
return
|
|
592
|
+
return {
|
|
590
593
|
accepted: true,
|
|
591
594
|
duplicate: false,
|
|
592
595
|
eventId: result.eventId,
|
|
593
596
|
approval: reactionApprovalResult.type,
|
|
594
|
-
}
|
|
597
|
+
};
|
|
595
598
|
}
|
|
596
599
|
}
|
|
597
600
|
|
|
@@ -604,11 +607,11 @@ export async function handleChannelInbound({
|
|
|
604
607
|
{ conversationId: result.conversationId, eventId: result.eventId },
|
|
605
608
|
"Skipping reaction persistence: missing sourceMetadata.messageId",
|
|
606
609
|
);
|
|
607
|
-
return
|
|
610
|
+
return {
|
|
608
611
|
accepted: result.accepted,
|
|
609
612
|
duplicate: result.duplicate,
|
|
610
613
|
eventId: result.eventId,
|
|
611
|
-
}
|
|
614
|
+
};
|
|
612
615
|
}
|
|
613
616
|
|
|
614
617
|
const threadTs =
|
|
@@ -634,11 +637,11 @@ export async function handleChannelInbound({
|
|
|
634
637
|
);
|
|
635
638
|
}
|
|
636
639
|
|
|
637
|
-
return
|
|
640
|
+
return {
|
|
638
641
|
accepted: result.accepted,
|
|
639
642
|
duplicate: result.duplicate,
|
|
640
643
|
eventId: result.eventId,
|
|
641
|
-
}
|
|
644
|
+
};
|
|
642
645
|
}
|
|
643
646
|
|
|
644
647
|
// ── Ingress escalation ──
|
|
@@ -670,6 +673,7 @@ export async function handleChannelInbound({
|
|
|
670
673
|
typeof hint === "string" && hint.trim().length > 0,
|
|
671
674
|
)
|
|
672
675
|
: [];
|
|
676
|
+
let slackRuntimeContextNotice: string | undefined;
|
|
673
677
|
|
|
674
678
|
// Inject channel-scoped permission hints for Slack channel messages
|
|
675
679
|
if (sourceChannel === "slack") {
|
|
@@ -734,24 +738,6 @@ export async function handleChannelInbound({
|
|
|
734
738
|
});
|
|
735
739
|
if (bootstrapResponse) return bootstrapResponse;
|
|
736
740
|
|
|
737
|
-
// ── Guardian verification code intercept (deterministic) ──
|
|
738
|
-
const verificationResponse = await handleVerificationIntercept({
|
|
739
|
-
isDuplicate: result.duplicate,
|
|
740
|
-
guardianVerifyCode,
|
|
741
|
-
rawSenderId,
|
|
742
|
-
canonicalSenderId,
|
|
743
|
-
canonicalAssistantId,
|
|
744
|
-
sourceChannel,
|
|
745
|
-
conversationExternalId,
|
|
746
|
-
conversationId: result.conversationId,
|
|
747
|
-
eventId: result.eventId,
|
|
748
|
-
replyCallbackUrl,
|
|
749
|
-
assistantId,
|
|
750
|
-
actorDisplayName: body.actorDisplayName,
|
|
751
|
-
actorUsername: body.actorUsername,
|
|
752
|
-
});
|
|
753
|
-
if (verificationResponse) return verificationResponse;
|
|
754
|
-
|
|
755
741
|
// Legacy voice guardian action interception removed — all guardian reply
|
|
756
742
|
// routing now flows through the canonical router below (routeGuardianReply),
|
|
757
743
|
// which handles request code matching, callback parsing, and NL classification
|
|
@@ -862,12 +848,12 @@ export async function handleChannelInbound({
|
|
|
862
848
|
}
|
|
863
849
|
}
|
|
864
850
|
|
|
865
|
-
return
|
|
851
|
+
return {
|
|
866
852
|
accepted: true,
|
|
867
853
|
duplicate: false,
|
|
868
854
|
eventId: result.eventId,
|
|
869
855
|
approval: approvalResult.type,
|
|
870
|
-
}
|
|
856
|
+
};
|
|
871
857
|
}
|
|
872
858
|
|
|
873
859
|
// When a callback payload was not handled by approval interception, it's
|
|
@@ -922,12 +908,12 @@ export async function handleChannelInbound({
|
|
|
922
908
|
});
|
|
923
909
|
}
|
|
924
910
|
|
|
925
|
-
return
|
|
911
|
+
return {
|
|
926
912
|
accepted: true,
|
|
927
913
|
duplicate: false,
|
|
928
914
|
eventId: result.eventId,
|
|
929
915
|
approval: "stale_ignored",
|
|
930
|
-
}
|
|
916
|
+
};
|
|
931
917
|
}
|
|
932
918
|
}
|
|
933
919
|
|
|
@@ -1032,25 +1018,27 @@ export async function handleChannelInbound({
|
|
|
1032
1018
|
});
|
|
1033
1019
|
}
|
|
1034
1020
|
|
|
1035
|
-
// ── Thread
|
|
1036
|
-
// When a Slack reply arrives
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
// swallowed inside the helper so they never block dispatch.
|
|
1021
|
+
// ── Thread gap/delta backfill ──
|
|
1022
|
+
// When a Slack thread reply arrives, compare the stored thread state
|
|
1023
|
+
// with the inbound message's ts and fetch only the bounded unseen
|
|
1024
|
+
// window. Initial late-join turns hydrate the earliest thread messages
|
|
1025
|
+
// plus a recent window adjacent to the inbound reply; later turns use
|
|
1026
|
+
// a delta window after the latest stored thread ts and before the
|
|
1027
|
+
// inbound ts. Awaited (mirrors the DM cold-start path above) so the
|
|
1028
|
+
// agent loop dispatched immediately afterwards observes hydrated
|
|
1029
|
+
// context. A late-join notice is added only to the current turn's
|
|
1030
|
+
// runtime context, not persisted as durable Slack metadata. Failures
|
|
1031
|
+
// are swallowed inside the helper so they never block dispatch.
|
|
1046
1032
|
if (slackThreadTs) {
|
|
1047
|
-
await triggerSlackThreadBackfillIfNeeded({
|
|
1033
|
+
const backfillResult = await triggerSlackThreadBackfillIfNeeded({
|
|
1048
1034
|
conversationId: result.conversationId,
|
|
1049
1035
|
channelId: conversationExternalId,
|
|
1050
1036
|
threadTs: slackThreadTs,
|
|
1051
1037
|
excludeChannelTs: slackInbound?.channelTs,
|
|
1052
1038
|
account: slackAccount,
|
|
1053
1039
|
});
|
|
1040
|
+
const lateJoinNotice = buildSlackLateJoinNotice(backfillResult);
|
|
1041
|
+
if (lateJoinNotice) slackRuntimeContextNotice = lateJoinNotice;
|
|
1054
1042
|
}
|
|
1055
1043
|
|
|
1056
1044
|
// Wrap non-guardian inbound content in external_content boundaries so
|
|
@@ -1078,6 +1066,7 @@ export async function handleChannelInbound({
|
|
|
1078
1066
|
externalChatId: conversationExternalId,
|
|
1079
1067
|
trustCtx,
|
|
1080
1068
|
metadataHints,
|
|
1069
|
+
slackRuntimeContextNotice,
|
|
1081
1070
|
metadataUxBrief,
|
|
1082
1071
|
commandIntent,
|
|
1083
1072
|
sourceLanguageCode,
|
|
@@ -1090,11 +1079,11 @@ export async function handleChannelInbound({
|
|
|
1090
1079
|
}
|
|
1091
1080
|
}
|
|
1092
1081
|
|
|
1093
|
-
return
|
|
1082
|
+
return {
|
|
1094
1083
|
accepted: result.accepted,
|
|
1095
1084
|
duplicate: result.duplicate,
|
|
1096
1085
|
eventId: result.eventId,
|
|
1097
|
-
}
|
|
1086
|
+
};
|
|
1098
1087
|
}
|
|
1099
1088
|
|
|
1100
1089
|
/**
|
|
@@ -1193,8 +1182,8 @@ async function persistSlackReactionAsMessage(params: {
|
|
|
1193
1182
|
},
|
|
1194
1183
|
};
|
|
1195
1184
|
|
|
1196
|
-
// Sentinel content —
|
|
1197
|
-
// reaction line; the literal text is never displayed to the model.
|
|
1185
|
+
// Sentinel content — Slack transcript renderers read `slackMeta` to format
|
|
1186
|
+
// the reaction line; the literal text is never displayed to the model.
|
|
1198
1187
|
const persisted = await addMessage(
|
|
1199
1188
|
params.conversationId,
|
|
1200
1189
|
"user",
|
|
@@ -1273,19 +1262,7 @@ function countSlackMetaMessages(conversationId: string): number {
|
|
|
1273
1262
|
);
|
|
1274
1263
|
if (candidates.length === 0) return count;
|
|
1275
1264
|
for (const raw of candidates) {
|
|
1276
|
-
|
|
1277
|
-
try {
|
|
1278
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
1279
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1280
|
-
parent = parsed as Record<string, unknown>;
|
|
1281
|
-
}
|
|
1282
|
-
} catch {
|
|
1283
|
-
continue;
|
|
1284
|
-
}
|
|
1285
|
-
if (!parent) continue;
|
|
1286
|
-
const inner = parent.slackMeta;
|
|
1287
|
-
if (typeof inner !== "string") continue;
|
|
1288
|
-
if (readSlackMetadata(inner)) {
|
|
1265
|
+
if (readSlackMetadataFromMessageMetadata(raw)) {
|
|
1289
1266
|
count++;
|
|
1290
1267
|
if (count >= SLACK_DM_BACKFILL_WARM_THRESHOLD) return count;
|
|
1291
1268
|
}
|
|
@@ -1296,44 +1273,110 @@ function countSlackMetaMessages(conversationId: string): number {
|
|
|
1296
1273
|
return count;
|
|
1297
1274
|
}
|
|
1298
1275
|
|
|
1276
|
+
function readSlackMetadataFromMessageMetadata(
|
|
1277
|
+
metadata: string | null | undefined,
|
|
1278
|
+
): SlackMessageMetadata | null {
|
|
1279
|
+
if (!metadata) return null;
|
|
1280
|
+
let parent: Record<string, unknown> | null = null;
|
|
1281
|
+
try {
|
|
1282
|
+
const parsed = JSON.parse(metadata) as unknown;
|
|
1283
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1284
|
+
parent = parsed as Record<string, unknown>;
|
|
1285
|
+
}
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
if (!parent) return null;
|
|
1290
|
+
const raw = parent.slackMeta;
|
|
1291
|
+
if (typeof raw !== "string") return null;
|
|
1292
|
+
return readSlackMetadata(raw);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1299
1295
|
/**
|
|
1300
1296
|
* Build the set of `slackMeta.channelTs` values already stored on a
|
|
1301
|
-
* conversation. Used by both DM cold-start backfill and thread
|
|
1297
|
+
* conversation. Used by both DM cold-start backfill and thread gap/delta
|
|
1302
1298
|
* backfill to dedupe rows so a partial prior backfill (or a single message
|
|
1303
1299
|
* that was already persisted via the live ingress path) does not double-write.
|
|
1304
1300
|
*/
|
|
1305
1301
|
function readStoredSlackChannelTs(conversationId: string): Set<string> {
|
|
1306
1302
|
const seen = new Set<string>();
|
|
1307
1303
|
for (const row of getMessages(conversationId)) {
|
|
1308
|
-
|
|
1309
|
-
let parent: Record<string, unknown> | null = null;
|
|
1310
|
-
try {
|
|
1311
|
-
const parsed = JSON.parse(row.metadata) as unknown;
|
|
1312
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1313
|
-
parent = parsed as Record<string, unknown>;
|
|
1314
|
-
}
|
|
1315
|
-
} catch {
|
|
1316
|
-
continue;
|
|
1317
|
-
}
|
|
1318
|
-
if (!parent) continue;
|
|
1319
|
-
const raw = parent.slackMeta;
|
|
1320
|
-
if (typeof raw !== "string") continue;
|
|
1321
|
-
const meta = readSlackMetadata(raw);
|
|
1304
|
+
const meta = readSlackMetadataFromMessageMetadata(row.metadata);
|
|
1322
1305
|
// Only message rows represent stored Slack messages. Reaction rows carry
|
|
1323
1306
|
// `channelTs` equal to the target message's ts, so including them would
|
|
1324
|
-
// make a reaction on a thread parent wrongly short-circuit
|
|
1307
|
+
// make a reaction on a thread parent wrongly short-circuit thread
|
|
1325
1308
|
// backfill (the parent itself may still be unseen).
|
|
1326
1309
|
if (meta && meta.eventKind === "message") seen.add(meta.channelTs);
|
|
1327
1310
|
}
|
|
1328
1311
|
return seen;
|
|
1329
1312
|
}
|
|
1330
1313
|
|
|
1314
|
+
interface ParsedSlackTimestamp {
|
|
1315
|
+
seconds: bigint;
|
|
1316
|
+
micros: bigint;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function parseSlackTimestamp(
|
|
1320
|
+
ts: string | undefined,
|
|
1321
|
+
): ParsedSlackTimestamp | null {
|
|
1322
|
+
if (!ts) return null;
|
|
1323
|
+
const match = /^(\d+)\.(\d{1,6})$/.exec(ts);
|
|
1324
|
+
if (!match) return null;
|
|
1325
|
+
const micros = BigInt(match[2]);
|
|
1326
|
+
if (micros > 999_999n) return null;
|
|
1327
|
+
return {
|
|
1328
|
+
seconds: BigInt(match[1]),
|
|
1329
|
+
micros,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function compareSlackTimestamps(left: string, right: string): number | null {
|
|
1334
|
+
const parsedLeft = parseSlackTimestamp(left);
|
|
1335
|
+
const parsedRight = parseSlackTimestamp(right);
|
|
1336
|
+
if (!parsedLeft || !parsedRight) return null;
|
|
1337
|
+
if (parsedLeft.seconds < parsedRight.seconds) return -1;
|
|
1338
|
+
if (parsedLeft.seconds > parsedRight.seconds) return 1;
|
|
1339
|
+
if (parsedLeft.micros < parsedRight.micros) return -1;
|
|
1340
|
+
if (parsedLeft.micros > parsedRight.micros) return 1;
|
|
1341
|
+
return 0;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
interface StoredSlackThreadState {
|
|
1345
|
+
storedChannelTs: Set<string>;
|
|
1346
|
+
latestStoredThreadTs: string | undefined;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function readStoredSlackThreadState(
|
|
1350
|
+
conversationId: string,
|
|
1351
|
+
threadTs: string,
|
|
1352
|
+
): StoredSlackThreadState {
|
|
1353
|
+
const storedChannelTs = new Set<string>();
|
|
1354
|
+
let latestStoredThreadTs: string | undefined;
|
|
1355
|
+
|
|
1356
|
+
for (const row of getMessages(conversationId)) {
|
|
1357
|
+
const meta = readSlackMetadataFromMessageMetadata(row.metadata);
|
|
1358
|
+
if (!meta || meta.eventKind !== "message") continue;
|
|
1359
|
+
if (meta.channelTs !== threadTs && meta.threadTs !== threadTs) continue;
|
|
1360
|
+
|
|
1361
|
+
storedChannelTs.add(meta.channelTs);
|
|
1362
|
+
if (!parseSlackTimestamp(meta.channelTs)) continue;
|
|
1363
|
+
if (
|
|
1364
|
+
latestStoredThreadTs === undefined ||
|
|
1365
|
+
compareSlackTimestamps(meta.channelTs, latestStoredThreadTs) === 1
|
|
1366
|
+
) {
|
|
1367
|
+
latestStoredThreadTs = meta.channelTs;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
return { storedChannelTs, latestStoredThreadTs };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1331
1374
|
/**
|
|
1332
1375
|
* Persist a single backfilled Slack message as a `messages` row with a
|
|
1333
1376
|
* `slackMeta` envelope.
|
|
1334
1377
|
*
|
|
1335
1378
|
* Shared insertion point for any path that hydrates Slack history lazily
|
|
1336
|
-
* (DM cold-start backfill, thread
|
|
1379
|
+
* (DM cold-start backfill, thread gap/delta backfill, etc.). Role is derived
|
|
1337
1380
|
* from `message.metadata.isBot` — bot-authored rows map to `"assistant"` so
|
|
1338
1381
|
* our own prior replies (and any other bot traffic) are not rehydrated as
|
|
1339
1382
|
* user turns, which would otherwise corrupt speaker attribution and make
|
|
@@ -1347,6 +1390,7 @@ async function persistBackfilledSlackMessage(params: {
|
|
|
1347
1390
|
message: ProviderMessage;
|
|
1348
1391
|
}): Promise<void> {
|
|
1349
1392
|
const { message } = params;
|
|
1393
|
+
const slackFiles = readSlackFilesFromProviderMetadata(message.metadata);
|
|
1350
1394
|
const slackMeta: SlackMessageMetadata = {
|
|
1351
1395
|
source: "slack",
|
|
1352
1396
|
channelId: params.channelId,
|
|
@@ -1354,6 +1398,7 @@ async function persistBackfilledSlackMessage(params: {
|
|
|
1354
1398
|
eventKind: "message",
|
|
1355
1399
|
...(message.threadId ? { threadTs: message.threadId } : {}),
|
|
1356
1400
|
...(message.sender?.name ? { displayName: message.sender.name } : {}),
|
|
1401
|
+
...(slackFiles.length > 0 ? { slackFiles } : {}),
|
|
1357
1402
|
};
|
|
1358
1403
|
const role = message.metadata?.isBot === true ? "assistant" : "user";
|
|
1359
1404
|
await addMessage(params.conversationId, role, message.text ?? "", {
|
|
@@ -1361,6 +1406,32 @@ async function persistBackfilledSlackMessage(params: {
|
|
|
1361
1406
|
});
|
|
1362
1407
|
}
|
|
1363
1408
|
|
|
1409
|
+
function readSlackFilesFromProviderMetadata(
|
|
1410
|
+
metadata: Record<string, unknown> | undefined,
|
|
1411
|
+
): SlackFileMetadata[] {
|
|
1412
|
+
const raw = metadata?.slackFiles;
|
|
1413
|
+
if (!Array.isArray(raw)) return [];
|
|
1414
|
+
const files: SlackFileMetadata[] = [];
|
|
1415
|
+
for (const item of raw) {
|
|
1416
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const record = item as Record<string, unknown>;
|
|
1420
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
1421
|
+
if (!name) continue;
|
|
1422
|
+
files.push({
|
|
1423
|
+
...(typeof record.id === "string" && record.id.length > 0
|
|
1424
|
+
? { id: record.id }
|
|
1425
|
+
: {}),
|
|
1426
|
+
name,
|
|
1427
|
+
...(typeof record.mimetype === "string" && record.mimetype.length > 0
|
|
1428
|
+
? { mimetype: record.mimetype }
|
|
1429
|
+
: {}),
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
return files;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1364
1435
|
/**
|
|
1365
1436
|
* In-memory map of in-flight DM cold-start backfills keyed by conversationId.
|
|
1366
1437
|
* Concurrent inbound DMs to the same cold conversation share a single
|
|
@@ -1503,13 +1574,11 @@ async function runBackfillSlackDmIfCold(params: {
|
|
|
1503
1574
|
// ---------------------------------------------------------------------------
|
|
1504
1575
|
|
|
1505
1576
|
/**
|
|
1506
|
-
* In-memory TTL cache keyed by
|
|
1507
|
-
*
|
|
1508
|
-
* thread
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
* backfill becomes a cheap "are the parents already stored?" DB lookup
|
|
1512
|
-
* that short-circuits before the Slack API is touched.
|
|
1577
|
+
* In-memory TTL cache keyed by
|
|
1578
|
+
* `<conversationId>:<threadTs>:<lowerBoundTs>:<upperBoundTs>`. Tracks recent
|
|
1579
|
+
* thread-backfill windows so repeated triggers for the same Slack gap do not
|
|
1580
|
+
* re-fetch identical rows while later replies in the same thread can still
|
|
1581
|
+
* request newer unseen windows.
|
|
1513
1582
|
*
|
|
1514
1583
|
* Exported only for tests; production callers should use
|
|
1515
1584
|
* {@link triggerSlackThreadBackfillIfNeeded}.
|
|
@@ -1518,6 +1587,39 @@ export const _backfillTriggerCache = new Map<string, number>();
|
|
|
1518
1587
|
|
|
1519
1588
|
const BACKFILL_TRIGGER_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
1520
1589
|
const BACKFILL_TRIGGER_CACHE_MAX = 1_000;
|
|
1590
|
+
const SLACK_THREAD_INITIAL_EARLY_LIMIT = 25;
|
|
1591
|
+
const SLACK_THREAD_INITIAL_RECENT_LIMIT = 50;
|
|
1592
|
+
const SLACK_THREAD_INITIAL_RECENT_MAX_PAGES = 5;
|
|
1593
|
+
const SLACK_THREAD_DELTA_LIMIT = 50;
|
|
1594
|
+
const SLACK_THREAD_UPPER_ADJACENT_MAX_ATTEMPTS = 5;
|
|
1595
|
+
const MICROS_PER_SECOND = 1_000_000n;
|
|
1596
|
+
const SLACK_UPPER_ADJACENT_EXPANDING_WINDOWS_MICROS = [
|
|
1597
|
+
5n * 60n * MICROS_PER_SECOND,
|
|
1598
|
+
60n * 60n * MICROS_PER_SECOND,
|
|
1599
|
+
24n * 60n * 60n * MICROS_PER_SECOND,
|
|
1600
|
+
7n * 24n * 60n * 60n * MICROS_PER_SECOND,
|
|
1601
|
+
30n * 24n * 60n * 60n * MICROS_PER_SECOND,
|
|
1602
|
+
];
|
|
1603
|
+
const SLACK_UPPER_ADJACENT_SHRINKING_WINDOWS_MICROS = [
|
|
1604
|
+
60n * MICROS_PER_SECOND,
|
|
1605
|
+
10n * MICROS_PER_SECOND,
|
|
1606
|
+
MICROS_PER_SECOND,
|
|
1607
|
+
100_000n,
|
|
1608
|
+
1_000n,
|
|
1609
|
+
];
|
|
1610
|
+
|
|
1611
|
+
export interface SlackThreadBackfillResult {
|
|
1612
|
+
fetched: number;
|
|
1613
|
+
persisted: number;
|
|
1614
|
+
reason?: SlackBackfillReason;
|
|
1615
|
+
omittedMiddle: boolean;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
type SlackBackfillReason = "thread_late_join" | "thread_delta";
|
|
1619
|
+
|
|
1620
|
+
function emptySlackThreadBackfillResult(): SlackThreadBackfillResult {
|
|
1621
|
+
return { fetched: 0, persisted: 0, omittedMiddle: false };
|
|
1622
|
+
}
|
|
1521
1623
|
|
|
1522
1624
|
function pruneBackfillCacheIfNeeded(): void {
|
|
1523
1625
|
if (_backfillTriggerCache.size < BACKFILL_TRIGGER_CACHE_MAX) return;
|
|
@@ -1552,23 +1654,315 @@ function isBackfillRecentlyTriggered(cacheKey: string): boolean {
|
|
|
1552
1654
|
return true;
|
|
1553
1655
|
}
|
|
1554
1656
|
|
|
1657
|
+
interface SlackInitialThreadWindowsResult {
|
|
1658
|
+
messages: ProviderMessage[];
|
|
1659
|
+
omittedMiddle: boolean;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
interface SlackUpperAdjacentWindowResult {
|
|
1663
|
+
messages: ProviderMessage[];
|
|
1664
|
+
omittedEarlierContent: boolean;
|
|
1665
|
+
truncatedBeforeUpperBound: boolean;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function slackPageHasMore(page: SlackBackfillWindowPage): boolean {
|
|
1669
|
+
return page.hasMore || page.nextCursor !== undefined;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function minSlackMessageTs(messages: ProviderMessage[]): string | undefined {
|
|
1673
|
+
return sortSlackProviderMessages(messages)[0]?.id;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function maxSlackMessageTs(messages: ProviderMessage[]): string | undefined {
|
|
1677
|
+
const sorted = sortSlackProviderMessages(messages);
|
|
1678
|
+
return sorted[sorted.length - 1]?.id;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function slackTimestampToMicros(ts: string | undefined): bigint | null {
|
|
1682
|
+
const parsed = parseSlackTimestamp(ts);
|
|
1683
|
+
if (!parsed) return null;
|
|
1684
|
+
return parsed.seconds * MICROS_PER_SECOND + parsed.micros;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function slackTimestampFromMicros(totalMicros: bigint): string | undefined {
|
|
1688
|
+
if (totalMicros < 0n) return undefined;
|
|
1689
|
+
const seconds = totalMicros / MICROS_PER_SECOND;
|
|
1690
|
+
const micros = totalMicros % MICROS_PER_SECOND;
|
|
1691
|
+
return `${seconds.toString()}.${micros.toString().padStart(6, "0")}`;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function didInitialWindowsLeaveGap(params: {
|
|
1695
|
+
early: SlackBackfillWindowPage;
|
|
1696
|
+
recent: SlackBackfillWindowPage;
|
|
1697
|
+
recentScanTruncated: boolean;
|
|
1698
|
+
}): boolean {
|
|
1699
|
+
if (params.recentScanTruncated) return true;
|
|
1700
|
+
if (!slackPageHasMore(params.early)) return false;
|
|
1701
|
+
const earlyMax = maxSlackMessageTs(params.early.messages);
|
|
1702
|
+
const recentMin = minSlackMessageTs(params.recent.messages);
|
|
1703
|
+
if (!earlyMax || !recentMin) return false;
|
|
1704
|
+
const compared = compareSlackTimestamps(earlyMax, recentMin);
|
|
1705
|
+
return compared !== null && compared < 0;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
async function fetchSlackThreadUpperAdjacentWindow(params: {
|
|
1709
|
+
channelId: string;
|
|
1710
|
+
threadTs: string;
|
|
1711
|
+
upperBoundTs: string;
|
|
1712
|
+
lowerBoundTs?: string;
|
|
1713
|
+
limit: number;
|
|
1714
|
+
account?: string;
|
|
1715
|
+
maxAttempts?: number;
|
|
1716
|
+
}): Promise<SlackUpperAdjacentWindowResult> {
|
|
1717
|
+
// Slack returns bounded conversations.replies pages earliest-first. To keep
|
|
1718
|
+
// the context closest to the inbound mention, narrow by timestamp instead
|
|
1719
|
+
// of cursoring forward from the oldest page in the bounded range.
|
|
1720
|
+
const upperMicros = slackTimestampToMicros(params.upperBoundTs);
|
|
1721
|
+
if (upperMicros === null) {
|
|
1722
|
+
const page = await backfillThreadWindowPage(
|
|
1723
|
+
params.channelId,
|
|
1724
|
+
params.threadTs,
|
|
1725
|
+
{
|
|
1726
|
+
limit: params.limit,
|
|
1727
|
+
account: params.account,
|
|
1728
|
+
before: params.upperBoundTs,
|
|
1729
|
+
...(params.lowerBoundTs !== undefined
|
|
1730
|
+
? { after: params.lowerBoundTs }
|
|
1731
|
+
: {}),
|
|
1732
|
+
},
|
|
1733
|
+
);
|
|
1734
|
+
return {
|
|
1735
|
+
messages: page.messages,
|
|
1736
|
+
omittedEarlierContent: slackPageHasMore(page),
|
|
1737
|
+
truncatedBeforeUpperBound: slackPageHasMore(page),
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const lowerMicros = slackTimestampToMicros(params.lowerBoundTs);
|
|
1742
|
+
const maxAttempts =
|
|
1743
|
+
params.maxAttempts ?? SLACK_THREAD_UPPER_ADJACENT_MAX_ATTEMPTS;
|
|
1744
|
+
let attempts = 0;
|
|
1745
|
+
let safePage: SlackBackfillWindowPage | undefined;
|
|
1746
|
+
let safeAfterTs: string | undefined;
|
|
1747
|
+
let truncatedBeforeUpperBound = false;
|
|
1748
|
+
|
|
1749
|
+
const fetchWindow = async (
|
|
1750
|
+
windowMicros: bigint,
|
|
1751
|
+
): Promise<{
|
|
1752
|
+
page: SlackBackfillWindowPage;
|
|
1753
|
+
after?: string;
|
|
1754
|
+
reachedLowerBound: boolean;
|
|
1755
|
+
}> => {
|
|
1756
|
+
let candidateMicros = upperMicros - windowMicros;
|
|
1757
|
+
let reachedLowerBound = false;
|
|
1758
|
+
if (lowerMicros !== null && candidateMicros <= lowerMicros) {
|
|
1759
|
+
candidateMicros = lowerMicros;
|
|
1760
|
+
reachedLowerBound = true;
|
|
1761
|
+
}
|
|
1762
|
+
const after = reachedLowerBound
|
|
1763
|
+
? params.lowerBoundTs
|
|
1764
|
+
: slackTimestampFromMicros(candidateMicros);
|
|
1765
|
+
const page = await backfillThreadWindowPage(
|
|
1766
|
+
params.channelId,
|
|
1767
|
+
params.threadTs,
|
|
1768
|
+
{
|
|
1769
|
+
limit: params.limit,
|
|
1770
|
+
account: params.account,
|
|
1771
|
+
before: params.upperBoundTs,
|
|
1772
|
+
...(after !== undefined ? { after } : {}),
|
|
1773
|
+
},
|
|
1774
|
+
);
|
|
1775
|
+
attempts++;
|
|
1776
|
+
return { page, after, reachedLowerBound };
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
const considerWindow = async (windowMicros: bigint): Promise<boolean> => {
|
|
1780
|
+
const { page, after, reachedLowerBound } = await fetchWindow(windowMicros);
|
|
1781
|
+
if (slackPageHasMore(page)) {
|
|
1782
|
+
truncatedBeforeUpperBound = true;
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
safePage = page;
|
|
1787
|
+
safeAfterTs = after;
|
|
1788
|
+
return page.messages.length < params.limit && !reachedLowerBound;
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
for (const windowMicros of SLACK_UPPER_ADJACENT_EXPANDING_WINDOWS_MICROS) {
|
|
1792
|
+
if (attempts >= maxAttempts) break;
|
|
1793
|
+
const shouldExpand = await considerWindow(windowMicros);
|
|
1794
|
+
if (!shouldExpand) break;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (truncatedBeforeUpperBound && !safePage && attempts < maxAttempts) {
|
|
1798
|
+
for (const windowMicros of SLACK_UPPER_ADJACENT_SHRINKING_WINDOWS_MICROS) {
|
|
1799
|
+
if (attempts >= maxAttempts) break;
|
|
1800
|
+
await considerWindow(windowMicros);
|
|
1801
|
+
if (safePage) break;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if (!safePage) {
|
|
1806
|
+
const after = slackTimestampFromMicros(upperMicros - 2n);
|
|
1807
|
+
const page = await backfillThreadWindowPage(
|
|
1808
|
+
params.channelId,
|
|
1809
|
+
params.threadTs,
|
|
1810
|
+
{
|
|
1811
|
+
limit: params.limit,
|
|
1812
|
+
account: params.account,
|
|
1813
|
+
before: params.upperBoundTs,
|
|
1814
|
+
...(after !== undefined ? { after } : {}),
|
|
1815
|
+
},
|
|
1816
|
+
);
|
|
1817
|
+
safePage = page;
|
|
1818
|
+
safeAfterTs = after;
|
|
1819
|
+
truncatedBeforeUpperBound =
|
|
1820
|
+
truncatedBeforeUpperBound || slackPageHasMore(page);
|
|
1821
|
+
}
|
|
1822
|
+
if (!safePage) {
|
|
1823
|
+
return {
|
|
1824
|
+
messages: [],
|
|
1825
|
+
omittedEarlierContent: true,
|
|
1826
|
+
truncatedBeforeUpperBound: true,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
let omittedEarlierContent = truncatedBeforeUpperBound;
|
|
1831
|
+
if (
|
|
1832
|
+
!omittedEarlierContent &&
|
|
1833
|
+
params.lowerBoundTs !== undefined &&
|
|
1834
|
+
safeAfterTs !== undefined &&
|
|
1835
|
+
compareSlackTimestamps(params.lowerBoundTs, safeAfterTs) === -1
|
|
1836
|
+
) {
|
|
1837
|
+
const coverageProbe = await backfillThreadWindowPage(
|
|
1838
|
+
params.channelId,
|
|
1839
|
+
params.threadTs,
|
|
1840
|
+
{
|
|
1841
|
+
limit: 1,
|
|
1842
|
+
account: params.account,
|
|
1843
|
+
after: params.lowerBoundTs,
|
|
1844
|
+
before: safeAfterTs,
|
|
1845
|
+
},
|
|
1846
|
+
);
|
|
1847
|
+
omittedEarlierContent =
|
|
1848
|
+
coverageProbe.messages.length > 0 || slackPageHasMore(coverageProbe);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
return {
|
|
1852
|
+
messages: safePage.messages,
|
|
1853
|
+
omittedEarlierContent,
|
|
1854
|
+
truncatedBeforeUpperBound,
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
async function fetchInitialSlackThreadWindows(params: {
|
|
1859
|
+
channelId: string;
|
|
1860
|
+
threadTs: string;
|
|
1861
|
+
upperBoundTs?: string;
|
|
1862
|
+
account?: string;
|
|
1863
|
+
}): Promise<SlackInitialThreadWindowsResult> {
|
|
1864
|
+
if (!params.upperBoundTs) {
|
|
1865
|
+
const early = await backfillThreadWindowPage(
|
|
1866
|
+
params.channelId,
|
|
1867
|
+
params.threadTs,
|
|
1868
|
+
{
|
|
1869
|
+
limit: SLACK_THREAD_INITIAL_EARLY_LIMIT,
|
|
1870
|
+
account: params.account,
|
|
1871
|
+
},
|
|
1872
|
+
);
|
|
1873
|
+
return {
|
|
1874
|
+
messages: sortSlackProviderMessages(
|
|
1875
|
+
dedupeSlackProviderMessages(early.messages),
|
|
1876
|
+
),
|
|
1877
|
+
omittedMiddle: slackPageHasMore(early),
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
const [early, recentResult] = await Promise.all([
|
|
1881
|
+
backfillThreadWindowPage(params.channelId, params.threadTs, {
|
|
1882
|
+
limit: SLACK_THREAD_INITIAL_EARLY_LIMIT,
|
|
1883
|
+
account: params.account,
|
|
1884
|
+
}),
|
|
1885
|
+
fetchSlackThreadUpperAdjacentWindow({
|
|
1886
|
+
channelId: params.channelId,
|
|
1887
|
+
threadTs: params.threadTs,
|
|
1888
|
+
account: params.account,
|
|
1889
|
+
upperBoundTs: params.upperBoundTs,
|
|
1890
|
+
limit: SLACK_THREAD_INITIAL_RECENT_LIMIT,
|
|
1891
|
+
maxAttempts: SLACK_THREAD_INITIAL_RECENT_MAX_PAGES,
|
|
1892
|
+
}),
|
|
1893
|
+
]);
|
|
1894
|
+
const recent: SlackBackfillWindowPage = {
|
|
1895
|
+
messages: recentResult.messages,
|
|
1896
|
+
hasMore: recentResult.truncatedBeforeUpperBound,
|
|
1897
|
+
};
|
|
1898
|
+
return {
|
|
1899
|
+
messages: sortSlackProviderMessages(
|
|
1900
|
+
dedupeSlackProviderMessages([...early.messages, ...recent.messages]),
|
|
1901
|
+
),
|
|
1902
|
+
omittedMiddle:
|
|
1903
|
+
recentResult.omittedEarlierContent ||
|
|
1904
|
+
didInitialWindowsLeaveGap({
|
|
1905
|
+
early,
|
|
1906
|
+
recent,
|
|
1907
|
+
recentScanTruncated: recentResult.truncatedBeforeUpperBound,
|
|
1908
|
+
}),
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function dedupeSlackProviderMessages(
|
|
1913
|
+
messages: ProviderMessage[],
|
|
1914
|
+
): ProviderMessage[] {
|
|
1915
|
+
const byTs = new Map<string, ProviderMessage>();
|
|
1916
|
+
for (const message of messages) {
|
|
1917
|
+
if (!message.id || byTs.has(message.id)) continue;
|
|
1918
|
+
byTs.set(message.id, message);
|
|
1919
|
+
}
|
|
1920
|
+
return [...byTs.values()];
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function sortSlackProviderMessages(
|
|
1924
|
+
messages: ProviderMessage[],
|
|
1925
|
+
): ProviderMessage[] {
|
|
1926
|
+
return [...messages].sort((left, right) => {
|
|
1927
|
+
const compared = compareSlackTimestamps(left.id, right.id);
|
|
1928
|
+
if (compared !== null) return compared;
|
|
1929
|
+
return left.id.localeCompare(right.id);
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function buildSlackLateJoinNotice(
|
|
1934
|
+
result: SlackThreadBackfillResult,
|
|
1935
|
+
): string | null {
|
|
1936
|
+
if (result.reason !== "thread_late_join" || result.persisted === 0) {
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
const omitted = result.omittedMiddle
|
|
1940
|
+
? " Some middle thread messages were intentionally omitted from this turn's hydrated context to keep latency bounded."
|
|
1941
|
+
: "";
|
|
1942
|
+
return `Slack context note: this turn joined an existing thread. ${result.persisted} earlier thread message${result.persisted === 1 ? " was" : "s were"} backfilled before the current message.${omitted}`;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1555
1945
|
/**
|
|
1556
|
-
* Lazily backfill
|
|
1946
|
+
* Lazily backfill Slack thread gaps for an inbound thread reply.
|
|
1557
1947
|
*
|
|
1558
|
-
* When a reply arrives for a thread
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
1562
|
-
*
|
|
1563
|
-
* appears in the conversation.
|
|
1948
|
+
* When a reply arrives for a thread with unseen Slack history, the assistant
|
|
1949
|
+
* fetches bounded `conversations.replies` pages via
|
|
1950
|
+
* {@link backfillThreadWindowPage}, persists each unseen message as a
|
|
1951
|
+
* `messages` row with a `slackMeta` envelope, and skips duplicates whose `ts`
|
|
1952
|
+
* already appears in the conversation.
|
|
1564
1953
|
*
|
|
1565
1954
|
* Behavior contracts:
|
|
1566
|
-
* - **
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1955
|
+
* - **Thread-state gap detection.** Looks up stored Slack message rows for
|
|
1956
|
+
* the same thread, excluding reactions, then fetches only the unseen
|
|
1957
|
+
* `(latestStoredThreadTs, excludeChannelTs)` window when the inbound Slack
|
|
1958
|
+
* timestamp is newer than local state.
|
|
1959
|
+
* - **Upper-bound windows.** Initial late-join backfill combines an early
|
|
1960
|
+
* thread page with a recent page adjacent to the inbound ts; delta backfill
|
|
1961
|
+
* fetches the page nearest the inbound upper bound so the current turn sees
|
|
1962
|
+
* the most relevant context while keeping latency bounded.
|
|
1963
|
+
* - **Exact-window TTL cache.** A 10-minute in-memory cache prevents repeated
|
|
1964
|
+
* fetches for the same exact lower/upper bounded window, without
|
|
1965
|
+
* suppressing later unseen windows in the same thread.
|
|
1572
1966
|
* - **Failure-tolerant.** Any error (Slack API failure, DB error, malformed
|
|
1573
1967
|
* payload) is logged at `warn` and swallowed — the inbound turn must
|
|
1574
1968
|
* never block on backfill.
|
|
@@ -1583,65 +1977,106 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
|
|
|
1583
1977
|
* `conversations.replies` returns it in the thread window. Necessary
|
|
1584
1978
|
* because thread backfill runs concurrently with
|
|
1585
1979
|
* `processChannelMessageInBackground`, so the inbound row may not yet be
|
|
1586
|
-
* in the DB when
|
|
1980
|
+
* in the DB when the thread-state scan snapshots the conversation.
|
|
1587
1981
|
*/
|
|
1588
1982
|
excludeChannelTs?: string;
|
|
1589
1983
|
/**
|
|
1590
1984
|
* OAuth account identifier used to disambiguate which Slack workspace the
|
|
1591
1985
|
* backfill should read from in multi-account setups. Passed through to
|
|
1592
|
-
* `
|
|
1593
|
-
* resolver falls back to the default-active
|
|
1986
|
+
* `backfillThreadWindowPage` page requests and then `resolveConnection`.
|
|
1987
|
+
* Best-effort: if omitted, the resolver falls back to the default-active
|
|
1988
|
+
* connection.
|
|
1594
1989
|
*/
|
|
1595
1990
|
account?: string;
|
|
1596
|
-
}): Promise<
|
|
1991
|
+
}): Promise<SlackThreadBackfillResult> {
|
|
1597
1992
|
const { conversationId, channelId, threadTs, excludeChannelTs, account } =
|
|
1598
1993
|
params;
|
|
1599
|
-
const cacheKey = `${conversationId}:${threadTs}`;
|
|
1600
1994
|
|
|
1601
1995
|
try {
|
|
1602
|
-
|
|
1603
|
-
|
|
1996
|
+
const upperBoundTs = parseSlackTimestamp(excludeChannelTs)
|
|
1997
|
+
? excludeChannelTs
|
|
1998
|
+
: undefined;
|
|
1999
|
+
const threadState = readStoredSlackThreadState(conversationId, threadTs);
|
|
2000
|
+
const lowerBoundTs = threadState.latestStoredThreadTs;
|
|
2001
|
+
|
|
2002
|
+
// Pre-seed only after computing lowerBoundTs. The current inbound row
|
|
2003
|
+
// may not have reached the DB yet, and treating it as stored state would
|
|
2004
|
+
// hide the gap we need to fetch.
|
|
2005
|
+
if (excludeChannelTs) threadState.storedChannelTs.add(excludeChannelTs);
|
|
2006
|
+
|
|
2007
|
+
if (upperBoundTs && lowerBoundTs) {
|
|
2008
|
+
const lowerVsUpper = compareSlackTimestamps(lowerBoundTs, upperBoundTs);
|
|
2009
|
+
if (lowerVsUpper !== null && lowerVsUpper >= 0) {
|
|
2010
|
+
return emptySlackThreadBackfillResult();
|
|
2011
|
+
}
|
|
2012
|
+
} else if (!upperBoundTs && lowerBoundTs) {
|
|
2013
|
+
return emptySlackThreadBackfillResult();
|
|
1604
2014
|
}
|
|
1605
2015
|
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
_backfillTriggerCache.set(cacheKey, Date.now());
|
|
1612
|
-
pruneBackfillCacheIfNeeded();
|
|
1613
|
-
return;
|
|
2016
|
+
const cacheKey = `${conversationId}:${threadTs}:${
|
|
2017
|
+
lowerBoundTs ?? "none"
|
|
2018
|
+
}:${upperBoundTs ?? "unbounded"}`;
|
|
2019
|
+
if (isBackfillRecentlyTriggered(cacheKey)) {
|
|
2020
|
+
return emptySlackThreadBackfillResult();
|
|
1614
2021
|
}
|
|
1615
2022
|
|
|
1616
2023
|
// Mark the trigger before issuing the network call. Doing this first
|
|
1617
|
-
// means a second concurrent
|
|
1618
|
-
// immediately even while the first call is still awaiting the Slack
|
|
1619
|
-
//
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
2024
|
+
// means a second concurrent request for the same window short-circuits
|
|
2025
|
+
// immediately even while the first call is still awaiting the Slack API.
|
|
2026
|
+
// The cost is a slightly larger window where a transient Slack failure
|
|
2027
|
+
// suppresses a retry, which the next reply outside the TTL (or a daemon
|
|
2028
|
+
// restart) will re-attempt anyway.
|
|
1622
2029
|
_backfillTriggerCache.set(cacheKey, Date.now());
|
|
1623
2030
|
pruneBackfillCacheIfNeeded();
|
|
1624
2031
|
|
|
1625
|
-
const
|
|
2032
|
+
const isInitialLateJoin =
|
|
2033
|
+
lowerBoundTs === undefined &&
|
|
2034
|
+
threadState.storedChannelTs.size === (excludeChannelTs ? 1 : 0);
|
|
2035
|
+
const reason: SlackBackfillReason = isInitialLateJoin
|
|
2036
|
+
? "thread_late_join"
|
|
2037
|
+
: "thread_delta";
|
|
2038
|
+
let omittedMiddle = false;
|
|
2039
|
+
let fetched: ProviderMessage[];
|
|
2040
|
+
if (isInitialLateJoin) {
|
|
2041
|
+
const initial = await fetchInitialSlackThreadWindows({
|
|
2042
|
+
channelId,
|
|
2043
|
+
threadTs,
|
|
2044
|
+
upperBoundTs,
|
|
2045
|
+
account,
|
|
2046
|
+
});
|
|
2047
|
+
fetched = initial.messages;
|
|
2048
|
+
omittedMiddle = initial.omittedMiddle;
|
|
2049
|
+
} else {
|
|
2050
|
+
const window = await fetchSlackThreadUpperAdjacentWindow({
|
|
2051
|
+
channelId,
|
|
2052
|
+
threadTs,
|
|
2053
|
+
limit: SLACK_THREAD_DELTA_LIMIT,
|
|
2054
|
+
account,
|
|
2055
|
+
...(lowerBoundTs !== undefined ? { lowerBoundTs } : {}),
|
|
2056
|
+
upperBoundTs: upperBoundTs ?? threadTs,
|
|
2057
|
+
});
|
|
2058
|
+
fetched = window.messages;
|
|
2059
|
+
omittedMiddle = window.omittedEarlierContent;
|
|
2060
|
+
}
|
|
1626
2061
|
if (fetched.length === 0) {
|
|
1627
2062
|
log.debug(
|
|
1628
2063
|
{ conversationId, channelId, threadTs },
|
|
1629
2064
|
"Slack thread backfill returned no messages",
|
|
1630
2065
|
);
|
|
1631
|
-
return;
|
|
2066
|
+
return emptySlackThreadBackfillResult();
|
|
1632
2067
|
}
|
|
1633
2068
|
|
|
1634
2069
|
let persisted = 0;
|
|
1635
2070
|
for (const message of fetched) {
|
|
1636
2071
|
if (!message.id) continue;
|
|
1637
|
-
if (storedChannelTs.has(message.id)) continue;
|
|
2072
|
+
if (threadState.storedChannelTs.has(message.id)) continue;
|
|
1638
2073
|
try {
|
|
1639
2074
|
await persistBackfilledSlackMessage({
|
|
1640
2075
|
conversationId,
|
|
1641
2076
|
channelId,
|
|
1642
2077
|
message,
|
|
1643
2078
|
});
|
|
1644
|
-
storedChannelTs.add(message.id);
|
|
2079
|
+
threadState.storedChannelTs.add(message.id);
|
|
1645
2080
|
persisted++;
|
|
1646
2081
|
} catch (err) {
|
|
1647
2082
|
log.warn(
|
|
@@ -1658,15 +2093,22 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
|
|
|
1658
2093
|
threadTs,
|
|
1659
2094
|
persisted,
|
|
1660
2095
|
fetched: fetched.length,
|
|
2096
|
+
omittedMiddle,
|
|
1661
2097
|
},
|
|
1662
|
-
"Slack thread backfill persisted
|
|
2098
|
+
"Slack thread backfill persisted thread messages",
|
|
1663
2099
|
);
|
|
2100
|
+
return {
|
|
2101
|
+
fetched: fetched.length,
|
|
2102
|
+
persisted,
|
|
2103
|
+
reason,
|
|
2104
|
+
omittedMiddle,
|
|
2105
|
+
};
|
|
1664
2106
|
} catch (err) {
|
|
1665
2107
|
// `channel_not_found` almost always means the resolved connection is
|
|
1666
2108
|
// pointing at the wrong Slack workspace (a real config bug), so log it
|
|
1667
2109
|
// at ERROR to match backfill's rethrow contract. Other failures
|
|
1668
2110
|
// (timeout, auth, ratelimited, …) stay at WARN — they're expected
|
|
1669
|
-
// transient blips and dispatch proceeds without the
|
|
2111
|
+
// transient blips and dispatch proceeds without the backfilled thread rows.
|
|
1670
2112
|
const channelNotFound =
|
|
1671
2113
|
err instanceof Error && /channel_not_found/i.test(err.message);
|
|
1672
2114
|
const payload = { err, conversationId, channelId, threadTs, account };
|
|
@@ -1678,5 +2120,6 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
|
|
|
1678
2120
|
} else {
|
|
1679
2121
|
log.warn(payload, "Slack thread backfill failed; proceeding without it");
|
|
1680
2122
|
}
|
|
2123
|
+
return emptySlackThreadBackfillResult();
|
|
1681
2124
|
}
|
|
1682
2125
|
}
|