@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.
Files changed (295) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +2 -2
  3. package/README.md +5 -5
  4. package/docs/architecture/http-token-refresh.md +252 -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 +331 -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 +16 -14
  22. package/src/__tests__/checker.test.ts +24 -0
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +358 -0
  25. package/src/__tests__/conversation-pairing.test.ts +24 -24
  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 +294 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  39. package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
  40. package/src/__tests__/guardian-outbound-http.test.ts +202 -10
  41. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  42. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  43. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  44. package/src/__tests__/hooks-runner.test.ts +13 -4
  45. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  46. package/src/__tests__/intent-routing.test.ts +14 -0
  47. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  48. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  49. package/src/__tests__/memory-regressions.test.ts +16 -12
  50. package/src/__tests__/non-member-access-request.test.ts +282 -0
  51. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  52. package/src/__tests__/notification-routing-intent.test.ts +11 -2
  53. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  54. package/src/__tests__/recording-intent-fallback.test.ts +0 -1
  55. package/src/__tests__/recording-intent-handler.test.ts +6 -3
  56. package/src/__tests__/recording-intent.test.ts +3 -2
  57. package/src/__tests__/recording-state-machine.test.ts +337 -26
  58. package/src/__tests__/registry.test.ts +17 -8
  59. package/src/__tests__/relay-server.test.ts +105 -0
  60. package/src/__tests__/reminder.test.ts +13 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  63. package/src/__tests__/server-history-render.test.ts +8 -8
  64. package/src/__tests__/session-agent-loop.test.ts +1 -0
  65. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  66. package/src/__tests__/session-skill-tools.test.ts +1 -0
  67. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  68. package/src/__tests__/slack-channel-config.test.ts +230 -0
  69. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  70. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  71. package/src/__tests__/system-prompt.test.ts +43 -0
  72. package/src/__tests__/task-management-tools.test.ts +3 -3
  73. package/src/__tests__/task-tools.test.ts +3 -3
  74. package/src/__tests__/trust-store.test.ts +17 -1
  75. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  76. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  77. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  78. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  79. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  80. package/src/__tests__/update-bulletin.test.ts +260 -0
  81. package/src/__tests__/update-template-contract.test.ts +29 -0
  82. package/src/agent/loop.ts +2 -2
  83. package/src/amazon/client.ts +2 -3
  84. package/src/calls/call-controller.ts +115 -34
  85. package/src/calls/call-conversation-messages.ts +2 -2
  86. package/src/calls/call-domain.ts +10 -3
  87. package/src/calls/call-pointer-messages.ts +17 -5
  88. package/src/calls/guardian-action-sweep.ts +77 -36
  89. package/src/calls/relay-server.ts +51 -12
  90. package/src/calls/twilio-routes.ts +3 -1
  91. package/src/calls/types.ts +1 -1
  92. package/src/calls/voice-session-bridge.ts +4 -4
  93. package/src/cli/core-commands.ts +3 -3
  94. package/src/cli/map.ts +8 -5
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  96. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  97. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  98. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  99. package/src/config/computer-use-prompt.ts +1 -0
  100. package/src/config/core-schema.ts +16 -0
  101. package/src/config/env-registry.ts +1 -0
  102. package/src/config/env.ts +16 -1
  103. package/src/config/memory-schema.ts +5 -0
  104. package/src/config/schema.ts +4 -0
  105. package/src/config/system-prompt.ts +69 -2
  106. package/src/config/templates/BOOTSTRAP.md +1 -1
  107. package/src/config/templates/IDENTITY.md +8 -4
  108. package/src/config/templates/SOUL.md +14 -0
  109. package/src/config/templates/UPDATES.md +16 -0
  110. package/src/config/templates/USER.md +5 -1
  111. package/src/config/types.ts +1 -0
  112. package/src/config/update-bulletin-format.ts +52 -0
  113. package/src/config/update-bulletin-state.ts +49 -0
  114. package/src/config/update-bulletin.ts +82 -0
  115. package/src/config/vellum-skills/catalog.json +6 -0
  116. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  117. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  119. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  120. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  121. package/src/context/window-manager.ts +43 -3
  122. package/src/daemon/config-watcher.ts +1 -0
  123. package/src/daemon/connection-policy.ts +21 -1
  124. package/src/daemon/daemon-control.ts +164 -7
  125. package/src/daemon/date-context.ts +174 -1
  126. package/src/daemon/guardian-action-generators.ts +175 -0
  127. package/src/daemon/guardian-verification-intent.ts +120 -0
  128. package/src/daemon/handlers/apps.ts +1 -3
  129. package/src/daemon/handlers/config-channels.ts +8 -8
  130. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  131. package/src/daemon/handlers/config-inbox.ts +55 -159
  132. package/src/daemon/handlers/config-ingress.ts +1 -1
  133. package/src/daemon/handlers/config-integrations.ts +1 -1
  134. package/src/daemon/handlers/config-platform.ts +1 -1
  135. package/src/daemon/handlers/config-scheduling.ts +2 -2
  136. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  137. package/src/daemon/handlers/config-telegram.ts +1 -1
  138. package/src/daemon/handlers/config-twilio.ts +1 -1
  139. package/src/daemon/handlers/config-voice.ts +100 -0
  140. package/src/daemon/handlers/config.ts +3 -0
  141. package/src/daemon/handlers/index.ts +1 -1
  142. package/src/daemon/handlers/misc.ts +84 -6
  143. package/src/daemon/handlers/navigate-settings.ts +27 -0
  144. package/src/daemon/handlers/recording.ts +270 -144
  145. package/src/daemon/handlers/sessions.ts +107 -24
  146. package/src/daemon/handlers/subagents.ts +3 -3
  147. package/src/daemon/handlers/work-items.ts +10 -7
  148. package/src/daemon/ipc-contract/integrations.ts +9 -1
  149. package/src/daemon/ipc-contract/messages.ts +4 -0
  150. package/src/daemon/ipc-contract/sessions.ts +1 -1
  151. package/src/daemon/ipc-contract/settings.ts +26 -0
  152. package/src/daemon/ipc-contract/shared.ts +2 -0
  153. package/src/daemon/ipc-contract/work-items.ts +1 -7
  154. package/src/daemon/ipc-contract-inventory.json +5 -1
  155. package/src/daemon/ipc-contract.ts +5 -1
  156. package/src/daemon/lifecycle.ts +306 -266
  157. package/src/daemon/recording-executor.ts +1 -1
  158. package/src/daemon/recording-intent.ts +0 -41
  159. package/src/daemon/response-tier.ts +2 -2
  160. package/src/daemon/server.ts +6 -6
  161. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  162. package/src/daemon/session-agent-loop.ts +15 -8
  163. package/src/daemon/session-history.ts +3 -2
  164. package/src/daemon/session-media-retry.ts +3 -0
  165. package/src/daemon/session-messaging.ts +38 -4
  166. package/src/daemon/session-notifiers.ts +2 -2
  167. package/src/daemon/session-process.ts +256 -23
  168. package/src/daemon/session-queue-manager.ts +2 -0
  169. package/src/daemon/session-runtime-assembly.ts +39 -0
  170. package/src/daemon/session-skill-tools.ts +13 -4
  171. package/src/daemon/session-tool-setup.ts +6 -7
  172. package/src/daemon/session.ts +19 -8
  173. package/src/daemon/tls-certs.ts +55 -13
  174. package/src/daemon/tool-side-effects.ts +13 -5
  175. package/src/gallery/default-gallery.ts +32 -9
  176. package/src/influencer/client.ts +2 -1
  177. package/src/memory/channel-delivery-store.ts +37 -567
  178. package/src/memory/channel-guardian-store.ts +66 -1317
  179. package/src/memory/conflict-store.ts +4 -4
  180. package/src/memory/conversation-attention-store.ts +4 -7
  181. package/src/memory/conversation-crud.ts +668 -0
  182. package/src/memory/conversation-queries.ts +361 -0
  183. package/src/memory/conversation-store.ts +45 -983
  184. package/src/memory/db-connection.ts +3 -0
  185. package/src/memory/db-init.ts +25 -0
  186. package/src/memory/delivery-channels.ts +175 -0
  187. package/src/memory/delivery-crud.ts +211 -0
  188. package/src/memory/delivery-status.ts +199 -0
  189. package/src/memory/embedding-backend.ts +70 -4
  190. package/src/memory/embedding-local.ts +12 -2
  191. package/src/memory/entity-extractor.ts +3 -8
  192. package/src/memory/fts-reconciler.ts +121 -0
  193. package/src/memory/guardian-action-store.ts +366 -3
  194. package/src/memory/guardian-approvals.ts +569 -0
  195. package/src/memory/guardian-bindings.ts +130 -0
  196. package/src/memory/guardian-rate-limits.ts +196 -0
  197. package/src/memory/guardian-verification.ts +520 -0
  198. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  199. package/src/memory/job-utils.ts +8 -5
  200. package/src/memory/jobs-store.ts +66 -6
  201. package/src/memory/jobs-worker.ts +23 -1
  202. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  203. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  204. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  205. package/src/memory/migrations/100-core-tables.ts +1 -1
  206. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  207. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  208. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  209. package/src/memory/migrations/113-late-migrations.ts +1 -1
  210. package/src/memory/migrations/116-messages-fts.ts +13 -0
  211. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  212. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  213. package/src/memory/migrations/index.ts +8 -3
  214. package/src/memory/migrations/validate-migration-state.ts +114 -15
  215. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  216. package/src/memory/retriever.ts +46 -13
  217. package/src/memory/schema-migration.ts +3 -0
  218. package/src/memory/schema.ts +25 -7
  219. package/src/memory/search/semantic.ts +8 -90
  220. package/src/notifications/README.md +1 -1
  221. package/src/notifications/broadcaster.ts +20 -2
  222. package/src/notifications/conversation-pairing.ts +3 -3
  223. package/src/notifications/decision-engine.ts +173 -8
  224. package/src/notifications/deliveries-store.ts +27 -8
  225. package/src/notifications/preferences-store.ts +7 -7
  226. package/src/notifications/thread-candidates.ts +234 -0
  227. package/src/notifications/types.ts +18 -0
  228. package/src/permissions/defaults.ts +11 -1
  229. package/src/permissions/prompter.ts +17 -0
  230. package/src/permissions/trust-store.ts +2 -0
  231. package/src/providers/failover.ts +19 -0
  232. package/src/providers/registry.ts +46 -1
  233. package/src/runtime/approval-message-composer.ts +1 -1
  234. package/src/runtime/channel-guardian-service.ts +15 -3
  235. package/src/runtime/channel-retry-sweep.ts +7 -2
  236. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  237. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  238. package/src/runtime/guardian-action-message-composer.ts +245 -0
  239. package/src/runtime/guardian-outbound-actions.ts +35 -15
  240. package/src/runtime/guardian-verification-templates.ts +15 -9
  241. package/src/runtime/http-errors.ts +93 -0
  242. package/src/runtime/http-server.ts +140 -51
  243. package/src/runtime/http-types.ts +53 -0
  244. package/src/runtime/ingress-service.ts +237 -0
  245. package/src/runtime/middleware/error-handler.ts +4 -3
  246. package/src/runtime/middleware/rate-limiter.ts +160 -0
  247. package/src/runtime/middleware/request-logger.ts +71 -0
  248. package/src/runtime/middleware/twilio-validation.ts +7 -6
  249. package/src/runtime/pending-interactions.ts +12 -0
  250. package/src/runtime/routes/access-request-decision.ts +215 -0
  251. package/src/runtime/routes/app-routes.ts +25 -18
  252. package/src/runtime/routes/approval-routes.ts +18 -47
  253. package/src/runtime/routes/attachment-routes.ts +15 -41
  254. package/src/runtime/routes/call-routes.ts +20 -20
  255. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  256. package/src/runtime/routes/contact-routes.ts +4 -9
  257. package/src/runtime/routes/conversation-attention-routes.ts +5 -4
  258. package/src/runtime/routes/conversation-routes.ts +26 -57
  259. package/src/runtime/routes/debug-routes.ts +71 -0
  260. package/src/runtime/routes/events-routes.ts +3 -2
  261. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  262. package/src/runtime/routes/identity-routes.ts +14 -10
  263. package/src/runtime/routes/inbound-conversation.ts +3 -2
  264. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  265. package/src/runtime/routes/ingress-routes.ts +174 -0
  266. package/src/runtime/routes/integration-routes.ts +82 -20
  267. package/src/runtime/routes/pairing-routes.ts +11 -10
  268. package/src/runtime/routes/secret-routes.ts +10 -18
  269. package/src/runtime/verification-rate-limiter.ts +83 -0
  270. package/src/schedule/schedule-store.ts +13 -1
  271. package/src/schedule/scheduler.ts +2 -2
  272. package/src/security/secret-ingress.ts +5 -2
  273. package/src/security/secret-scanner.ts +72 -6
  274. package/src/subagent/manager.ts +6 -4
  275. package/src/swarm/plan-validator.ts +4 -1
  276. package/src/tasks/task-runner.ts +3 -1
  277. package/src/tools/browser/api-map.ts +9 -6
  278. package/src/tools/calls/call-start.ts +20 -0
  279. package/src/tools/executor.ts +50 -568
  280. package/src/tools/permission-checker.ts +272 -0
  281. package/src/tools/registry.ts +14 -6
  282. package/src/tools/reminder/reminder-store.ts +7 -7
  283. package/src/tools/reminder/reminder.ts +6 -3
  284. package/src/tools/secret-detection-handler.ts +301 -0
  285. package/src/tools/subagent/message.ts +1 -1
  286. package/src/tools/system/voice-config.ts +62 -0
  287. package/src/tools/tasks/index.ts +3 -3
  288. package/src/tools/tasks/work-item-list.ts +3 -3
  289. package/src/tools/tasks/work-item-update.ts +4 -5
  290. package/src/tools/tool-approval-handler.ts +192 -0
  291. package/src/tools/tool-manifest.ts +2 -0
  292. package/src/watcher/watcher-store.ts +9 -9
  293. package/src/work-items/work-item-runner.ts +9 -6
  294. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  295. /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, 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,26 @@ 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
+
174
215
  /**
175
216
  * First-response-wins resolution. Checks that the request is still
176
217
  * 'pending' before updating; returns the updated request on success
@@ -217,13 +258,14 @@ export function resolveGuardianActionRequest(
217
258
 
218
259
  /**
219
260
  * Expire a guardian action request and all its deliveries.
261
+ * When reason is not provided, defaults to 'sweep_timeout' for backward compatibility.
220
262
  */
221
- export function expireGuardianActionRequest(id: string): void {
263
+ export function expireGuardianActionRequest(id: string, reason?: ExpiredReason): void {
222
264
  const db = getDb();
223
265
  const now = Date.now();
224
266
 
225
267
  db.update(guardianActionRequests)
226
- .set({ status: 'expired', updatedAt: now })
268
+ .set({ status: 'expired', expiredReason: reason ?? 'sweep_timeout', updatedAt: now })
227
269
  .where(
228
270
  and(
229
271
  eq(guardianActionRequests.id, id),
@@ -303,6 +345,175 @@ export function cancelGuardianActionRequest(id: string): void {
303
345
  .run();
304
346
  }
305
347
 
348
+ // ---------------------------------------------------------------------------
349
+ // Follow-up lifecycle helpers
350
+ // ---------------------------------------------------------------------------
351
+
352
+ /** Valid non-terminal followup_state transitions for progressFollowupState.
353
+ * Terminal states (completed, declined, failed) are only reachable via
354
+ * finalizeFollowup, which properly sets followupCompletedAt. */
355
+ const FOLLOWUP_TRANSITIONS: Record<FollowupState, FollowupState[]> = {
356
+ none: [],
357
+ awaiting_guardian_choice: ['dispatching'],
358
+ dispatching: [],
359
+ completed: [],
360
+ declined: [],
361
+ failed: [],
362
+ };
363
+
364
+ /** Valid terminal transitions for finalizeFollowup. Maps from current
365
+ * followup_state to the terminal states reachable from it. */
366
+ const FOLLOWUP_FINALIZE_TRANSITIONS: Partial<Record<FollowupState, FollowupState[]>> = {
367
+ awaiting_guardian_choice: ['declined'],
368
+ dispatching: ['completed', 'failed'],
369
+ };
370
+
371
+ /**
372
+ * Atomically set status='expired' and expired_reason on a pending request.
373
+ * Returns the updated request on success, or null if the request was not
374
+ * in 'pending' status (first-writer-wins).
375
+ */
376
+ export function markTimedOutWithReason(id: string, reason: ExpiredReason): GuardianActionRequest | null {
377
+ const db = getDb();
378
+ const now = Date.now();
379
+
380
+ db.update(guardianActionRequests)
381
+ .set({ status: 'expired', expiredReason: reason, updatedAt: now })
382
+ .where(
383
+ and(
384
+ eq(guardianActionRequests.id, id),
385
+ eq(guardianActionRequests.status, 'pending'),
386
+ ),
387
+ )
388
+ .run();
389
+
390
+ if (rawChanges() === 0) return null;
391
+
392
+ // Also expire active deliveries
393
+ db.update(guardianActionDeliveries)
394
+ .set({ status: 'expired', updatedAt: now })
395
+ .where(
396
+ and(
397
+ eq(guardianActionDeliveries.requestId, id),
398
+ inArray(guardianActionDeliveries.status, ['pending', 'sent']),
399
+ ),
400
+ )
401
+ .run();
402
+
403
+ return getGuardianActionRequest(id);
404
+ }
405
+
406
+ /**
407
+ * Atomically transition an expired request into the follow-up flow.
408
+ * Sets followup_state='awaiting_guardian_choice', records the late answer
409
+ * text and timestamp. Only succeeds if status='expired' and followup_state='none'.
410
+ * Returns the updated request on success, or null on conflict.
411
+ */
412
+ export function startFollowupFromExpiredRequest(
413
+ id: string,
414
+ lateAnswerText: string,
415
+ ): GuardianActionRequest | null {
416
+ const db = getDb();
417
+ const now = Date.now();
418
+
419
+ db.update(guardianActionRequests)
420
+ .set({
421
+ followupState: 'awaiting_guardian_choice',
422
+ lateAnswerText,
423
+ lateAnsweredAt: now,
424
+ updatedAt: now,
425
+ })
426
+ .where(
427
+ and(
428
+ eq(guardianActionRequests.id, id),
429
+ eq(guardianActionRequests.status, 'expired'),
430
+ eq(guardianActionRequests.followupState, 'none'),
431
+ ),
432
+ )
433
+ .run();
434
+
435
+ if (rawChanges() === 0) return null;
436
+ return getGuardianActionRequest(id);
437
+ }
438
+
439
+ /**
440
+ * Atomically progress the followup_state. Validates that the transition
441
+ * is allowed (see FOLLOWUP_TRANSITIONS). Optionally sets the followup_action.
442
+ * Returns the updated request on success, or null if the transition was
443
+ * invalid or the prior state didn't match.
444
+ */
445
+ export function progressFollowupState(
446
+ id: string,
447
+ newState: FollowupState,
448
+ action?: FollowupAction,
449
+ ): GuardianActionRequest | null {
450
+ const request = getGuardianActionRequest(id);
451
+ if (!request) return null;
452
+
453
+ const allowed = FOLLOWUP_TRANSITIONS[request.followupState];
454
+ if (!allowed.includes(newState)) return null;
455
+
456
+ const db = getDb();
457
+ const now = Date.now();
458
+
459
+ const updates: Record<string, unknown> = {
460
+ followupState: newState,
461
+ updatedAt: now,
462
+ };
463
+ if (action !== undefined) updates.followupAction = action;
464
+
465
+ db.update(guardianActionRequests)
466
+ .set(updates)
467
+ .where(
468
+ and(
469
+ eq(guardianActionRequests.id, id),
470
+ eq(guardianActionRequests.status, 'expired'),
471
+ eq(guardianActionRequests.followupState, request.followupState),
472
+ ),
473
+ )
474
+ .run();
475
+
476
+ if (rawChanges() === 0) return null;
477
+ return getGuardianActionRequest(id);
478
+ }
479
+
480
+ /**
481
+ * Finalize a follow-up by setting the terminal followup_state and
482
+ * recording followup_completed_at. Only succeeds from a non-terminal state
483
+ * and only on expired requests.
484
+ */
485
+ export function finalizeFollowup(
486
+ id: string,
487
+ finalState: 'completed' | 'declined' | 'failed',
488
+ ): GuardianActionRequest | null {
489
+ const request = getGuardianActionRequest(id);
490
+ if (!request) return null;
491
+
492
+ const allowed = FOLLOWUP_FINALIZE_TRANSITIONS[request.followupState];
493
+ if (!allowed?.includes(finalState)) return null;
494
+
495
+ const db = getDb();
496
+ const now = Date.now();
497
+
498
+ db.update(guardianActionRequests)
499
+ .set({
500
+ followupState: finalState,
501
+ followupCompletedAt: now,
502
+ updatedAt: now,
503
+ })
504
+ .where(
505
+ and(
506
+ eq(guardianActionRequests.id, id),
507
+ eq(guardianActionRequests.status, 'expired'),
508
+ eq(guardianActionRequests.followupState, request.followupState),
509
+ ),
510
+ )
511
+ .run();
512
+
513
+ if (rawChanges() === 0) return null;
514
+ return getGuardianActionRequest(id);
515
+ }
516
+
306
517
  // ---------------------------------------------------------------------------
307
518
  // Guardian Action Deliveries
308
519
  // ---------------------------------------------------------------------------
@@ -411,6 +622,158 @@ export function getPendingDeliveryByConversation(conversationId: string): Guardi
411
622
  }
412
623
  }
413
624
 
625
+ /**
626
+ * Look up sent deliveries for expired requests eligible for follow-up.
627
+ * Used by inbound message routing to match late guardian answers to expired requests.
628
+ */
629
+ export function getExpiredDeliveriesByDestination(
630
+ assistantId: string,
631
+ channel: string,
632
+ chatId: string,
633
+ ): GuardianActionDelivery[] {
634
+ try {
635
+ const db = getDb();
636
+
637
+ const rows = db
638
+ .select({
639
+ delivery: guardianActionDeliveries,
640
+ })
641
+ .from(guardianActionDeliveries)
642
+ .innerJoin(
643
+ guardianActionRequests,
644
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
645
+ )
646
+ .where(
647
+ and(
648
+ eq(guardianActionRequests.assistantId, assistantId),
649
+ eq(guardianActionRequests.status, 'expired'),
650
+ eq(guardianActionRequests.followupState, 'none'),
651
+ eq(guardianActionDeliveries.destinationChannel, channel),
652
+ eq(guardianActionDeliveries.destinationChatId, chatId),
653
+ eq(guardianActionDeliveries.status, 'expired'),
654
+ ),
655
+ )
656
+ .all();
657
+
658
+ return rows.map((r) => rowToDelivery(r.delivery));
659
+ } catch (err) {
660
+ if (err instanceof Error && err.message.includes('no such table')) {
661
+ log.warn({ err }, 'guardian tables not yet created');
662
+ return [];
663
+ }
664
+ throw err;
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Look up an expired delivery by destination conversation ID (for mac channel routing).
670
+ */
671
+ export function getExpiredDeliveryByConversation(conversationId: string): GuardianActionDelivery | null {
672
+ try {
673
+ const db = getDb();
674
+ const rows = db
675
+ .select({ delivery: guardianActionDeliveries })
676
+ .from(guardianActionDeliveries)
677
+ .innerJoin(
678
+ guardianActionRequests,
679
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
680
+ )
681
+ .where(
682
+ and(
683
+ eq(guardianActionDeliveries.destinationConversationId, conversationId),
684
+ eq(guardianActionDeliveries.status, 'expired'),
685
+ eq(guardianActionRequests.status, 'expired'),
686
+ eq(guardianActionRequests.followupState, 'none'),
687
+ ),
688
+ )
689
+ .all();
690
+ return rows.length > 0 ? rowToDelivery(rows[0].delivery) : null;
691
+ } catch (err) {
692
+ if (err instanceof Error && err.message.includes('no such table')) {
693
+ log.warn({ err }, 'guardian tables not yet created');
694
+ return null;
695
+ }
696
+ throw err;
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Look up deliveries for requests in `awaiting_guardian_choice` follow-up state.
702
+ * Used by inbound message routing to intercept guardian follow-up replies
703
+ * on channel paths (Telegram, SMS).
704
+ */
705
+ export function getFollowupDeliveriesByDestination(
706
+ assistantId: string,
707
+ channel: string,
708
+ chatId: string,
709
+ ): GuardianActionDelivery[] {
710
+ try {
711
+ const db = getDb();
712
+
713
+ const rows = db
714
+ .select({
715
+ delivery: guardianActionDeliveries,
716
+ })
717
+ .from(guardianActionDeliveries)
718
+ .innerJoin(
719
+ guardianActionRequests,
720
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
721
+ )
722
+ .where(
723
+ and(
724
+ eq(guardianActionRequests.assistantId, assistantId),
725
+ eq(guardianActionRequests.status, 'expired'),
726
+ eq(guardianActionRequests.followupState, 'awaiting_guardian_choice'),
727
+ eq(guardianActionDeliveries.destinationChannel, channel),
728
+ eq(guardianActionDeliveries.destinationChatId, chatId),
729
+ eq(guardianActionDeliveries.status, 'expired'),
730
+ ),
731
+ )
732
+ .all();
733
+
734
+ return rows.map((r) => rowToDelivery(r.delivery));
735
+ } catch (err) {
736
+ if (err instanceof Error && err.message.includes('no such table')) {
737
+ log.warn({ err }, 'guardian tables not yet created');
738
+ return [];
739
+ }
740
+ throw err;
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Look up a delivery for a request in `awaiting_guardian_choice` follow-up
746
+ * state by destination conversation ID (for mac channel routing).
747
+ */
748
+ export function getFollowupDeliveryByConversation(conversationId: string): GuardianActionDelivery | null {
749
+ try {
750
+ const db = getDb();
751
+ const rows = db
752
+ .select({ delivery: guardianActionDeliveries })
753
+ .from(guardianActionDeliveries)
754
+ .innerJoin(
755
+ guardianActionRequests,
756
+ eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
757
+ )
758
+ .where(
759
+ and(
760
+ eq(guardianActionDeliveries.destinationConversationId, conversationId),
761
+ eq(guardianActionDeliveries.status, 'expired'),
762
+ eq(guardianActionRequests.status, 'expired'),
763
+ eq(guardianActionRequests.followupState, 'awaiting_guardian_choice'),
764
+ ),
765
+ )
766
+ .all();
767
+ return rows.length > 0 ? rowToDelivery(rows[0].delivery) : null;
768
+ } catch (err) {
769
+ if (err instanceof Error && err.message.includes('no such table')) {
770
+ log.warn({ err }, 'guardian tables not yet created');
771
+ return null;
772
+ }
773
+ throw err;
774
+ }
775
+ }
776
+
414
777
  export function updateDeliveryStatus(
415
778
  deliveryId: string,
416
779
  status: GuardianActionDeliveryStatus,