@vellumai/assistant 0.3.18 → 0.3.20
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.
- package/ARCHITECTURE.md +155 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +605 -104
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/checker.test.ts +60 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +779 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +185 -1
- package/src/__tests__/guardian-grant-minting.test.ts +532 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +58 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trust-store.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +276 -212
- package/src/calls/call-domain.ts +56 -6
- package/src/calls/guardian-dispatch.ts +56 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +59 -4
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +26 -0
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +260 -422
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +13 -1
- package/src/memory/embedding-local.ts +22 -8
- package/src/memory/guardian-action-store.ts +133 -2
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +35 -1
- package/src/memory/scoped-approval-grants.ts +518 -0
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/permissions/checker.ts +27 -0
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +154 -0
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -74
- package/src/runtime/routes/inbound-message-handler.ts +568 -409
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -484,15 +484,15 @@ describe('ToolExecutor guardian-only policy gate', () => {
|
|
|
484
484
|
expect(result.content).toBe('ok');
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
-
test('non-guardian invocation of unrelated
|
|
487
|
+
test('non-guardian invocation of unrelated bash command is blocked by guardian approval gate', async () => {
|
|
488
488
|
const executor = new ToolExecutor(makePrompter());
|
|
489
489
|
const result = await executor.execute(
|
|
490
490
|
'bash',
|
|
491
491
|
{ command: 'curl http://localhost:3000/v1/messages' },
|
|
492
492
|
makeContext({ guardianActorRole: 'non-guardian' }),
|
|
493
493
|
);
|
|
494
|
-
expect(result.isError).toBe(
|
|
495
|
-
expect(result.content).
|
|
494
|
+
expect(result.isError).toBe(true);
|
|
495
|
+
expect(result.content).toContain('requires guardian approval');
|
|
496
496
|
});
|
|
497
497
|
|
|
498
498
|
test('non-guardian invocation of unrelated tool is unaffected', async () => {
|
|
@@ -579,4 +579,37 @@ describe('ToolExecutor guardian-only policy gate', () => {
|
|
|
579
579
|
expect(result.content).toContain('restricted to guardian users');
|
|
580
580
|
}
|
|
581
581
|
});
|
|
582
|
+
|
|
583
|
+
test('non-guardian actor is blocked from host read tools (host execution)', async () => {
|
|
584
|
+
const executor = new ToolExecutor(makePrompter());
|
|
585
|
+
const result = await executor.execute(
|
|
586
|
+
'host_file_read',
|
|
587
|
+
{ path: '/Users/noaflaherty/.ssh/config' },
|
|
588
|
+
makeContext({ guardianActorRole: 'non-guardian' }),
|
|
589
|
+
);
|
|
590
|
+
expect(result.isError).toBe(true);
|
|
591
|
+
expect(result.content).toContain('requires guardian approval');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('unverified channel actor is blocked from side-effect tools', async () => {
|
|
595
|
+
const executor = new ToolExecutor(makePrompter());
|
|
596
|
+
const result = await executor.execute(
|
|
597
|
+
'reminder_create',
|
|
598
|
+
{ fire_at: '2026-02-27T12:00:00-05:00', label: 'test', message: 'hello' },
|
|
599
|
+
makeContext({ guardianActorRole: 'unverified_channel' }),
|
|
600
|
+
);
|
|
601
|
+
expect(result.isError).toBe(true);
|
|
602
|
+
expect(result.content).toContain('verified channel identity');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test('guardian actor can execute side-effect tools', async () => {
|
|
606
|
+
const executor = new ToolExecutor(makePrompter());
|
|
607
|
+
const result = await executor.execute(
|
|
608
|
+
'reminder_create',
|
|
609
|
+
{ fire_at: '2026-02-27T12:00:00-05:00', label: 'test', message: 'hello' },
|
|
610
|
+
makeContext({ guardianActorRole: 'guardian' }),
|
|
611
|
+
);
|
|
612
|
+
expect(result.isError).toBe(false);
|
|
613
|
+
expect(result.content).toBe('ok');
|
|
614
|
+
});
|
|
582
615
|
});
|
|
@@ -336,10 +336,70 @@ describe('guardian-dispatch', () => {
|
|
|
336
336
|
expect(vellumDelivery!.destination_conversation_id).toBe('conv-from-thread-created');
|
|
337
337
|
});
|
|
338
338
|
|
|
339
|
-
test('
|
|
339
|
+
test('persists toolName and inputDigest on guardian action request for tool-approval dispatches', async () => {
|
|
340
340
|
const convId = 'conv-dispatch-5';
|
|
341
341
|
ensureConversation(convId);
|
|
342
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
|
+
|
|
343
403
|
const session = createCallSession({
|
|
344
404
|
conversationId: convId,
|
|
345
405
|
provider: 'twilio',
|
|
@@ -455,4 +515,128 @@ describe('guardian-dispatch', () => {
|
|
|
455
515
|
const secondPayload = (emitCalls[0] as Record<string, unknown>).contextPayload as Record<string, unknown>;
|
|
456
516
|
expect(secondPayload.activeGuardianRequestCount).toBe(2);
|
|
457
517
|
});
|
|
518
|
+
|
|
519
|
+
test('second guardian question in same call session passes conversationAffinityHint with first conversation ID', async () => {
|
|
520
|
+
const convId = 'conv-dispatch-affinity-1';
|
|
521
|
+
ensureConversation(convId);
|
|
522
|
+
|
|
523
|
+
const sharedConversationId = 'conv-affinity-guardian';
|
|
524
|
+
|
|
525
|
+
const session = createCallSession({
|
|
526
|
+
conversationId: convId,
|
|
527
|
+
provider: 'twilio',
|
|
528
|
+
fromNumber: '+15550001111',
|
|
529
|
+
toNumber: '+15550002222',
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// First dispatch — no affinity hint expected (no prior delivery exists)
|
|
533
|
+
const pq1 = createPendingQuestion(session.id, 'First question');
|
|
534
|
+
mockEmitResult = {
|
|
535
|
+
signalId: 'sig-affinity-1',
|
|
536
|
+
deduplicated: false,
|
|
537
|
+
dispatched: true,
|
|
538
|
+
reason: 'ok',
|
|
539
|
+
deliveryResults: [
|
|
540
|
+
{
|
|
541
|
+
channel: 'vellum',
|
|
542
|
+
destination: 'vellum',
|
|
543
|
+
status: 'sent',
|
|
544
|
+
conversationId: sharedConversationId,
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
await dispatchGuardianQuestion({
|
|
550
|
+
callSessionId: session.id,
|
|
551
|
+
conversationId: convId,
|
|
552
|
+
assistantId: 'self',
|
|
553
|
+
pendingQuestion: pq1,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const firstParams = emitCalls[0] as Record<string, unknown>;
|
|
557
|
+
// First dispatch should not have an affinity hint
|
|
558
|
+
expect(firstParams.conversationAffinityHint).toBeUndefined();
|
|
559
|
+
|
|
560
|
+
// Second dispatch — should carry the affinity hint from the first delivery
|
|
561
|
+
emitCalls.length = 0;
|
|
562
|
+
const pq2 = createPendingQuestion(session.id, 'Second question');
|
|
563
|
+
mockEmitResult = {
|
|
564
|
+
signalId: 'sig-affinity-2',
|
|
565
|
+
deduplicated: false,
|
|
566
|
+
dispatched: true,
|
|
567
|
+
reason: 'ok',
|
|
568
|
+
deliveryResults: [
|
|
569
|
+
{
|
|
570
|
+
channel: 'vellum',
|
|
571
|
+
destination: 'vellum',
|
|
572
|
+
status: 'sent',
|
|
573
|
+
conversationId: sharedConversationId,
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
await dispatchGuardianQuestion({
|
|
579
|
+
callSessionId: session.id,
|
|
580
|
+
conversationId: convId,
|
|
581
|
+
assistantId: 'self',
|
|
582
|
+
pendingQuestion: pq2,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const secondParams = emitCalls[0] as Record<string, unknown>;
|
|
586
|
+
expect(secondParams.conversationAffinityHint).toEqual({
|
|
587
|
+
vellum: sharedConversationId,
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('third guardian question in same call session also carries affinity hint', async () => {
|
|
592
|
+
const convId = 'conv-dispatch-affinity-2';
|
|
593
|
+
ensureConversation(convId);
|
|
594
|
+
|
|
595
|
+
const sharedConversationId = 'conv-affinity-triple';
|
|
596
|
+
|
|
597
|
+
const session = createCallSession({
|
|
598
|
+
conversationId: convId,
|
|
599
|
+
provider: 'twilio',
|
|
600
|
+
fromNumber: '+15550001111',
|
|
601
|
+
toNumber: '+15550002222',
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Dispatch three guardian questions in the same call session
|
|
605
|
+
for (let i = 0; i < 3; i++) {
|
|
606
|
+
emitCalls.length = 0;
|
|
607
|
+
const pq = createPendingQuestion(session.id, `Question ${i + 1}`);
|
|
608
|
+
mockEmitResult = {
|
|
609
|
+
signalId: `sig-triple-${i}`,
|
|
610
|
+
deduplicated: false,
|
|
611
|
+
dispatched: true,
|
|
612
|
+
reason: 'ok',
|
|
613
|
+
deliveryResults: [
|
|
614
|
+
{
|
|
615
|
+
channel: 'vellum',
|
|
616
|
+
destination: 'vellum',
|
|
617
|
+
status: 'sent',
|
|
618
|
+
conversationId: sharedConversationId,
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
await dispatchGuardianQuestion({
|
|
624
|
+
callSessionId: session.id,
|
|
625
|
+
conversationId: convId,
|
|
626
|
+
assistantId: 'self',
|
|
627
|
+
pendingQuestion: pq,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const params = emitCalls[0] as Record<string, unknown>;
|
|
631
|
+
if (i === 0) {
|
|
632
|
+
// First dispatch — no affinity hint
|
|
633
|
+
expect(params.conversationAffinityHint).toBeUndefined();
|
|
634
|
+
} else {
|
|
635
|
+
// Subsequent dispatches — affinity hint points to the shared conversation
|
|
636
|
+
expect(params.conversationAffinityHint).toEqual({
|
|
637
|
+
vellum: sharedConversationId,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
458
642
|
});
|