@vellumai/assistant 0.3.28 → 0.4.0

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 (199) hide show
  1. package/ARCHITECTURE.md +33 -3
  2. package/bun.lock +4 -1
  3. package/docs/trusted-contact-access.md +9 -2
  4. package/package.json +6 -3
  5. package/scripts/ipc/generate-swift.ts +3 -3
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  7. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +13 -5
  9. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  10. package/src/__tests__/asset-search-tool.test.ts +2 -0
  11. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  12. package/src/__tests__/attachments-store.test.ts +2 -0
  13. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  14. package/src/__tests__/call-controller.test.ts +30 -29
  15. package/src/__tests__/call-routes-http.test.ts +34 -32
  16. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  17. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  18. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  19. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  20. package/src/__tests__/clarification-resolver.test.ts +2 -0
  21. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  22. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  24. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  25. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  26. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  27. package/src/__tests__/config-schema.test.ts +5 -5
  28. package/src/__tests__/config-watcher.test.ts +3 -1
  29. package/src/__tests__/connection-policy.test.ts +14 -5
  30. package/src/__tests__/contacts-tools.test.ts +3 -1
  31. package/src/__tests__/contradiction-checker.test.ts +2 -0
  32. package/src/__tests__/conversation-pairing.test.ts +10 -0
  33. package/src/__tests__/conversation-routes.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  35. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  36. package/src/__tests__/credential-vault.test.ts +5 -4
  37. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  38. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  39. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  40. package/src/__tests__/encrypted-store.test.ts +10 -5
  41. package/src/__tests__/followup-tools.test.ts +3 -1
  42. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  43. package/src/__tests__/gmail-integration.test.ts +0 -1
  44. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  45. package/src/__tests__/guardian-dispatch.test.ts +2 -0
  46. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  47. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  48. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  49. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  50. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  51. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  52. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  53. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  55. package/src/__tests__/heartbeat-service.test.ts +20 -0
  56. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  57. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  58. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  59. package/src/__tests__/intent-routing.test.ts +2 -0
  60. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  61. package/src/__tests__/media-generate-image.test.ts +21 -0
  62. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  63. package/src/__tests__/memory-regressions.test.ts +20 -20
  64. package/src/__tests__/non-member-access-request.test.ts +183 -9
  65. package/src/__tests__/notification-decision-fallback.test.ts +2 -0
  66. package/src/__tests__/notification-decision-strategy.test.ts +61 -0
  67. package/src/__tests__/notification-guardian-path.test.ts +2 -0
  68. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  69. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  70. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  71. package/src/__tests__/pairing-routes.test.ts +171 -0
  72. package/src/__tests__/playbook-execution.test.ts +3 -1
  73. package/src/__tests__/playbook-tools.test.ts +3 -1
  74. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  75. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  76. package/src/__tests__/recording-handler.test.ts +11 -0
  77. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  78. package/src/__tests__/recording-state-machine.test.ts +13 -2
  79. package/src/__tests__/registry.test.ts +7 -3
  80. package/src/__tests__/relay-server.test.ts +148 -28
  81. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  82. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  83. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  84. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  85. package/src/__tests__/schedule-tools.test.ts +3 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  87. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  88. package/src/__tests__/session-agent-loop.test.ts +16 -0
  89. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  90. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  91. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  92. package/src/__tests__/session-profile-injection.test.ts +21 -0
  93. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  94. package/src/__tests__/session-queue.test.ts +23 -0
  95. package/src/__tests__/session-runtime-assembly.test.ts +50 -12
  96. package/src/__tests__/session-skill-tools.test.ts +27 -5
  97. package/src/__tests__/session-slash-known.test.ts +23 -0
  98. package/src/__tests__/session-slash-queue.test.ts +23 -0
  99. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  100. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  101. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  102. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  103. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  104. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  105. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  107. package/src/__tests__/skills.test.ts +8 -4
  108. package/src/__tests__/slack-channel-config.test.ts +3 -1
  109. package/src/__tests__/subagent-tools.test.ts +19 -0
  110. package/src/__tests__/swarm-recursion.test.ts +2 -0
  111. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  112. package/src/__tests__/swarm-tool.test.ts +2 -0
  113. package/src/__tests__/system-prompt.test.ts +3 -1
  114. package/src/__tests__/task-compiler.test.ts +3 -1
  115. package/src/__tests__/task-management-tools.test.ts +3 -1
  116. package/src/__tests__/task-tools.test.ts +3 -1
  117. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  118. package/src/__tests__/terminal-tools.test.ts +2 -0
  119. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  120. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  121. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  122. package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
  123. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  124. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  125. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  126. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  127. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  128. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  129. package/src/__tests__/view-image-tool.test.ts +3 -1
  130. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  131. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  132. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  133. package/src/__tests__/work-item-output.test.ts +3 -1
  134. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  135. package/src/calls/call-controller.ts +26 -23
  136. package/src/calls/guardian-action-sweep.ts +10 -2
  137. package/src/calls/relay-server.ts +216 -27
  138. package/src/calls/types.ts +1 -1
  139. package/src/calls/voice-session-bridge.ts +3 -3
  140. package/src/cli.ts +12 -0
  141. package/src/config/agent-schema.ts +14 -3
  142. package/src/config/calls-schema.ts +6 -6
  143. package/src/config/core-schema.ts +3 -3
  144. package/src/config/feature-flag-registry.json +8 -0
  145. package/src/config/mcp-schema.ts +1 -1
  146. package/src/config/memory-schema.ts +27 -19
  147. package/src/config/schema.ts +21 -21
  148. package/src/config/skills-schema.ts +7 -7
  149. package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
  150. package/src/daemon/handlers/config-inbox.ts +4 -4
  151. package/src/daemon/handlers/sessions.ts +148 -4
  152. package/src/daemon/ipc-contract/messages.ts +16 -0
  153. package/src/daemon/ipc-contract-inventory.json +1 -0
  154. package/src/daemon/lifecycle.ts +19 -0
  155. package/src/daemon/pairing-store.ts +86 -3
  156. package/src/daemon/session-agent-loop.ts +5 -5
  157. package/src/daemon/session-lifecycle.ts +25 -17
  158. package/src/daemon/session-memory.ts +2 -2
  159. package/src/daemon/session-process.ts +1 -20
  160. package/src/daemon/session-runtime-assembly.ts +28 -22
  161. package/src/daemon/session-tool-setup.ts +2 -2
  162. package/src/daemon/session.ts +3 -3
  163. package/src/memory/canonical-guardian-store.ts +63 -1
  164. package/src/memory/channel-guardian-store.ts +1 -0
  165. package/src/memory/conversation-crud.ts +7 -7
  166. package/src/memory/db-init.ts +4 -0
  167. package/src/memory/embedding-local.ts +257 -39
  168. package/src/memory/embedding-runtime-manager.ts +471 -0
  169. package/src/memory/guardian-bindings.ts +25 -1
  170. package/src/memory/indexer.ts +3 -3
  171. package/src/memory/ingress-invite-store.ts +45 -0
  172. package/src/memory/job-handlers/backfill.ts +16 -9
  173. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  174. package/src/memory/migrations/index.ts +1 -0
  175. package/src/memory/qdrant-client.ts +31 -22
  176. package/src/memory/schema.ts +4 -0
  177. package/src/notifications/copy-composer.ts +15 -0
  178. package/src/runtime/access-request-helper.ts +43 -7
  179. package/src/runtime/actor-trust-resolver.ts +46 -50
  180. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  181. package/src/runtime/channel-retry-sweep.ts +18 -6
  182. package/src/runtime/guardian-context-resolver.ts +38 -96
  183. package/src/runtime/guardian-reply-router.ts +31 -1
  184. package/src/runtime/ingress-service.ts +80 -3
  185. package/src/runtime/invite-redemption-service.ts +141 -2
  186. package/src/runtime/routes/channel-route-shared.ts +1 -1
  187. package/src/runtime/routes/channel-routes.ts +1 -1
  188. package/src/runtime/routes/conversation-routes.ts +2 -2
  189. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  190. package/src/runtime/routes/inbound-message-handler.ts +41 -10
  191. package/src/runtime/routes/ingress-routes.ts +52 -4
  192. package/src/runtime/routes/pairing-routes.ts +3 -0
  193. package/src/tools/guardian-control-plane-policy.ts +2 -2
  194. package/src/tools/tool-approval-handler.ts +11 -11
  195. package/src/tools/types.ts +2 -2
  196. package/src/util/logger.ts +20 -8
  197. package/src/util/platform.ts +10 -0
  198. package/src/util/voice-code.ts +29 -0
  199. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -92,7 +92,7 @@ describe('ingress member HTTP routes', () => {
92
92
  });
93
93
 
94
94
  const res = await handleUpsertMember(req);
95
- const body = await res.json() as Record<string, unknown>;
95
+ const body = await res.json() as { ok: boolean; error: string };
96
96
 
97
97
  expect(res.status).toBe(400);
98
98
  expect(body.ok).toBe(false);
@@ -109,7 +109,7 @@ describe('ingress member HTTP routes', () => {
109
109
  });
110
110
 
111
111
  const res = await handleUpsertMember(req);
112
- const body = await res.json() as Record<string, unknown>;
112
+ const body = await res.json() as { ok: boolean; error: string };
113
113
 
114
114
  expect(res.status).toBe(400);
115
115
  expect(body.ok).toBe(false);
@@ -278,6 +278,42 @@ describe('ingress invite HTTP routes', () => {
278
278
  expect((invite.token as string).length).toBeGreaterThan(0);
279
279
  });
280
280
 
281
+ test('POST /v1/ingress/invites — includes canonical share URL when bot username is configured', async () => {
282
+ const prevBotUsername = process.env.TELEGRAM_BOT_USERNAME;
283
+ process.env.TELEGRAM_BOT_USERNAME = 'test_invite_bot';
284
+
285
+ try {
286
+ const req = new Request('http://localhost/v1/ingress/invites', {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({
290
+ sourceChannel: 'telegram',
291
+ note: 'Share link test',
292
+ }),
293
+ });
294
+
295
+ const res = await handleCreateInvite(req);
296
+ const body = await res.json() as Record<string, unknown>;
297
+ const invite = body.invite as Record<string, unknown>;
298
+ const token = invite.token as string;
299
+ const share = invite.share as Record<string, unknown>;
300
+
301
+ expect(res.status).toBe(201);
302
+ expect(body.ok).toBe(true);
303
+ expect(typeof token).toBe('string');
304
+ expect(token.length).toBeGreaterThan(0);
305
+ expect(share).toBeDefined();
306
+ expect(share.url).toBe(`https://t.me/test_invite_bot?start=iv_${token}`);
307
+ expect(typeof share.displayText).toBe('string');
308
+ } finally {
309
+ if (prevBotUsername === undefined) {
310
+ delete process.env.TELEGRAM_BOT_USERNAME;
311
+ } else {
312
+ process.env.TELEGRAM_BOT_USERNAME = prevBotUsername;
313
+ }
314
+ }
315
+ });
316
+
281
317
  test('POST /v1/ingress/invites — missing sourceChannel returns 400', async () => {
282
318
  const req = new Request('http://localhost/v1/ingress/invites', {
283
319
  method: 'POST',
@@ -286,7 +322,7 @@ describe('ingress invite HTTP routes', () => {
286
322
  });
287
323
 
288
324
  const res = await handleCreateInvite(req);
289
- const body = await res.json() as Record<string, unknown>;
325
+ const body = await res.json() as { ok: boolean; error: string };
290
326
 
291
327
  expect(res.status).toBe(400);
292
328
  expect(body.ok).toBe(false);
@@ -376,7 +412,7 @@ describe('ingress invite HTTP routes', () => {
376
412
  });
377
413
 
378
414
  const res = await handleRedeemInvite(req);
379
- const body = await res.json() as Record<string, unknown>;
415
+ const body = await res.json() as { ok: boolean; error: string };
380
416
 
381
417
  expect(res.status).toBe(400);
382
418
  expect(body.ok).toBe(false);
@@ -441,3 +477,194 @@ describe('ingress service shared logic', () => {
441
477
  expect(revoked.invite.id).toBe(created.invite.id);
442
478
  });
443
479
  });
480
+
481
+ // ---------------------------------------------------------------------------
482
+ // Voice invite routes
483
+ // ---------------------------------------------------------------------------
484
+
485
+ describe('voice invite HTTP routes', () => {
486
+ beforeEach(resetTables);
487
+
488
+ test('POST /v1/ingress/invites with sourceChannel voice — creates invite with voiceCode, stores hash only', async () => {
489
+ const req = new Request('http://localhost/v1/ingress/invites', {
490
+ method: 'POST',
491
+ headers: { 'Content-Type': 'application/json' },
492
+ body: JSON.stringify({
493
+ sourceChannel: 'voice',
494
+ expectedExternalUserId: '+15551234567',
495
+ maxUses: 3,
496
+ }),
497
+ });
498
+
499
+ const res = await handleCreateInvite(req);
500
+ const body = await res.json() as Record<string, unknown>;
501
+
502
+ expect(res.status).toBe(201);
503
+ expect(body.ok).toBe(true);
504
+ const invite = body.invite as Record<string, unknown>;
505
+ expect(invite.sourceChannel).toBe('voice');
506
+ // Voice code should be returned (6 digits by default)
507
+ expect(typeof invite.voiceCode).toBe('string');
508
+ expect((invite.voiceCode as string).length).toBe(6);
509
+ expect(/^\d{6}$/.test(invite.voiceCode as string)).toBe(true);
510
+ // Hash should be stored
511
+ expect(typeof invite.tokenHash).toBe('string');
512
+ expect((invite.tokenHash as string).length).toBeGreaterThan(0);
513
+ // voiceCodeDigits should be recorded
514
+ expect(invite.voiceCodeDigits).toBe(6);
515
+ // expectedExternalUserId should be recorded
516
+ expect(invite.expectedExternalUserId).toBe('+15551234567');
517
+ });
518
+
519
+ test('voice invite creation requires expectedExternalUserId', async () => {
520
+ const req = new Request('http://localhost/v1/ingress/invites', {
521
+ method: 'POST',
522
+ headers: { 'Content-Type': 'application/json' },
523
+ body: JSON.stringify({
524
+ sourceChannel: 'voice',
525
+ }),
526
+ });
527
+
528
+ const res = await handleCreateInvite(req);
529
+ const body = await res.json() as Record<string, unknown>;
530
+
531
+ expect(res.status).toBe(400);
532
+ expect(body.ok).toBe(false);
533
+ expect(body.error).toContain('expectedExternalUserId');
534
+ });
535
+
536
+ test('voice invite creation validates E.164 format', async () => {
537
+ const req = new Request('http://localhost/v1/ingress/invites', {
538
+ method: 'POST',
539
+ headers: { 'Content-Type': 'application/json' },
540
+ body: JSON.stringify({
541
+ sourceChannel: 'voice',
542
+ expectedExternalUserId: 'not-a-phone-number',
543
+ }),
544
+ });
545
+
546
+ const res = await handleCreateInvite(req);
547
+ const body = await res.json() as Record<string, unknown>;
548
+
549
+ expect(res.status).toBe(400);
550
+ expect(body.ok).toBe(false);
551
+ expect(body.error).toContain('E.164');
552
+ });
553
+
554
+ test('voiceCodeDigits is always 6 — custom values are ignored', async () => {
555
+ const req = new Request('http://localhost/v1/ingress/invites', {
556
+ method: 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify({
559
+ sourceChannel: 'voice',
560
+ expectedExternalUserId: '+15551234567',
561
+ voiceCodeDigits: 8,
562
+ }),
563
+ });
564
+
565
+ const res = await handleCreateInvite(req);
566
+ const body = await res.json() as Record<string, unknown>;
567
+
568
+ expect(res.status).toBe(201);
569
+ expect(body.ok).toBe(true);
570
+ const invite = body.invite as Record<string, unknown>;
571
+ expect((invite.voiceCode as string).length).toBe(6);
572
+ expect(invite.voiceCodeDigits).toBe(6);
573
+ });
574
+
575
+ test('voice invites do NOT return token in response', async () => {
576
+ const req = new Request('http://localhost/v1/ingress/invites', {
577
+ method: 'POST',
578
+ headers: { 'Content-Type': 'application/json' },
579
+ body: JSON.stringify({
580
+ sourceChannel: 'voice',
581
+ expectedExternalUserId: '+15551234567',
582
+ }),
583
+ });
584
+
585
+ const res = await handleCreateInvite(req);
586
+ const body = await res.json() as Record<string, unknown>;
587
+
588
+ expect(res.status).toBe(201);
589
+ const invite = body.invite as Record<string, unknown>;
590
+ // Voice invites must not expose the raw token — callers redeem via
591
+ // the identity-bound voice code flow
592
+ expect(invite.token).toBeUndefined();
593
+ });
594
+
595
+ test('POST /v1/ingress/invites/redeem — redeems a voice invite code via unified endpoint', async () => {
596
+ // Create a voice invite
597
+ const createRes = await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
598
+ method: 'POST',
599
+ headers: { 'Content-Type': 'application/json' },
600
+ body: JSON.stringify({
601
+ sourceChannel: 'voice',
602
+ expectedExternalUserId: '+15551234567',
603
+ maxUses: 1,
604
+ }),
605
+ }));
606
+ const created = await createRes.json() as { invite: { voiceCode: string } };
607
+
608
+ // Redeem the voice code via the unified /redeem endpoint
609
+ const redeemReq = new Request('http://localhost/v1/ingress/invites/redeem', {
610
+ method: 'POST',
611
+ headers: { 'Content-Type': 'application/json' },
612
+ body: JSON.stringify({
613
+ callerExternalUserId: '+15551234567',
614
+ code: created.invite.voiceCode,
615
+ }),
616
+ });
617
+
618
+ const res = await handleRedeemInvite(redeemReq);
619
+ const body = await res.json() as Record<string, unknown>;
620
+
621
+ expect(res.status).toBe(200);
622
+ expect(body.ok).toBe(true);
623
+ expect(body.type).toBe('redeemed');
624
+ expect(typeof body.memberId).toBe('string');
625
+ expect(typeof body.inviteId).toBe('string');
626
+ });
627
+
628
+ test('POST /v1/ingress/invites/redeem — voice code missing fields returns 400', async () => {
629
+ const req = new Request('http://localhost/v1/ingress/invites/redeem', {
630
+ method: 'POST',
631
+ headers: { 'Content-Type': 'application/json' },
632
+ body: JSON.stringify({ callerExternalUserId: '+15551234567' }),
633
+ });
634
+
635
+ const res = await handleRedeemInvite(req);
636
+ const body = await res.json() as Record<string, unknown>;
637
+
638
+ // No `code` and no `token` → falls through to token-based path which requires token
639
+ expect(res.status).toBe(400);
640
+ expect(body.ok).toBe(false);
641
+ });
642
+
643
+ test('POST /v1/ingress/invites/redeem — wrong voice code returns 400', async () => {
644
+ // Create a voice invite
645
+ await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
646
+ method: 'POST',
647
+ headers: { 'Content-Type': 'application/json' },
648
+ body: JSON.stringify({
649
+ sourceChannel: 'voice',
650
+ expectedExternalUserId: '+15551234567',
651
+ maxUses: 1,
652
+ }),
653
+ }));
654
+
655
+ const req = new Request('http://localhost/v1/ingress/invites/redeem', {
656
+ method: 'POST',
657
+ headers: { 'Content-Type': 'application/json' },
658
+ body: JSON.stringify({
659
+ callerExternalUserId: '+15551234567',
660
+ code: '000000',
661
+ }),
662
+ });
663
+
664
+ const res = await handleRedeemInvite(req);
665
+ const body = await res.json() as Record<string, unknown>;
666
+
667
+ expect(res.status).toBe(400);
668
+ expect(body.ok).toBe(false);
669
+ });
670
+ });
@@ -48,6 +48,8 @@ mock.module('../util/logger.js', () => ({
48
48
 
49
49
  mock.module('../config/loader.js', () => ({
50
50
  getConfig: () => ({
51
+ ui: {},
52
+
51
53
  sandbox: { enabled: false, backend: 'local' },
52
54
  }),
53
55
  }));
@@ -830,6 +830,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
830
830
  { filename: 'chart.png', mimeType: 'image/png', data: 'iVBORw0K' },
831
831
  ],
832
832
  },
833
+ message_request_complete: {
834
+ type: 'message_request_complete',
835
+ sessionId: 'sess-001',
836
+ requestId: 'req-inline-001',
837
+ runStillActive: true,
838
+ },
833
839
  session_info: {
834
840
  type: 'session_info',
835
841
  sessionId: 'sess-001',
@@ -2179,6 +2185,13 @@ describe('IPC message snapshots', () => {
2179
2185
  const complete = serverMessages.message_complete;
2180
2186
  expect(complete.type).toBe('message_complete');
2181
2187
  });
2188
+
2189
+ test('message_request_complete has sessionId and requestId fields', () => {
2190
+ const complete = serverMessages.message_request_complete;
2191
+ expect(complete.type).toBe('message_request_complete');
2192
+ expect((complete as unknown as { sessionId: string }).sessionId).toBe('sess-001');
2193
+ expect((complete as unknown as { requestId: string }).requestId).toBe('req-inline-001');
2194
+ });
2182
2195
  });
2183
2196
 
2184
2197
  // Baseline: session contract includes threadType metadata
@@ -19,6 +19,8 @@ let mockGenerateError: Error | null = null;
19
19
 
20
20
  mock.module('../config/loader.js', () => ({
21
21
  getConfig: () => ({
22
+ ui: {},
23
+
22
24
  apiKeys: { gemini: mockApiKey },
23
25
  }),
24
26
  }));
@@ -70,6 +72,25 @@ mock.module('drizzle-orm', () => ({
70
72
  }));
71
73
 
72
74
  mock.module('../memory/conversation-store.js', () => ({
75
+ setConversationOriginChannelIfUnset: () => {},
76
+ updateConversationContextWindow: () => {},
77
+ deleteMessageById: () => {},
78
+ updateConversationTitle: () => {},
79
+ updateConversationUsage: () => {},
80
+ addMessage: () => ({ id: 'mock-msg-id' }),
81
+ getMessages: () => [],
82
+ getConversation: () => ({
83
+ id: 'conv-1',
84
+ contextSummary: null,
85
+ contextCompactedMessageCount: 0,
86
+ totalInputTokens: 0,
87
+ totalOutputTokens: 0,
88
+ totalEstimatedCost: 0,
89
+ title: null,
90
+ }),
91
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
92
+ getConversationOriginInterface: () => null,
93
+ getConversationOriginChannel: () => null,
73
94
  getConversationThreadType: () => 'standard',
74
95
  }));
75
96
 
@@ -49,6 +49,8 @@ mock.module('../util/logger.js', () => ({
49
49
 
50
50
  mock.module('../config/loader.js', () => ({
51
51
  getConfig: () => ({
52
+ ui: {},
53
+
52
54
  model: 'test',
53
55
  provider: 'test',
54
56
  apiKeys: {},
@@ -4448,7 +4448,7 @@ describe('Memory regressions', () => {
4448
4448
  role: 'user',
4449
4449
  content: 'Untrusted sender says preferences should not become durable profile memory.',
4450
4450
  metadata: JSON.stringify({
4451
- provenanceActorRole: 'non-guardian',
4451
+ provenanceTrustClass: 'trusted_contact',
4452
4452
  provenanceSourceChannel: 'telegram',
4453
4453
  }),
4454
4454
  createdAt: conv.createdAt + 1,
@@ -4516,7 +4516,7 @@ describe('Memory regressions', () => {
4516
4516
  conversationId: 'conv-relation-provenance-gate',
4517
4517
  role: 'user',
4518
4518
  content: JSON.stringify([{ type: 'text', text: 'Trusted guardian message for relation backfill.' }]),
4519
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
4519
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
4520
4520
  createdAt: now + 1,
4521
4521
  },
4522
4522
  {
@@ -4524,7 +4524,7 @@ describe('Memory regressions', () => {
4524
4524
  conversationId: 'conv-relation-provenance-gate',
4525
4525
  role: 'user',
4526
4526
  content: JSON.stringify([{ type: 'text', text: 'Untrusted message that should be excluded from relation backfill extraction.' }]),
4527
- metadata: JSON.stringify({ provenanceActorRole: 'non-guardian', provenanceSourceChannel: 'telegram' }),
4527
+ metadata: JSON.stringify({ provenanceTrustClass: 'trusted_contact', provenanceSourceChannel: 'telegram' }),
4528
4528
  createdAt: now + 2,
4529
4529
  },
4530
4530
  ]).run();
@@ -4566,7 +4566,7 @@ describe('Memory regressions', () => {
4566
4566
  const conv = createConversation('provenance-preserve');
4567
4567
  const metadata = {
4568
4568
  userMessageChannel: 'telegram' as const,
4569
- provenanceActorRole: 'non-guardian' as const,
4569
+ provenanceTrustClass: 'trusted_contact' as const,
4570
4570
  provenanceSourceChannel: 'telegram' as const,
4571
4571
  provenanceGuardianExternalUserId: 'guardian-123',
4572
4572
  provenanceRequesterIdentifier: 'Alice',
@@ -4582,7 +4582,7 @@ describe('Memory regressions', () => {
4582
4582
 
4583
4583
  expect(stored).toBeTruthy();
4584
4584
  const parsed = JSON.parse(stored!.metadata!);
4585
- expect(parsed.provenanceActorRole).toBe('non-guardian');
4585
+ expect(parsed.provenanceTrustClass).toBe('trusted_contact');
4586
4586
  expect(parsed.provenanceSourceChannel).toBe('telegram');
4587
4587
  expect(parsed.provenanceGuardianExternalUserId).toBe('guardian-123');
4588
4588
  expect(parsed.provenanceRequesterIdentifier).toBe('Alice');
@@ -4590,13 +4590,13 @@ describe('Memory regressions', () => {
4590
4590
 
4591
4591
  test('messageMetadataSchema validates provenance fields', () => {
4592
4592
  const valid = messageMetadataSchema.safeParse({
4593
- provenanceActorRole: 'guardian',
4594
- provenanceSourceChannel: 'macos',
4593
+ provenanceTrustClass: 'guardian',
4594
+ provenanceSourceChannel: 'vellum',
4595
4595
  });
4596
4596
  expect(valid.success).toBe(true);
4597
4597
 
4598
4598
  const validNonGuardian = messageMetadataSchema.safeParse({
4599
- provenanceActorRole: 'non-guardian',
4599
+ provenanceTrustClass: 'trusted_contact',
4600
4600
  provenanceSourceChannel: 'telegram',
4601
4601
  provenanceGuardianExternalUserId: 'g-123',
4602
4602
  provenanceRequesterIdentifier: 'Bob',
@@ -4604,41 +4604,41 @@ describe('Memory regressions', () => {
4604
4604
  expect(validNonGuardian.success).toBe(true);
4605
4605
 
4606
4606
  const validUnverified = messageMetadataSchema.safeParse({
4607
- provenanceActorRole: 'unverified_channel',
4607
+ provenanceTrustClass: 'unknown',
4608
4608
  });
4609
4609
  expect(validUnverified.success).toBe(true);
4610
4610
  });
4611
4611
 
4612
4612
  test('provenanceFromGuardianContext returns unverified_channel default when no context', () => {
4613
4613
  const result = provenanceFromGuardianContext(null);
4614
- expect(result.provenanceActorRole).toBe('unverified_channel');
4614
+ expect(result.provenanceTrustClass).toBe('unknown');
4615
4615
  expect(result.provenanceSourceChannel).toBeUndefined();
4616
4616
 
4617
4617
  const result2 = provenanceFromGuardianContext(undefined);
4618
- expect(result2.provenanceActorRole).toBe('unverified_channel');
4618
+ expect(result2.provenanceTrustClass).toBe('unknown');
4619
4619
  });
4620
4620
 
4621
4621
  test('provenanceFromGuardianContext extracts fields from guardian context', () => {
4622
4622
  const ctx = {
4623
4623
  sourceChannel: 'telegram' as const,
4624
- actorRole: 'non-guardian' as const,
4624
+ trustClass: 'trusted_contact' as const,
4625
4625
  guardianExternalUserId: 'g-456',
4626
4626
  requesterIdentifier: 'Charlie',
4627
4627
  };
4628
4628
  const result = provenanceFromGuardianContext(ctx);
4629
- expect(result.provenanceActorRole).toBe('non-guardian');
4629
+ expect(result.provenanceTrustClass).toBe('trusted_contact');
4630
4630
  expect(result.provenanceSourceChannel).toBe('telegram');
4631
4631
  expect(result.provenanceGuardianExternalUserId).toBe('g-456');
4632
4632
  expect(result.provenanceRequesterIdentifier).toBe('Charlie');
4633
4633
  });
4634
4634
 
4635
- test('indexMessageNow receives provenanceActorRole when metadata includes it', async () => {
4635
+ test('indexMessageNow receives provenanceTrustClass when metadata includes it', async () => {
4636
4636
  const conv = createConversation('provenance-indexer');
4637
4637
  const metadata = {
4638
- provenanceActorRole: 'non-guardian' as const,
4638
+ provenanceTrustClass: 'trusted_contact' as const,
4639
4639
  provenanceSourceChannel: 'telegram' as const,
4640
4640
  };
4641
- // addMessage parses metadata and passes provenanceActorRole to indexMessageNow.
4641
+ // addMessage parses metadata and passes provenanceTrustClass to indexMessageNow.
4642
4642
  // We verify indirectly: the message is persisted with metadata and segments are indexed.
4643
4643
  const msg = await addMessage(conv.id, 'user', 'Test provenance indexing message with enough content to segment', metadata);
4644
4644
  expect(msg.id).toBeTruthy();
@@ -4683,7 +4683,7 @@ describe('Memory regressions', () => {
4683
4683
  role: 'user',
4684
4684
  content: JSON.stringify([{ type: 'text', text: 'Untrusted user preference for dark mode.' }]),
4685
4685
  createdAt: now,
4686
- provenanceActorRole: 'non-guardian',
4686
+ provenanceTrustClass: 'trusted_contact',
4687
4687
  }, DEFAULT_CONFIG.memory);
4688
4688
 
4689
4689
  expect(result.indexedSegments).toBeGreaterThan(0);
@@ -4733,7 +4733,7 @@ describe('Memory regressions', () => {
4733
4733
  role: 'user',
4734
4734
  content: JSON.stringify([{ type: 'text', text: 'Trusted guardian preference for light mode.' }]),
4735
4735
  createdAt: now,
4736
- provenanceActorRole: 'guardian',
4736
+ provenanceTrustClass: 'guardian',
4737
4737
  }, DEFAULT_CONFIG.memory);
4738
4738
 
4739
4739
  expect(result.indexedSegments).toBeGreaterThan(0);
@@ -4779,7 +4779,7 @@ describe('Memory regressions', () => {
4779
4779
  role: 'user',
4780
4780
  content: JSON.stringify([{ type: 'text', text: 'Legacy message with no provenance info.' }]),
4781
4781
  createdAt: now,
4782
- // provenanceActorRole is intentionally omitted (undefined) for backwards compat
4782
+ // provenanceTrustClass is intentionally omitted (undefined) for backwards compat
4783
4783
  }, DEFAULT_CONFIG.memory);
4784
4784
 
4785
4785
  expect(result.indexedSegments).toBeGreaterThan(0);
@@ -4824,7 +4824,7 @@ describe('Memory regressions', () => {
4824
4824
  role: 'user',
4825
4825
  content: JSON.stringify([{ type: 'text', text: 'Unverified channel preference for compact layout.' }]),
4826
4826
  createdAt: now,
4827
- provenanceActorRole: 'unverified_channel',
4827
+ provenanceTrustClass: 'unknown',
4828
4828
  }, DEFAULT_CONFIG.memory);
4829
4829
 
4830
4830
  expect(result.indexedSegments).toBeGreaterThan(0);