@vellumai/assistant 0.4.4 → 0.4.6

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 +4 -4
  2. package/README.md +6 -6
  3. package/bun.lock +6 -2
  4. package/docs/architecture/memory.md +4 -4
  5. package/package.json +2 -2
  6. package/src/__tests__/actor-token-service.test.ts +5 -2
  7. package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
  8. package/src/__tests__/call-controller.test.ts +78 -0
  9. package/src/__tests__/call-domain.test.ts +148 -10
  10. package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
  11. package/src/__tests__/call-pointer-messages.test.ts +105 -43
  12. package/src/__tests__/canonical-guardian-store.test.ts +44 -10
  13. package/src/__tests__/channel-approval-routes.test.ts +67 -65
  14. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
  15. package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
  16. package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
  17. package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
  18. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
  19. package/src/__tests__/guardian-grant-minting.test.ts +24 -24
  20. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
  21. package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
  22. package/src/__tests__/guardian-routing-state.test.ts +4 -4
  23. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
  24. package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
  25. package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
  26. package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
  27. package/src/__tests__/non-member-access-request.test.ts +50 -47
  28. package/src/__tests__/relay-server.test.ts +71 -0
  29. package/src/__tests__/send-endpoint-busy.test.ts +6 -0
  30. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
  31. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
  32. package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
  33. package/src/__tests__/system-prompt.test.ts +1 -0
  34. package/src/__tests__/tool-approval-handler.test.ts +1 -1
  35. package/src/__tests__/tool-grant-request-escalation.test.ts +9 -2
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
  39. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  40. package/src/approvals/guardian-decision-primitive.ts +29 -25
  41. package/src/approvals/guardian-request-resolvers.ts +9 -5
  42. package/src/calls/call-pointer-message-composer.ts +27 -85
  43. package/src/calls/call-pointer-messages.ts +54 -21
  44. package/src/calls/guardian-dispatch.ts +30 -0
  45. package/src/calls/relay-server.ts +13 -13
  46. package/src/config/system-prompt.ts +10 -3
  47. package/src/config/templates/BOOTSTRAP.md +6 -5
  48. package/src/config/templates/USER.md +1 -0
  49. package/src/config/user-reference.ts +44 -0
  50. package/src/daemon/handlers/guardian-actions.ts +5 -2
  51. package/src/daemon/handlers/sessions.ts +8 -3
  52. package/src/daemon/lifecycle.ts +109 -3
  53. package/src/daemon/server.ts +32 -24
  54. package/src/daemon/session-agent-loop.ts +4 -3
  55. package/src/daemon/session-lifecycle.ts +1 -9
  56. package/src/daemon/session-process.ts +2 -2
  57. package/src/daemon/session-runtime-assembly.ts +2 -0
  58. package/src/daemon/session-tool-setup.ts +10 -0
  59. package/src/daemon/session.ts +1 -0
  60. package/src/memory/canonical-guardian-store.ts +40 -0
  61. package/src/memory/conversation-crud.ts +26 -0
  62. package/src/memory/conversation-store.ts +1 -0
  63. package/src/memory/db-init.ts +8 -0
  64. package/src/memory/guardian-bindings.ts +4 -0
  65. package/src/memory/job-handlers/backfill.ts +2 -9
  66. package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
  67. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
  68. package/src/memory/migrations/index.ts +2 -0
  69. package/src/memory/migrations/registry.ts +5 -0
  70. package/src/memory/schema.ts +3 -0
  71. package/src/notifications/copy-composer.ts +2 -2
  72. package/src/runtime/access-request-helper.ts +43 -28
  73. package/src/runtime/actor-trust-resolver.ts +19 -14
  74. package/src/runtime/channel-guardian-service.ts +6 -0
  75. package/src/runtime/guardian-context-resolver.ts +6 -2
  76. package/src/runtime/guardian-reply-router.ts +33 -16
  77. package/src/runtime/guardian-vellum-migration.ts +29 -5
  78. package/src/runtime/http-types.ts +0 -13
  79. package/src/runtime/local-actor-identity.ts +19 -13
  80. package/src/runtime/middleware/actor-token.ts +2 -2
  81. package/src/runtime/routes/channel-delivery-routes.ts +5 -5
  82. package/src/runtime/routes/conversation-routes.ts +45 -35
  83. package/src/runtime/routes/guardian-action-routes.ts +7 -1
  84. package/src/runtime/routes/guardian-approval-interception.ts +52 -52
  85. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -0
  86. package/src/runtime/routes/inbound-conversation.ts +7 -7
  87. package/src/runtime/routes/inbound-message-handler.ts +105 -94
  88. package/src/runtime/tool-grant-request-helper.ts +1 -0
  89. package/src/util/logger.ts +10 -0
  90. package/src/daemon/call-pointer-generators.ts +0 -59
@@ -51,9 +51,9 @@ export interface ApprovalInterceptionParams {
51
51
  conversationId: string;
52
52
  callbackData?: string;
53
53
  content: string;
54
- externalChatId: string;
54
+ conversationExternalId: string;
55
55
  sourceChannel: ChannelId;
56
- senderExternalUserId?: string;
56
+ actorExternalId?: string;
57
57
  replyCallbackUrl: string;
58
58
  bearerToken?: string;
59
59
  guardianCtx: GuardianContext;
@@ -84,9 +84,9 @@ export async function handleApprovalInterception(
84
84
  conversationId,
85
85
  callbackData,
86
86
  content,
87
- externalChatId,
87
+ conversationExternalId,
88
88
  sourceChannel,
89
- senderExternalUserId,
89
+ actorExternalId,
90
90
  replyCallbackUrl,
91
91
  bearerToken,
92
92
  guardianCtx,
@@ -101,7 +101,7 @@ export async function handleApprovalInterception(
101
101
  // of a non-guardian requester.
102
102
  if (
103
103
  guardianCtx.trustClass === 'guardian' &&
104
- senderExternalUserId
104
+ actorExternalId
105
105
  ) {
106
106
  // Callback/button path: deterministic and takes priority.
107
107
  let callbackDecision: ApprovalDecisionResult | null = null;
@@ -113,14 +113,14 @@ export async function handleApprovalInterception(
113
113
  // the decision resolves to exactly the right approval even when
114
114
  // multiple approvals target the same guardian chat.
115
115
  let guardianApproval = callbackDecision?.requestId
116
- ? getPendingApprovalByRequestAndGuardianChat(callbackDecision.requestId, sourceChannel, externalChatId, assistantId)
116
+ ? getPendingApprovalByRequestAndGuardianChat(callbackDecision.requestId, sourceChannel, conversationExternalId, assistantId)
117
117
  : null;
118
118
 
119
119
  // When the scoped lookup didn't resolve an approval (either because
120
120
  // there was no callback or the requestId pointed to a stale/expired request),
121
121
  // fall back to checking all pending approvals for this guardian chat.
122
122
  if (!guardianApproval && callbackDecision) {
123
- const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
123
+ const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, conversationExternalId, assistantId);
124
124
  if (allPending.length === 1) {
125
125
  guardianApproval = allPending[0];
126
126
  } else if (allPending.length > 1) {
@@ -133,12 +133,12 @@ export async function handleApprovalInterception(
133
133
  channel: sourceChannel,
134
134
  }, {}, approvalCopyGenerator);
135
135
  await deliverChannelReply(replyCallbackUrl, {
136
- chatId: externalChatId,
136
+ chatId: conversationExternalId,
137
137
  text: staleText,
138
138
  assistantId,
139
139
  }, bearerToken);
140
140
  } catch (err) {
141
- log.error({ err, externalChatId }, 'Failed to deliver stale callback disambiguation notice');
141
+ log.error({ err, conversationExternalId }, 'Failed to deliver stale callback disambiguation notice');
142
142
  }
143
143
  return { handled: true, type: 'stale_ignored' };
144
144
  }
@@ -147,14 +147,14 @@ export async function handleApprovalInterception(
147
147
  // For plain-text messages (no callback), check if there are any pending
148
148
  // approvals for this guardian chat to route through the conversation engine.
149
149
  if (!guardianApproval && !callbackDecision) {
150
- const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
150
+ const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, conversationExternalId, assistantId);
151
151
  if (allPending.length === 1) {
152
152
  guardianApproval = allPending[0];
153
153
  } else if (allPending.length > 1) {
154
154
  // Multiple pending — pick the first approval matching this sender as
155
155
  // primary context. The conversation engine sees all matching approvals
156
156
  // via pendingApprovals and can disambiguate.
157
- guardianApproval = allPending.find(a => a.guardianExternalUserId === senderExternalUserId) ?? allPending[0];
157
+ guardianApproval = allPending.find(a => a.guardianExternalUserId === actorExternalId) ?? allPending[0];
158
158
  }
159
159
  }
160
160
 
@@ -164,9 +164,9 @@ export async function handleApprovalInterception(
164
164
  // trustClass check above already verifies the sender is a guardian,
165
165
  // but this catches edge cases like binding rotation between request
166
166
  // creation and decision.
167
- if (senderExternalUserId !== guardianApproval.guardianExternalUserId) {
167
+ if (actorExternalId !== guardianApproval.guardianExternalUserId) {
168
168
  log.warn(
169
- { externalChatId, senderExternalUserId, expectedGuardian: guardianApproval.guardianExternalUserId },
169
+ { conversationExternalId, actorExternalId, expectedGuardian: guardianApproval.guardianExternalUserId },
170
170
  'Non-guardian sender attempted to act on guardian approval request',
171
171
  );
172
172
  try {
@@ -175,12 +175,12 @@ export async function handleApprovalInterception(
175
175
  channel: sourceChannel,
176
176
  }, {}, approvalCopyGenerator);
177
177
  await deliverChannelReply(replyCallbackUrl, {
178
- chatId: externalChatId,
178
+ chatId: conversationExternalId,
179
179
  text: mismatchText,
180
180
  assistantId,
181
181
  }, bearerToken);
182
182
  } catch (err) {
183
- log.error({ err, externalChatId }, 'Failed to deliver guardian identity rejection notice');
183
+ log.error({ err, conversationExternalId }, 'Failed to deliver guardian identity rejection notice');
184
184
  }
185
185
  return { handled: true, type: 'guardian_decision_applied' };
186
186
  }
@@ -193,7 +193,7 @@ export async function handleApprovalInterception(
193
193
  const accessResult = await handleAccessRequestApproval(
194
194
  guardianApproval,
195
195
  callbackDecision.action === 'reject' ? 'deny' : 'approve',
196
- senderExternalUserId,
196
+ actorExternalId,
197
197
  replyCallbackUrl,
198
198
  assistantId,
199
199
  bearerToken,
@@ -207,7 +207,7 @@ export async function handleApprovalInterception(
207
207
  const result = applyGuardianDecision({
208
208
  approval: guardianApproval,
209
209
  decision: callbackDecision,
210
- actorExternalUserId: senderExternalUserId,
210
+ actorExternalUserId: actorExternalId,
211
211
  actorChannel: sourceChannel,
212
212
  });
213
213
 
@@ -242,11 +242,11 @@ export async function handleApprovalInterception(
242
242
  // ── Conversational engine for guardian plain-text messages ──
243
243
  // Gather all pending guardian approvals for this chat so the engine
244
244
  // can handle disambiguation when multiple are pending.
245
- const allGuardianPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
245
+ const allGuardianPending = getAllPendingApprovalsByGuardianChat(sourceChannel, conversationExternalId, assistantId);
246
246
  // Only present approvals that belong to this sender so the engine
247
247
  // does not offer disambiguation for requests assigned to a rotated
248
248
  // guardian the sender cannot act on.
249
- const senderPending = allGuardianPending.filter(a => a.guardianExternalUserId === senderExternalUserId);
249
+ const senderPending = allGuardianPending.filter(a => a.guardianExternalUserId === actorExternalId);
250
250
  const effectivePending = senderPending.length > 0 ? senderPending : allGuardianPending;
251
251
  if (effectivePending.length > 0 && approvalConversationGenerator && content) {
252
252
  const guardianAllowedActions = ['approve_once', 'reject'];
@@ -264,7 +264,7 @@ export async function handleApprovalInterception(
264
264
  // Non-decision follow-up (clarification, disambiguation, etc.)
265
265
  try {
266
266
  await deliverChannelReply(replyCallbackUrl, {
267
- chatId: externalChatId,
267
+ chatId: conversationExternalId,
268
268
  text: engineResult.replyText,
269
269
  assistantId,
270
270
  }, bearerToken);
@@ -295,9 +295,9 @@ export async function handleApprovalInterception(
295
295
  // that was assigned to a different guardian. Without this check a
296
296
  // currently bound guardian could act on a request assigned to a
297
297
  // previous guardian after a binding rotation.
298
- if (senderExternalUserId !== targetApproval.guardianExternalUserId) {
298
+ if (actorExternalId !== targetApproval.guardianExternalUserId) {
299
299
  log.warn(
300
- { externalChatId, senderExternalUserId, expectedGuardian: targetApproval.guardianExternalUserId, targetRequestId: engineResult.targetRequestId },
300
+ { conversationExternalId, actorExternalId, expectedGuardian: targetApproval.guardianExternalUserId, targetRequestId: engineResult.targetRequestId },
301
301
  'Guardian identity mismatch on engine-selected target approval',
302
302
  );
303
303
  try {
@@ -306,12 +306,12 @@ export async function handleApprovalInterception(
306
306
  channel: sourceChannel,
307
307
  }, {}, approvalCopyGenerator);
308
308
  await deliverChannelReply(replyCallbackUrl, {
309
- chatId: externalChatId,
309
+ chatId: conversationExternalId,
310
310
  text: mismatchText,
311
311
  assistantId,
312
312
  }, bearerToken);
313
313
  } catch (err) {
314
- log.error({ err, externalChatId }, 'Failed to deliver guardian identity mismatch notice for engine target');
314
+ log.error({ err, conversationExternalId }, 'Failed to deliver guardian identity mismatch notice for engine target');
315
315
  }
316
316
  return { handled: true, type: 'guardian_decision_applied' };
317
317
  }
@@ -321,7 +321,7 @@ export async function handleApprovalInterception(
321
321
  const accessResult = await handleAccessRequestApproval(
322
322
  targetApproval,
323
323
  decisionAction === 'reject' ? 'deny' : 'approve',
324
- senderExternalUserId,
324
+ actorExternalId,
325
325
  replyCallbackUrl,
326
326
  assistantId,
327
327
  bearerToken,
@@ -339,7 +339,7 @@ export async function handleApprovalInterception(
339
339
  const result = applyGuardianDecision({
340
340
  approval: targetApproval,
341
341
  decision: engineDecision,
342
- actorExternalUserId: senderExternalUserId,
342
+ actorExternalUserId: actorExternalId,
343
343
  actorChannel: sourceChannel,
344
344
  });
345
345
 
@@ -364,7 +364,7 @@ export async function handleApprovalInterception(
364
364
  // Deliver the engine's reply to the guardian
365
365
  try {
366
366
  await deliverChannelReply(replyCallbackUrl, {
367
- chatId: externalChatId,
367
+ chatId: conversationExternalId,
368
368
  text: engineResult.replyText,
369
369
  assistantId,
370
370
  }, bearerToken);
@@ -383,7 +383,7 @@ export async function handleApprovalInterception(
383
383
  channel: sourceChannel,
384
384
  }, {}, approvalCopyGenerator);
385
385
  await deliverChannelReply(replyCallbackUrl, {
386
- chatId: externalChatId,
386
+ chatId: conversationExternalId,
387
387
  text: staleText,
388
388
  assistantId,
389
389
  }, bearerToken);
@@ -414,7 +414,7 @@ export async function handleApprovalInterception(
414
414
  const resolvedByRequest = getPendingApprovalByRequestAndGuardianChat(
415
415
  legacyGuardianDecision.requestId,
416
416
  sourceChannel,
417
- externalChatId,
417
+ conversationExternalId,
418
418
  assistantId,
419
419
  );
420
420
  if (!resolvedByRequest) {
@@ -426,12 +426,12 @@ export async function handleApprovalInterception(
426
426
  channel: sourceChannel,
427
427
  }, {}, approvalCopyGenerator);
428
428
  await deliverChannelReply(replyCallbackUrl, {
429
- chatId: externalChatId,
429
+ chatId: conversationExternalId,
430
430
  text: staleText,
431
431
  assistantId,
432
432
  }, bearerToken);
433
433
  } catch (err) {
434
- log.error({ err, externalChatId }, 'Failed to deliver stale approval notice (legacy path)');
434
+ log.error({ err, conversationExternalId }, 'Failed to deliver stale approval notice (legacy path)');
435
435
  }
436
436
  return { handled: true, type: 'stale_ignored' };
437
437
  }
@@ -441,9 +441,9 @@ export async function handleApprovalInterception(
441
441
  // Re-validate guardian identity against the resolved target.
442
442
  // The default guardianApproval was already checked, but a
443
443
  // requestId-resolved approval may belong to a different guardian.
444
- if (senderExternalUserId !== targetLegacyApproval.guardianExternalUserId) {
444
+ if (actorExternalId !== targetLegacyApproval.guardianExternalUserId) {
445
445
  log.warn(
446
- { externalChatId, senderExternalUserId, expectedGuardian: targetLegacyApproval.guardianExternalUserId, requestId: legacyGuardianDecision.requestId },
446
+ { conversationExternalId, actorExternalId, expectedGuardian: targetLegacyApproval.guardianExternalUserId, requestId: legacyGuardianDecision.requestId },
447
447
  'Guardian identity mismatch on legacy ref-resolved target approval',
448
448
  );
449
449
  try {
@@ -452,12 +452,12 @@ export async function handleApprovalInterception(
452
452
  channel: sourceChannel,
453
453
  }, {}, approvalCopyGenerator);
454
454
  await deliverChannelReply(replyCallbackUrl, {
455
- chatId: externalChatId,
455
+ chatId: conversationExternalId,
456
456
  text: mismatchText,
457
457
  assistantId,
458
458
  }, bearerToken);
459
459
  } catch (err) {
460
- log.error({ err, externalChatId }, 'Failed to deliver guardian identity mismatch notice (legacy path)');
460
+ log.error({ err, conversationExternalId }, 'Failed to deliver guardian identity mismatch notice (legacy path)');
461
461
  }
462
462
  return { handled: true, type: 'guardian_decision_applied' };
463
463
  }
@@ -467,7 +467,7 @@ export async function handleApprovalInterception(
467
467
  const accessResult = await handleAccessRequestApproval(
468
468
  targetLegacyApproval,
469
469
  legacyGuardianDecision.action === 'reject' ? 'deny' : 'approve',
470
- senderExternalUserId,
470
+ actorExternalId,
471
471
  replyCallbackUrl,
472
472
  assistantId,
473
473
  bearerToken,
@@ -479,7 +479,7 @@ export async function handleApprovalInterception(
479
479
  const result = applyGuardianDecision({
480
480
  approval: targetLegacyApproval,
481
481
  decision: legacyGuardianDecision,
482
- actorExternalUserId: senderExternalUserId,
482
+ actorExternalUserId: actorExternalId,
483
483
  actorChannel: sourceChannel,
484
484
  });
485
485
 
@@ -511,7 +511,7 @@ export async function handleApprovalInterception(
511
511
  channel: sourceChannel,
512
512
  }, {}, approvalCopyGenerator);
513
513
  await deliverChannelReply(replyCallbackUrl, {
514
- chatId: externalChatId,
514
+ chatId: conversationExternalId,
515
515
  text: staleText,
516
516
  assistantId,
517
517
  }, bearerToken);
@@ -529,7 +529,7 @@ export async function handleApprovalInterception(
529
529
  channel: sourceChannel,
530
530
  }, {}, approvalCopyGenerator);
531
531
  await deliverChannelReply(replyCallbackUrl, {
532
- chatId: externalChatId,
532
+ chatId: conversationExternalId,
533
533
  text: reminderText,
534
534
  assistantId,
535
535
  }, bearerToken);
@@ -622,7 +622,7 @@ export async function handleApprovalInterception(
622
622
  const cancelApplyResult = applyGuardianDecision({
623
623
  approval: guardianApprovalForRequest,
624
624
  decision: rejectDecision,
625
- actorExternalUserId: senderExternalUserId,
625
+ actorExternalUserId: actorExternalId,
626
626
  actorChannel: sourceChannel,
627
627
  });
628
628
  if (cancelApplyResult.applied) {
@@ -634,7 +634,7 @@ export async function handleApprovalInterception(
634
634
  }, {}, approvalCopyGenerator);
635
635
  try {
636
636
  await deliverChannelReply(replyCallbackUrl, {
637
- chatId: externalChatId,
637
+ chatId: conversationExternalId,
638
638
  text: replyText,
639
639
  assistantId,
640
640
  }, bearerToken);
@@ -669,7 +669,7 @@ export async function handleApprovalInterception(
669
669
  channel: sourceChannel,
670
670
  }, {}, approvalCopyGenerator);
671
671
  await deliverChannelReply(replyCallbackUrl, {
672
- chatId: externalChatId,
672
+ chatId: conversationExternalId,
673
673
  text: staleText,
674
674
  assistantId,
675
675
  }, bearerToken);
@@ -682,7 +682,7 @@ export async function handleApprovalInterception(
682
682
  if (requesterFollowupReplyText) {
683
683
  try {
684
684
  await deliverChannelReply(replyCallbackUrl, {
685
- chatId: externalChatId,
685
+ chatId: conversationExternalId,
686
686
  text: requesterFollowupReplyText,
687
687
  assistantId,
688
688
  }, bearerToken);
@@ -700,7 +700,7 @@ export async function handleApprovalInterception(
700
700
  channel: sourceChannel,
701
701
  }, {}, approvalCopyGenerator);
702
702
  await deliverChannelReply(replyCallbackUrl, {
703
- chatId: externalChatId,
703
+ chatId: conversationExternalId,
704
704
  text: pendingText,
705
705
  assistantId,
706
706
  }, bearerToken);
@@ -732,7 +732,7 @@ export async function handleApprovalInterception(
732
732
  channel: sourceChannel,
733
733
  }, {}, approvalCopyGenerator);
734
734
  await deliverChannelReply(replyCallbackUrl, {
735
- chatId: externalChatId,
735
+ chatId: conversationExternalId,
736
736
  text: expiredText,
737
737
  assistantId,
738
738
  }, bearerToken);
@@ -752,7 +752,7 @@ export async function handleApprovalInterception(
752
752
  // pending request via handleChannelDecision.
753
753
  if (guardianCtx.trustClass !== 'guardian' && guardianCtx.guardianExternalUserId) {
754
754
  log.info(
755
- { conversationId, externalChatId, guardianExternalUserId: guardianCtx.guardianExternalUserId },
755
+ { conversationId, conversationExternalId, guardianExternalUserId: guardianCtx.guardianExternalUserId },
756
756
  'Blocking non-guardian self-approval: pending confirmation exists but guardian approval row not yet created',
757
757
  );
758
758
  try {
@@ -761,7 +761,7 @@ export async function handleApprovalInterception(
761
761
  channel: sourceChannel,
762
762
  }, {}, approvalCopyGenerator);
763
763
  await deliverChannelReply(replyCallbackUrl, {
764
- chatId: externalChatId,
764
+ chatId: conversationExternalId,
765
765
  text: pendingText,
766
766
  assistantId,
767
767
  }, bearerToken);
@@ -827,7 +827,7 @@ export async function handleApprovalInterception(
827
827
  // Non-decision follow-up — deliver the engine's reply and keep the request pending
828
828
  try {
829
829
  await deliverChannelReply(replyCallbackUrl, {
830
- chatId: externalChatId,
830
+ chatId: conversationExternalId,
831
831
  text: engineResult.replyText,
832
832
  assistantId,
833
833
  }, bearerToken);
@@ -851,7 +851,7 @@ export async function handleApprovalInterception(
851
851
  // Deliver the engine's reply text to the user
852
852
  try {
853
853
  await deliverChannelReply(replyCallbackUrl, {
854
- chatId: externalChatId,
854
+ chatId: conversationExternalId,
855
855
  text: engineResult.replyText,
856
856
  assistantId,
857
857
  }, bearerToken);
@@ -871,7 +871,7 @@ export async function handleApprovalInterception(
871
871
  channel: sourceChannel,
872
872
  }, {}, approvalCopyGenerator);
873
873
  await deliverChannelReply(replyCallbackUrl, {
874
- chatId: externalChatId,
874
+ chatId: conversationExternalId,
875
875
  text: staleText,
876
876
  assistantId,
877
877
  }, bearerToken);
@@ -905,7 +905,7 @@ export async function handleApprovalInterception(
905
905
  channel: sourceChannel,
906
906
  }, {}, approvalCopyGenerator);
907
907
  await deliverChannelReply(replyCallbackUrl, {
908
- chatId: externalChatId,
908
+ chatId: conversationExternalId,
909
909
  text: staleText,
910
910
  assistantId,
911
911
  }, bearerToken);
@@ -925,7 +925,7 @@ export async function handleApprovalInterception(
925
925
  toolName: pending.length > 0 ? pending[0].toolName : undefined,
926
926
  }, {}, approvalCopyGenerator);
927
927
  await deliverChannelReply(replyCallbackUrl, {
928
- chatId: externalChatId,
928
+ chatId: conversationExternalId,
929
929
  text: statusText,
930
930
  assistantId,
931
931
  }, bearerToken);
@@ -56,6 +56,7 @@ function ensureGuardianPrincipal(assistantId: string): {
56
56
  channel: 'vellum',
57
57
  guardianExternalUserId: guardianPrincipalId,
58
58
  guardianDeliveryChatId: 'local',
59
+ guardianPrincipalId,
59
60
  verifiedVia: 'bootstrap',
60
61
  metadataJson: JSON.stringify({ bootstrappedAt: Date.now() }),
61
62
  });
@@ -9,23 +9,23 @@ import { httpError } from '../http-errors.js';
9
9
  export async function handleDeleteConversation(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
10
10
  const body = await req.json() as {
11
11
  sourceChannel?: string;
12
- externalChatId?: string;
12
+ conversationExternalId?: string;
13
13
  };
14
14
 
15
- const { sourceChannel, externalChatId } = body;
15
+ const { sourceChannel, conversationExternalId } = body;
16
16
 
17
17
  if (!sourceChannel || typeof sourceChannel !== 'string') {
18
18
  return httpError('BAD_REQUEST', 'sourceChannel is required', 400);
19
19
  }
20
- if (!externalChatId || typeof externalChatId !== 'string') {
21
- return httpError('BAD_REQUEST', 'externalChatId is required', 400);
20
+ if (!conversationExternalId || typeof conversationExternalId !== 'string') {
21
+ return httpError('BAD_REQUEST', 'conversationExternalId is required', 400);
22
22
  }
23
23
 
24
24
  // Delete the assistant-scoped key unconditionally. The legacy key is
25
25
  // canonical for the self assistant and must not be deleted from non-self
26
26
  // routes, otherwise a non-self reset can accidentally reset self state.
27
- const legacyKey = `${sourceChannel}:${externalChatId}`;
28
- const scopedKey = `asst:${assistantId}:${sourceChannel}:${externalChatId}`;
27
+ const legacyKey = `${sourceChannel}:${conversationExternalId}`;
28
+ const scopedKey = `asst:${assistantId}:${sourceChannel}:${conversationExternalId}`;
29
29
  deleteConversationKey(scopedKey);
30
30
  if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
31
31
  deleteConversationKey(legacyKey);
@@ -35,7 +35,7 @@ export async function handleDeleteConversation(req: Request, assistantId: string
35
35
  // canonical self-assistant route so multi-assistant legacy routes do not
36
36
  // clobber each other's bindings.
37
37
  if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
38
- externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
38
+ externalConversationStore.deleteBindingByChannelChat(sourceChannel, conversationExternalId);
39
39
  }
40
40
 
41
41
  return Response.json({ ok: true });