@vellumai/assistant 0.4.30 → 0.4.31

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 (91) hide show
  1. package/Dockerfile +14 -8
  2. package/README.md +2 -2
  3. package/docs/architecture/memory.md +28 -29
  4. package/docs/runbook-trusted-contacts.md +1 -4
  5. package/package.json +1 -1
  6. package/src/__tests__/commit-message-enrichment-service.test.ts +0 -4
  7. package/src/__tests__/config-schema.test.ts +0 -9
  8. package/src/__tests__/conflict-policy.test.ts +76 -0
  9. package/src/__tests__/conflict-store.test.ts +14 -20
  10. package/src/__tests__/contacts-tools.test.ts +8 -61
  11. package/src/__tests__/contradiction-checker.test.ts +5 -1
  12. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  13. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  14. package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
  15. package/src/__tests__/registry.test.ts +0 -10
  16. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  17. package/src/__tests__/session-agent-loop.test.ts +0 -2
  18. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  19. package/src/__tests__/session-profile-injection.test.ts +0 -2
  20. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  21. package/src/__tests__/session-skill-tools.test.ts +0 -49
  22. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  23. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  24. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  25. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  26. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  27. package/src/approvals/guardian-decision-primitive.ts +11 -7
  28. package/src/approvals/guardian-request-resolvers.ts +5 -3
  29. package/src/calls/relay-server.ts +5 -0
  30. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  31. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  32. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  33. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  34. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  35. package/src/config/bundled-tool-registry.ts +0 -5
  36. package/src/config/memory-schema.ts +0 -10
  37. package/src/config/system-prompt.ts +6 -0
  38. package/src/contacts/contact-store.ts +36 -62
  39. package/src/contacts/contacts-write.ts +14 -3
  40. package/src/contacts/types.ts +9 -4
  41. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  42. package/src/daemon/handlers/contacts.ts +2 -2
  43. package/src/daemon/handlers/guardian-actions.ts +1 -1
  44. package/src/daemon/handlers/sessions.ts +2 -1
  45. package/src/daemon/ipc-contract/contacts.ts +2 -2
  46. package/src/daemon/session-agent-loop.ts +1 -45
  47. package/src/daemon/session-conflict-gate.ts +21 -82
  48. package/src/daemon/session-memory.ts +7 -52
  49. package/src/daemon/session-process.ts +3 -1
  50. package/src/daemon/session-runtime-assembly.ts +18 -35
  51. package/src/heartbeat/heartbeat-service.ts +5 -1
  52. package/src/memory/conflict-intent.ts +3 -6
  53. package/src/memory/conflict-policy.ts +34 -0
  54. package/src/memory/conflict-store.ts +10 -18
  55. package/src/memory/contradiction-checker.ts +2 -2
  56. package/src/memory/db-init.ts +4 -0
  57. package/src/memory/job-handlers/conflict.ts +0 -7
  58. package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
  59. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  60. package/src/memory/migrations/index.ts +2 -0
  61. package/src/memory/schema.ts +1 -18
  62. package/src/messaging/index.ts +0 -1
  63. package/src/messaging/types.ts +0 -38
  64. package/src/runtime/guardian-action-service.ts +3 -2
  65. package/src/runtime/guardian-outbound-actions.ts +3 -3
  66. package/src/runtime/guardian-reply-router.ts +4 -4
  67. package/src/runtime/http-server.ts +12 -0
  68. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  69. package/src/runtime/routes/contact-routes.ts +308 -29
  70. package/src/runtime/routes/conversation-routes.ts +2 -1
  71. package/src/runtime/routes/global-search-routes.ts +2 -2
  72. package/src/runtime/routes/guardian-action-routes.ts +1 -1
  73. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  74. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
  75. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  76. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  77. package/src/runtime/routes/migration-routes.ts +17 -17
  78. package/src/workspace/git-service.ts +6 -4
  79. package/src/__tests__/get-weather.test.ts +0 -393
  80. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  81. package/src/autonomy/autonomy-resolver.ts +0 -62
  82. package/src/autonomy/autonomy-store.ts +0 -138
  83. package/src/autonomy/disposition-mapper.ts +0 -31
  84. package/src/autonomy/index.ts +0 -11
  85. package/src/autonomy/types.ts +0 -43
  86. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  87. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  88. package/src/config/bundled-skills/weather/icon.svg +0 -24
  89. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  90. package/src/messaging/triage-engine.ts +0 -344
  91. package/src/tools/weather/service.ts +0 -712
@@ -140,6 +140,7 @@ import {
140
140
  handleMergeContacts,
141
141
  handleUpdateContactChannel,
142
142
  handleUpsertContact,
143
+ handleVerifyContactChannel,
143
144
  } from "./routes/contact-routes.js";
144
145
  import { handleListConversationAttention } from "./routes/conversation-attention-routes.js";
145
146
  // Route handlers — grouped by domain
@@ -1128,6 +1129,17 @@ export class RuntimeHttpServer {
1128
1129
  handler: async ({ req, params, authContext }) =>
1129
1130
  handleUpdateContactChannel(req, params.id, authContext.assistantId),
1130
1131
  },
1132
+ {
1133
+ endpoint: "contacts/:contactId/channels/:channelId/verify",
1134
+ method: "POST",
1135
+ policyKey: "contacts/channels",
1136
+ handler: async ({ params, authContext }) =>
1137
+ handleVerifyContactChannel(
1138
+ params.contactId,
1139
+ params.channelId,
1140
+ authContext.assistantId,
1141
+ ),
1142
+ },
1131
1143
 
1132
1144
  // ------------------------------------------------------------------
1133
1145
  // Contacts invites — must precede contacts/:id to avoid shadowing
@@ -297,7 +297,8 @@ async function handleCallbackDecision(params: {
297
297
  const result = applyGuardianDecision({
298
298
  approval: guardianApproval,
299
299
  decision: callbackDecision,
300
- actorExternalUserId: actorExternalId,
300
+ actorPrincipalId: undefined, // Callback path — principal not available at this layer
301
+ actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
301
302
  actorChannel: sourceChannel,
302
303
  });
303
304
 
@@ -491,7 +492,8 @@ async function handleConversationalDecision(params: {
491
492
  const result = applyGuardianDecision({
492
493
  approval: targetApproval,
493
494
  decision: engineDecision,
494
- actorExternalUserId: actorExternalId,
495
+ actorPrincipalId: undefined, // Callback path — principal not available at this layer
496
+ actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
495
497
  actorChannel: sourceChannel,
496
498
  });
497
499
 
@@ -675,7 +677,8 @@ async function handleLegacyDecision(params: {
675
677
  const result = applyGuardianDecision({
676
678
  approval: targetLegacyApproval,
677
679
  decision: legacyGuardianDecision,
678
- actorExternalUserId: actorExternalId,
680
+ actorPrincipalId: undefined, // Callback path — principal not available at this layer
681
+ actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
679
682
  actorChannel: sourceChannel,
680
683
  });
681
684
 
@@ -6,8 +6,12 @@
6
6
  * GET /v1/contacts/:id — get a contact by ID
7
7
  * POST /v1/contacts/merge — merge two contacts
8
8
  * PATCH /v1/contacts/channels/:id — update a contact channel's status/policy
9
+ * POST /v1/contacts/:contactId/channels/:channelId/verify — initiate trusted contact verification
9
10
  */
10
11
 
12
+ import { createHash, randomBytes } from "node:crypto";
13
+
14
+ import type { ChannelId } from "../../channels/types.js";
11
15
  import {
12
16
  getAssistantContactMetadata,
13
17
  getChannelById,
@@ -20,6 +24,7 @@ import {
20
24
  upsertContact,
21
25
  validateSpeciesMetadata,
22
26
  } from "../../contacts/contact-store.js";
27
+ import type { ContactChannel } from "../../contacts/types.js";
23
28
  import type {
24
29
  AssistantSpecies,
25
30
  ChannelPolicy,
@@ -27,6 +32,27 @@ import type {
27
32
  ContactRole,
28
33
  ContactType,
29
34
  } from "../../contacts/types.js";
35
+ import { getCredentialMetadata } from "../../tools/credentials/metadata-store.js";
36
+ import { normalizePhoneNumber } from "../../util/phone.js";
37
+ import {
38
+ countRecentSendsToDestination,
39
+ createOutboundSession,
40
+ updateSessionDelivery,
41
+ } from "../channel-guardian-service.js";
42
+ import {
43
+ deliverVerificationSlack,
44
+ deliverVerificationSms,
45
+ deliverVerificationTelegram,
46
+ DESTINATION_RATE_WINDOW_MS,
47
+ MAX_SENDS_PER_DESTINATION_WINDOW,
48
+ normalizeTelegramDestination,
49
+ } from "../guardian-outbound-actions.js";
50
+ import {
51
+ composeVerificationSlack,
52
+ composeVerificationSms,
53
+ composeVerificationTelegram,
54
+ GUARDIAN_VERIFY_TEMPLATE_KEYS,
55
+ } from "../guardian-verification-templates.js";
30
56
  import { httpError } from "../http-errors.js";
31
57
 
32
58
  const VALID_CONTACT_TYPES: readonly ContactType[] = ["human", "assistant"];
@@ -38,7 +64,7 @@ const VALID_ASSISTANT_SPECIES: readonly AssistantSpecies[] = [
38
64
  /**
39
65
  * GET /v1/contacts?limit=50&role=guardian&contactType=human
40
66
  *
41
- * Also supports search query params: query, channelAddress, channelType, relationship.
67
+ * Also supports search query params: query, channelAddress, channelType.
42
68
  * When any search param is provided, delegates to searchContacts() instead of listContacts().
43
69
  */
44
70
  export function handleListContacts(url: URL, assistantId: string): Response {
@@ -48,8 +74,6 @@ export function handleListContacts(url: URL, assistantId: string): Response {
48
74
  const query = url.searchParams.get("query");
49
75
  const channelAddress = url.searchParams.get("channelAddress");
50
76
  const channelType = url.searchParams.get("channelType");
51
- const relationship = url.searchParams.get("relationship");
52
-
53
77
  if (contactTypeParam && !isContactType(contactTypeParam)) {
54
78
  return httpError(
55
79
  "BAD_REQUEST",
@@ -58,8 +82,7 @@ export function handleListContacts(url: URL, assistantId: string): Response {
58
82
  );
59
83
  }
60
84
 
61
- const hasSearchParams =
62
- query || channelAddress || channelType || relationship;
85
+ const hasSearchParams = query || channelAddress || channelType;
63
86
 
64
87
  const contactType = contactTypeParam
65
88
  ? (contactTypeParam as ContactType)
@@ -71,7 +94,6 @@ export function handleListContacts(url: URL, assistantId: string): Response {
71
94
  query: query ?? undefined,
72
95
  channelAddress: channelAddress ?? undefined,
73
96
  channelType: channelType ?? undefined,
74
- relationship: relationship ?? undefined,
75
97
  role: role ?? undefined,
76
98
  contactType,
77
99
  limit,
@@ -162,7 +184,7 @@ function isChannelPolicy(value: string): value is ChannelPolicy {
162
184
  }
163
185
 
164
186
  /**
165
- * POST /v1/contacts { displayName, id?, relationship?, importance?, contactType?, assistantMetadata?, ... }
187
+ * POST /v1/contacts { displayName, id?, notes?, contactType?, assistantMetadata?, ... }
166
188
  */
167
189
  export async function handleUpsertContact(
168
190
  req: Request,
@@ -171,10 +193,7 @@ export async function handleUpsertContact(
171
193
  const body = (await req.json()) as {
172
194
  id?: string;
173
195
  displayName?: string;
174
- relationship?: string;
175
- importance?: number;
176
- responseExpectation?: string;
177
- preferredTone?: string;
196
+ notes?: string;
178
197
  role?: string;
179
198
  contactType?: string;
180
199
  assistantMetadata?: {
@@ -204,20 +223,6 @@ export async function handleUpsertContact(
204
223
  );
205
224
  }
206
225
 
207
- if (
208
- body.importance !== undefined &&
209
- (typeof body.importance !== "number" ||
210
- Number.isNaN(body.importance) ||
211
- body.importance < 0 ||
212
- body.importance > 1)
213
- ) {
214
- return httpError(
215
- "BAD_REQUEST",
216
- "importance must be a number between 0 and 1",
217
- 400,
218
- );
219
- }
220
-
221
226
  if (body.contactType !== undefined && !isContactType(body.contactType)) {
222
227
  return httpError(
223
228
  "BAD_REQUEST",
@@ -297,10 +302,7 @@ export async function handleUpsertContact(
297
302
  const contact = upsertContact({
298
303
  id: body.id,
299
304
  displayName: body.displayName.trim(),
300
- relationship: body.relationship,
301
- importance: body.importance,
302
- responseExpectation: body.responseExpectation,
303
- preferredTone: body.preferredTone,
305
+ notes: body.notes,
304
306
  role: body.role as ContactRole | undefined,
305
307
  contactType: body.contactType as ContactType | undefined,
306
308
  assistantId,
@@ -404,3 +406,280 @@ export async function handleUpdateContactChannel(
404
406
  const parentContact = getContact(updated.contactId, assistantId);
405
407
  return Response.json({ ok: true, contact: parentContact ?? undefined });
406
408
  }
409
+
410
+ // ---------------------------------------------------------------------------
411
+ // Channel verification
412
+ // ---------------------------------------------------------------------------
413
+
414
+ /** Session TTL in seconds (matches challenge TTL of 10 minutes). */
415
+ const SESSION_TTL_SECONDS = 600;
416
+
417
+ /**
418
+ * Map a contact channel type to the verification ChannelId used by the
419
+ * guardian service. Returns null for unsupported channel types.
420
+ */
421
+ function toVerificationChannel(channelType: string): ChannelId | null {
422
+ switch (channelType) {
423
+ case "phone":
424
+ return "sms";
425
+ case "telegram":
426
+ return "telegram";
427
+ case "slack":
428
+ return "slack";
429
+ default:
430
+ return null;
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Get the Telegram bot username from credential metadata.
436
+ * Falls back to process.env.TELEGRAM_BOT_USERNAME.
437
+ */
438
+ function getTelegramBotUsername(): string | undefined {
439
+ const meta = getCredentialMetadata("telegram", "bot_token");
440
+ if (
441
+ meta?.accountInfo &&
442
+ typeof meta.accountInfo === "string" &&
443
+ meta.accountInfo.trim().length > 0
444
+ ) {
445
+ return meta.accountInfo.trim();
446
+ }
447
+ return process.env.TELEGRAM_BOT_USERNAME || undefined;
448
+ }
449
+
450
+ /**
451
+ * POST /v1/contacts/:contactId/channels/:channelId/verify
452
+ *
453
+ * Initiate trusted contact verification for a specific channel. Sends a
454
+ * verification code via SMS, Telegram, Slack, or voice and returns session
455
+ * info so the client can track the verification flow.
456
+ */
457
+ export async function handleVerifyContactChannel(
458
+ contactId: string,
459
+ channelId: string,
460
+ assistantId: string,
461
+ ): Promise<Response> {
462
+ const contact = getContact(contactId, assistantId);
463
+ if (!contact) {
464
+ return httpError("NOT_FOUND", `Contact "${contactId}" not found`, 404);
465
+ }
466
+
467
+ const channel: ContactChannel | undefined = contact.channels.find(
468
+ (ch) => ch.id === channelId,
469
+ );
470
+ if (!channel) {
471
+ return httpError("NOT_FOUND", `Channel "${channelId}" not found`, 404);
472
+ }
473
+
474
+ // Already verified — no need to re-verify
475
+ if (channel.status === "active" && channel.verifiedAt != null) {
476
+ return httpError("CONFLICT", "Channel is already verified", 409);
477
+ }
478
+
479
+ const verificationChannel = toVerificationChannel(channel.type);
480
+ if (!verificationChannel) {
481
+ return httpError(
482
+ "BAD_REQUEST",
483
+ `Verification is not supported for channel type "${channel.type}"`,
484
+ 400,
485
+ );
486
+ }
487
+
488
+ const destination = channel.address;
489
+ if (!destination) {
490
+ return httpError(
491
+ "BAD_REQUEST",
492
+ "Channel has no address to send verification to",
493
+ 400,
494
+ );
495
+ }
496
+
497
+ // Normalize Telegram destinations so rate-limit lookups are consistent with
498
+ // guardian-outbound-actions (strips leading '@', lowercases handles).
499
+ const effectiveDestination =
500
+ verificationChannel === "telegram"
501
+ ? normalizeTelegramDestination(destination)
502
+ : destination;
503
+
504
+ // Rate limit check
505
+ const recentSendCount = countRecentSendsToDestination(
506
+ verificationChannel,
507
+ effectiveDestination,
508
+ DESTINATION_RATE_WINDOW_MS,
509
+ );
510
+ if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
511
+ return httpError(
512
+ "RATE_LIMITED",
513
+ "Too many verification attempts to this destination. Please try again later.",
514
+ 429,
515
+ );
516
+ }
517
+
518
+ // --- SMS verification ---
519
+ if (verificationChannel === "sms") {
520
+ const phoneE164 = normalizePhoneNumber(destination);
521
+ if (!phoneE164) {
522
+ return httpError(
523
+ "BAD_REQUEST",
524
+ "Channel address is not a valid phone number",
525
+ 400,
526
+ );
527
+ }
528
+
529
+ const sessionResult = createOutboundSession({
530
+ assistantId,
531
+ channel: verificationChannel,
532
+ expectedPhoneE164: phoneE164,
533
+ expectedExternalUserId: channel.externalUserId ?? undefined,
534
+ destinationAddress: phoneE164,
535
+ verificationPurpose: "trusted_contact",
536
+ });
537
+
538
+ const smsBody = composeVerificationSms(
539
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.CHALLENGE_REQUEST,
540
+ {
541
+ code: sessionResult.secret,
542
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
543
+ },
544
+ );
545
+
546
+ const now = Date.now();
547
+ const sendCount = 1;
548
+ updateSessionDelivery(sessionResult.sessionId, now, sendCount, null);
549
+ deliverVerificationSms(phoneE164, smsBody, assistantId);
550
+
551
+ return Response.json({
552
+ ok: true,
553
+ verificationSessionId: sessionResult.sessionId,
554
+ expiresAt: sessionResult.expiresAt,
555
+ sendCount,
556
+ });
557
+ }
558
+
559
+ // --- Telegram verification ---
560
+ if (verificationChannel === "telegram") {
561
+ // Telegram with known chat ID: identity is already bound
562
+ if (channel.externalChatId) {
563
+ const sessionResult = createOutboundSession({
564
+ assistantId,
565
+ channel: verificationChannel,
566
+ expectedChatId: channel.externalChatId,
567
+ expectedExternalUserId: channel.externalUserId ?? undefined,
568
+ identityBindingStatus: "bound",
569
+ destinationAddress: effectiveDestination,
570
+ verificationPurpose: "trusted_contact",
571
+ });
572
+
573
+ const telegramBody = composeVerificationTelegram(
574
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_CHALLENGE_REQUEST,
575
+ {
576
+ code: sessionResult.secret,
577
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
578
+ },
579
+ );
580
+
581
+ const now = Date.now();
582
+ const sendCount = 1;
583
+ updateSessionDelivery(sessionResult.sessionId, now, sendCount, null);
584
+ deliverVerificationTelegram(
585
+ channel.externalChatId,
586
+ telegramBody,
587
+ assistantId,
588
+ );
589
+
590
+ return Response.json({
591
+ ok: true,
592
+ verificationSessionId: sessionResult.sessionId,
593
+ expiresAt: sessionResult.expiresAt,
594
+ sendCount,
595
+ });
596
+ }
597
+
598
+ // Telegram handle only (no chat ID): bootstrap flow
599
+ const botUsername = getTelegramBotUsername();
600
+ if (!botUsername) {
601
+ return httpError(
602
+ "BAD_REQUEST",
603
+ "Telegram bot username is not configured. Set up the Telegram integration first.",
604
+ 400,
605
+ );
606
+ }
607
+
608
+ const bootstrapToken = randomBytes(16).toString("hex");
609
+ const bootstrapTokenHash = createHash("sha256")
610
+ .update(bootstrapToken)
611
+ .digest("hex");
612
+
613
+ const sessionResult = createOutboundSession({
614
+ assistantId,
615
+ channel: verificationChannel,
616
+ identityBindingStatus: "pending_bootstrap",
617
+ destinationAddress: effectiveDestination,
618
+ bootstrapTokenHash,
619
+ verificationPurpose: "trusted_contact",
620
+ });
621
+
622
+ const telegramBootstrapUrl = `https://t.me/${botUsername}?start=gv_${bootstrapToken}`;
623
+
624
+ return Response.json({
625
+ ok: true,
626
+ verificationSessionId: sessionResult.sessionId,
627
+ expiresAt: sessionResult.expiresAt,
628
+ sendCount: 0,
629
+ telegramBootstrapUrl,
630
+ });
631
+ }
632
+
633
+ // --- Slack verification ---
634
+ if (verificationChannel === "slack") {
635
+ const slackUserId = channel.externalUserId ?? destination;
636
+
637
+ // Only claim identity is bound when we have at least one platform identifier
638
+ const hasIdentityBinding = Boolean(
639
+ channel.externalUserId || channel.externalChatId,
640
+ );
641
+ if (!hasIdentityBinding) {
642
+ return httpError(
643
+ "BAD_REQUEST",
644
+ "Slack verification requires an externalUserId or externalChatId for identity binding",
645
+ 400,
646
+ );
647
+ }
648
+
649
+ const sessionResult = createOutboundSession({
650
+ assistantId,
651
+ channel: verificationChannel,
652
+ expectedExternalUserId: channel.externalUserId ?? undefined,
653
+ expectedChatId: channel.externalChatId ?? undefined,
654
+ identityBindingStatus: "bound",
655
+ destinationAddress: slackUserId,
656
+ verificationPurpose: "trusted_contact",
657
+ });
658
+
659
+ const slackBody = composeVerificationSlack(
660
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.SLACK_CHALLENGE_REQUEST,
661
+ {
662
+ code: sessionResult.secret,
663
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
664
+ },
665
+ );
666
+
667
+ const now = Date.now();
668
+ const sendCount = 1;
669
+ updateSessionDelivery(sessionResult.sessionId, now, sendCount, null);
670
+ deliverVerificationSlack(slackUserId, slackBody, assistantId);
671
+
672
+ return Response.json({
673
+ ok: true,
674
+ verificationSessionId: sessionResult.sessionId,
675
+ expiresAt: sessionResult.expiresAt,
676
+ sendCount,
677
+ });
678
+ }
679
+
680
+ return httpError(
681
+ "BAD_REQUEST",
682
+ `Verification is not supported for channel type "${channel.type}"`,
683
+ 400,
684
+ );
685
+ }
@@ -124,7 +124,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
124
124
  messageText: trimmedContent,
125
125
  channel: sourceChannel,
126
126
  actor: {
127
- externalUserId: verifiedActorExternalUserId,
127
+ actorPrincipalId: verifiedActorPrincipalId,
128
+ actorExternalUserId: verifiedActorExternalUserId,
128
129
  channel: sourceChannel,
129
130
  guardianPrincipalId: verifiedActorPrincipalId,
130
131
  },
@@ -58,7 +58,7 @@ interface GlobalSearchSchedule {
58
58
  interface GlobalSearchContact {
59
59
  id: string;
60
60
  displayName: string;
61
- relationship: string | null;
61
+ notes: string | null;
62
62
  lastInteraction: number | null;
63
63
  }
64
64
 
@@ -256,7 +256,7 @@ export async function handleGlobalSearch(url: URL): Promise<Response> {
256
256
  results.contacts = contactResults.map((c) => ({
257
257
  id: c.id,
258
258
  displayName: c.displayName,
259
- relationship: c.relationship,
259
+ notes: c.notes,
260
260
  lastInteraction: c.lastInteraction,
261
261
  }));
262
262
  }
@@ -96,7 +96,7 @@ export async function handleGuardianActionDecision(
96
96
  conversationId,
97
97
  channel: "vellum",
98
98
  actorContext: {
99
- externalUserId: authContext.actorPrincipalId ?? undefined,
99
+ actorPrincipalId: authContext.actorPrincipalId ?? undefined,
100
100
  guardianPrincipalId: authContext.actorPrincipalId ?? undefined,
101
101
  },
102
102
  });
@@ -232,7 +232,8 @@ export async function handleApprovalInterception(
232
232
  const cancelApplyResult = applyGuardianDecision({
233
233
  approval: guardianApprovalForRequest,
234
234
  decision: rejectDecision,
235
- actorExternalUserId: actorExternalId,
235
+ actorPrincipalId: undefined, // Interception path — principal not available
236
+ actorExternalUserId: actorExternalId, // Channel-native ID
236
237
  actorChannel: sourceChannel,
237
238
  });
238
239
  if (cancelApplyResult.applied) {
@@ -13,6 +13,7 @@ import { createHash } from "node:crypto";
13
13
 
14
14
  import { v4 as uuid } from "uuid";
15
15
 
16
+ import { resolveUserReference } from "../../config/user-reference.js";
16
17
  import { findGuardianForChannel } from "../../contacts/contact-store.js";
17
18
  import { createGuardianBinding } from "../../contacts/contacts-write.js";
18
19
  import { getLogger } from "../../util/logger.js";
@@ -54,6 +55,7 @@ function ensureGuardianPrincipal(assistantId: string): {
54
55
 
55
56
  // Mint a new principal ID for the vellum channel
56
57
  const guardianPrincipalId = `vellum-principal-${uuid()}`;
58
+ const guardianDisplayName = resolveUserReference();
57
59
 
58
60
  createGuardianBinding({
59
61
  assistantId,
@@ -62,7 +64,10 @@ function ensureGuardianPrincipal(assistantId: string): {
62
64
  guardianDeliveryChatId: "local",
63
65
  guardianPrincipalId,
64
66
  verifiedVia: "bootstrap",
65
- metadataJson: JSON.stringify({ bootstrappedAt: Date.now() }),
67
+ metadataJson: JSON.stringify({
68
+ bootstrappedAt: Date.now(),
69
+ displayName: guardianDisplayName,
70
+ }),
66
71
  });
67
72
 
68
73
  log.info(
@@ -8,7 +8,10 @@
8
8
  */
9
9
  import type { ChannelId } from "../../../channels/types.js";
10
10
  import { findContactChannel } from "../../../contacts/contact-store.js";
11
- import { touchChannelLastSeen } from "../../../contacts/contacts-write.js";
11
+ import {
12
+ touchChannelLastSeen,
13
+ touchContactInteraction,
14
+ } from "../../../contacts/contacts-write.js";
12
15
  import type {
13
16
  ChannelStatus,
14
17
  ContactChannel,
@@ -634,6 +637,7 @@ export async function enforceIngressAcl(
634
637
 
635
638
  // 'allow' or 'escalate' — update last seen and continue
636
639
  touchChannelLastSeen(resolvedMember.channel.id);
640
+ touchContactInteraction(resolvedMember.contact.id);
637
641
  }
638
642
  }
639
643
 
@@ -129,7 +129,8 @@ export async function handleGuardianReplyIntercept(
129
129
  messageText: trimmedContent,
130
130
  channel: sourceChannel,
131
131
  actor: {
132
- externalUserId: canonicalSenderId ?? rawSenderId!,
132
+ actorPrincipalId: guardianPrincipalId ?? undefined,
133
+ actorExternalUserId: canonicalSenderId ?? rawSenderId!,
133
134
  channel: sourceChannel,
134
135
  guardianPrincipalId: guardianPrincipalId ?? undefined,
135
136
  },