@vellumai/assistant 0.3.15 → 0.3.18

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.
Files changed (306) hide show
  1. package/ARCHITECTURE.md +211 -12
  2. package/Dockerfile +1 -1
  3. package/README.md +11 -5
  4. package/docs/architecture/http-token-refresh.md +274 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +328 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +19 -15
  22. package/src/__tests__/checker.test.ts +103 -48
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +356 -0
  25. package/src/__tests__/conversation-pairing.test.ts +127 -27
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-store.test.ts +182 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  40. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  41. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  42. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  43. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  44. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  45. package/src/__tests__/hooks-runner.test.ts +13 -4
  46. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  47. package/src/__tests__/intent-routing.test.ts +14 -0
  48. package/src/__tests__/ipc-snapshot.test.ts +23 -5
  49. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  50. package/src/__tests__/memory-regressions.test.ts +16 -12
  51. package/src/__tests__/non-member-access-request.test.ts +281 -0
  52. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  53. package/src/__tests__/notification-decision-strategy.test.ts +138 -1
  54. package/src/__tests__/notification-deep-link.test.ts +44 -1
  55. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  56. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  57. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  58. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  59. package/src/__tests__/recording-intent.test.ts +1 -0
  60. package/src/__tests__/recording-state-machine.test.ts +328 -17
  61. package/src/__tests__/registry.test.ts +17 -8
  62. package/src/__tests__/relay-server.test.ts +105 -0
  63. package/src/__tests__/reminder.test.ts +13 -0
  64. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  65. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  66. package/src/__tests__/server-history-render.test.ts +8 -8
  67. package/src/__tests__/session-agent-loop.test.ts +1 -0
  68. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -0
  70. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  71. package/src/__tests__/slack-channel-config.test.ts +230 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  73. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  74. package/src/__tests__/system-prompt.test.ts +43 -0
  75. package/src/__tests__/task-management-tools.test.ts +3 -3
  76. package/src/__tests__/task-tools.test.ts +3 -3
  77. package/src/__tests__/trust-store.test.ts +38 -22
  78. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
  79. package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
  80. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  81. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  82. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  83. package/src/__tests__/update-bulletin.test.ts +323 -0
  84. package/src/__tests__/update-template-contract.test.ts +24 -0
  85. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  86. package/src/agent/loop.ts +2 -2
  87. package/src/amazon/client.ts +2 -3
  88. package/src/calls/call-controller.ts +241 -39
  89. package/src/calls/call-conversation-messages.ts +2 -2
  90. package/src/calls/call-domain.ts +10 -3
  91. package/src/calls/call-pointer-messages.ts +17 -5
  92. package/src/calls/guardian-action-sweep.ts +77 -36
  93. package/src/calls/guardian-dispatch.ts +8 -0
  94. package/src/calls/relay-server.ts +51 -12
  95. package/src/calls/twilio-routes.ts +3 -1
  96. package/src/calls/types.ts +1 -1
  97. package/src/calls/voice-session-bridge.ts +8 -6
  98. package/src/cli/core-commands.ts +43 -3
  99. package/src/cli/map.ts +8 -5
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  101. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  102. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  103. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  104. package/src/config/computer-use-prompt.ts +1 -0
  105. package/src/config/core-schema.ts +16 -0
  106. package/src/config/env-registry.ts +1 -0
  107. package/src/config/env.ts +16 -1
  108. package/src/config/memory-schema.ts +5 -0
  109. package/src/config/schema.ts +4 -0
  110. package/src/config/system-prompt.ts +69 -2
  111. package/src/config/templates/BOOTSTRAP.md +1 -1
  112. package/src/config/templates/IDENTITY.md +8 -4
  113. package/src/config/templates/SOUL.md +14 -0
  114. package/src/config/templates/UPDATES.md +15 -0
  115. package/src/config/templates/USER.md +5 -1
  116. package/src/config/types.ts +1 -0
  117. package/src/config/update-bulletin-format.ts +54 -0
  118. package/src/config/update-bulletin-state.ts +49 -0
  119. package/src/config/update-bulletin-template-path.ts +6 -0
  120. package/src/config/update-bulletin.ts +97 -0
  121. package/src/config/vellum-skills/catalog.json +6 -0
  122. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  123. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  124. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  125. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  126. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  127. package/src/context/window-manager.ts +43 -3
  128. package/src/daemon/config-watcher.ts +4 -2
  129. package/src/daemon/connection-policy.ts +21 -1
  130. package/src/daemon/daemon-control.ts +219 -8
  131. package/src/daemon/date-context.ts +174 -1
  132. package/src/daemon/guardian-action-generators.ts +175 -0
  133. package/src/daemon/guardian-verification-intent.ts +120 -0
  134. package/src/daemon/handlers/apps.ts +1 -3
  135. package/src/daemon/handlers/config-channels.ts +2 -2
  136. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  137. package/src/daemon/handlers/config-inbox.ts +55 -159
  138. package/src/daemon/handlers/config-ingress.ts +1 -1
  139. package/src/daemon/handlers/config-integrations.ts +1 -1
  140. package/src/daemon/handlers/config-platform.ts +1 -1
  141. package/src/daemon/handlers/config-scheduling.ts +2 -2
  142. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  143. package/src/daemon/handlers/config-telegram.ts +1 -1
  144. package/src/daemon/handlers/config-twilio.ts +1 -1
  145. package/src/daemon/handlers/config-voice.ts +100 -0
  146. package/src/daemon/handlers/config.ts +3 -0
  147. package/src/daemon/handlers/identity.ts +45 -25
  148. package/src/daemon/handlers/misc.ts +83 -5
  149. package/src/daemon/handlers/navigate-settings.ts +27 -0
  150. package/src/daemon/handlers/recording.ts +270 -144
  151. package/src/daemon/handlers/sessions.ts +100 -17
  152. package/src/daemon/handlers/subagents.ts +3 -3
  153. package/src/daemon/handlers/work-items.ts +10 -7
  154. package/src/daemon/ipc-contract/integrations.ts +9 -1
  155. package/src/daemon/ipc-contract/messages.ts +4 -0
  156. package/src/daemon/ipc-contract/sessions.ts +1 -1
  157. package/src/daemon/ipc-contract/settings.ts +26 -0
  158. package/src/daemon/ipc-contract/shared.ts +2 -0
  159. package/src/daemon/ipc-contract/work-items.ts +1 -7
  160. package/src/daemon/ipc-contract/workspace.ts +12 -1
  161. package/src/daemon/ipc-contract-inventory.json +6 -1
  162. package/src/daemon/ipc-contract.ts +5 -1
  163. package/src/daemon/lifecycle.ts +314 -266
  164. package/src/daemon/recording-intent.ts +0 -41
  165. package/src/daemon/response-tier.ts +2 -2
  166. package/src/daemon/server.ts +31 -9
  167. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  168. package/src/daemon/session-agent-loop.ts +15 -8
  169. package/src/daemon/session-history.ts +3 -2
  170. package/src/daemon/session-media-retry.ts +3 -0
  171. package/src/daemon/session-messaging.ts +38 -4
  172. package/src/daemon/session-notifiers.ts +2 -2
  173. package/src/daemon/session-process.ts +546 -59
  174. package/src/daemon/session-queue-manager.ts +2 -0
  175. package/src/daemon/session-runtime-assembly.ts +39 -0
  176. package/src/daemon/session-skill-tools.ts +13 -4
  177. package/src/daemon/session-tool-setup.ts +5 -6
  178. package/src/daemon/session.ts +19 -8
  179. package/src/daemon/tls-certs.ts +60 -13
  180. package/src/daemon/tool-side-effects.ts +13 -5
  181. package/src/gallery/default-gallery.ts +32 -9
  182. package/src/influencer/client.ts +2 -1
  183. package/src/memory/channel-delivery-store.ts +35 -567
  184. package/src/memory/channel-guardian-store.ts +63 -1317
  185. package/src/memory/conflict-store.ts +4 -4
  186. package/src/memory/conversation-attention-store.ts +0 -3
  187. package/src/memory/conversation-crud.ts +668 -0
  188. package/src/memory/conversation-queries.ts +361 -0
  189. package/src/memory/conversation-store.ts +44 -983
  190. package/src/memory/db-connection.ts +3 -0
  191. package/src/memory/db-init.ts +33 -0
  192. package/src/memory/delivery-channels.ts +175 -0
  193. package/src/memory/delivery-crud.ts +211 -0
  194. package/src/memory/delivery-status.ts +199 -0
  195. package/src/memory/embedding-backend.ts +70 -4
  196. package/src/memory/embedding-local.ts +12 -2
  197. package/src/memory/entity-extractor.ts +3 -8
  198. package/src/memory/fts-reconciler.ts +136 -0
  199. package/src/memory/guardian-action-store.ts +418 -5
  200. package/src/memory/guardian-approvals.ts +569 -0
  201. package/src/memory/guardian-bindings.ts +130 -0
  202. package/src/memory/guardian-rate-limits.ts +196 -0
  203. package/src/memory/guardian-verification.ts +521 -0
  204. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  205. package/src/memory/job-utils.ts +8 -5
  206. package/src/memory/jobs-store.ts +66 -6
  207. package/src/memory/jobs-worker.ts +23 -1
  208. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  209. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  210. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  211. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  212. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  213. package/src/memory/migrations/100-core-tables.ts +1 -1
  214. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  215. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  216. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  217. package/src/memory/migrations/113-late-migrations.ts +1 -1
  218. package/src/memory/migrations/116-messages-fts.ts +13 -0
  219. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  220. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  221. package/src/memory/migrations/index.ts +10 -3
  222. package/src/memory/migrations/validate-migration-state.ts +114 -15
  223. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  224. package/src/memory/retriever.ts +46 -13
  225. package/src/memory/schema-migration.ts +4 -0
  226. package/src/memory/schema.ts +31 -8
  227. package/src/memory/search/semantic.ts +8 -90
  228. package/src/notifications/README.md +159 -18
  229. package/src/notifications/broadcaster.ts +69 -33
  230. package/src/notifications/conversation-pairing.ts +99 -21
  231. package/src/notifications/decision-engine.ts +176 -8
  232. package/src/notifications/deliveries-store.ts +39 -8
  233. package/src/notifications/emit-signal.ts +1 -0
  234. package/src/notifications/preferences-store.ts +7 -7
  235. package/src/notifications/thread-candidates.ts +269 -0
  236. package/src/notifications/types.ts +19 -0
  237. package/src/permissions/checker.ts +1 -16
  238. package/src/permissions/defaults.ts +25 -5
  239. package/src/permissions/prompter.ts +17 -0
  240. package/src/permissions/trust-store.ts +2 -0
  241. package/src/providers/failover.ts +19 -0
  242. package/src/providers/registry.ts +46 -1
  243. package/src/runtime/approval-message-composer.ts +1 -1
  244. package/src/runtime/channel-guardian-service.ts +15 -3
  245. package/src/runtime/channel-retry-sweep.ts +7 -2
  246. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  247. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  248. package/src/runtime/guardian-action-message-composer.ts +245 -0
  249. package/src/runtime/guardian-outbound-actions.ts +26 -6
  250. package/src/runtime/guardian-verification-templates.ts +15 -9
  251. package/src/runtime/http-errors.ts +93 -0
  252. package/src/runtime/http-server.ts +133 -44
  253. package/src/runtime/http-types.ts +53 -0
  254. package/src/runtime/ingress-service.ts +237 -0
  255. package/src/runtime/middleware/error-handler.ts +4 -3
  256. package/src/runtime/middleware/rate-limiter.ts +160 -0
  257. package/src/runtime/middleware/request-logger.ts +71 -0
  258. package/src/runtime/middleware/twilio-validation.ts +7 -6
  259. package/src/runtime/pending-interactions.ts +12 -0
  260. package/src/runtime/routes/access-request-decision.ts +215 -0
  261. package/src/runtime/routes/app-routes.ts +25 -18
  262. package/src/runtime/routes/approval-routes.ts +18 -47
  263. package/src/runtime/routes/attachment-routes.ts +15 -41
  264. package/src/runtime/routes/call-routes.ts +20 -20
  265. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  266. package/src/runtime/routes/contact-routes.ts +4 -9
  267. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  268. package/src/runtime/routes/conversation-routes.ts +26 -57
  269. package/src/runtime/routes/debug-routes.ts +71 -0
  270. package/src/runtime/routes/events-routes.ts +3 -2
  271. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  272. package/src/runtime/routes/identity-routes.ts +14 -10
  273. package/src/runtime/routes/inbound-conversation.ts +3 -2
  274. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  275. package/src/runtime/routes/ingress-routes.ts +174 -0
  276. package/src/runtime/routes/integration-routes.ts +78 -16
  277. package/src/runtime/routes/pairing-routes.ts +11 -10
  278. package/src/runtime/routes/secret-routes.ts +10 -18
  279. package/src/runtime/verification-rate-limiter.ts +83 -0
  280. package/src/schedule/schedule-store.ts +13 -1
  281. package/src/schedule/scheduler.ts +1 -1
  282. package/src/security/secret-ingress.ts +5 -2
  283. package/src/security/secret-scanner.ts +72 -6
  284. package/src/subagent/manager.ts +6 -4
  285. package/src/swarm/plan-validator.ts +4 -1
  286. package/src/tasks/task-runner.ts +3 -1
  287. package/src/tools/browser/api-map.ts +9 -6
  288. package/src/tools/calls/call-start.ts +20 -0
  289. package/src/tools/executor.ts +50 -568
  290. package/src/tools/permission-checker.ts +271 -0
  291. package/src/tools/registry.ts +14 -6
  292. package/src/tools/reminder/reminder-store.ts +7 -7
  293. package/src/tools/reminder/reminder.ts +6 -3
  294. package/src/tools/secret-detection-handler.ts +301 -0
  295. package/src/tools/subagent/message.ts +1 -1
  296. package/src/tools/system/voice-config.ts +62 -0
  297. package/src/tools/tasks/index.ts +3 -3
  298. package/src/tools/tasks/work-item-list.ts +3 -3
  299. package/src/tools/tasks/work-item-update.ts +4 -5
  300. package/src/tools/tool-approval-handler.ts +192 -0
  301. package/src/tools/tool-manifest.ts +2 -0
  302. package/src/version.ts +29 -2
  303. package/src/watcher/watcher-store.ts +9 -9
  304. package/src/work-items/work-item-runner.ts +9 -6
  305. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  306. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -1,570 +1,38 @@
1
1
  /**
2
2
  * Channel inbound idempotency + delivery state tracking.
3
3
  *
4
- * Ensures duplicate channel messages (e.g. Telegram webhook retries)
5
- * don't produce duplicate replies. Tracks delivery acknowledgement
6
- * so the runtime owns the full lifecycle instead of web Postgres.
7
- *
8
- * Dead-letter support: when processMessage fails, the event is marked
9
- * with processing_status='failed' (retryable) or 'dead_letter' (fatal
10
- * or max attempts exceeded). A periodic sweep retries failed events,
11
- * and a replay endpoint allows manual recovery of dead-lettered ones.
12
- */
13
-
14
- import { and, desc, eq, isNotNull,lte } from 'drizzle-orm';
15
- import { v4 as uuid } from 'uuid';
16
-
17
- import { getConversationByKey, getOrCreateConversation, setConversationKeyIfAbsent } from './conversation-key-store.js';
18
- import { getDb } from './db.js';
19
- import {
20
- classifyError,
21
- RETRY_MAX_ATTEMPTS,
22
- retryDelayForAttempt,
23
- } from './job-utils.js';
24
- import { channelInboundEvents, conversations } from './schema.js';
25
-
26
- export interface InboundResult {
27
- accepted: boolean;
28
- eventId: string;
29
- conversationId: string;
30
- duplicate: boolean;
31
- }
32
-
33
- export interface RecordInboundOptions {
34
- sourceMessageId?: string;
35
- assistantId?: string;
36
- }
37
-
38
- /**
39
- * Record an inbound channel event. Returns `duplicate: true` if this
40
- * exact (channel, chat, message) combination was already seen.
41
- */
42
- export function recordInbound(
43
- sourceChannel: string,
44
- externalChatId: string,
45
- externalMessageId: string,
46
- options?: RecordInboundOptions,
47
- ): InboundResult {
48
- const db = getDb();
49
-
50
- const existing = db
51
- .select({
52
- id: channelInboundEvents.id,
53
- conversationId: channelInboundEvents.conversationId,
54
- })
55
- .from(channelInboundEvents)
56
- .where(
57
- and(
58
- eq(channelInboundEvents.sourceChannel, sourceChannel),
59
- eq(channelInboundEvents.externalChatId, externalChatId),
60
- eq(channelInboundEvents.externalMessageId, externalMessageId),
61
- ),
62
- )
63
- .get();
64
-
65
- if (existing) {
66
- return {
67
- accepted: true,
68
- eventId: existing.id,
69
- conversationId: existing.conversationId,
70
- duplicate: true,
71
- };
72
- }
73
-
74
- const assistantId = options?.assistantId;
75
- const legacyKey = `${sourceChannel}:${externalChatId}`;
76
- const scopedKey = assistantId ? `asst:${assistantId}:${sourceChannel}:${externalChatId}` : legacyKey;
77
-
78
- // Resolve conversation mapping with assistant-scoped keying:
79
- // 1. If scoped key exists, use it directly.
80
- // 2. If assistantId is "self" and legacy key exists, reuse the legacy
81
- // conversation and create a scoped alias to prevent future bleed.
82
- // 3. Otherwise, create/get conversation from the scoped key.
83
- let mapping: { conversationId: string; created: boolean };
84
- const scopedMapping = assistantId ? getConversationByKey(scopedKey) : null;
85
- if (scopedMapping) {
86
- mapping = { conversationId: scopedMapping.conversationId, created: false };
87
- } else if (assistantId === 'self') {
88
- const legacyMapping = getConversationByKey(legacyKey);
89
- if (legacyMapping) {
90
- mapping = { conversationId: legacyMapping.conversationId, created: false };
91
- setConversationKeyIfAbsent(scopedKey, legacyMapping.conversationId);
92
- } else {
93
- mapping = getOrCreateConversation(scopedKey);
94
- }
95
- } else {
96
- mapping = getOrCreateConversation(scopedKey);
97
- }
98
- const now = Date.now();
99
- const eventId = uuid();
100
-
101
- db.transaction((tx) => {
102
- tx.update(conversations)
103
- .set({ updatedAt: now })
104
- .where(eq(conversations.id, mapping.conversationId))
105
- .run();
106
- tx.insert(channelInboundEvents)
107
- .values({
108
- id: eventId,
109
- sourceChannel,
110
- externalChatId,
111
- externalMessageId,
112
- sourceMessageId: options?.sourceMessageId ?? null,
113
- conversationId: mapping.conversationId,
114
- deliveryStatus: 'pending',
115
- createdAt: now,
116
- updatedAt: now,
117
- })
118
- .run();
119
- });
120
-
121
- return {
122
- accepted: true,
123
- eventId,
124
- conversationId: mapping.conversationId,
125
- duplicate: false,
126
- };
127
- }
128
-
129
- /**
130
- * Link an inbound event to the user message it created, so edits can
131
- * later find the correct message by source_message_id → message_id.
132
- */
133
- export function linkMessage(eventId: string, messageId: string): void {
134
- const db = getDb();
135
- db.update(channelInboundEvents)
136
- .set({ messageId, updatedAt: Date.now() })
137
- .where(eq(channelInboundEvents.id, eventId))
138
- .run();
139
- }
140
-
141
- /**
142
- * Find the message ID linked to the original inbound event for a given
143
- * platform-level message identifier (e.g. Telegram message_id).
144
- */
145
- export function findMessageBySourceId(
146
- sourceChannel: string,
147
- externalChatId: string,
148
- sourceMessageId: string,
149
- ): { messageId: string; conversationId: string } | null {
150
- const db = getDb();
151
- const row = db
152
- .select({
153
- messageId: channelInboundEvents.messageId,
154
- conversationId: channelInboundEvents.conversationId,
155
- })
156
- .from(channelInboundEvents)
157
- .where(
158
- and(
159
- eq(channelInboundEvents.sourceChannel, sourceChannel),
160
- eq(channelInboundEvents.externalChatId, externalChatId),
161
- eq(channelInboundEvents.sourceMessageId, sourceMessageId),
162
- isNotNull(channelInboundEvents.messageId),
163
- ),
164
- )
165
- .get();
166
-
167
- if (!row || !row.messageId) return null;
168
- return { messageId: row.messageId, conversationId: row.conversationId };
169
- }
170
-
171
- /**
172
- * Acknowledge delivery of an outbound message for a channel event.
173
- */
174
- export function acknowledgeDelivery(
175
- sourceChannel: string,
176
- externalChatId: string,
177
- externalMessageId: string,
178
- ): boolean {
179
- const db = getDb();
180
- const now = Date.now();
181
-
182
- const existing = db
183
- .select({ id: channelInboundEvents.id })
184
- .from(channelInboundEvents)
185
- .where(
186
- and(
187
- eq(channelInboundEvents.sourceChannel, sourceChannel),
188
- eq(channelInboundEvents.externalChatId, externalChatId),
189
- eq(channelInboundEvents.externalMessageId, externalMessageId),
190
- ),
191
- )
192
- .get();
193
-
194
- if (!existing) return false;
195
-
196
- db.update(channelInboundEvents)
197
- .set({
198
- deliveryStatus: 'delivered',
199
- updatedAt: now,
200
- })
201
- .where(eq(channelInboundEvents.id, existing.id))
202
- .run();
203
-
204
- return true;
205
- }
206
-
207
- // ── Pending verification reply helpers ───────────────────────────────
208
- //
209
- // When a guardian verification succeeds but the confirmation reply fails
210
- // to deliver, we persist the reply details on the inbound event so that
211
- // gateway retries (which arrive as duplicates) can re-attempt delivery.
212
-
213
- export interface PendingVerificationReply {
214
- __pendingVerificationReply: true;
215
- chatId: string;
216
- text: string;
217
- assistantId: string;
218
- }
219
-
220
- /**
221
- * Store a pending verification reply on an inbound event. Called when
222
- * `deliverChannelReply` fails after challenge consumption so the reply
223
- * can be retried on subsequent duplicate deliveries.
224
- */
225
- export function storePendingVerificationReply(
226
- eventId: string,
227
- reply: Omit<PendingVerificationReply, '__pendingVerificationReply'>,
228
- ): void {
229
- const db = getDb();
230
- const payload: PendingVerificationReply = { __pendingVerificationReply: true, ...reply };
231
- db.update(channelInboundEvents)
232
- .set({ rawPayload: JSON.stringify(payload), updatedAt: Date.now() })
233
- .where(eq(channelInboundEvents.id, eventId))
234
- .run();
235
- }
236
-
237
- /**
238
- * Retrieve a pending verification reply for a given event, if one exists.
239
- */
240
- export function getPendingVerificationReply(
241
- eventId: string,
242
- ): PendingVerificationReply | null {
243
- const db = getDb();
244
- const row = db
245
- .select({ rawPayload: channelInboundEvents.rawPayload })
246
- .from(channelInboundEvents)
247
- .where(eq(channelInboundEvents.id, eventId))
248
- .get();
249
-
250
- if (!row?.rawPayload) return null;
251
- try {
252
- const parsed = JSON.parse(row.rawPayload);
253
- if (parsed && parsed.__pendingVerificationReply === true) {
254
- return parsed as PendingVerificationReply;
255
- }
256
- return null;
257
- } catch {
258
- return null;
259
- }
260
- }
261
-
262
- /**
263
- * Clear a pending verification reply after successful delivery.
264
- */
265
- export function clearPendingVerificationReply(eventId: string): void {
266
- const db = getDb();
267
- db.update(channelInboundEvents)
268
- .set({ rawPayload: null, updatedAt: Date.now() })
269
- .where(eq(channelInboundEvents.id, eventId))
270
- .run();
271
- }
272
-
273
- // ── Per-segment delivery progress ──────────────────────────────────
274
- //
275
- // When a split reply (multiple text segments from tool boundaries) fails
276
- // partway through delivery, we persist how many segments were sent so
277
- // the retry can resume from where it left off.
278
-
279
- /**
280
- * Read the number of reply segments already delivered for an event.
281
- */
282
- export function getDeliveredSegmentCount(eventId: string): number {
283
- const db = getDb();
284
- const row = db
285
- .select({ count: channelInboundEvents.deliveredSegmentCount })
286
- .from(channelInboundEvents)
287
- .where(eq(channelInboundEvents.id, eventId))
288
- .get();
289
- return row?.count ?? 0;
290
- }
291
-
292
- /**
293
- * Update the delivered segment count after successful delivery of one
294
- * or more segments. Called incrementally as segments are sent.
295
- */
296
- export function updateDeliveredSegmentCount(eventId: string, count: number): void {
297
- const db = getDb();
298
- db.update(channelInboundEvents)
299
- .set({ deliveredSegmentCount: count, updatedAt: Date.now() })
300
- .where(eq(channelInboundEvents.id, eventId))
301
- .run();
302
- }
303
-
304
- // ── Dead-letter queue helpers ───────────────────────────────────────
305
-
306
- /**
307
- * Store the raw request payload on an inbound event so it can be
308
- * replayed later if processing fails.
309
- */
310
- export function storePayload(eventId: string, payload: Record<string, unknown>): void {
311
- const db = getDb();
312
- db.update(channelInboundEvents)
313
- .set({ rawPayload: JSON.stringify(payload), updatedAt: Date.now() })
314
- .where(eq(channelInboundEvents.id, eventId))
315
- .run();
316
- }
317
-
318
- /**
319
- * Clear a previously stored payload. Used when the ingress check
320
- * detects secret-bearing content — the payload must not remain on disk.
321
- */
322
- export function clearPayload(eventId: string): void {
323
- const db = getDb();
324
- db.update(channelInboundEvents)
325
- .set({ rawPayload: null, updatedAt: Date.now() })
326
- .where(eq(channelInboundEvents.id, eventId))
327
- .run();
328
- }
329
-
330
- /**
331
- * Retrieve the stored raw payload for a given conversation's most recent
332
- * inbound event. Used by the escalation decide flow to recover the
333
- * original message content after an approve/deny decision.
334
- */
335
- export function getLatestStoredPayload(conversationId: string): Record<string, unknown> | null {
336
- const db = getDb();
337
- const row = db
338
- .select({
339
- rawPayload: channelInboundEvents.rawPayload,
340
- })
341
- .from(channelInboundEvents)
342
- .where(
343
- and(
344
- eq(channelInboundEvents.conversationId, conversationId),
345
- isNotNull(channelInboundEvents.rawPayload),
346
- ),
347
- )
348
- .orderBy(desc(channelInboundEvents.createdAt))
349
- .get();
350
-
351
- if (!row?.rawPayload) return null;
352
- try {
353
- return JSON.parse(row.rawPayload) as Record<string, unknown>;
354
- } catch {
355
- return null;
356
- }
357
- }
358
-
359
- /** Mark an event as successfully processed. */
360
- export function markProcessed(eventId: string): void {
361
- const db = getDb();
362
- db.update(channelInboundEvents)
363
- .set({ processingStatus: 'processed', updatedAt: Date.now() })
364
- .where(eq(channelInboundEvents.id, eventId))
365
- .run();
366
- }
367
-
368
- /**
369
- * Record a processing failure. Classifies the error to decide whether
370
- * the event should be retried (status='failed') or dead-lettered
371
- * (status='dead_letter') when the error is fatal or max attempts
372
- * are exhausted.
373
- */
374
- export function recordProcessingFailure(eventId: string, err: unknown): void {
375
- const db = getDb();
376
- const now = Date.now();
377
-
378
- const row = db
379
- .select({ attempts: channelInboundEvents.processingAttempts })
380
- .from(channelInboundEvents)
381
- .where(eq(channelInboundEvents.id, eventId))
382
- .get();
383
-
384
- const attempts = (row?.attempts ?? 0) + 1;
385
- const category = classifyError(err);
386
- const errorMsg = err instanceof Error ? err.message : String(err);
387
-
388
- if (category === 'fatal' || attempts >= RETRY_MAX_ATTEMPTS) {
389
- db.update(channelInboundEvents)
390
- .set({
391
- processingStatus: 'dead_letter',
392
- processingAttempts: attempts,
393
- lastProcessingError: errorMsg,
394
- retryAfter: null,
395
- updatedAt: now,
396
- })
397
- .where(eq(channelInboundEvents.id, eventId))
398
- .run();
399
- } else {
400
- const delay = retryDelayForAttempt(attempts);
401
- db.update(channelInboundEvents)
402
- .set({
403
- processingStatus: 'failed',
404
- processingAttempts: attempts,
405
- lastProcessingError: errorMsg,
406
- retryAfter: now + delay,
407
- updatedAt: now,
408
- })
409
- .where(eq(channelInboundEvents.id, eventId))
410
- .run();
411
- }
412
- }
413
-
414
- /** Fetch events eligible for automatic retry (failed + past their backoff). */
415
- export function getRetryableEvents(limit = 20): Array<{
416
- id: string;
417
- conversationId: string;
418
- processingAttempts: number;
419
- rawPayload: string | null;
420
- }> {
421
- const db = getDb();
422
- const now = Date.now();
423
- return db
424
- .select({
425
- id: channelInboundEvents.id,
426
- conversationId: channelInboundEvents.conversationId,
427
- processingAttempts: channelInboundEvents.processingAttempts,
428
- rawPayload: channelInboundEvents.rawPayload,
429
- })
430
- .from(channelInboundEvents)
431
- .where(
432
- and(
433
- eq(channelInboundEvents.processingStatus, 'failed'),
434
- lte(channelInboundEvents.retryAfter, now),
435
- ),
436
- )
437
- .limit(limit)
438
- .all();
439
- }
440
-
441
- /** Fetch dead-lettered events. */
442
- export function getDeadLetterEvents(): Array<{
443
- id: string;
444
- sourceChannel: string;
445
- externalChatId: string;
446
- externalMessageId: string;
447
- conversationId: string;
448
- processingAttempts: number;
449
- lastProcessingError: string | null;
450
- createdAt: number;
451
- }> {
452
- const db = getDb();
453
- return db
454
- .select({
455
- id: channelInboundEvents.id,
456
- sourceChannel: channelInboundEvents.sourceChannel,
457
- externalChatId: channelInboundEvents.externalChatId,
458
- externalMessageId: channelInboundEvents.externalMessageId,
459
- conversationId: channelInboundEvents.conversationId,
460
- processingAttempts: channelInboundEvents.processingAttempts,
461
- lastProcessingError: channelInboundEvents.lastProcessingError,
462
- createdAt: channelInboundEvents.createdAt,
463
- })
464
- .from(channelInboundEvents)
465
- .where(eq(channelInboundEvents.processingStatus, 'dead_letter'))
466
- .all();
467
- }
468
-
469
- // ── Deliver-once guard for terminal reply idempotency ────────────────
470
- //
471
- // When both the main poll (processChannelMessageWithApprovals) and the
472
- // post-decision poll (schedulePostDecisionDelivery) race to deliver the
473
- // final assistant reply for the same run, this guard ensures only one
474
- // of them actually sends the message. The guard is run-scoped so old
475
- // assistant messages from previous runs are not affected.
476
-
477
- /** Map from runId to insertion timestamp (ms). */
478
- const deliveredRuns = new Map<string, number>();
479
-
480
- /** TTL for delivery claims — 10 minutes, well beyond the poll max-wait. */
481
- const CLAIM_TTL_MS = 10 * 60 * 1000;
482
-
483
- /** Hard cap to bound memory even under sustained high throughput within the TTL window. */
484
- const MAX_DELIVERED_RUNS = 10_000;
485
-
486
- /**
487
- * Atomically claim the right to deliver the final reply for a run.
488
- * Returns `true` if this caller won the claim (and should proceed with
489
- * delivery). Returns `false` if another caller already claimed it.
490
- *
491
- * This is an in-memory guard — sufficient because both racing pollers
492
- * execute within the same process. The Map is never persisted; on restart
493
- * there are no in-flight pollers to race.
494
- *
495
- * Claims are evicted after CLAIM_TTL_MS. When the hard cap is reached,
496
- * only TTL-expired entries are evicted — active claims are never removed
497
- * early, preserving the at-most-once delivery guarantee.
498
- */
499
- export function claimRunDelivery(runId: string): boolean {
500
- if (deliveredRuns.has(runId)) return false;
501
- if (deliveredRuns.size >= MAX_DELIVERED_RUNS) {
502
- // Only evict entries whose TTL has expired. Map iteration order
503
- // matches insertion order, so oldest entries come first.
504
- const now = Date.now();
505
- for (const [id, insertedAt] of deliveredRuns) {
506
- if (now - insertedAt >= CLAIM_TTL_MS) {
507
- deliveredRuns.delete(id);
508
- } else {
509
- // Remaining entries are newer; stop scanning.
510
- break;
511
- }
512
- }
513
- }
514
- const now = Date.now();
515
- deliveredRuns.set(runId, now);
516
- setTimeout(() => deliveredRuns.delete(runId), CLAIM_TTL_MS);
517
- return true;
518
- }
519
-
520
- /**
521
- * Reset the deliver-once guard for a run. Used to release a claim when
522
- * delivery fails (so the other racing poller can retry) and in tests
523
- * for isolation between test cases.
524
- */
525
- export function resetRunDeliveryClaim(runId: string): void {
526
- deliveredRuns.delete(runId);
527
- }
528
-
529
- /**
530
- * Clear all delivery claims. Used in tests for full isolation.
531
- */
532
- export function resetAllRunDeliveryClaims(): void {
533
- deliveredRuns.clear();
534
- }
535
-
536
- /**
537
- * Reset dead-lettered events back to 'failed' so the sweep can retry
538
- * them. Resets attempt counter and sets an immediate retry_after.
539
- */
540
- export function replayDeadLetters(eventIds: string[]): number {
541
- const db = getDb();
542
- const now = Date.now();
543
- let count = 0;
544
- for (const id of eventIds) {
545
- const existing = db
546
- .select({ id: channelInboundEvents.id })
547
- .from(channelInboundEvents)
548
- .where(
549
- and(
550
- eq(channelInboundEvents.id, id),
551
- eq(channelInboundEvents.processingStatus, 'dead_letter'),
552
- ),
553
- )
554
- .get();
555
- if (!existing) continue;
556
-
557
- db.update(channelInboundEvents)
558
- .set({
559
- processingStatus: 'failed',
560
- processingAttempts: 0,
561
- lastProcessingError: null,
562
- retryAfter: now,
563
- updatedAt: now,
564
- })
565
- .where(eq(channelInboundEvents.id, id))
566
- .run();
567
- count++;
568
- }
569
- return count;
570
- }
4
+ * This module re-exports from focused sub-modules for backward compatibility.
5
+ * New code should import directly from the relevant sub-module:
6
+ * - delivery-crud.ts — inbound event CRUD and payload management
7
+ * - delivery-status.ts — processing status tracking and dead-letter queue
8
+ * - delivery-channels.ts verification replies, segment progress, delivery guards
9
+ */
10
+
11
+ export type { PendingVerificationReply } from './delivery-channels.js';
12
+ export {
13
+ claimRunDelivery,
14
+ clearPendingVerificationReply,
15
+ getDeliveredSegmentCount,
16
+ getPendingVerificationReply,
17
+ resetAllRunDeliveryClaims,
18
+ resetRunDeliveryClaim,
19
+ storePendingVerificationReply,
20
+ updateDeliveredSegmentCount,
21
+ } from './delivery-channels.js';
22
+ export type { InboundResult, RecordInboundOptions } from './delivery-crud.js';
23
+ export {
24
+ clearPayload,
25
+ findMessageBySourceId,
26
+ getLatestStoredPayload,
27
+ linkMessage,
28
+ recordInbound,
29
+ storePayload,
30
+ } from './delivery-crud.js';
31
+ export {
32
+ acknowledgeDelivery,
33
+ getDeadLetterEvents,
34
+ getRetryableEvents,
35
+ markProcessed,
36
+ recordProcessingFailure,
37
+ replayDeadLetters,
38
+ } from './delivery-status.js';