@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
@@ -7,7 +7,7 @@
7
7
  * answer resolves the request and all other deliveries are marked answered.
8
8
  */
9
9
 
10
- import { and, eq, inArray, lt } from 'drizzle-orm';
10
+ import { and, count, desc, eq, inArray, lt } from 'drizzle-orm';
11
11
  import { v4 as uuid } from 'uuid';
12
12
 
13
13
  import { getLogger } from '../util/logger.js';
@@ -25,6 +25,9 @@ const log = getLogger('guardian-action-store');
25
25
 
26
26
  export type GuardianActionRequestStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
27
27
  export type GuardianActionDeliveryStatus = 'pending' | 'sent' | 'failed' | 'answered' | 'expired' | 'cancelled';
28
+ export type ExpiredReason = 'call_timeout' | 'sweep_timeout' | 'cancelled';
29
+ export type FollowupState = 'none' | 'awaiting_guardian_choice' | 'dispatching' | 'completed' | 'declined' | 'failed';
30
+ export type FollowupAction = 'call_back' | 'message_back' | 'decline';
28
31
 
29
32
  export interface GuardianActionRequest {
30
33
  id: string;
@@ -42,6 +45,12 @@ export interface GuardianActionRequest {
42
45
  answeredByExternalUserId: string | null;
43
46
  answeredAt: number | null;
44
47
  expiresAt: number;
48
+ expiredReason: ExpiredReason | null;
49
+ followupState: FollowupState;
50
+ lateAnswerText: string | null;
51
+ lateAnsweredAt: number | null;
52
+ followupAction: FollowupAction | null;
53
+ followupCompletedAt: number | null;
45
54
  createdAt: number;
46
55
  updatedAt: number;
47
56
  }
@@ -82,6 +91,12 @@ function rowToRequest(row: typeof guardianActionRequests.$inferSelect): Guardian
82
91
  answeredByExternalUserId: row.answeredByExternalUserId,
83
92
  answeredAt: row.answeredAt,
84
93
  expiresAt: row.expiresAt,
94
+ expiredReason: (row.expiredReason as ExpiredReason) ?? null,
95
+ followupState: (row.followupState as FollowupState) ?? 'none',
96
+ lateAnswerText: row.lateAnswerText ?? null,
97
+ lateAnsweredAt: row.lateAnsweredAt ?? null,
98
+ followupAction: (row.followupAction as FollowupAction) ?? null,
99
+ followupCompletedAt: row.followupCompletedAt ?? null,
85
100
  createdAt: row.createdAt,
86
101
  updatedAt: row.updatedAt,
87
102
  };
@@ -143,6 +158,12 @@ export function createGuardianActionRequest(params: {
143
158
  answeredByExternalUserId: null,
144
159
  answeredAt: null,
145
160
  expiresAt: params.expiresAt,
161
+ expiredReason: null,
162
+ followupState: 'none' as const,
163
+ lateAnswerText: null,
164
+ lateAnsweredAt: null,
165
+ followupAction: null,
166
+ followupCompletedAt: null,
146
167
  createdAt: now,
147
168
  updatedAt: now,
148
169
  };
@@ -171,6 +192,46 @@ export function getByPendingQuestionId(questionId: string): GuardianActionReques
171
192
  return row ? rowToRequest(row) : null;
172
193
  }
173
194
 
195
+ /**
196
+ * Find the most recent pending guardian action request for a given call session.
197
+ * Used by the consultation timeout handler to mark the linked request as timed out.
198
+ */
199
+ export function getPendingRequestByCallSessionId(callSessionId: string): GuardianActionRequest | null {
200
+ const db = getDb();
201
+ const row = db
202
+ .select()
203
+ .from(guardianActionRequests)
204
+ .where(
205
+ and(
206
+ eq(guardianActionRequests.callSessionId, callSessionId),
207
+ eq(guardianActionRequests.status, 'pending'),
208
+ ),
209
+ )
210
+ .orderBy(desc(guardianActionRequests.createdAt))
211
+ .get();
212
+ return row ? rowToRequest(row) : null;
213
+ }
214
+
215
+ /**
216
+ * Count pending guardian action requests for a given call session.
217
+ * Used as a candidate-affinity hint so the decision engine knows how many
218
+ * active guardian requests already exist for the current call.
219
+ */
220
+ export function countPendingRequestsByCallSessionId(callSessionId: string): number {
221
+ const db = getDb();
222
+ const row = db
223
+ .select({ count: count() })
224
+ .from(guardianActionRequests)
225
+ .where(
226
+ and(
227
+ eq(guardianActionRequests.callSessionId, callSessionId),
228
+ eq(guardianActionRequests.status, 'pending'),
229
+ ),
230
+ )
231
+ .get();
232
+ return row?.count ?? 0;
233
+ }
234
+
174
235
  /**
175
236
  * First-response-wins resolution. Checks that the request is still
176
237
  * 'pending' before updating; returns the updated request on success
@@ -217,13 +278,14 @@ export function resolveGuardianActionRequest(
217
278
 
218
279
  /**
219
280
  * Expire a guardian action request and all its deliveries.
281
+ * When reason is not provided, defaults to 'sweep_timeout' for backward compatibility.
220
282
  */
221
- export function expireGuardianActionRequest(id: string): void {
283
+ export function expireGuardianActionRequest(id: string, reason?: ExpiredReason): void {
222
284
  const db = getDb();
223
285
  const now = Date.now();
224
286
 
225
287
  db.update(guardianActionRequests)
226
- .set({ status: 'expired', updatedAt: now })
288
+ .set({ status: 'expired', expiredReason: reason ?? 'sweep_timeout', updatedAt: now })
227
289
  .where(
228
290
  and(
229
291
  eq(guardianActionRequests.id, id),
@@ -303,6 +365,175 @@ export function cancelGuardianActionRequest(id: string): void {
303
365
  .run();
304
366
  }
305
367
 
368
+ // ---------------------------------------------------------------------------
369
+ // Follow-up lifecycle helpers
370
+ // ---------------------------------------------------------------------------
371
+
372
+ /** Valid non-terminal followup_state transitions for progressFollowupState.
373
+ * Terminal states (completed, declined, failed) are only reachable via
374
+ * finalizeFollowup, which properly sets followupCompletedAt. */
375
+ const FOLLOWUP_TRANSITIONS: Record<FollowupState, FollowupState[]> = {
376
+ none: [],
377
+ awaiting_guardian_choice: ['dispatching'],
378
+ dispatching: [],
379
+ completed: [],
380
+ declined: [],
381
+ failed: [],
382
+ };
383
+
384
+ /** Valid terminal transitions for finalizeFollowup. Maps from current
385
+ * followup_state to the terminal states reachable from it. */
386
+ const FOLLOWUP_FINALIZE_TRANSITIONS: Partial<Record<FollowupState, FollowupState[]>> = {
387
+ awaiting_guardian_choice: ['declined'],
388
+ dispatching: ['completed', 'failed'],
389
+ };
390
+
391
+ /**
392
+ * Atomically set status='expired' and expired_reason on a pending request.
393
+ * Returns the updated request on success, or null if the request was not
394
+ * in 'pending' status (first-writer-wins).
395
+ */
396
+ export function markTimedOutWithReason(id: string, reason: ExpiredReason): GuardianActionRequest | null {
397
+ const db = getDb();
398
+ const now = Date.now();
399
+
400
+ db.update(guardianActionRequests)
401
+ .set({ status: 'expired', expiredReason: reason, updatedAt: now })
402
+ .where(
403
+ and(
404
+ eq(guardianActionRequests.id, id),
405
+ eq(guardianActionRequests.status, 'pending'),
406
+ ),
407
+ )
408
+ .run();
409
+
410
+ if (rawChanges() === 0) return null;
411
+
412
+ // Also expire active deliveries
413
+ db.update(guardianActionDeliveries)
414
+ .set({ status: 'expired', updatedAt: now })
415
+ .where(
416
+ and(
417
+ eq(guardianActionDeliveries.requestId, id),
418
+ inArray(guardianActionDeliveries.status, ['pending', 'sent']),
419
+ ),
420
+ )
421
+ .run();
422
+
423
+ return getGuardianActionRequest(id);
424
+ }
425
+
426
+ /**
427
+ * Atomically transition an expired request into the follow-up flow.
428
+ * Sets followup_state='awaiting_guardian_choice', records the late answer
429
+ * text and timestamp. Only succeeds if status='expired' and followup_state='none'.
430
+ * Returns the updated request on success, or null on conflict.
431
+ */
432
+ export function startFollowupFromExpiredRequest(
433
+ id: string,
434
+ lateAnswerText: string,
435
+ ): GuardianActionRequest | null {
436
+ const db = getDb();
437
+ const now = Date.now();
438
+
439
+ db.update(guardianActionRequests)
440
+ .set({
441
+ followupState: 'awaiting_guardian_choice',
442
+ lateAnswerText,
443
+ lateAnsweredAt: now,
444
+ updatedAt: now,
445
+ })
446
+ .where(
447
+ and(
448
+ eq(guardianActionRequests.id, id),
449
+ eq(guardianActionRequests.status, 'expired'),
450
+ eq(guardianActionRequests.followupState, 'none'),
451
+ ),
452
+ )
453
+ .run();
454
+
455
+ if (rawChanges() === 0) return null;
456
+ return getGuardianActionRequest(id);
457
+ }
458
+
459
+ /**
460
+ * Atomically progress the followup_state. Validates that the transition
461
+ * is allowed (see FOLLOWUP_TRANSITIONS). Optionally sets the followup_action.
462
+ * Returns the updated request on success, or null if the transition was
463
+ * invalid or the prior state didn't match.
464
+ */
465
+ export function progressFollowupState(
466
+ id: string,
467
+ newState: FollowupState,
468
+ action?: FollowupAction,
469
+ ): GuardianActionRequest | null {
470
+ const request = getGuardianActionRequest(id);
471
+ if (!request) return null;
472
+
473
+ const allowed = FOLLOWUP_TRANSITIONS[request.followupState];
474
+ if (!allowed.includes(newState)) return null;
475
+
476
+ const db = getDb();
477
+ const now = Date.now();
478
+
479
+ const updates: Record<string, unknown> = {
480
+ followupState: newState,
481
+ updatedAt: now,
482
+ };
483
+ if (action !== undefined) updates.followupAction = action;
484
+
485
+ db.update(guardianActionRequests)
486
+ .set(updates)
487
+ .where(
488
+ and(
489
+ eq(guardianActionRequests.id, id),
490
+ eq(guardianActionRequests.status, 'expired'),
491
+ eq(guardianActionRequests.followupState, request.followupState),
492
+ ),
493
+ )
494
+ .run();
495
+
496
+ if (rawChanges() === 0) return null;
497
+ return getGuardianActionRequest(id);
498
+ }
499
+
500
+ /**
501
+ * Finalize a follow-up by setting the terminal followup_state and
502
+ * recording followup_completed_at. Only succeeds from a non-terminal state
503
+ * and only on expired requests.
504
+ */
505
+ export function finalizeFollowup(
506
+ id: string,
507
+ finalState: 'completed' | 'declined' | 'failed',
508
+ ): GuardianActionRequest | null {
509
+ const request = getGuardianActionRequest(id);
510
+ if (!request) return null;
511
+
512
+ const allowed = FOLLOWUP_FINALIZE_TRANSITIONS[request.followupState];
513
+ if (!allowed?.includes(finalState)) return null;
514
+
515
+ const db = getDb();
516
+ const now = Date.now();
517
+
518
+ db.update(guardianActionRequests)
519
+ .set({
520
+ followupState: finalState,
521
+ followupCompletedAt: now,
522
+ updatedAt: now,
523
+ })
524
+ .where(
525
+ and(
526
+ eq(guardianActionRequests.id, id),
527
+ eq(guardianActionRequests.status, 'expired'),
528
+ eq(guardianActionRequests.followupState, request.followupState),
529
+ ),
530
+ )
531
+ .run();
532
+
533
+ if (rawChanges() === 0) return null;
534
+ return getGuardianActionRequest(id);
535
+ }
536
+
306
537
  // ---------------------------------------------------------------------------
307
538
  // Guardian Action Deliveries
308
539
  // ---------------------------------------------------------------------------
@@ -384,6 +615,16 @@ export function getPendingDeliveriesByDestination(
384
615
  * Look up a pending delivery by destination conversation ID (for mac channel routing).
385
616
  */
386
617
  export function getPendingDeliveryByConversation(conversationId: string): GuardianActionDelivery | null {
618
+ const all = getPendingDeliveriesByConversation(conversationId);
619
+ return all.length > 0 ? all[0] : null;
620
+ }
621
+
622
+ /**
623
+ * Look up all pending deliveries by destination conversation ID.
624
+ * Used for disambiguation when a reused vellum thread has multiple active
625
+ * guardian requests.
626
+ */
627
+ export function getPendingDeliveriesByConversation(conversationId: string): GuardianActionDelivery[] {
387
628
  try {
388
629
  const db = getDb();
389
630
  const rows = db
@@ -401,11 +642,183 @@ export function getPendingDeliveryByConversation(conversationId: string): Guardi
401
642
  ),
402
643
  )
403
644
  .all();
404
- return rows.length > 0 ? rowToDelivery(rows[0].delivery) : null;
645
+ return rows.map((r) => rowToDelivery(r.delivery));
405
646
  } catch (err) {
406
647
  if (err instanceof Error && err.message.includes('no such table')) {
407
648
  log.warn({ err }, 'guardian tables not yet created');
408
- return null;
649
+ return [];
650
+ }
651
+ throw err;
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Look up sent deliveries for expired requests eligible for follow-up.
657
+ * Used by inbound message routing to match late guardian answers to expired requests.
658
+ */
659
+ export function getExpiredDeliveriesByDestination(
660
+ assistantId: string,
661
+ channel: string,
662
+ chatId: string,
663
+ ): GuardianActionDelivery[] {
664
+ try {
665
+ const db = getDb();
666
+
667
+ const rows = db
668
+ .select({
669
+ delivery: guardianActionDeliveries,
670
+ })
671
+ .from(guardianActionDeliveries)
672
+ .innerJoin(
673
+ guardianActionRequests,
674
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
675
+ )
676
+ .where(
677
+ and(
678
+ eq(guardianActionRequests.assistantId, assistantId),
679
+ eq(guardianActionRequests.status, 'expired'),
680
+ eq(guardianActionRequests.followupState, 'none'),
681
+ eq(guardianActionDeliveries.destinationChannel, channel),
682
+ eq(guardianActionDeliveries.destinationChatId, chatId),
683
+ eq(guardianActionDeliveries.status, 'expired'),
684
+ ),
685
+ )
686
+ .all();
687
+
688
+ return rows.map((r) => rowToDelivery(r.delivery));
689
+ } catch (err) {
690
+ if (err instanceof Error && err.message.includes('no such table')) {
691
+ log.warn({ err }, 'guardian tables not yet created');
692
+ return [];
693
+ }
694
+ throw err;
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Look up an expired delivery by destination conversation ID (for mac channel routing).
700
+ */
701
+ export function getExpiredDeliveryByConversation(conversationId: string): GuardianActionDelivery | null {
702
+ const all = getExpiredDeliveriesByConversation(conversationId);
703
+ return all.length > 0 ? all[0] : null;
704
+ }
705
+
706
+ /**
707
+ * Look up all expired deliveries by destination conversation ID.
708
+ * Used for disambiguation when a reused vellum thread has multiple expired
709
+ * guardian requests eligible for follow-up.
710
+ */
711
+ export function getExpiredDeliveriesByConversation(conversationId: string): GuardianActionDelivery[] {
712
+ try {
713
+ const db = getDb();
714
+ const rows = db
715
+ .select({ delivery: guardianActionDeliveries })
716
+ .from(guardianActionDeliveries)
717
+ .innerJoin(
718
+ guardianActionRequests,
719
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
720
+ )
721
+ .where(
722
+ and(
723
+ eq(guardianActionDeliveries.destinationConversationId, conversationId),
724
+ eq(guardianActionDeliveries.status, 'expired'),
725
+ eq(guardianActionRequests.status, 'expired'),
726
+ eq(guardianActionRequests.followupState, 'none'),
727
+ ),
728
+ )
729
+ .all();
730
+ return rows.map((r) => rowToDelivery(r.delivery));
731
+ } catch (err) {
732
+ if (err instanceof Error && err.message.includes('no such table')) {
733
+ log.warn({ err }, 'guardian tables not yet created');
734
+ return [];
735
+ }
736
+ throw err;
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Look up deliveries for requests in `awaiting_guardian_choice` follow-up state.
742
+ * Used by inbound message routing to intercept guardian follow-up replies
743
+ * on channel paths (Telegram, SMS).
744
+ */
745
+ export function getFollowupDeliveriesByDestination(
746
+ assistantId: string,
747
+ channel: string,
748
+ chatId: string,
749
+ ): GuardianActionDelivery[] {
750
+ try {
751
+ const db = getDb();
752
+
753
+ const rows = db
754
+ .select({
755
+ delivery: guardianActionDeliveries,
756
+ })
757
+ .from(guardianActionDeliveries)
758
+ .innerJoin(
759
+ guardianActionRequests,
760
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
761
+ )
762
+ .where(
763
+ and(
764
+ eq(guardianActionRequests.assistantId, assistantId),
765
+ eq(guardianActionRequests.status, 'expired'),
766
+ eq(guardianActionRequests.followupState, 'awaiting_guardian_choice'),
767
+ eq(guardianActionDeliveries.destinationChannel, channel),
768
+ eq(guardianActionDeliveries.destinationChatId, chatId),
769
+ eq(guardianActionDeliveries.status, 'expired'),
770
+ ),
771
+ )
772
+ .all();
773
+
774
+ return rows.map((r) => rowToDelivery(r.delivery));
775
+ } catch (err) {
776
+ if (err instanceof Error && err.message.includes('no such table')) {
777
+ log.warn({ err }, 'guardian tables not yet created');
778
+ return [];
779
+ }
780
+ throw err;
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Look up a delivery for a request in `awaiting_guardian_choice` follow-up
786
+ * state by destination conversation ID (for mac channel routing).
787
+ */
788
+ export function getFollowupDeliveryByConversation(conversationId: string): GuardianActionDelivery | null {
789
+ const all = getFollowupDeliveriesByConversation(conversationId);
790
+ return all.length > 0 ? all[0] : null;
791
+ }
792
+
793
+ /**
794
+ * Look up all deliveries for requests in `awaiting_guardian_choice` follow-up
795
+ * state by destination conversation ID. Used for disambiguation when a reused
796
+ * vellum thread has multiple follow-up guardian requests.
797
+ */
798
+ export function getFollowupDeliveriesByConversation(conversationId: string): GuardianActionDelivery[] {
799
+ try {
800
+ const db = getDb();
801
+ const rows = db
802
+ .select({ delivery: guardianActionDeliveries })
803
+ .from(guardianActionDeliveries)
804
+ .innerJoin(
805
+ guardianActionRequests,
806
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
807
+ )
808
+ .where(
809
+ and(
810
+ eq(guardianActionDeliveries.destinationConversationId, conversationId),
811
+ eq(guardianActionDeliveries.status, 'expired'),
812
+ eq(guardianActionRequests.status, 'expired'),
813
+ eq(guardianActionRequests.followupState, 'awaiting_guardian_choice'),
814
+ ),
815
+ )
816
+ .all();
817
+ return rows.map((r) => rowToDelivery(r.delivery));
818
+ } catch (err) {
819
+ if (err instanceof Error && err.message.includes('no such table')) {
820
+ log.warn({ err }, 'guardian tables not yet created');
821
+ return [];
409
822
  }
410
823
  throw err;
411
824
  }