@vellumai/assistant 0.3.16 → 0.3.19

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 (114) hide show
  1. package/ARCHITECTURE.md +74 -13
  2. package/README.md +6 -0
  3. package/docs/architecture/http-token-refresh.md +23 -1
  4. package/docs/architecture/security.md +80 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -0
  7. package/src/__tests__/access-request-decision.test.ts +4 -7
  8. package/src/__tests__/call-controller.test.ts +170 -0
  9. package/src/__tests__/channel-guardian.test.ts +3 -1
  10. package/src/__tests__/checker.test.ts +139 -48
  11. package/src/__tests__/config-watcher.test.ts +11 -13
  12. package/src/__tests__/conversation-pairing.test.ts +103 -3
  13. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -1
  14. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -1
  15. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +511 -0
  16. package/src/__tests__/guardian-action-late-reply.test.ts +131 -0
  17. package/src/__tests__/guardian-action-store.test.ts +182 -0
  18. package/src/__tests__/guardian-dispatch.test.ts +180 -0
  19. package/src/__tests__/guardian-grant-minting.test.ts +543 -0
  20. package/src/__tests__/ipc-snapshot.test.ts +22 -0
  21. package/src/__tests__/non-member-access-request.test.ts +1 -2
  22. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  23. package/src/__tests__/notification-decision-strategy.test.ts +2 -1
  24. package/src/__tests__/notification-deep-link.test.ts +44 -1
  25. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  26. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  27. package/src/__tests__/remote-skill-policy.test.ts +215 -0
  28. package/src/__tests__/scoped-approval-grants.test.ts +521 -0
  29. package/src/__tests__/scoped-grant-security-matrix.test.ts +443 -0
  30. package/src/__tests__/slack-channel-config.test.ts +3 -3
  31. package/src/__tests__/trust-store.test.ts +23 -21
  32. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +5 -7
  33. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  34. package/src/__tests__/trusted-contact-verification.test.ts +9 -9
  35. package/src/__tests__/update-bulletin-state.test.ts +1 -1
  36. package/src/__tests__/update-bulletin.test.ts +66 -3
  37. package/src/__tests__/update-template-contract.test.ts +6 -11
  38. package/src/__tests__/voice-scoped-grant-consumer.test.ts +571 -0
  39. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  40. package/src/calls/call-controller.ts +150 -8
  41. package/src/calls/call-domain.ts +12 -0
  42. package/src/calls/guardian-action-sweep.ts +1 -1
  43. package/src/calls/guardian-dispatch.ts +16 -0
  44. package/src/calls/relay-server.ts +13 -0
  45. package/src/calls/voice-session-bridge.ts +46 -5
  46. package/src/cli/core-commands.ts +41 -1
  47. package/src/config/bundled-skills/notifications/SKILL.md +18 -0
  48. package/src/config/schema.ts +6 -0
  49. package/src/config/skills-schema.ts +27 -0
  50. package/src/config/templates/UPDATES.md +5 -6
  51. package/src/config/update-bulletin-format.ts +2 -0
  52. package/src/config/update-bulletin-state.ts +1 -1
  53. package/src/config/update-bulletin-template-path.ts +6 -0
  54. package/src/config/update-bulletin.ts +21 -6
  55. package/src/daemon/config-watcher.ts +3 -2
  56. package/src/daemon/daemon-control.ts +64 -10
  57. package/src/daemon/handlers/config-channels.ts +18 -0
  58. package/src/daemon/handlers/config-slack-channel.ts +1 -1
  59. package/src/daemon/handlers/identity.ts +45 -25
  60. package/src/daemon/handlers/sessions.ts +1 -1
  61. package/src/daemon/handlers/skills.ts +45 -2
  62. package/src/daemon/ipc-contract/sessions.ts +1 -1
  63. package/src/daemon/ipc-contract/skills.ts +1 -0
  64. package/src/daemon/ipc-contract/workspace.ts +12 -1
  65. package/src/daemon/ipc-contract-inventory.json +1 -0
  66. package/src/daemon/lifecycle.ts +8 -0
  67. package/src/daemon/server.ts +25 -3
  68. package/src/daemon/session-process.ts +450 -184
  69. package/src/daemon/tls-certs.ts +17 -12
  70. package/src/daemon/tool-side-effects.ts +1 -1
  71. package/src/memory/channel-delivery-store.ts +18 -20
  72. package/src/memory/channel-guardian-store.ts +39 -42
  73. package/src/memory/conversation-crud.ts +2 -2
  74. package/src/memory/conversation-queries.ts +2 -2
  75. package/src/memory/conversation-store.ts +24 -25
  76. package/src/memory/db-init.ts +17 -1
  77. package/src/memory/embedding-local.ts +16 -7
  78. package/src/memory/fts-reconciler.ts +41 -26
  79. package/src/memory/guardian-action-store.ts +65 -7
  80. package/src/memory/guardian-verification.ts +1 -0
  81. package/src/memory/jobs-worker.ts +2 -2
  82. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  83. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  84. package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
  85. package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
  86. package/src/memory/migrations/index.ts +6 -2
  87. package/src/memory/schema-migration.ts +1 -0
  88. package/src/memory/schema.ts +36 -1
  89. package/src/memory/scoped-approval-grants.ts +509 -0
  90. package/src/memory/search/semantic.ts +3 -3
  91. package/src/notifications/README.md +158 -17
  92. package/src/notifications/broadcaster.ts +68 -50
  93. package/src/notifications/conversation-pairing.ts +96 -18
  94. package/src/notifications/decision-engine.ts +6 -3
  95. package/src/notifications/deliveries-store.ts +12 -0
  96. package/src/notifications/emit-signal.ts +1 -0
  97. package/src/notifications/thread-candidates.ts +60 -25
  98. package/src/notifications/types.ts +2 -1
  99. package/src/permissions/checker.ts +28 -16
  100. package/src/permissions/defaults.ts +14 -4
  101. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  102. package/src/runtime/guardian-action-grant-minter.ts +97 -0
  103. package/src/runtime/http-server.ts +11 -11
  104. package/src/runtime/routes/access-request-decision.ts +1 -1
  105. package/src/runtime/routes/debug-routes.ts +4 -4
  106. package/src/runtime/routes/guardian-approval-interception.ts +120 -4
  107. package/src/runtime/routes/inbound-message-handler.ts +100 -33
  108. package/src/runtime/routes/integration-routes.ts +2 -2
  109. package/src/security/tool-approval-digest.ts +67 -0
  110. package/src/skills/remote-skill-policy.ts +131 -0
  111. package/src/tools/permission-checker.ts +1 -2
  112. package/src/tools/secret-detection-handler.ts +1 -1
  113. package/src/tools/system/voice-config.ts +1 -1
  114. package/src/version.ts +29 -2
@@ -31,8 +31,16 @@ import {
31
31
  cancelGuardianActionRequest,
32
32
  createGuardianActionDelivery,
33
33
  createGuardianActionRequest,
34
+ expireGuardianActionRequest,
34
35
  getDeliveriesByRequestId,
36
+ getExpiredDeliveriesByConversation,
37
+ getExpiredDeliveryByConversation,
38
+ getFollowupDeliveriesByConversation,
39
+ getFollowupDeliveryByConversation,
35
40
  getGuardianActionRequest,
41
+ getPendingDeliveriesByConversation,
42
+ getPendingDeliveryByConversation,
43
+ startFollowupFromExpiredRequest,
36
44
  updateDeliveryStatus,
37
45
  } from '../memory/guardian-action-store.js';
38
46
  import { conversations } from '../memory/schema.js';
@@ -74,6 +82,180 @@ describe('guardian-action-store', () => {
74
82
  }
75
83
  });
76
84
 
85
+ // ── Helper to create a pending request+delivery targeting a conversation ──
86
+ function createPendingRequestWithDelivery(convId: string, deliveryConvId: string) {
87
+ ensureConversation(convId);
88
+ const session = createCallSession({
89
+ conversationId: convId,
90
+ provider: 'twilio',
91
+ fromNumber: '+15550001111',
92
+ toNumber: '+15550002222',
93
+ });
94
+ const pq = createPendingQuestion(session.id, `Question for ${convId}`);
95
+ const request = createGuardianActionRequest({
96
+ kind: 'ask_guardian',
97
+ sourceChannel: 'voice',
98
+ sourceConversationId: convId,
99
+ callSessionId: session.id,
100
+ pendingQuestionId: pq.id,
101
+ questionText: pq.questionText,
102
+ expiresAt: Date.now() + 60_000,
103
+ });
104
+ const delivery = createGuardianActionDelivery({
105
+ requestId: request.id,
106
+ destinationChannel: 'vellum',
107
+ destinationConversationId: deliveryConvId,
108
+ });
109
+ updateDeliveryStatus(delivery.id, 'sent');
110
+ return { request, delivery };
111
+ }
112
+
113
+ // ── getPendingDeliveriesByConversation ──────────────────────────────
114
+
115
+ test('getPendingDeliveriesByConversation returns all pending deliveries for a conversation', () => {
116
+ const sharedConvId = 'shared-pending-conv';
117
+ ensureConversation(sharedConvId);
118
+
119
+ const { request: req1 } = createPendingRequestWithDelivery('source-conv-p1', sharedConvId);
120
+ const { request: req2 } = createPendingRequestWithDelivery('source-conv-p2', sharedConvId);
121
+
122
+ const deliveries = getPendingDeliveriesByConversation(sharedConvId);
123
+ expect(deliveries).toHaveLength(2);
124
+
125
+ const requestIds = deliveries.map((d) => d.requestId);
126
+ expect(requestIds).toContain(req1.id);
127
+ expect(requestIds).toContain(req2.id);
128
+ });
129
+
130
+ test('getPendingDeliveriesByConversation returns single delivery (fast path preserved)', () => {
131
+ const convId = 'single-pending-conv';
132
+ ensureConversation(convId);
133
+
134
+ const { request } = createPendingRequestWithDelivery('source-conv-single-p', convId);
135
+
136
+ const deliveries = getPendingDeliveriesByConversation(convId);
137
+ expect(deliveries).toHaveLength(1);
138
+ expect(deliveries[0].requestId).toBe(request.id);
139
+ });
140
+
141
+ test('getPendingDeliveryByConversation returns first from multiple (backward compat)', () => {
142
+ const convId = 'compat-pending-conv';
143
+ ensureConversation(convId);
144
+
145
+ createPendingRequestWithDelivery('source-conv-compat-p1', convId);
146
+ createPendingRequestWithDelivery('source-conv-compat-p2', convId);
147
+
148
+ const single = getPendingDeliveryByConversation(convId);
149
+ expect(single).not.toBeNull();
150
+ });
151
+
152
+ test('getPendingDeliveriesByConversation returns empty for non-matching conversation', () => {
153
+ ensureConversation('other-conv');
154
+ createPendingRequestWithDelivery('source-conv-no-match', 'other-conv');
155
+
156
+ const deliveries = getPendingDeliveriesByConversation('nonexistent-conv');
157
+ expect(deliveries).toHaveLength(0);
158
+ });
159
+
160
+ // ── getExpiredDeliveriesByConversation ──────────────────────────────
161
+
162
+ test('getExpiredDeliveriesByConversation returns all expired deliveries for a conversation', () => {
163
+ const sharedConvId = 'shared-expired-conv';
164
+ ensureConversation(sharedConvId);
165
+
166
+ const { request: req1 } = createPendingRequestWithDelivery('source-conv-e1', sharedConvId);
167
+ const { request: req2 } = createPendingRequestWithDelivery('source-conv-e2', sharedConvId);
168
+
169
+ expireGuardianActionRequest(req1.id, 'sweep_timeout');
170
+ expireGuardianActionRequest(req2.id, 'sweep_timeout');
171
+
172
+ const deliveries = getExpiredDeliveriesByConversation(sharedConvId);
173
+ expect(deliveries).toHaveLength(2);
174
+
175
+ const requestIds = deliveries.map((d) => d.requestId);
176
+ expect(requestIds).toContain(req1.id);
177
+ expect(requestIds).toContain(req2.id);
178
+ });
179
+
180
+ test('getExpiredDeliveryByConversation returns first from multiple (backward compat)', () => {
181
+ const convId = 'compat-expired-conv';
182
+ ensureConversation(convId);
183
+
184
+ const { request: req1 } = createPendingRequestWithDelivery('source-conv-compat-e1', convId);
185
+ const { request: req2 } = createPendingRequestWithDelivery('source-conv-compat-e2', convId);
186
+
187
+ expireGuardianActionRequest(req1.id, 'sweep_timeout');
188
+ expireGuardianActionRequest(req2.id, 'sweep_timeout');
189
+
190
+ const single = getExpiredDeliveryByConversation(convId);
191
+ expect(single).not.toBeNull();
192
+ });
193
+
194
+ test('getExpiredDeliveriesByConversation excludes deliveries with followup already started', () => {
195
+ const convId = 'expired-with-followup-conv';
196
+ ensureConversation(convId);
197
+
198
+ const { request: req1 } = createPendingRequestWithDelivery('source-conv-ef1', convId);
199
+ const { request: req2 } = createPendingRequestWithDelivery('source-conv-ef2', convId);
200
+
201
+ expireGuardianActionRequest(req1.id, 'sweep_timeout');
202
+ expireGuardianActionRequest(req2.id, 'sweep_timeout');
203
+
204
+ // Start followup on req1 — only req2 should remain in the expired query
205
+ startFollowupFromExpiredRequest(req1.id, 'late answer');
206
+
207
+ const deliveries = getExpiredDeliveriesByConversation(convId);
208
+ expect(deliveries).toHaveLength(1);
209
+ expect(deliveries[0].requestId).toBe(req2.id);
210
+ });
211
+
212
+ // ── getFollowupDeliveriesByConversation ─────────────────────────────
213
+
214
+ test('getFollowupDeliveriesByConversation returns all awaiting_guardian_choice deliveries', () => {
215
+ const convId = 'shared-followup-conv';
216
+ ensureConversation(convId);
217
+
218
+ const { request: req1 } = createPendingRequestWithDelivery('source-conv-f1', convId);
219
+ const { request: req2 } = createPendingRequestWithDelivery('source-conv-f2', convId);
220
+
221
+ expireGuardianActionRequest(req1.id, 'sweep_timeout');
222
+ expireGuardianActionRequest(req2.id, 'sweep_timeout');
223
+
224
+ startFollowupFromExpiredRequest(req1.id, 'late answer 1');
225
+ startFollowupFromExpiredRequest(req2.id, 'late answer 2');
226
+
227
+ const deliveries = getFollowupDeliveriesByConversation(convId);
228
+ expect(deliveries).toHaveLength(2);
229
+
230
+ const requestIds = deliveries.map((d) => d.requestId);
231
+ expect(requestIds).toContain(req1.id);
232
+ expect(requestIds).toContain(req2.id);
233
+ });
234
+
235
+ test('getFollowupDeliveryByConversation returns first from multiple (backward compat)', () => {
236
+ const convId = 'compat-followup-conv';
237
+ ensureConversation(convId);
238
+
239
+ const { request: req1 } = createPendingRequestWithDelivery('source-conv-compat-f1', convId);
240
+ const { request: req2 } = createPendingRequestWithDelivery('source-conv-compat-f2', convId);
241
+
242
+ expireGuardianActionRequest(req1.id, 'sweep_timeout');
243
+ expireGuardianActionRequest(req2.id, 'sweep_timeout');
244
+
245
+ startFollowupFromExpiredRequest(req1.id, 'late 1');
246
+ startFollowupFromExpiredRequest(req2.id, 'late 2');
247
+
248
+ const single = getFollowupDeliveryByConversation(convId);
249
+ expect(single).not.toBeNull();
250
+ });
251
+
252
+ test('getFollowupDeliveriesByConversation returns empty for non-matching conversation', () => {
253
+ const deliveries = getFollowupDeliveriesByConversation('nonexistent-conv');
254
+ expect(deliveries).toHaveLength(0);
255
+ });
256
+
257
+ // ── cancelGuardianActionRequest ─────────────────────────────────────
258
+
77
259
  test('cancelGuardianActionRequest cancels both pending and sent deliveries', () => {
78
260
  const conversationId = 'conv-guardian-cancel';
79
261
  ensureConversation(conversationId);
@@ -335,4 +335,184 @@ describe('guardian-dispatch', () => {
335
335
  expect(vellumDelivery).toBeDefined();
336
336
  expect(vellumDelivery!.destination_conversation_id).toBe('conv-from-thread-created');
337
337
  });
338
+
339
+ test('persists toolName and inputDigest on guardian action request for tool-approval dispatches', async () => {
340
+ const convId = 'conv-dispatch-5';
341
+ ensureConversation(convId);
342
+
343
+ const session = createCallSession({
344
+ conversationId: convId,
345
+ provider: 'twilio',
346
+ fromNumber: '+15550001111',
347
+ toNumber: '+15550002222',
348
+ });
349
+ const pq = createPendingQuestion(session.id, 'Allow send_email to bob@example.com?');
350
+
351
+ await dispatchGuardianQuestion({
352
+ callSessionId: session.id,
353
+ conversationId: convId,
354
+ assistantId: 'self',
355
+ pendingQuestion: pq,
356
+ toolName: 'send_email',
357
+ inputDigest: 'abc123def456',
358
+ });
359
+
360
+ const db = getDb();
361
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
362
+ const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
363
+ | { id: string; tool_name: string | null; input_digest: string | null }
364
+ | undefined;
365
+ expect(request).toBeDefined();
366
+ expect(request!.tool_name).toBe('send_email');
367
+ expect(request!.input_digest).toBe('abc123def456');
368
+ });
369
+
370
+ test('omitting toolName and inputDigest stores null for informational ASK_GUARDIAN dispatches', async () => {
371
+ const convId = 'conv-dispatch-6';
372
+ ensureConversation(convId);
373
+
374
+ const session = createCallSession({
375
+ conversationId: convId,
376
+ provider: 'twilio',
377
+ fromNumber: '+15550001111',
378
+ toNumber: '+15550002222',
379
+ });
380
+ const pq = createPendingQuestion(session.id, 'What time works?');
381
+
382
+ await dispatchGuardianQuestion({
383
+ callSessionId: session.id,
384
+ conversationId: convId,
385
+ assistantId: 'self',
386
+ pendingQuestion: pq,
387
+ });
388
+
389
+ const db = getDb();
390
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
391
+ const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
392
+ | { id: string; tool_name: string | null; input_digest: string | null }
393
+ | undefined;
394
+ expect(request).toBeDefined();
395
+ expect(request!.tool_name).toBeNull();
396
+ expect(request!.input_digest).toBeNull();
397
+ });
398
+
399
+ test('includes activeGuardianRequestCount in context payload', async () => {
400
+ const convId = 'conv-dispatch-7';
401
+ ensureConversation(convId);
402
+
403
+ const session = createCallSession({
404
+ conversationId: convId,
405
+ provider: 'twilio',
406
+ fromNumber: '+15550001111',
407
+ toNumber: '+15550002222',
408
+ });
409
+ const pq = createPendingQuestion(session.id, 'First question');
410
+
411
+ await dispatchGuardianQuestion({
412
+ callSessionId: session.id,
413
+ conversationId: convId,
414
+ assistantId: 'self',
415
+ pendingQuestion: pq,
416
+ });
417
+
418
+ const signalParams = emitCalls[0] as Record<string, unknown>;
419
+ const payload = signalParams.contextPayload as Record<string, unknown>;
420
+ // The request was just created so there is 1 pending request for this session
421
+ expect(payload.activeGuardianRequestCount).toBe(1);
422
+ expect(payload.callSessionId).toBe(session.id);
423
+ });
424
+
425
+ test('repeated guardian questions in the same call each create per-request delivery rows even when sharing a conversation', async () => {
426
+ const convId = 'conv-dispatch-reuse-1';
427
+ ensureConversation(convId);
428
+
429
+ // Both dispatches deliver to the same vellum conversation (simulating thread reuse)
430
+ const sharedConversationId = 'conv-shared-guardian';
431
+
432
+ const session = createCallSession({
433
+ conversationId: convId,
434
+ provider: 'twilio',
435
+ fromNumber: '+15550001111',
436
+ toNumber: '+15550002222',
437
+ });
438
+
439
+ // First dispatch
440
+ const pq1 = createPendingQuestion(session.id, 'What is the gate code?');
441
+ mockEmitResult = {
442
+ signalId: 'sig-reuse-1',
443
+ deduplicated: false,
444
+ dispatched: true,
445
+ reason: 'ok',
446
+ deliveryResults: [
447
+ {
448
+ channel: 'vellum',
449
+ destination: 'vellum',
450
+ status: 'sent',
451
+ conversationId: sharedConversationId,
452
+ },
453
+ ],
454
+ };
455
+
456
+ await dispatchGuardianQuestion({
457
+ callSessionId: session.id,
458
+ conversationId: convId,
459
+ assistantId: 'self',
460
+ pendingQuestion: pq1,
461
+ });
462
+
463
+ // Second dispatch (same call session, same shared conversation)
464
+ emitCalls.length = 0;
465
+ const pq2 = createPendingQuestion(session.id, 'Should I let them in?');
466
+ mockEmitResult = {
467
+ signalId: 'sig-reuse-2',
468
+ deduplicated: false,
469
+ dispatched: true,
470
+ reason: 'ok',
471
+ deliveryResults: [
472
+ {
473
+ channel: 'vellum',
474
+ destination: 'vellum',
475
+ status: 'sent',
476
+ conversationId: sharedConversationId,
477
+ },
478
+ ],
479
+ };
480
+
481
+ await dispatchGuardianQuestion({
482
+ callSessionId: session.id,
483
+ conversationId: convId,
484
+ assistantId: 'self',
485
+ pendingQuestion: pq2,
486
+ });
487
+
488
+ // Both dispatches should have created separate action requests
489
+ const db = getDb();
490
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
491
+ const requests = raw.query(
492
+ 'SELECT * FROM guardian_action_requests WHERE call_session_id = ? ORDER BY created_at ASC',
493
+ ).all(session.id) as Array<{ id: string; question_text: string }>;
494
+ expect(requests).toHaveLength(2);
495
+ expect(requests[0].question_text).toBe('What is the gate code?');
496
+ expect(requests[1].question_text).toBe('Should I let them in?');
497
+
498
+ // Each request should have its own delivery row, both pointing to the shared conversation
499
+ for (const req of requests) {
500
+ const delivery = raw.query(
501
+ 'SELECT * FROM guardian_action_deliveries WHERE request_id = ? AND destination_channel = ?',
502
+ ).get(req.id, 'vellum') as { status: string; destination_conversation_id: string | null } | undefined;
503
+ expect(delivery).toBeDefined();
504
+ expect(delivery!.status).toBe('sent');
505
+ expect(delivery!.destination_conversation_id).toBe(sharedConversationId);
506
+ }
507
+
508
+ // Total delivery rows should be 2 (one per request), not 1
509
+ const allDeliveries = raw.query(
510
+ 'SELECT * FROM guardian_action_deliveries WHERE destination_conversation_id = ?',
511
+ ).all(sharedConversationId) as Array<{ request_id: string }>;
512
+ expect(allDeliveries).toHaveLength(2);
513
+
514
+ // Second dispatch should report a higher activeGuardianRequestCount
515
+ const secondPayload = (emitCalls[0] as Record<string, unknown>).contextPayload as Record<string, unknown>;
516
+ expect(secondPayload.activeGuardianRequestCount).toBe(2);
517
+ });
338
518
  });