@vellumai/assistant 0.4.5 → 0.4.7

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 (112) hide show
  1. package/ARCHITECTURE.md +27 -10
  2. package/README.md +6 -6
  3. package/bun.lock +57 -2
  4. package/docs/architecture/memory.md +4 -4
  5. package/docs/trusted-contact-access.md +8 -0
  6. package/package.json +3 -2
  7. package/src/__tests__/actor-token-service.test.ts +9 -6
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
  9. package/src/__tests__/call-controller.test.ts +115 -0
  10. package/src/__tests__/call-domain.test.ts +148 -10
  11. package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
  12. package/src/__tests__/call-pointer-messages.test.ts +105 -43
  13. package/src/__tests__/canonical-guardian-store.test.ts +44 -10
  14. package/src/__tests__/channel-approval-routes.test.ts +67 -65
  15. package/src/__tests__/channel-delivery-store.test.ts +2 -2
  16. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
  17. package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
  18. package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
  19. package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
  20. package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
  22. package/src/__tests__/guardian-dispatch.test.ts +39 -1
  23. package/src/__tests__/guardian-grant-minting.test.ts +24 -24
  24. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
  25. package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
  26. package/src/__tests__/guardian-routing-state.test.ts +10 -32
  27. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
  28. package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
  29. package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
  30. package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
  31. package/src/__tests__/non-member-access-request.test.ts +57 -47
  32. package/src/__tests__/notification-decision-fallback.test.ts +232 -0
  33. package/src/__tests__/notification-decision-strategy.test.ts +304 -8
  34. package/src/__tests__/notification-guardian-path.test.ts +38 -1
  35. package/src/__tests__/relay-server.test.ts +136 -5
  36. package/src/__tests__/send-endpoint-busy.test.ts +35 -1
  37. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
  39. package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
  40. package/src/__tests__/system-prompt.test.ts +1 -0
  41. package/src/__tests__/tool-approval-handler.test.ts +1 -1
  42. package/src/__tests__/tool-grant-request-escalation.test.ts +10 -2
  43. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +14 -1
  44. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +24 -24
  45. package/src/__tests__/trusted-contact-multichannel.test.ts +5 -5
  46. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  47. package/src/approvals/guardian-decision-primitive.ts +29 -25
  48. package/src/approvals/guardian-request-resolvers.ts +9 -5
  49. package/src/calls/call-controller.ts +15 -0
  50. package/src/calls/call-pointer-message-composer.ts +27 -85
  51. package/src/calls/call-pointer-messages.ts +54 -21
  52. package/src/calls/guardian-dispatch.ts +30 -0
  53. package/src/calls/relay-server.ts +58 -24
  54. package/src/calls/types.ts +1 -0
  55. package/src/config/system-prompt.ts +10 -3
  56. package/src/config/templates/BOOTSTRAP.md +6 -5
  57. package/src/config/templates/USER.md +1 -0
  58. package/src/config/user-reference.ts +44 -0
  59. package/src/daemon/handlers/guardian-actions.ts +5 -2
  60. package/src/daemon/handlers/sessions.ts +8 -3
  61. package/src/daemon/lifecycle.ts +109 -3
  62. package/src/daemon/providers-setup.ts +0 -8
  63. package/src/daemon/server.ts +32 -24
  64. package/src/daemon/session-agent-loop.ts +4 -3
  65. package/src/daemon/session-lifecycle.ts +1 -9
  66. package/src/daemon/session-process.ts +2 -2
  67. package/src/daemon/session-runtime-assembly.ts +2 -0
  68. package/src/daemon/session-slash.ts +35 -2
  69. package/src/daemon/session-tool-setup.ts +10 -0
  70. package/src/daemon/session.ts +1 -0
  71. package/src/memory/canonical-guardian-store.ts +40 -0
  72. package/src/memory/conversation-crud.ts +26 -0
  73. package/src/memory/conversation-store.ts +1 -0
  74. package/src/memory/db-init.ts +12 -0
  75. package/src/memory/guardian-bindings.ts +4 -0
  76. package/src/memory/job-handlers/backfill.ts +2 -9
  77. package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
  78. package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
  79. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
  80. package/src/memory/migrations/index.ts +3 -0
  81. package/src/memory/migrations/registry.ts +5 -0
  82. package/src/memory/schema.ts +22 -0
  83. package/src/notifications/README.md +8 -1
  84. package/src/notifications/copy-composer.ts +160 -30
  85. package/src/notifications/decision-engine.ts +98 -1
  86. package/src/runtime/access-request-helper.ts +43 -28
  87. package/src/runtime/actor-refresh-token-service.ts +309 -0
  88. package/src/runtime/actor-refresh-token-store.ts +157 -0
  89. package/src/runtime/actor-token-service.ts +3 -3
  90. package/src/runtime/actor-trust-resolver.ts +19 -14
  91. package/src/runtime/channel-guardian-service.ts +6 -0
  92. package/src/runtime/gateway-client.ts +239 -0
  93. package/src/runtime/guardian-context-resolver.ts +6 -2
  94. package/src/runtime/guardian-reply-router.ts +33 -16
  95. package/src/runtime/guardian-vellum-migration.ts +29 -5
  96. package/src/runtime/http-server.ts +2 -0
  97. package/src/runtime/http-types.ts +0 -13
  98. package/src/runtime/local-actor-identity.ts +19 -13
  99. package/src/runtime/middleware/actor-token.ts +2 -2
  100. package/src/runtime/routes/channel-delivery-routes.ts +5 -5
  101. package/src/runtime/routes/conversation-routes.ts +45 -35
  102. package/src/runtime/routes/guardian-action-routes.ts +7 -1
  103. package/src/runtime/routes/guardian-approval-interception.ts +52 -52
  104. package/src/runtime/routes/guardian-bootstrap-routes.ts +11 -24
  105. package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
  106. package/src/runtime/routes/inbound-conversation.ts +7 -7
  107. package/src/runtime/routes/inbound-message-handler.ts +105 -94
  108. package/src/runtime/routes/pairing-routes.ts +60 -50
  109. package/src/runtime/tool-grant-request-helper.ts +1 -0
  110. package/src/types/qrcode.d.ts +10 -0
  111. package/src/util/logger.ts +10 -0
  112. package/src/daemon/call-pointer-generators.ts +0 -59
@@ -31,16 +31,13 @@ mock.module('../util/logger.js', () => ({
31
31
  }),
32
32
  }));
33
33
 
34
- // Simulated network delay for semantic search (ms). When > 0, the mock
35
- // semantic search sleeps for this duration before returning, simulating the
36
- // Qdrant network round-trip that early termination is designed to skip.
37
- let semanticSearchDelayMs = 0;
34
+ // Counter for semantic search invocations used to verify early termination
35
+ // skips the call entirely rather than relying on flaky wall-clock comparisons.
36
+ let semanticSearchCallCount = 0;
38
37
 
39
38
  mock.module('../memory/search/semantic.js', () => ({
40
39
  semanticSearch: async () => {
41
- if (semanticSearchDelayMs > 0) {
42
- await Bun.sleep(semanticSearchDelayMs);
43
- }
40
+ semanticSearchCallCount++;
44
41
  return [];
45
42
  },
46
43
  isQdrantConnectionError: () => false,
@@ -305,15 +302,11 @@ describe('Memory retrieval benchmark', () => {
305
302
  expect(recall.selectedCount).toBeGreaterThan(0);
306
303
  });
307
304
 
308
- test('early termination is measurably faster than baseline', async () => {
309
- const conversationId = 'conv-bench-et-delta';
305
+ test('early termination skips semantic search entirely', async () => {
306
+ const conversationId = 'conv-bench-et-skip';
310
307
  const now = 1_700_500_000_000;
311
308
  seedMemoryItems(conversationId, 500, now);
312
309
 
313
- // Simulate the Qdrant network round-trip that ET is designed to skip.
314
- // Use 100ms to dominate over variable CPU-bound work on slower hosts.
315
- semanticSearchDelayMs = 100;
316
-
317
310
  const query = 'What do we know about topic-5 and keyword-3?';
318
311
 
319
312
  const etConfig: AssistantConfig = {
@@ -363,40 +356,22 @@ describe('Memory retrieval benchmark', () => {
363
356
  },
364
357
  };
365
358
 
366
- try {
367
- // Warm up to avoid cold-start bias
368
- await buildMemoryRecall(query, conversationId, etConfig);
369
- await buildMemoryRecall(query, conversationId, noEtConfig);
370
-
371
- const iterations = 5;
372
- const etTimes: number[] = [];
373
- const baselineTimes: number[] = [];
374
-
375
- for (let i = 0; i < iterations; i++) {
376
- const t0 = performance.now();
377
- const etRecall = await buildMemoryRecall(query, conversationId, etConfig);
378
- etTimes.push(performance.now() - t0);
379
- expect(etRecall.earlyTerminated).toBe(true);
380
-
381
- const t1 = performance.now();
382
- const baselineRecall = await buildMemoryRecall(query, conversationId, noEtConfig);
383
- baselineTimes.push(performance.now() - t1);
384
- expect(baselineRecall.earlyTerminated).toBe(false);
385
- }
386
-
387
- etTimes.sort((a, b) => a - b);
388
- baselineTimes.sort((a, b) => a - b);
389
- const medianEt = etTimes[Math.floor(iterations / 2)];
390
- const medianBaseline = baselineTimes[Math.floor(iterations / 2)];
391
-
392
- // ET skips the mocked network delay, so it should be measurably faster.
393
- // Use a 15% threshold to tolerate slower CI hosts where CPU-bound work
394
- // takes longer relative to the fixed mock delay.
395
- const speedup = 1 - medianEt / medianBaseline;
396
- expect(speedup).toBeGreaterThanOrEqual(0.15);
397
- } finally {
398
- semanticSearchDelayMs = 0;
399
- }
359
+ // Run with ET enabled — semantic search should be skipped
360
+ semanticSearchCallCount = 0;
361
+ const etRecall = await buildMemoryRecall(query, conversationId, etConfig);
362
+ const etCalls = semanticSearchCallCount;
363
+
364
+ expect(etRecall.earlyTerminated).toBe(true);
365
+ expect(etRecall.semanticHits).toBe(0);
366
+ expect(etCalls).toBe(0);
367
+
368
+ // Run without ET semantic search should be invoked
369
+ semanticSearchCallCount = 0;
370
+ const baselineRecall = await buildMemoryRecall(query, conversationId, noEtConfig);
371
+ const baselineCalls = semanticSearchCallCount;
372
+
373
+ expect(baselineRecall.earlyTerminated).toBe(false);
374
+ expect(baselineCalls).toBeGreaterThan(0);
400
375
  });
401
376
 
402
377
  test('recall.latencyMs tracks wall-clock within 50% tolerance', async () => {
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Guard test: `isTrusted` must not appear in production code.
3
+ *
4
+ * The authorization model was migrated from a boolean `isTrusted` flag to
5
+ * principal-based authorization (`guardianPrincipalId` matching). This guard
6
+ * ensures the legacy pattern is never reintroduced in production source files.
7
+ *
8
+ * The invariant: `actor.guardianPrincipalId === request.guardianPrincipalId`
9
+ * (with cross-channel fallback via the vellum canonical principal).
10
+ *
11
+ * Allowed exceptions:
12
+ * - Variable names like `isTrustedActor` or `isTrustedContact` that refer
13
+ * to trust-class checks (e.g. `trustClass === 'guardian'`), NOT to a
14
+ * boolean `isTrusted` property on ActorContext.
15
+ * - Test files (__tests__/) — may reference `isTrusted` in test descriptions
16
+ * or comments about the migration.
17
+ */
18
+
19
+ import { execSync } from 'node:child_process';
20
+ import { resolve } from 'node:path';
21
+
22
+ import { describe, expect, test } from 'bun:test';
23
+
24
+ const repoRoot = resolve(__dirname, '..', '..', '..');
25
+
26
+ describe('isTrusted guard', () => {
27
+ test('isTrusted property must not exist in production ActorContext usage', () => {
28
+ // Search for `isTrusted` used as a property (e.g., `.isTrusted`, `isTrusted:`,
29
+ // `isTrusted =`) in production TypeScript files, excluding tests, node_modules,
30
+ // and the allowed trust-class variable pattern.
31
+ const raw = execSync(
32
+ [
33
+ 'grep -rn "isTrusted" assistant/src/ --include="*.ts"',
34
+ 'grep -v "__tests__"',
35
+ 'grep -v "node_modules"',
36
+ ].join(' | ') + ' || true',
37
+ { encoding: 'utf-8', cwd: repoRoot },
38
+ );
39
+
40
+ // Filter in JS: strip allowed token names from each line, then check if
41
+ // `isTrusted` still appears. This avoids the grep -v approach which could
42
+ // mask forbidden usage on lines that also contain allowed tokens.
43
+ const ALLOWED_TOKENS = ['isTrustedActor', 'isTrustedContact', 'isTrustedTrustClass'];
44
+ const offending = raw
45
+ .trim()
46
+ .split('\n')
47
+ .filter((line) => {
48
+ if (!line) return false;
49
+ let stripped = line;
50
+ for (const token of ALLOWED_TOKENS) {
51
+ stripped = stripped.replaceAll(token, '');
52
+ }
53
+ return stripped.includes('isTrusted');
54
+ });
55
+
56
+ if (offending.length > 0) {
57
+ throw new Error(
58
+ 'Found `isTrusted` references in production code. Authorization must use ' +
59
+ '`guardianPrincipalId` matching instead. Offending lines:\n' +
60
+ offending.join('\n'),
61
+ );
62
+ }
63
+ });
64
+
65
+ test('ActorContext interface must not declare isTrusted field', () => {
66
+ // Verify the ActorContext type definition does not include isTrusted
67
+ const result = execSync(
68
+ [
69
+ 'grep -n "isTrusted" assistant/src/approvals/guardian-request-resolvers.ts',
70
+ 'true',
71
+ ].join(' || '),
72
+ { encoding: 'utf-8', cwd: repoRoot },
73
+ );
74
+
75
+ expect(result.trim()).toBe('');
76
+ });
77
+ });
@@ -138,12 +138,12 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
138
138
  const body: Record<string, unknown> = {
139
139
  sourceChannel: 'telegram',
140
140
  interface: 'telegram',
141
- externalChatId: 'chat-123',
141
+ conversationExternalId: 'chat-123',
142
142
  externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
143
143
  content: 'Hello, can I use this assistant?',
144
- senderExternalUserId: 'user-unknown-456',
145
- senderName: 'Alice Unknown',
146
- senderUsername: 'alice_unknown',
144
+ actorExternalId: 'user-unknown-456',
145
+ actorDisplayName: 'Alice Unknown',
146
+ actorUsername: 'alice_unknown',
147
147
  replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
148
148
  ...overrides,
149
149
  };
@@ -188,6 +188,7 @@ describe('non-member access request notification', () => {
188
188
  channel: 'telegram',
189
189
  guardianExternalUserId: 'guardian-user-789',
190
190
  guardianDeliveryChatId: 'guardian-chat-789',
191
+ guardianPrincipalId: 'test-principal-id',
191
192
  });
192
193
 
193
194
  const req = buildInboundRequest();
@@ -206,8 +207,8 @@ describe('non-member access request notification', () => {
206
207
  expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
207
208
  expect(emitSignalCalls[0].sourceChannel).toBe('telegram');
208
209
  const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
209
- expect(payload.senderExternalUserId).toBe('user-unknown-456');
210
- expect(payload.senderName).toBe('Alice Unknown');
210
+ expect(payload.actorExternalId).toBe('user-unknown-456');
211
+ expect(payload.actorDisplayName).toBe('Alice Unknown');
211
212
 
212
213
  // A canonical access request was created
213
214
  const pending = listCanonicalGuardianRequests({
@@ -229,6 +230,7 @@ describe('non-member access request notification', () => {
229
230
  channel: 'telegram',
230
231
  guardianExternalUserId: 'guardian-user-789',
231
232
  guardianDeliveryChatId: 'guardian-chat-789',
233
+ guardianPrincipalId: 'test-principal-id',
232
234
  });
233
235
 
234
236
  // First message
@@ -258,9 +260,9 @@ describe('non-member access request notification', () => {
258
260
  expect(pending.length).toBe(1);
259
261
  });
260
262
 
261
- test('access request is created and signal emitted even without same-channel guardian binding', async () => {
262
- // No guardian binding on any channel — access request should still be
263
- // created and notification signal emitted (null guardianExternalUserId).
263
+ test('access request is created with self-healed principal even without same-channel guardian binding', async () => {
264
+ // No guardian binding on any channel — self-heal creates a vellum binding
265
+ // so the access_request (now decisionable) has a guardianPrincipalId.
264
266
  const req = buildInboundRequest();
265
267
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
266
268
  const json = await resp.json() as Record<string, unknown>;
@@ -276,7 +278,7 @@ describe('non-member access request notification', () => {
276
278
  expect(emitSignalCalls.length).toBe(1);
277
279
  expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
278
280
 
279
- // Canonical request was created with null guardianExternalUserId
281
+ // Canonical request was created with a self-healed principal
280
282
  const pending = listCanonicalGuardianRequests({
281
283
  status: 'pending',
282
284
  requesterExternalUserId: 'user-unknown-456',
@@ -284,7 +286,9 @@ describe('non-member access request notification', () => {
284
286
  kind: 'access_request',
285
287
  });
286
288
  expect(pending.length).toBe(1);
287
- expect(pending[0].guardianExternalUserId).toBeNull();
289
+ // Self-heal bootstraps a vellum binding — guardianExternalUserId is now set
290
+ expect(pending[0].guardianExternalUserId).toBeDefined();
291
+ expect(pending[0].guardianPrincipalId).toBeDefined();
288
292
  });
289
293
 
290
294
  test('cross-channel fallback: SMS guardian binding resolves for Telegram access request', async () => {
@@ -294,6 +298,7 @@ describe('non-member access request notification', () => {
294
298
  channel: 'sms',
295
299
  guardianExternalUserId: 'guardian-sms-user',
296
300
  guardianDeliveryChatId: 'guardian-sms-chat',
301
+ guardianPrincipalId: 'test-principal-id',
297
302
  });
298
303
 
299
304
  const req = buildInboundRequest();
@@ -319,21 +324,21 @@ describe('non-member access request notification', () => {
319
324
  expect(pending[0].guardianExternalUserId).toBe('guardian-sms-user');
320
325
  });
321
326
 
322
- test('no notification when senderExternalUserId is absent', async () => {
327
+ test('no notification when actorExternalId is absent', async () => {
323
328
  createBinding({
324
329
  assistantId: 'self',
325
330
  channel: 'telegram',
326
331
  guardianExternalUserId: 'guardian-user-789',
327
332
  guardianDeliveryChatId: 'guardian-chat-789',
333
+ guardianPrincipalId: 'test-principal-id',
328
334
  });
329
335
 
330
- // Message without senderExternalUserIdcan't identify the requester.
331
- // The ACL check requires senderExternalUserId to look up members,
332
- // so without it the non-member gate is bypassed entirely.
336
+ // Message without actorExternalIdthe handler returns BAD_REQUEST.
333
337
  const req = buildInboundRequest({
334
- senderExternalUserId: undefined,
338
+ actorExternalId: undefined,
335
339
  });
336
- await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
340
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
341
+ expect(resp.status).toBe(400);
337
342
 
338
343
  // No access request notification should fire (no identity to notify about)
339
344
  expect(emitSignalCalls.length).toBe(0);
@@ -345,12 +350,12 @@ describe('access-request-helper unit tests', () => {
345
350
  resetState();
346
351
  });
347
352
 
348
- test('notifyGuardianOfAccessRequest returns no_sender_id when senderExternalUserId is absent', () => {
353
+ test('notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent', () => {
349
354
  const result = notifyGuardianOfAccessRequest({
350
355
  canonicalAssistantId: 'self',
351
356
  sourceChannel: 'telegram',
352
- externalChatId: 'chat-123',
353
- senderExternalUserId: undefined,
357
+ conversationExternalId: 'chat-123',
358
+ actorExternalId: undefined,
354
359
  });
355
360
 
356
361
  expect(result.notified).toBe(false);
@@ -363,13 +368,13 @@ describe('access-request-helper unit tests', () => {
363
368
  expect(pending.length).toBe(0);
364
369
  });
365
370
 
366
- test('notifyGuardianOfAccessRequest creates request with null guardianExternalUserId when no binding exists', () => {
371
+ test('notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists', () => {
367
372
  const result = notifyGuardianOfAccessRequest({
368
373
  canonicalAssistantId: 'self',
369
374
  sourceChannel: 'telegram',
370
- externalChatId: 'chat-123',
371
- senderExternalUserId: 'unknown-user',
372
- senderName: 'Bob',
375
+ conversationExternalId: 'chat-123',
376
+ actorExternalId: 'unknown-user',
377
+ actorDisplayName: 'Bob',
373
378
  });
374
379
 
375
380
  expect(result.notified).toBe(true);
@@ -383,7 +388,9 @@ describe('access-request-helper unit tests', () => {
383
388
  kind: 'access_request',
384
389
  });
385
390
  expect(pending.length).toBe(1);
386
- expect(pending[0].guardianExternalUserId).toBeNull();
391
+ // Self-heal bootstraps a vellum binding
392
+ expect(pending[0].guardianExternalUserId).toBeDefined();
393
+ expect(pending[0].guardianPrincipalId).toBeDefined();
387
394
 
388
395
  // Signal was emitted
389
396
  expect(emitSignalCalls.length).toBe(1);
@@ -396,13 +403,14 @@ describe('access-request-helper unit tests', () => {
396
403
  channel: 'sms',
397
404
  guardianExternalUserId: 'guardian-sms',
398
405
  guardianDeliveryChatId: 'sms-chat',
406
+ guardianPrincipalId: 'test-principal-id',
399
407
  });
400
408
 
401
409
  const result = notifyGuardianOfAccessRequest({
402
410
  canonicalAssistantId: 'self',
403
411
  sourceChannel: 'telegram',
404
- externalChatId: 'tg-chat',
405
- senderExternalUserId: 'unknown-tg-user',
412
+ conversationExternalId: 'tg-chat',
413
+ actorExternalId: 'unknown-tg-user',
406
414
  });
407
415
 
408
416
  expect(result.notified).toBe(true);
@@ -427,19 +435,21 @@ describe('access-request-helper unit tests', () => {
427
435
  channel: 'telegram',
428
436
  guardianExternalUserId: 'guardian-tg',
429
437
  guardianDeliveryChatId: 'tg-chat',
438
+ guardianPrincipalId: 'test-principal-tg',
430
439
  });
431
440
  createBinding({
432
441
  assistantId: 'self',
433
442
  channel: 'sms',
434
443
  guardianExternalUserId: 'guardian-sms',
435
444
  guardianDeliveryChatId: 'sms-chat',
445
+ guardianPrincipalId: 'test-principal-sms',
436
446
  });
437
447
 
438
448
  const result = notifyGuardianOfAccessRequest({
439
449
  canonicalAssistantId: 'self',
440
450
  sourceChannel: 'telegram',
441
- externalChatId: 'chat-123',
442
- senderExternalUserId: 'unknown-user',
451
+ conversationExternalId: 'chat-123',
452
+ actorExternalId: 'unknown-user',
443
453
  });
444
454
 
445
455
  expect(result.notified).toBe(true);
@@ -457,13 +467,13 @@ describe('access-request-helper unit tests', () => {
457
467
  expect(payload.guardianBindingChannel).toBe('telegram');
458
468
  });
459
469
 
460
- test('notifyGuardianOfAccessRequest for voice channel includes senderName in contextPayload', () => {
470
+ test('notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload', () => {
461
471
  const result = notifyGuardianOfAccessRequest({
462
472
  canonicalAssistantId: 'self',
463
473
  sourceChannel: 'voice',
464
- externalChatId: '+15559998888',
465
- senderExternalUserId: '+15559998888',
466
- senderName: 'Alice Caller',
474
+ conversationExternalId: '+15559998888',
475
+ actorExternalId: '+15559998888',
476
+ actorDisplayName: 'Alice Caller',
467
477
  });
468
478
 
469
479
  expect(result.notified).toBe(true);
@@ -471,8 +481,8 @@ describe('access-request-helper unit tests', () => {
471
481
 
472
482
  const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
473
483
  expect(payload.sourceChannel).toBe('voice');
474
- expect(payload.senderName).toBe('Alice Caller');
475
- expect(payload.senderExternalUserId).toBe('+15559998888');
484
+ expect(payload.actorDisplayName).toBe('Alice Caller');
485
+ expect(payload.actorExternalId).toBe('+15559998888');
476
486
  expect(payload.senderIdentifier).toBe('Alice Caller');
477
487
 
478
488
  // Canonical request should exist
@@ -489,9 +499,9 @@ describe('access-request-helper unit tests', () => {
489
499
  const result = notifyGuardianOfAccessRequest({
490
500
  canonicalAssistantId: 'self',
491
501
  sourceChannel: 'telegram',
492
- externalChatId: 'chat-123',
493
- senderExternalUserId: 'unknown-user',
494
- senderName: 'Test User',
502
+ conversationExternalId: 'chat-123',
503
+ actorExternalId: 'unknown-user',
504
+ actorDisplayName: 'Test User',
495
505
  });
496
506
 
497
507
  expect(result.notified).toBe(true);
@@ -507,9 +517,9 @@ describe('access-request-helper unit tests', () => {
507
517
  const result = notifyGuardianOfAccessRequest({
508
518
  canonicalAssistantId: 'self',
509
519
  sourceChannel: 'telegram',
510
- externalChatId: 'chat-123',
511
- senderExternalUserId: 'revoked-user',
512
- senderName: 'Revoked User',
520
+ conversationExternalId: 'chat-123',
521
+ actorExternalId: 'revoked-user',
522
+ actorDisplayName: 'Revoked User',
513
523
  previousMemberStatus: 'revoked',
514
524
  });
515
525
 
@@ -544,9 +554,9 @@ describe('access-request-helper unit tests', () => {
544
554
  const result = notifyGuardianOfAccessRequest({
545
555
  canonicalAssistantId: 'self',
546
556
  sourceChannel: 'voice',
547
- externalChatId: '+15556667777',
548
- senderExternalUserId: '+15556667777',
549
- senderName: 'Noah',
557
+ conversationExternalId: '+15556667777',
558
+ actorExternalId: '+15556667777',
559
+ actorDisplayName: 'Noah',
550
560
  });
551
561
 
552
562
  expect(result.notified).toBe(true);
@@ -584,9 +594,9 @@ describe('access-request-helper unit tests', () => {
584
594
  const result = notifyGuardianOfAccessRequest({
585
595
  canonicalAssistantId: 'self',
586
596
  sourceChannel: 'telegram',
587
- externalChatId: 'chat-123',
588
- senderExternalUserId: 'unknown-user',
589
- senderName: 'Alice',
597
+ conversationExternalId: 'chat-123',
598
+ actorExternalId: 'unknown-user',
599
+ actorDisplayName: 'Alice',
590
600
  });
591
601
 
592
602
  expect(result.notified).toBe(true);