@vellumai/assistant 0.3.14 → 0.3.16
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 +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +8 -8
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +5 -4
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +82 -20
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +2 -2
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -40,6 +40,9 @@ export function getSqlite(): Database {
|
|
|
40
40
|
* Useful in migrations and tests that receive the Drizzle instance as a parameter.
|
|
41
41
|
*/
|
|
42
42
|
export function getSqliteFrom(drizzleDb: DrizzleDb): Database {
|
|
43
|
+
// Drizzle's bun:sqlite adapter stores the raw Database as $client but
|
|
44
|
+
// doesn't expose it in its public type. This is the single canonical
|
|
45
|
+
// location for this cast — all callers should use getSqlite/getSqliteFrom.
|
|
43
46
|
return (drizzleDb as unknown as { $client: Database }).$client;
|
|
44
47
|
}
|
|
45
48
|
|
package/src/memory/db-init.ts
CHANGED
|
@@ -17,11 +17,17 @@ import {
|
|
|
17
17
|
createTasksAndWorkItemsTables,
|
|
18
18
|
createWatchersAndLogsTables,
|
|
19
19
|
migrateCallSessionMode,
|
|
20
|
+
migrateFkCascadeRebuilds,
|
|
20
21
|
migrateChannelInboundDeliveredSegments,
|
|
22
|
+
migrateConversationsThreadTypeIndex,
|
|
23
|
+
migrateGuardianActionFollowup,
|
|
21
24
|
migrateGuardianBootstrapToken,
|
|
25
|
+
migrateGuardianVerificationPurpose,
|
|
22
26
|
migrateGuardianVerificationSessions,
|
|
23
27
|
migrateMessagesFtsBackfill,
|
|
24
28
|
migrateReminderRoutingIntent,
|
|
29
|
+
migrateSchemaIndexesAndColumns,
|
|
30
|
+
recoverCrashedMigrations,
|
|
25
31
|
runComplexMigrations,
|
|
26
32
|
runLateMigrations,
|
|
27
33
|
validateMigrationState,
|
|
@@ -33,6 +39,10 @@ export function initializeDb(): void {
|
|
|
33
39
|
// 1. Create core tables (conversations, messages, memory, etc.)
|
|
34
40
|
createCoreTables(database);
|
|
35
41
|
|
|
42
|
+
// 1b. Clear any stalled 'started' checkpoints left by previous crashes
|
|
43
|
+
// so the affected migrations can re-run from scratch.
|
|
44
|
+
recoverCrashedMigrations(database);
|
|
45
|
+
|
|
36
46
|
// 2. Create watchers, logs, entities, FTS, and conversation keys
|
|
37
47
|
createWatchersAndLogsTables(database);
|
|
38
48
|
|
|
@@ -72,6 +82,9 @@ export function initializeDb(): void {
|
|
|
72
82
|
// 11c. Guardian bootstrap token hash column (Telegram deep-link flow)
|
|
73
83
|
migrateGuardianBootstrapToken(database);
|
|
74
84
|
|
|
85
|
+
// 11d. Guardian verification purpose discriminator (guardian vs trusted_contact)
|
|
86
|
+
migrateGuardianVerificationPurpose(database);
|
|
87
|
+
|
|
75
88
|
// 12. Media assets
|
|
76
89
|
createMediaAssetsTables(database);
|
|
77
90
|
|
|
@@ -84,6 +97,12 @@ export function initializeDb(): void {
|
|
|
84
97
|
// 14b. Track per-segment delivery progress for split channel replies
|
|
85
98
|
migrateChannelInboundDeliveredSegments(database);
|
|
86
99
|
|
|
100
|
+
// 14c. Guardian action follow-up lifecycle columns (timeout reason, late answers)
|
|
101
|
+
migrateGuardianActionFollowup(database);
|
|
102
|
+
|
|
103
|
+
// 14d. Index on conversations.thread_type for frequent WHERE filters
|
|
104
|
+
migrateConversationsThreadTypeIndex(database);
|
|
105
|
+
|
|
87
106
|
// 15. Notification system
|
|
88
107
|
createNotificationTables(database);
|
|
89
108
|
|
|
@@ -100,5 +119,11 @@ export function initializeDb(): void {
|
|
|
100
119
|
// 19. Reminder routing metadata (routing_intent + routing_hints_json columns)
|
|
101
120
|
migrateReminderRoutingIntent(database);
|
|
102
121
|
|
|
122
|
+
// 20. Schema indexes, columns, and constraints
|
|
123
|
+
migrateSchemaIndexesAndColumns(database);
|
|
124
|
+
|
|
125
|
+
// 21. Rebuild tables to add ON DELETE CASCADE to FK constraints
|
|
126
|
+
migrateFkCascadeRebuilds(database);
|
|
127
|
+
|
|
103
128
|
validateMigrationState(database);
|
|
104
129
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-specific delivery logic for inbound events.
|
|
3
|
+
*
|
|
4
|
+
* Handles verification reply persistence, per-segment delivery progress
|
|
5
|
+
* tracking, and the deliver-once guard for terminal reply idempotency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { eq } from 'drizzle-orm';
|
|
9
|
+
|
|
10
|
+
import { getDb } from './db.js';
|
|
11
|
+
import { channelInboundEvents } from './schema.js';
|
|
12
|
+
|
|
13
|
+
// ── Pending verification reply helpers ───────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// When a guardian verification succeeds but the confirmation reply fails
|
|
16
|
+
// to deliver, we persist the reply details on the inbound event so that
|
|
17
|
+
// gateway retries (which arrive as duplicates) can re-attempt delivery.
|
|
18
|
+
|
|
19
|
+
export interface PendingVerificationReply {
|
|
20
|
+
__pendingVerificationReply: true;
|
|
21
|
+
chatId: string;
|
|
22
|
+
text: string;
|
|
23
|
+
assistantId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Store a pending verification reply on an inbound event. Called when
|
|
28
|
+
* `deliverChannelReply` fails after challenge consumption so the reply
|
|
29
|
+
* can be retried on subsequent duplicate deliveries.
|
|
30
|
+
*/
|
|
31
|
+
export function storePendingVerificationReply(
|
|
32
|
+
eventId: string,
|
|
33
|
+
reply: Omit<PendingVerificationReply, '__pendingVerificationReply'>,
|
|
34
|
+
): void {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const payload: PendingVerificationReply = { __pendingVerificationReply: true, ...reply };
|
|
37
|
+
db.update(channelInboundEvents)
|
|
38
|
+
.set({ rawPayload: JSON.stringify(payload), updatedAt: Date.now() })
|
|
39
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
40
|
+
.run();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retrieve a pending verification reply for a given event, if one exists.
|
|
45
|
+
*/
|
|
46
|
+
export function getPendingVerificationReply(
|
|
47
|
+
eventId: string,
|
|
48
|
+
): PendingVerificationReply | null {
|
|
49
|
+
const db = getDb();
|
|
50
|
+
const row = db
|
|
51
|
+
.select({ rawPayload: channelInboundEvents.rawPayload })
|
|
52
|
+
.from(channelInboundEvents)
|
|
53
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
54
|
+
.get();
|
|
55
|
+
|
|
56
|
+
if (!row?.rawPayload) return null;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(row.rawPayload);
|
|
59
|
+
if (parsed && parsed.__pendingVerificationReply === true) {
|
|
60
|
+
return parsed as PendingVerificationReply;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clear a pending verification reply after successful delivery.
|
|
70
|
+
*/
|
|
71
|
+
export function clearPendingVerificationReply(eventId: string): void {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
db.update(channelInboundEvents)
|
|
74
|
+
.set({ rawPayload: null, updatedAt: Date.now() })
|
|
75
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
76
|
+
.run();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Per-segment delivery progress ──────────────────────────────────
|
|
80
|
+
//
|
|
81
|
+
// When a split reply (multiple text segments from tool boundaries) fails
|
|
82
|
+
// partway through delivery, we persist how many segments were sent so
|
|
83
|
+
// the retry can resume from where it left off.
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read the number of reply segments already delivered for an event.
|
|
87
|
+
*/
|
|
88
|
+
export function getDeliveredSegmentCount(eventId: string): number {
|
|
89
|
+
const db = getDb();
|
|
90
|
+
const row = db
|
|
91
|
+
.select({ count: channelInboundEvents.deliveredSegmentCount })
|
|
92
|
+
.from(channelInboundEvents)
|
|
93
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
94
|
+
.get();
|
|
95
|
+
return row?.count ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update the delivered segment count after successful delivery of one
|
|
100
|
+
* or more segments. Called incrementally as segments are sent.
|
|
101
|
+
*/
|
|
102
|
+
export function updateDeliveredSegmentCount(eventId: string, count: number): void {
|
|
103
|
+
const db = getDb();
|
|
104
|
+
db.update(channelInboundEvents)
|
|
105
|
+
.set({ deliveredSegmentCount: count, updatedAt: Date.now() })
|
|
106
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
107
|
+
.run();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Deliver-once guard for terminal reply idempotency ────────────────
|
|
111
|
+
//
|
|
112
|
+
// When both the main poll (processChannelMessageWithApprovals) and the
|
|
113
|
+
// post-decision poll (schedulePostDecisionDelivery) race to deliver the
|
|
114
|
+
// final assistant reply for the same run, this guard ensures only one
|
|
115
|
+
// of them actually sends the message. The guard is run-scoped so old
|
|
116
|
+
// assistant messages from previous runs are not affected.
|
|
117
|
+
|
|
118
|
+
/** Map from runId to insertion timestamp (ms). */
|
|
119
|
+
const deliveredRuns = new Map<string, number>();
|
|
120
|
+
|
|
121
|
+
/** TTL for delivery claims — 10 minutes, well beyond the poll max-wait. */
|
|
122
|
+
const CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
123
|
+
|
|
124
|
+
/** Hard cap to bound memory even under sustained high throughput within the TTL window. */
|
|
125
|
+
const MAX_DELIVERED_RUNS = 10_000;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Atomically claim the right to deliver the final reply for a run.
|
|
129
|
+
* Returns `true` if this caller won the claim (and should proceed with
|
|
130
|
+
* delivery). Returns `false` if another caller already claimed it.
|
|
131
|
+
*
|
|
132
|
+
* This is an in-memory guard — sufficient because both racing pollers
|
|
133
|
+
* execute within the same process. The Map is never persisted; on restart
|
|
134
|
+
* there are no in-flight pollers to race.
|
|
135
|
+
*
|
|
136
|
+
* Claims are evicted after CLAIM_TTL_MS. When the hard cap is reached,
|
|
137
|
+
* only TTL-expired entries are evicted — active claims are never removed
|
|
138
|
+
* early, preserving the at-most-once delivery guarantee.
|
|
139
|
+
*/
|
|
140
|
+
export function claimRunDelivery(runId: string): boolean {
|
|
141
|
+
if (deliveredRuns.has(runId)) return false;
|
|
142
|
+
if (deliveredRuns.size >= MAX_DELIVERED_RUNS) {
|
|
143
|
+
// Only evict entries whose TTL has expired. Map iteration order
|
|
144
|
+
// matches insertion order, so oldest entries come first.
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
for (const [id, insertedAt] of deliveredRuns) {
|
|
147
|
+
if (now - insertedAt >= CLAIM_TTL_MS) {
|
|
148
|
+
deliveredRuns.delete(id);
|
|
149
|
+
} else {
|
|
150
|
+
// Remaining entries are newer; stop scanning.
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
deliveredRuns.set(runId, now);
|
|
157
|
+
setTimeout(() => deliveredRuns.delete(runId), CLAIM_TTL_MS);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Reset the deliver-once guard for a run. Used to release a claim when
|
|
163
|
+
* delivery fails (so the other racing poller can retry) and in tests
|
|
164
|
+
* for isolation between test cases.
|
|
165
|
+
*/
|
|
166
|
+
export function resetRunDeliveryClaim(runId: string): void {
|
|
167
|
+
deliveredRuns.delete(runId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear all delivery claims. Used in tests for full isolation.
|
|
172
|
+
*/
|
|
173
|
+
export function resetAllRunDeliveryClaims(): void {
|
|
174
|
+
deliveredRuns.clear();
|
|
175
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core CRUD operations for channel inbound events.
|
|
3
|
+
*
|
|
4
|
+
* Handles recording inbound messages, linking them to internal message IDs,
|
|
5
|
+
* finding messages by source identifiers, and managing raw payload storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { and, desc, eq, isNotNull } from 'drizzle-orm';
|
|
9
|
+
import { v4 as uuid } from 'uuid';
|
|
10
|
+
|
|
11
|
+
import { getConversationByKey, getOrCreateConversation, setConversationKeyIfAbsent } from './conversation-key-store.js';
|
|
12
|
+
import { getDb } from './db.js';
|
|
13
|
+
import { channelInboundEvents, conversations } from './schema.js';
|
|
14
|
+
|
|
15
|
+
export interface InboundResult {
|
|
16
|
+
accepted: boolean;
|
|
17
|
+
eventId: string;
|
|
18
|
+
conversationId: string;
|
|
19
|
+
duplicate: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RecordInboundOptions {
|
|
23
|
+
sourceMessageId?: string;
|
|
24
|
+
assistantId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Record an inbound channel event. Returns `duplicate: true` if this
|
|
29
|
+
* exact (channel, chat, message) combination was already seen.
|
|
30
|
+
*/
|
|
31
|
+
export function recordInbound(
|
|
32
|
+
sourceChannel: string,
|
|
33
|
+
externalChatId: string,
|
|
34
|
+
externalMessageId: string,
|
|
35
|
+
options?: RecordInboundOptions,
|
|
36
|
+
): InboundResult {
|
|
37
|
+
const db = getDb();
|
|
38
|
+
|
|
39
|
+
const existing = db
|
|
40
|
+
.select({
|
|
41
|
+
id: channelInboundEvents.id,
|
|
42
|
+
conversationId: channelInboundEvents.conversationId,
|
|
43
|
+
})
|
|
44
|
+
.from(channelInboundEvents)
|
|
45
|
+
.where(
|
|
46
|
+
and(
|
|
47
|
+
eq(channelInboundEvents.sourceChannel, sourceChannel),
|
|
48
|
+
eq(channelInboundEvents.externalChatId, externalChatId),
|
|
49
|
+
eq(channelInboundEvents.externalMessageId, externalMessageId),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
.get();
|
|
53
|
+
|
|
54
|
+
if (existing) {
|
|
55
|
+
return {
|
|
56
|
+
accepted: true,
|
|
57
|
+
eventId: existing.id,
|
|
58
|
+
conversationId: existing.conversationId,
|
|
59
|
+
duplicate: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const assistantId = options?.assistantId;
|
|
64
|
+
const legacyKey = `${sourceChannel}:${externalChatId}`;
|
|
65
|
+
const scopedKey = assistantId ? `asst:${assistantId}:${sourceChannel}:${externalChatId}` : legacyKey;
|
|
66
|
+
|
|
67
|
+
// Resolve conversation mapping with assistant-scoped keying:
|
|
68
|
+
// 1. If scoped key exists, use it directly.
|
|
69
|
+
// 2. If assistantId is "self" and legacy key exists, reuse the legacy
|
|
70
|
+
// conversation and create a scoped alias to prevent future bleed.
|
|
71
|
+
// 3. Otherwise, create/get conversation from the scoped key.
|
|
72
|
+
let mapping: { conversationId: string; created: boolean };
|
|
73
|
+
const scopedMapping = assistantId ? getConversationByKey(scopedKey) : null;
|
|
74
|
+
if (scopedMapping) {
|
|
75
|
+
mapping = { conversationId: scopedMapping.conversationId, created: false };
|
|
76
|
+
} else if (assistantId === 'self') {
|
|
77
|
+
const legacyMapping = getConversationByKey(legacyKey);
|
|
78
|
+
if (legacyMapping) {
|
|
79
|
+
mapping = { conversationId: legacyMapping.conversationId, created: false };
|
|
80
|
+
setConversationKeyIfAbsent(scopedKey, legacyMapping.conversationId);
|
|
81
|
+
} else {
|
|
82
|
+
mapping = getOrCreateConversation(scopedKey);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
mapping = getOrCreateConversation(scopedKey);
|
|
86
|
+
}
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const eventId = uuid();
|
|
89
|
+
|
|
90
|
+
db.transaction((tx) => {
|
|
91
|
+
tx.update(conversations)
|
|
92
|
+
.set({ updatedAt: now })
|
|
93
|
+
.where(eq(conversations.id, mapping.conversationId))
|
|
94
|
+
.run();
|
|
95
|
+
tx.insert(channelInboundEvents)
|
|
96
|
+
.values({
|
|
97
|
+
id: eventId,
|
|
98
|
+
sourceChannel,
|
|
99
|
+
externalChatId,
|
|
100
|
+
externalMessageId,
|
|
101
|
+
sourceMessageId: options?.sourceMessageId ?? null,
|
|
102
|
+
conversationId: mapping.conversationId,
|
|
103
|
+
deliveryStatus: 'pending',
|
|
104
|
+
createdAt: now,
|
|
105
|
+
updatedAt: now,
|
|
106
|
+
})
|
|
107
|
+
.run();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
accepted: true,
|
|
112
|
+
eventId,
|
|
113
|
+
conversationId: mapping.conversationId,
|
|
114
|
+
duplicate: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Link an inbound event to the user message it created, so edits can
|
|
120
|
+
* later find the correct message by source_message_id -> message_id.
|
|
121
|
+
*/
|
|
122
|
+
export function linkMessage(eventId: string, messageId: string): void {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
db.update(channelInboundEvents)
|
|
125
|
+
.set({ messageId, updatedAt: Date.now() })
|
|
126
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
127
|
+
.run();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find the message ID linked to the original inbound event for a given
|
|
132
|
+
* platform-level message identifier (e.g. Telegram message_id).
|
|
133
|
+
*/
|
|
134
|
+
export function findMessageBySourceId(
|
|
135
|
+
sourceChannel: string,
|
|
136
|
+
externalChatId: string,
|
|
137
|
+
sourceMessageId: string,
|
|
138
|
+
): { messageId: string; conversationId: string } | null {
|
|
139
|
+
const db = getDb();
|
|
140
|
+
const row = db
|
|
141
|
+
.select({
|
|
142
|
+
messageId: channelInboundEvents.messageId,
|
|
143
|
+
conversationId: channelInboundEvents.conversationId,
|
|
144
|
+
})
|
|
145
|
+
.from(channelInboundEvents)
|
|
146
|
+
.where(
|
|
147
|
+
and(
|
|
148
|
+
eq(channelInboundEvents.sourceChannel, sourceChannel),
|
|
149
|
+
eq(channelInboundEvents.externalChatId, externalChatId),
|
|
150
|
+
eq(channelInboundEvents.sourceMessageId, sourceMessageId),
|
|
151
|
+
isNotNull(channelInboundEvents.messageId),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
.get();
|
|
155
|
+
|
|
156
|
+
if (!row || !row.messageId) return null;
|
|
157
|
+
return { messageId: row.messageId, conversationId: row.conversationId };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Store the raw request payload on an inbound event so it can be
|
|
162
|
+
* replayed later if processing fails.
|
|
163
|
+
*/
|
|
164
|
+
export function storePayload(eventId: string, payload: Record<string, unknown>): void {
|
|
165
|
+
const db = getDb();
|
|
166
|
+
db.update(channelInboundEvents)
|
|
167
|
+
.set({ rawPayload: JSON.stringify(payload), updatedAt: Date.now() })
|
|
168
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
169
|
+
.run();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clear a previously stored payload. Used when the ingress check
|
|
174
|
+
* detects secret-bearing content — the payload must not remain on disk.
|
|
175
|
+
*/
|
|
176
|
+
export function clearPayload(eventId: string): void {
|
|
177
|
+
const db = getDb();
|
|
178
|
+
db.update(channelInboundEvents)
|
|
179
|
+
.set({ rawPayload: null, updatedAt: Date.now() })
|
|
180
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
181
|
+
.run();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Retrieve the stored raw payload for a given conversation's most recent
|
|
186
|
+
* inbound event. Used by the escalation decide flow to recover the
|
|
187
|
+
* original message content after an approve/deny decision.
|
|
188
|
+
*/
|
|
189
|
+
export function getLatestStoredPayload(conversationId: string): Record<string, unknown> | null {
|
|
190
|
+
const db = getDb();
|
|
191
|
+
const row = db
|
|
192
|
+
.select({
|
|
193
|
+
rawPayload: channelInboundEvents.rawPayload,
|
|
194
|
+
})
|
|
195
|
+
.from(channelInboundEvents)
|
|
196
|
+
.where(
|
|
197
|
+
and(
|
|
198
|
+
eq(channelInboundEvents.conversationId, conversationId),
|
|
199
|
+
isNotNull(channelInboundEvents.rawPayload),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
.orderBy(desc(channelInboundEvents.createdAt))
|
|
203
|
+
.get();
|
|
204
|
+
|
|
205
|
+
if (!row?.rawPayload) return null;
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(row.rawPayload) as Record<string, unknown>;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processing status tracking and dead-letter queue management for
|
|
3
|
+
* channel inbound events.
|
|
4
|
+
*
|
|
5
|
+
* Handles marking events as processed/failed/dead-lettered, fetching
|
|
6
|
+
* retryable and dead-lettered events, and replaying dead letters.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { and, eq, lte } from 'drizzle-orm';
|
|
10
|
+
|
|
11
|
+
import { getDb } from './db.js';
|
|
12
|
+
import {
|
|
13
|
+
classifyError,
|
|
14
|
+
RETRY_MAX_ATTEMPTS,
|
|
15
|
+
retryDelayForAttempt,
|
|
16
|
+
} from './job-utils.js';
|
|
17
|
+
import { channelInboundEvents } from './schema.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Acknowledge delivery of an outbound message for a channel event.
|
|
21
|
+
*/
|
|
22
|
+
export function acknowledgeDelivery(
|
|
23
|
+
sourceChannel: string,
|
|
24
|
+
externalChatId: string,
|
|
25
|
+
externalMessageId: string,
|
|
26
|
+
): boolean {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
|
|
30
|
+
const existing = db
|
|
31
|
+
.select({ id: channelInboundEvents.id })
|
|
32
|
+
.from(channelInboundEvents)
|
|
33
|
+
.where(
|
|
34
|
+
and(
|
|
35
|
+
eq(channelInboundEvents.sourceChannel, sourceChannel),
|
|
36
|
+
eq(channelInboundEvents.externalChatId, externalChatId),
|
|
37
|
+
eq(channelInboundEvents.externalMessageId, externalMessageId),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
.get();
|
|
41
|
+
|
|
42
|
+
if (!existing) return false;
|
|
43
|
+
|
|
44
|
+
db.update(channelInboundEvents)
|
|
45
|
+
.set({
|
|
46
|
+
deliveryStatus: 'delivered',
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
})
|
|
49
|
+
.where(eq(channelInboundEvents.id, existing.id))
|
|
50
|
+
.run();
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mark an event as successfully processed. */
|
|
56
|
+
export function markProcessed(eventId: string): void {
|
|
57
|
+
const db = getDb();
|
|
58
|
+
db.update(channelInboundEvents)
|
|
59
|
+
.set({ processingStatus: 'processed', updatedAt: Date.now() })
|
|
60
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
61
|
+
.run();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record a processing failure. Classifies the error to decide whether
|
|
66
|
+
* the event should be retried (status='failed') or dead-lettered
|
|
67
|
+
* (status='dead_letter') when the error is fatal or max attempts
|
|
68
|
+
* are exhausted.
|
|
69
|
+
*/
|
|
70
|
+
export function recordProcessingFailure(eventId: string, err: unknown): void {
|
|
71
|
+
const db = getDb();
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
|
|
74
|
+
const row = db
|
|
75
|
+
.select({ attempts: channelInboundEvents.processingAttempts })
|
|
76
|
+
.from(channelInboundEvents)
|
|
77
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
78
|
+
.get();
|
|
79
|
+
|
|
80
|
+
const attempts = (row?.attempts ?? 0) + 1;
|
|
81
|
+
const category = classifyError(err);
|
|
82
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
|
|
84
|
+
if (category === 'fatal' || attempts >= RETRY_MAX_ATTEMPTS) {
|
|
85
|
+
db.update(channelInboundEvents)
|
|
86
|
+
.set({
|
|
87
|
+
processingStatus: 'dead_letter',
|
|
88
|
+
processingAttempts: attempts,
|
|
89
|
+
lastProcessingError: errorMsg,
|
|
90
|
+
retryAfter: null,
|
|
91
|
+
updatedAt: now,
|
|
92
|
+
})
|
|
93
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
94
|
+
.run();
|
|
95
|
+
} else {
|
|
96
|
+
const delay = retryDelayForAttempt(attempts);
|
|
97
|
+
db.update(channelInboundEvents)
|
|
98
|
+
.set({
|
|
99
|
+
processingStatus: 'failed',
|
|
100
|
+
processingAttempts: attempts,
|
|
101
|
+
lastProcessingError: errorMsg,
|
|
102
|
+
retryAfter: now + delay,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
})
|
|
105
|
+
.where(eq(channelInboundEvents.id, eventId))
|
|
106
|
+
.run();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Fetch events eligible for automatic retry (failed + past their backoff). */
|
|
111
|
+
export function getRetryableEvents(limit = 20): Array<{
|
|
112
|
+
id: string;
|
|
113
|
+
conversationId: string;
|
|
114
|
+
processingAttempts: number;
|
|
115
|
+
rawPayload: string | null;
|
|
116
|
+
}> {
|
|
117
|
+
const db = getDb();
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
return db
|
|
120
|
+
.select({
|
|
121
|
+
id: channelInboundEvents.id,
|
|
122
|
+
conversationId: channelInboundEvents.conversationId,
|
|
123
|
+
processingAttempts: channelInboundEvents.processingAttempts,
|
|
124
|
+
rawPayload: channelInboundEvents.rawPayload,
|
|
125
|
+
})
|
|
126
|
+
.from(channelInboundEvents)
|
|
127
|
+
.where(
|
|
128
|
+
and(
|
|
129
|
+
eq(channelInboundEvents.processingStatus, 'failed'),
|
|
130
|
+
lte(channelInboundEvents.retryAfter, now),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
.limit(limit)
|
|
134
|
+
.all();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Fetch dead-lettered events. */
|
|
138
|
+
export function getDeadLetterEvents(): Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
sourceChannel: string;
|
|
141
|
+
externalChatId: string;
|
|
142
|
+
externalMessageId: string;
|
|
143
|
+
conversationId: string;
|
|
144
|
+
processingAttempts: number;
|
|
145
|
+
lastProcessingError: string | null;
|
|
146
|
+
createdAt: number;
|
|
147
|
+
}> {
|
|
148
|
+
const db = getDb();
|
|
149
|
+
return db
|
|
150
|
+
.select({
|
|
151
|
+
id: channelInboundEvents.id,
|
|
152
|
+
sourceChannel: channelInboundEvents.sourceChannel,
|
|
153
|
+
externalChatId: channelInboundEvents.externalChatId,
|
|
154
|
+
externalMessageId: channelInboundEvents.externalMessageId,
|
|
155
|
+
conversationId: channelInboundEvents.conversationId,
|
|
156
|
+
processingAttempts: channelInboundEvents.processingAttempts,
|
|
157
|
+
lastProcessingError: channelInboundEvents.lastProcessingError,
|
|
158
|
+
createdAt: channelInboundEvents.createdAt,
|
|
159
|
+
})
|
|
160
|
+
.from(channelInboundEvents)
|
|
161
|
+
.where(eq(channelInboundEvents.processingStatus, 'dead_letter'))
|
|
162
|
+
.all();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reset dead-lettered events back to 'failed' so the sweep can retry
|
|
167
|
+
* them. Resets attempt counter and sets an immediate retry_after.
|
|
168
|
+
*/
|
|
169
|
+
export function replayDeadLetters(eventIds: string[]): number {
|
|
170
|
+
const db = getDb();
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
let count = 0;
|
|
173
|
+
for (const id of eventIds) {
|
|
174
|
+
const existing = db
|
|
175
|
+
.select({ id: channelInboundEvents.id })
|
|
176
|
+
.from(channelInboundEvents)
|
|
177
|
+
.where(
|
|
178
|
+
and(
|
|
179
|
+
eq(channelInboundEvents.id, id),
|
|
180
|
+
eq(channelInboundEvents.processingStatus, 'dead_letter'),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
.get();
|
|
184
|
+
if (!existing) continue;
|
|
185
|
+
|
|
186
|
+
db.update(channelInboundEvents)
|
|
187
|
+
.set({
|
|
188
|
+
processingStatus: 'failed',
|
|
189
|
+
processingAttempts: 0,
|
|
190
|
+
lastProcessingError: null,
|
|
191
|
+
retryAfter: now,
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
})
|
|
194
|
+
.where(eq(channelInboundEvents.id, id))
|
|
195
|
+
.run();
|
|
196
|
+
count++;
|
|
197
|
+
}
|
|
198
|
+
return count;
|
|
199
|
+
}
|