@vellumai/assistant 0.3.16 → 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 (90) hide show
  1. package/ARCHITECTURE.md +70 -13
  2. package/README.md +6 -0
  3. package/docs/architecture/http-token-refresh.md +23 -1
  4. package/package.json +1 -1
  5. package/src/__tests__/access-request-decision.test.ts +4 -7
  6. package/src/__tests__/channel-guardian.test.ts +3 -1
  7. package/src/__tests__/checker.test.ts +79 -48
  8. package/src/__tests__/config-watcher.test.ts +11 -13
  9. package/src/__tests__/conversation-pairing.test.ts +103 -3
  10. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -1
  11. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -1
  12. package/src/__tests__/guardian-action-late-reply.test.ts +131 -0
  13. package/src/__tests__/guardian-action-store.test.ts +182 -0
  14. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  15. package/src/__tests__/ipc-snapshot.test.ts +21 -0
  16. package/src/__tests__/non-member-access-request.test.ts +1 -2
  17. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  18. package/src/__tests__/notification-decision-strategy.test.ts +2 -1
  19. package/src/__tests__/notification-deep-link.test.ts +44 -1
  20. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  21. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  22. package/src/__tests__/slack-channel-config.test.ts +3 -3
  23. package/src/__tests__/trust-store.test.ts +21 -21
  24. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +5 -7
  25. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  26. package/src/__tests__/trusted-contact-verification.test.ts +9 -9
  27. package/src/__tests__/update-bulletin-state.test.ts +1 -1
  28. package/src/__tests__/update-bulletin.test.ts +66 -3
  29. package/src/__tests__/update-template-contract.test.ts +6 -11
  30. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  31. package/src/calls/call-controller.ts +129 -8
  32. package/src/calls/guardian-action-sweep.ts +1 -1
  33. package/src/calls/guardian-dispatch.ts +8 -0
  34. package/src/calls/voice-session-bridge.ts +4 -2
  35. package/src/cli/core-commands.ts +41 -1
  36. package/src/config/templates/UPDATES.md +5 -6
  37. package/src/config/update-bulletin-format.ts +2 -0
  38. package/src/config/update-bulletin-state.ts +1 -1
  39. package/src/config/update-bulletin-template-path.ts +6 -0
  40. package/src/config/update-bulletin.ts +21 -6
  41. package/src/daemon/config-watcher.ts +3 -2
  42. package/src/daemon/daemon-control.ts +64 -10
  43. package/src/daemon/handlers/config-slack-channel.ts +1 -1
  44. package/src/daemon/handlers/identity.ts +45 -25
  45. package/src/daemon/handlers/sessions.ts +1 -1
  46. package/src/daemon/ipc-contract/sessions.ts +1 -1
  47. package/src/daemon/ipc-contract/workspace.ts +12 -1
  48. package/src/daemon/ipc-contract-inventory.json +1 -0
  49. package/src/daemon/lifecycle.ts +8 -0
  50. package/src/daemon/server.ts +25 -3
  51. package/src/daemon/session-process.ts +438 -184
  52. package/src/daemon/tls-certs.ts +17 -12
  53. package/src/daemon/tool-side-effects.ts +1 -1
  54. package/src/memory/channel-delivery-store.ts +18 -20
  55. package/src/memory/channel-guardian-store.ts +39 -42
  56. package/src/memory/conversation-crud.ts +2 -2
  57. package/src/memory/conversation-queries.ts +2 -2
  58. package/src/memory/conversation-store.ts +24 -25
  59. package/src/memory/db-init.ts +9 -1
  60. package/src/memory/fts-reconciler.ts +41 -26
  61. package/src/memory/guardian-action-store.ts +57 -7
  62. package/src/memory/guardian-verification.ts +1 -0
  63. package/src/memory/jobs-worker.ts +2 -2
  64. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  65. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  66. package/src/memory/migrations/index.ts +4 -2
  67. package/src/memory/schema-migration.ts +1 -0
  68. package/src/memory/schema.ts +6 -1
  69. package/src/memory/search/semantic.ts +3 -3
  70. package/src/notifications/README.md +158 -17
  71. package/src/notifications/broadcaster.ts +68 -50
  72. package/src/notifications/conversation-pairing.ts +96 -18
  73. package/src/notifications/decision-engine.ts +6 -3
  74. package/src/notifications/deliveries-store.ts +12 -0
  75. package/src/notifications/emit-signal.ts +1 -0
  76. package/src/notifications/thread-candidates.ts +60 -25
  77. package/src/notifications/types.ts +2 -1
  78. package/src/permissions/checker.ts +1 -16
  79. package/src/permissions/defaults.ts +14 -4
  80. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  81. package/src/runtime/http-server.ts +11 -11
  82. package/src/runtime/routes/access-request-decision.ts +1 -1
  83. package/src/runtime/routes/debug-routes.ts +4 -4
  84. package/src/runtime/routes/guardian-approval-interception.ts +4 -4
  85. package/src/runtime/routes/inbound-message-handler.ts +6 -6
  86. package/src/runtime/routes/integration-routes.ts +2 -2
  87. package/src/tools/permission-checker.ts +1 -2
  88. package/src/tools/secret-detection-handler.ts +1 -1
  89. package/src/tools/system/voice-config.ts +1 -1
  90. package/src/version.ts +29 -2
@@ -152,20 +152,25 @@ export async function ensureTlsCert(): Promise<{ cert: string; key: string; fing
152
152
  }
153
153
 
154
154
  if (status === 'approaching_expiry') {
155
- // Buffer existing cert/key/fingerprint before attempting renewal.
156
- // generateNewCert() overwrites key.pem in-place, so if it fails mid-flight
157
- // (e.g., key written but cert generation fails), reading from disk in the
158
- // catch block would return a mismatched key/cert pair.
159
- const [existingCert, existingKey, existingFp] = await Promise.all([
160
- readFile(certPath, 'utf-8'),
161
- readFile(keyPath, 'utf-8'),
162
- readFile(fpPath, 'utf-8'),
163
- ]);
164
155
  try {
165
- return await generateNewCert(tlsDir, certPath, keyPath, fpPath);
156
+ // Buffer existing cert/key/fingerprint before attempting renewal.
157
+ // generateNewCert() overwrites key.pem in-place, so if it fails mid-flight
158
+ // (e.g., key written but cert generation fails), reading from disk in the
159
+ // catch block would return a mismatched key/cert pair.
160
+ const [existingCert, existingKey, existingFp] = await Promise.all([
161
+ readFile(certPath, 'utf-8'),
162
+ readFile(keyPath, 'utf-8'),
163
+ readFile(fpPath, 'utf-8'),
164
+ ]);
165
+ try {
166
+ return await generateNewCert(tlsDir, certPath, keyPath, fpPath);
167
+ } catch (err) {
168
+ log.warn({ err }, 'Proactive TLS renewal failed, continuing with existing certificate');
169
+ return { cert: existingCert, key: existingKey, fingerprint: existingFp.trim() };
170
+ }
166
171
  } catch (err) {
167
- log.warn({ err }, 'Proactive TLS renewal failed, continuing with existing certificate');
168
- return { cert: existingCert, key: existingKey, fingerprint: existingFp.trim() };
172
+ log.warn({ err }, 'Failed to read existing TLS cert for buffering, attempting regeneration');
173
+ return await generateNewCert(tlsDir, certPath, keyPath, fpPath);
169
174
  }
170
175
  }
171
176
 
@@ -7,11 +7,11 @@
7
7
  * registry entry instead of another if/else branch.
8
8
  */
9
9
 
10
- import { normalizeActivationKey } from './handlers/config-voice.js';
11
10
  import { updatePublishedAppDeployment } from '../services/published-app-updater.js';
12
11
  import { openAppViaSurface } from '../tools/apps/open-proxy.js';
13
12
  import type { ToolExecutionResult } from '../tools/types.js';
14
13
  import { isDoordashCommand, updateDoordashProgress } from './doordash-steps.js';
14
+ import { normalizeActivationKey } from './handlers/config-voice.js';
15
15
  import type { ServerMessage } from './ipc-protocol.js';
16
16
  import {
17
17
  refreshSurfacesForApp,
@@ -8,33 +8,31 @@
8
8
  * - delivery-channels.ts — verification replies, segment progress, delivery guards
9
9
  */
10
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';
11
23
  export {
12
- recordInbound,
13
- linkMessage,
14
- findMessageBySourceId,
15
- storePayload,
16
24
  clearPayload,
25
+ findMessageBySourceId,
17
26
  getLatestStoredPayload,
27
+ linkMessage,
28
+ recordInbound,
29
+ storePayload,
18
30
  } from './delivery-crud.js';
19
- export type { InboundResult, RecordInboundOptions } from './delivery-crud.js';
20
-
21
31
  export {
22
32
  acknowledgeDelivery,
33
+ getDeadLetterEvents,
34
+ getRetryableEvents,
23
35
  markProcessed,
24
36
  recordProcessingFailure,
25
- getRetryableEvents,
26
- getDeadLetterEvents,
27
37
  replayDeadLetters,
28
38
  } from './delivery-status.js';
29
-
30
- export {
31
- storePendingVerificationReply,
32
- getPendingVerificationReply,
33
- clearPendingVerificationReply,
34
- getDeliveredSegmentCount,
35
- updateDeliveredSegmentCount,
36
- claimRunDelivery,
37
- resetRunDeliveryClaim,
38
- resetAllRunDeliveryClaims,
39
- } from './delivery-channels.js';
40
- export type { PendingVerificationReply } from './delivery-channels.js';
@@ -10,60 +10,57 @@
10
10
  * This file re-exports everything for backward compatibility.
11
11
  */
12
12
 
13
+ export {
14
+ type ApprovalRequestStatus,
15
+ countPendingByConversation,
16
+ createApprovalRequest,
17
+ findPendingAccessRequestForRequester,
18
+ getAllPendingApprovalsByGuardianChat,
19
+ getApprovalRequestById,
20
+ getApprovalRequestByRunId,
21
+ getExpiredPendingApprovals,
22
+ getPendingApprovalByGuardianChat,
23
+ getPendingApprovalByRequestAndGuardianChat,
24
+ getPendingApprovalByRunAndGuardianChat,
25
+ getPendingApprovalForRequest,
26
+ getPendingApprovalForRun,
27
+ getUnresolvedApprovalForRequest,
28
+ getUnresolvedApprovalForRun,
29
+ type GuardianApprovalRequest,
30
+ listPendingApprovalRequests,
31
+ resolveApprovalRequest,
32
+ updateApprovalDecision,
33
+ } from './guardian-approvals.js';
13
34
  export {
14
35
  type BindingStatus,
15
- type GuardianBinding,
16
36
  createBinding,
17
37
  getActiveBinding,
38
+ type GuardianBinding,
18
39
  revokeBinding,
19
40
  } from './guardian-bindings.js';
20
-
21
41
  export {
42
+ getRateLimit,
43
+ recordInvalidAttempt,
44
+ resetRateLimit,
45
+ type VerificationRateLimit,
46
+ } from './guardian-rate-limits.js';
47
+ export {
48
+ bindSessionIdentity,
22
49
  type ChallengeStatus,
23
- type SessionStatus,
24
- type IdentityBindingStatus,
25
- type VerificationPurpose,
26
- type VerificationChallenge,
27
- createChallenge,
28
- revokePendingChallenges,
29
- findPendingChallengeByHash,
30
- findPendingChallengeForChannel,
31
50
  consumeChallenge,
51
+ countRecentSendsToDestination,
52
+ createChallenge,
32
53
  createVerificationSession,
33
54
  findActiveSession,
55
+ findPendingChallengeByHash,
56
+ findPendingChallengeForChannel,
34
57
  findSessionByBootstrapTokenHash,
35
58
  findSessionByIdentity,
36
- updateSessionStatus,
59
+ type IdentityBindingStatus,
60
+ revokePendingChallenges,
61
+ type SessionStatus,
37
62
  updateSessionDelivery,
38
- countRecentSendsToDestination,
39
- bindSessionIdentity,
63
+ updateSessionStatus,
64
+ type VerificationChallenge,
65
+ type VerificationPurpose,
40
66
  } from './guardian-verification.js';
41
-
42
- export {
43
- type ApprovalRequestStatus,
44
- type GuardianApprovalRequest,
45
- createApprovalRequest,
46
- getPendingApprovalForRun,
47
- getPendingApprovalForRequest,
48
- getUnresolvedApprovalForRun,
49
- getUnresolvedApprovalForRequest,
50
- getPendingApprovalByGuardianChat,
51
- getPendingApprovalByRunAndGuardianChat,
52
- getPendingApprovalByRequestAndGuardianChat,
53
- getAllPendingApprovalsByGuardianChat,
54
- getExpiredPendingApprovals,
55
- updateApprovalDecision,
56
- listPendingApprovalRequests,
57
- getApprovalRequestById,
58
- getApprovalRequestByRunId,
59
- resolveApprovalRequest,
60
- countPendingByConversation,
61
- findPendingAccessRequestForRequester,
62
- } from './guardian-approvals.js';
63
-
64
- export {
65
- type VerificationRateLimit,
66
- getRateLimit,
67
- recordInvalidAttempt,
68
- resetRateLimit,
69
- } from './guardian-rate-limits.js';
@@ -1,4 +1,4 @@
1
- import { and, count, eq, inArray, isNull, sql, asc } from 'drizzle-orm';
1
+ import { and, asc,count, eq, inArray, isNull, sql } from 'drizzle-orm';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import { z } from 'zod';
4
4
 
@@ -11,7 +11,7 @@ import { getLogger } from '../util/logger.js';
11
11
  import { createRowMapper } from '../util/row-mapper.js';
12
12
  import { deleteOrphanAttachments } from './attachments-store.js';
13
13
  import { projectAssistantMessage } from './conversation-attention-store.js';
14
- import { getDb, rawAll, rawExec, rawGet } from './db.js';
14
+ import { getDb, rawExec, rawGet } from './db.js';
15
15
  import { indexMessageNow } from './indexer.js';
16
16
  import { channelInboundEvents, conversations, llmRequestLogs, memoryEmbeddings, memoryItemEntities, memoryItems, memoryItemSources, memorySegments, messageAttachments, messages, toolInvocations } from './schema.js';
17
17
 
@@ -1,9 +1,9 @@
1
1
  import { and, asc, count, desc, eq, gte, lt, ne, or, sql } from 'drizzle-orm';
2
2
 
3
3
  import { getLogger } from '../util/logger.js';
4
- import { getDb, rawAll } from './db.js';
5
- import { parseConversation, parseMessage } from './conversation-crud.js';
6
4
  import type { ConversationRow, MessageRow } from './conversation-crud.js';
5
+ import { parseConversation, parseMessage } from './conversation-crud.js';
6
+ import { getDb, rawAll } from './db.js';
7
7
  import { conversations, messages } from './schema.js';
8
8
  import { buildFtsMatchQuery } from './search/lexical.js';
9
9
 
@@ -2,44 +2,43 @@
2
2
  // Existing imports from this file continue to work without changes.
3
3
 
4
4
  export {
5
- messageMetadataSchema,
6
- type MessageMetadata,
7
- provenanceFromGuardianContext,
5
+ addMessage,
6
+ clearAll,
8
7
  type ConversationRow,
9
- parseConversation,
10
- type MessageRow,
11
- parseMessage,
12
8
  createConversation,
9
+ deleteConversation,
10
+ type DeletedMemoryIds,
11
+ deleteLastExchange,
12
+ deleteMessageById,
13
13
  getConversation,
14
- getConversationThreadType,
15
14
  getConversationMemoryScopeId,
16
- deleteConversation,
17
- addMessage,
18
- getMessages,
15
+ getConversationOriginChannel,
16
+ getConversationOriginInterface,
17
+ getConversationThreadType,
19
18
  getMessageById,
20
- updateConversationTitle,
21
- updateConversationUsage,
22
- updateConversationContextWindow,
23
- clearAll,
24
- deleteLastExchange,
25
- type DeletedMemoryIds,
26
- updateMessageContent,
19
+ getMessages,
20
+ type MessageMetadata,
21
+ messageMetadataSchema,
22
+ type MessageRow,
23
+ parseConversation,
24
+ parseMessage,
25
+ provenanceFromGuardianContext,
27
26
  relinkAttachments,
28
- deleteMessageById,
29
27
  setConversationOriginChannelIfUnset,
30
- getConversationOriginChannel,
31
28
  setConversationOriginInterfaceIfUnset,
32
- getConversationOriginInterface,
29
+ updateConversationContextWindow,
30
+ updateConversationTitle,
31
+ updateConversationUsage,
32
+ updateMessageContent,
33
33
  } from './conversation-crud.js';
34
-
35
34
  export {
36
- listConversations,
35
+ type ConversationSearchResult,
37
36
  countConversations,
38
37
  getLatestConversation,
39
- getNextMessage,
40
- type PaginatedMessagesResult,
41
38
  getMessagesPaginated,
39
+ getNextMessage,
42
40
  isLastUserMessageToolResult,
43
- type ConversationSearchResult,
41
+ listConversations,
42
+ type PaginatedMessagesResult,
44
43
  searchConversations,
45
44
  } from './conversation-queries.js';
@@ -17,14 +17,16 @@ import {
17
17
  createTasksAndWorkItemsTables,
18
18
  createWatchersAndLogsTables,
19
19
  migrateCallSessionMode,
20
- migrateFkCascadeRebuilds,
21
20
  migrateChannelInboundDeliveredSegments,
22
21
  migrateConversationsThreadTypeIndex,
22
+ migrateFkCascadeRebuilds,
23
23
  migrateGuardianActionFollowup,
24
24
  migrateGuardianBootstrapToken,
25
+ migrateGuardianDeliveryConversationIndex,
25
26
  migrateGuardianVerificationPurpose,
26
27
  migrateGuardianVerificationSessions,
27
28
  migrateMessagesFtsBackfill,
29
+ migrateNotificationDeliveryThreadDecision,
28
30
  migrateReminderRoutingIntent,
29
31
  migrateSchemaIndexesAndColumns,
30
32
  recoverCrashedMigrations,
@@ -103,6 +105,9 @@ export function initializeDb(): void {
103
105
  // 14d. Index on conversations.thread_type for frequent WHERE filters
104
106
  migrateConversationsThreadTypeIndex(database);
105
107
 
108
+ // 14e. Index on guardian_action_deliveries.destination_conversation_id for conversation-based lookups
109
+ migrateGuardianDeliveryConversationIndex(database);
110
+
106
111
  // 15. Notification system
107
112
  createNotificationTables(database);
108
113
 
@@ -125,5 +130,8 @@ export function initializeDb(): void {
125
130
  // 21. Rebuild tables to add ON DELETE CASCADE to FK constraints
126
131
  migrateFkCascadeRebuilds(database);
127
132
 
133
+ // 22. Thread decision audit columns on notification_deliveries
134
+ migrateNotificationDeliveryThreadDecision(database);
135
+
128
136
  validateMigrationState(database);
129
137
  }
@@ -84,37 +84,52 @@ function reconcileTable(opts: {
84
84
  */
85
85
  export function reconcileFtsIndexes(): FtsReconciliationResult[] {
86
86
  const results: FtsReconciliationResult[] = [];
87
+ const errors: unknown[] = [];
87
88
 
88
89
  // memory_segment_fts tracks memory_segments
89
- const memResult = reconcileTable({
90
- ftsTable: 'memory_segment_fts',
91
- ftsIdColumn: 'segment_id',
92
- ftsContentColumn: 'text',
93
- baseTable: 'memory_segments',
94
- baseIdColumn: 'id',
95
- baseContentColumn: 'text',
96
- });
97
- results.push(memResult);
98
- if (memResult.missingInserted > 0 || memResult.orphansRemoved > 0 || memResult.staleRefreshed > 0) {
99
- log.info(memResult, 'Reconciled memory_segment_fts');
100
- } else {
101
- log.debug(memResult, 'memory_segment_fts is in sync');
90
+ try {
91
+ const memResult = reconcileTable({
92
+ ftsTable: 'memory_segment_fts',
93
+ ftsIdColumn: 'segment_id',
94
+ ftsContentColumn: 'text',
95
+ baseTable: 'memory_segments',
96
+ baseIdColumn: 'id',
97
+ baseContentColumn: 'text',
98
+ });
99
+ results.push(memResult);
100
+ if (memResult.missingInserted > 0 || memResult.orphansRemoved > 0 || memResult.staleRefreshed > 0) {
101
+ log.info(memResult, 'Reconciled memory_segment_fts');
102
+ } else {
103
+ log.debug(memResult, 'memory_segment_fts is in sync');
104
+ }
105
+ } catch (err) {
106
+ log.error({ err }, 'Failed to reconcile memory_segment_fts');
107
+ errors.push(err);
102
108
  }
103
109
 
104
110
  // messages_fts tracks messages
105
- const msgResult = reconcileTable({
106
- ftsTable: 'messages_fts',
107
- ftsIdColumn: 'message_id',
108
- ftsContentColumn: 'content',
109
- baseTable: 'messages',
110
- baseIdColumn: 'id',
111
- baseContentColumn: 'content',
112
- });
113
- results.push(msgResult);
114
- if (msgResult.missingInserted > 0 || msgResult.orphansRemoved > 0 || msgResult.staleRefreshed > 0) {
115
- log.info(msgResult, 'Reconciled messages_fts');
116
- } else {
117
- log.debug(msgResult, 'messages_fts is in sync');
111
+ try {
112
+ const msgResult = reconcileTable({
113
+ ftsTable: 'messages_fts',
114
+ ftsIdColumn: 'message_id',
115
+ ftsContentColumn: 'content',
116
+ baseTable: 'messages',
117
+ baseIdColumn: 'id',
118
+ baseContentColumn: 'content',
119
+ });
120
+ results.push(msgResult);
121
+ if (msgResult.missingInserted > 0 || msgResult.orphansRemoved > 0 || msgResult.staleRefreshed > 0) {
122
+ log.info(msgResult, 'Reconciled messages_fts');
123
+ } else {
124
+ log.debug(msgResult, 'messages_fts is in sync');
125
+ }
126
+ } catch (err) {
127
+ log.error({ err }, 'Failed to reconcile messages_fts');
128
+ errors.push(err);
129
+ }
130
+
131
+ if (errors.length > 0) {
132
+ throw new AggregateError(errors, `FTS reconciliation failed for ${errors.length} table(s)`);
118
133
  }
119
134
 
120
135
  return results;
@@ -7,7 +7,7 @@
7
7
  * answer resolves the request and all other deliveries are marked answered.
8
8
  */
9
9
 
10
- import { and, desc, 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';
@@ -212,6 +212,26 @@ export function getPendingRequestByCallSessionId(callSessionId: string): Guardia
212
212
  return row ? rowToRequest(row) : null;
213
213
  }
214
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
+
215
235
  /**
216
236
  * First-response-wins resolution. Checks that the request is still
217
237
  * 'pending' before updating; returns the updated request on success
@@ -595,6 +615,16 @@ export function getPendingDeliveriesByDestination(
595
615
  * Look up a pending delivery by destination conversation ID (for mac channel routing).
596
616
  */
597
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[] {
598
628
  try {
599
629
  const db = getDb();
600
630
  const rows = db
@@ -612,11 +642,11 @@ export function getPendingDeliveryByConversation(conversationId: string): Guardi
612
642
  ),
613
643
  )
614
644
  .all();
615
- return rows.length > 0 ? rowToDelivery(rows[0].delivery) : null;
645
+ return rows.map((r) => rowToDelivery(r.delivery));
616
646
  } catch (err) {
617
647
  if (err instanceof Error && err.message.includes('no such table')) {
618
648
  log.warn({ err }, 'guardian tables not yet created');
619
- return null;
649
+ return [];
620
650
  }
621
651
  throw err;
622
652
  }
@@ -669,6 +699,16 @@ export function getExpiredDeliveriesByDestination(
669
699
  * Look up an expired delivery by destination conversation ID (for mac channel routing).
670
700
  */
671
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[] {
672
712
  try {
673
713
  const db = getDb();
674
714
  const rows = db
@@ -687,11 +727,11 @@ export function getExpiredDeliveryByConversation(conversationId: string): Guardi
687
727
  ),
688
728
  )
689
729
  .all();
690
- return rows.length > 0 ? rowToDelivery(rows[0].delivery) : null;
730
+ return rows.map((r) => rowToDelivery(r.delivery));
691
731
  } catch (err) {
692
732
  if (err instanceof Error && err.message.includes('no such table')) {
693
733
  log.warn({ err }, 'guardian tables not yet created');
694
- return null;
734
+ return [];
695
735
  }
696
736
  throw err;
697
737
  }
@@ -746,6 +786,16 @@ export function getFollowupDeliveriesByDestination(
746
786
  * state by destination conversation ID (for mac channel routing).
747
787
  */
748
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[] {
749
799
  try {
750
800
  const db = getDb();
751
801
  const rows = db
@@ -764,11 +814,11 @@ export function getFollowupDeliveryByConversation(conversationId: string): Guard
764
814
  ),
765
815
  )
766
816
  .all();
767
- return rows.length > 0 ? rowToDelivery(rows[0].delivery) : null;
817
+ return rows.map((r) => rowToDelivery(r.delivery));
768
818
  } catch (err) {
769
819
  if (err instanceof Error && err.message.includes('no such table')) {
770
820
  log.warn({ err }, 'guardian tables not yet created');
771
- return null;
821
+ return [];
772
822
  }
773
823
  throw err;
774
824
  }
@@ -132,6 +132,7 @@ export function createChallenge(params: {
132
132
  codeDigits: 6,
133
133
  maxAttempts: 3,
134
134
  bootstrapTokenHash: null,
135
+ verificationPurpose: 'guardian' as const,
135
136
  createdAt: now,
136
137
  updatedAt: now,
137
138
  };
@@ -2,6 +2,7 @@ import { getConfig } from '../config/loader.js';
2
2
  import type { AssistantConfig } from '../config/types.js';
3
3
  import { getLogger } from '../util/logger.js';
4
4
  import { rawRun } from './db.js';
5
+ import { reconcileFtsIndexes } from './fts-reconciler.js';
5
6
  import { backfillEntityRelationsJob,backfillJob } from './job-handlers/backfill.js';
6
7
  import { checkContradictionsJob, cleanupStaleSupersededItemsJob, pruneOldConversationsJob } from './job-handlers/cleanup.js';
7
8
  import { cleanupResolvedConflictsJob,resolvePendingConflictsForMessageJob } from './job-handlers/conflict.js';
@@ -9,7 +10,6 @@ import { cleanupResolvedConflictsJob,resolvePendingConflictsForMessageJob } from
9
10
  import { embedItemJob, embedSegmentJob, embedSummaryJob } from './job-handlers/embedding.js';
10
11
  import { extractEntitiesJob,extractItemsJob } from './job-handlers/extraction.js';
11
12
  import { deleteQdrantVectorsJob,rebuildIndexJob } from './job-handlers/index-maintenance.js';
12
- import { reconcileFtsIndexes } from './fts-reconciler.js';
13
13
  import { mediaProcessingJob } from './job-handlers/media-processing.js';
14
14
  import { buildConversationSummaryJob, buildGlobalSummaryJob } from './job-handlers/summarization.js';
15
15
  import {
@@ -18,7 +18,6 @@ import {
18
18
  RETRY_MAX_ATTEMPTS,
19
19
  retryDelayForAttempt,
20
20
  } from './job-utils.js';
21
- import { QdrantCircuitOpenError } from './qdrant-circuit-breaker.js';
22
21
  import {
23
22
  claimMemoryJobs,
24
23
  completeMemoryJob,
@@ -32,6 +31,7 @@ import {
32
31
  type MemoryJob,
33
32
  resetRunningJobsToPending,
34
33
  } from './jobs-store.js';
34
+ import { QdrantCircuitOpenError } from './qdrant-circuit-breaker.js';
35
35
  import { bumpMemoryVersion } from './recall-cache.js';
36
36
 
37
37
  // Re-export public utilities consumed by tests and other modules
@@ -0,0 +1,15 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Add index on guardian_action_deliveries.destination_conversation_id.
5
+ *
6
+ * Several lookup paths (getPendingDeliveryByConversation,
7
+ * getExpiredDeliveryByConversation, getFollowupDeliveryByConversation)
8
+ * filter deliveries by destination_conversation_id. Without an index
9
+ * these degrade to full table scans as delivery history grows.
10
+ */
11
+ export function migrateGuardianDeliveryConversationIndex(database: DrizzleDb): void {
12
+ database.run(
13
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_action_deliveries_dest_conversation ON guardian_action_deliveries(destination_conversation_id)`,
14
+ );
15
+ }
@@ -0,0 +1,20 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Add thread decision audit columns to notification_deliveries.
5
+ *
6
+ * These columns record the model's per-channel thread action (start_new
7
+ * or reuse_existing), the target conversation ID for reuse, and whether
8
+ * a fallback to start_new was needed due to an invalid/stale target.
9
+ */
10
+ export function migrateNotificationDeliveryThreadDecision(database: DrizzleDb): void {
11
+ try {
12
+ database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN thread_action TEXT`);
13
+ } catch { /* Column already exists */ }
14
+ try {
15
+ database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN thread_target_conversation_id TEXT`);
16
+ } catch { /* Column already exists */ }
17
+ try {
18
+ database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN thread_decision_fallback_used INTEGER`);
19
+ } catch { /* Column already exists */ }
20
+ }
@@ -23,15 +23,17 @@ export { migrateAddOriginInterface } from './022-add-origin-interface.js';
23
23
  export { migrateMemoryItemSourcesIndexes } from './023-memory-item-sources-indexes.js';
24
24
  export { migrateEmbeddingVectorBlob } from './024-embedding-vector-blob.js';
25
25
  export { migrateMessagesFtsBackfill } from './025-messages-fts-backfill.js';
26
- export { migrateEmbeddingsNullableVectorJson } from './026a-embeddings-nullable-vector-json.js';
27
26
  export { migrateGuardianVerificationSessions } from './026-guardian-verification-sessions.js';
28
- export { migrateGuardianBootstrapToken } from './027a-guardian-bootstrap-token.js';
27
+ export { migrateEmbeddingsNullableVectorJson } from './026a-embeddings-nullable-vector-json.js';
29
28
  export { migrateNotificationDeliveryPairingColumns } from './027-notification-delivery-pairing-columns.js';
29
+ export { migrateGuardianBootstrapToken } from './027a-guardian-bootstrap-token.js';
30
30
  export { migrateCallSessionMode } from './028-call-session-mode.js';
31
31
  export { migrateChannelInboundDeliveredSegments } from './029-channel-inbound-delivered-segments.js';
32
32
  export { migrateGuardianActionFollowup } from './030-guardian-action-followup.js';
33
33
  export { migrateGuardianVerificationPurpose } from './030-guardian-verification-purpose.js';
34
34
  export { migrateConversationsThreadTypeIndex } from './031-conversations-thread-type-index.js';
35
+ export { migrateGuardianDeliveryConversationIndex } from './032-guardian-delivery-conversation-index.js';
36
+ export { migrateNotificationDeliveryThreadDecision } from './032-notification-delivery-thread-decision.js';
35
37
  export { createCoreTables } from './100-core-tables.js';
36
38
  export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
37
39
  export { addCoreColumns } from './102-alter-table-columns.js';
@@ -20,6 +20,7 @@ export {
20
20
  migrateMemorySegmentsIndexes,
21
21
  migrateMessagesFtsBackfill,
22
22
  migrateNotificationDeliveryPairingColumns,
23
+ migrateNotificationDeliveryThreadDecision,
23
24
  migrateNotificationTablesSchema,
24
25
  migrateRemainingTableIndexes,
25
26
  migrateReminderRoutingIntent,