@vellumai/assistant 0.3.28 → 0.4.1

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 (201) 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 +25 -21
  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 +288 -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/response-tier.ts +6 -5
  157. package/src/daemon/session-agent-loop.ts +5 -5
  158. package/src/daemon/session-lifecycle.ts +25 -17
  159. package/src/daemon/session-memory.ts +2 -2
  160. package/src/daemon/session-process.ts +1 -20
  161. package/src/daemon/session-runtime-assembly.ts +28 -22
  162. package/src/daemon/session-tool-setup.ts +2 -2
  163. package/src/daemon/session.ts +3 -3
  164. package/src/memory/canonical-guardian-store.ts +63 -1
  165. package/src/memory/channel-guardian-store.ts +1 -0
  166. package/src/memory/conversation-crud.ts +7 -7
  167. package/src/memory/db-init.ts +4 -0
  168. package/src/memory/embedding-local.ts +257 -39
  169. package/src/memory/embedding-runtime-manager.ts +471 -0
  170. package/src/memory/guardian-bindings.ts +25 -1
  171. package/src/memory/indexer.ts +3 -3
  172. package/src/memory/ingress-invite-store.ts +45 -0
  173. package/src/memory/job-handlers/backfill.ts +16 -9
  174. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  175. package/src/memory/migrations/index.ts +1 -0
  176. package/src/memory/qdrant-client.ts +31 -22
  177. package/src/memory/schema.ts +4 -0
  178. package/src/notifications/copy-composer.ts +15 -0
  179. package/src/runtime/access-request-helper.ts +43 -7
  180. package/src/runtime/actor-trust-resolver.ts +46 -50
  181. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  182. package/src/runtime/channel-retry-sweep.ts +18 -6
  183. package/src/runtime/guardian-context-resolver.ts +38 -96
  184. package/src/runtime/guardian-reply-router.ts +31 -1
  185. package/src/runtime/ingress-service.ts +80 -3
  186. package/src/runtime/invite-redemption-service.ts +141 -2
  187. package/src/runtime/routes/channel-route-shared.ts +1 -1
  188. package/src/runtime/routes/channel-routes.ts +1 -1
  189. package/src/runtime/routes/conversation-routes.ts +166 -2
  190. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  191. package/src/runtime/routes/inbound-message-handler.ts +41 -10
  192. package/src/runtime/routes/ingress-routes.ts +52 -4
  193. package/src/runtime/routes/pairing-routes.ts +3 -0
  194. package/src/tools/guardian-control-plane-policy.ts +2 -2
  195. package/src/tools/reminder/reminder-store.ts +10 -14
  196. package/src/tools/tool-approval-handler.ts +11 -11
  197. package/src/tools/types.ts +2 -2
  198. package/src/util/logger.ts +20 -8
  199. package/src/util/platform.ts +10 -0
  200. package/src/util/voice-code.ts +29 -0
  201. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -47,6 +47,8 @@ const mockCallsConfig = {
47
47
 
48
48
  mock.module('../config/loader.js', () => ({
49
49
  getConfig: () => ({
50
+ ui: {},
51
+
50
52
  model: 'test',
51
53
  provider: 'test',
52
54
  apiKeys: {},
@@ -227,8 +229,8 @@ describe('runtime call routes — HTTP layer', () => {
227
229
  });
228
230
 
229
231
  expect(res.status).toBe(400);
230
- const body = await res.json() as { error: string };
231
- expect(body.error).toContain('conversationId');
232
+ const body = await res.json() as { error: { message: string; code?: string } };
233
+ expect(body.error.message).toContain('conversationId');
232
234
 
233
235
  await stopServer();
234
236
  });
@@ -269,8 +271,8 @@ describe('runtime call routes — HTTP layer', () => {
269
271
  });
270
272
 
271
273
  expect(res.status).toBe(400);
272
- const body = await res.json() as { error: string };
273
- expect(body.error).toContain('E.164');
274
+ const body = await res.json() as { error: { message: string; code?: string } };
275
+ expect(body.error.message).toContain('E.164');
274
276
 
275
277
  await stopServer();
276
278
  });
@@ -285,8 +287,8 @@ describe('runtime call routes — HTTP layer', () => {
285
287
  });
286
288
 
287
289
  expect(res.status).toBe(400);
288
- const body = await res.json() as { error: string };
289
- expect(body.error).toContain('Invalid JSON');
290
+ const body = await res.json() as { error: { message: string; code?: string } };
291
+ expect(body.error.message).toContain('Invalid JSON');
290
292
 
291
293
  await stopServer();
292
294
  });
@@ -309,8 +311,8 @@ describe('runtime call routes — HTTP layer', () => {
309
311
  // user_number mode requires a configured user phone number;
310
312
  // since we haven't set one, this should return a 400 explaining why
311
313
  expect(res.status).toBe(400);
312
- const body = await res.json() as { error: string };
313
- expect(body.error).toContain('user_number');
314
+ const body = await res.json() as { error: { message: string; code?: string } };
315
+ expect(body.error.message).toContain('user_number');
314
316
 
315
317
  await stopServer();
316
318
  });
@@ -364,11 +366,11 @@ describe('runtime call routes — HTTP layer', () => {
364
366
  });
365
367
 
366
368
  expect(res.status).toBe(400);
367
- const body = await res.json() as { error: string };
368
- expect(body.error).toContain('Invalid callerIdentityMode');
369
- expect(body.error).toContain('bogus');
370
- expect(body.error).toContain('assistant_number');
371
- expect(body.error).toContain('user_number');
369
+ const body = await res.json() as { error: { message: string; code?: string } };
370
+ expect(body.error.message).toContain('Invalid callerIdentityMode');
371
+ expect(body.error.message).toContain('bogus');
372
+ expect(body.error.message).toContain('assistant_number');
373
+ expect(body.error.message).toContain('user_number');
372
374
 
373
375
  await stopServer();
374
376
  });
@@ -510,8 +512,8 @@ describe('runtime call routes — HTTP layer', () => {
510
512
  });
511
513
 
512
514
  expect(res.status).toBe(400);
513
- const body = await res.json() as { error: string };
514
- expect(body.error).toContain('Invalid JSON');
515
+ const body = await res.json() as { error: { message: string; code?: string } };
516
+ expect(body.error.message).toContain('Invalid JSON');
515
517
 
516
518
  await stopServer();
517
519
  });
@@ -533,9 +535,9 @@ describe('runtime call routes — HTTP layer', () => {
533
535
  body: JSON.stringify({ answer: 'Yes, please' }),
534
536
  });
535
537
 
536
- expect(res.status).toBe(404);
537
- const body = await res.json() as { error: string };
538
- expect(body.error).toContain('pending question');
538
+ expect(res.status).toBe(409);
539
+ const body = await res.json() as { error: { message: string; code?: string } };
540
+ expect(body.error.message).toContain('No active controller');
539
541
 
540
542
  await stopServer();
541
543
  });
@@ -583,8 +585,8 @@ describe('runtime call routes — HTTP layer', () => {
583
585
  });
584
586
 
585
587
  expect(res.status).toBe(409);
586
- const body = await res.json() as { error: string };
587
- expect(body.error).toContain('orchestrator');
588
+ const body = await res.json() as { error: { message: string; code?: string } };
589
+ expect(body.error.message).toContain('No active controller');
588
590
 
589
591
  await stopServer();
590
592
  });
@@ -609,8 +611,8 @@ describe('runtime call routes — HTTP layer', () => {
609
611
  });
610
612
 
611
613
  expect(res.status).toBe(400);
612
- const body = await res.json() as { error: string };
613
- expect(body.error).toContain('Invalid JSON');
614
+ const body = await res.json() as { error: { message: string; code?: string } };
615
+ expect(body.error.message).toContain('Invalid JSON');
614
616
 
615
617
  await stopServer();
616
618
  });
@@ -633,8 +635,8 @@ describe('runtime call routes — HTTP layer', () => {
633
635
  });
634
636
 
635
637
  expect(res.status).toBe(400);
636
- const body = await res.json() as { error: string };
637
- expect(body.error).toContain('instructionText');
638
+ const body = await res.json() as { error: { message: string; code?: string } };
639
+ expect(body.error.message).toContain('instructionText');
638
640
 
639
641
  await stopServer();
640
642
  });
@@ -657,8 +659,8 @@ describe('runtime call routes — HTTP layer', () => {
657
659
  });
658
660
 
659
661
  expect(res.status).toBe(400);
660
- const body = await res.json() as { error: string };
661
- expect(body.error).toContain('instructionText');
662
+ const body = await res.json() as { error: { message: string; code?: string } };
663
+ expect(body.error.message).toContain('instructionText');
662
664
 
663
665
  await stopServer();
664
666
  });
@@ -673,8 +675,8 @@ describe('runtime call routes — HTTP layer', () => {
673
675
  });
674
676
 
675
677
  expect(res.status).toBe(404);
676
- const body = await res.json() as { error: string };
677
- expect(body.error).toContain('No call session found');
678
+ const body = await res.json() as { error: { message: string; code?: string } };
679
+ expect(body.error.message).toContain('No call session found');
678
680
 
679
681
  await stopServer();
680
682
  });
@@ -699,8 +701,8 @@ describe('runtime call routes — HTTP layer', () => {
699
701
  });
700
702
 
701
703
  expect(res.status).toBe(409);
702
- const body = await res.json() as { error: string };
703
- expect(body.error).toContain('not active');
704
+ const body = await res.json() as { error: { message: string; code?: string } };
705
+ expect(body.error.message).toContain('not active');
704
706
 
705
707
  await stopServer();
706
708
  });
@@ -723,8 +725,8 @@ describe('runtime call routes — HTTP layer', () => {
723
725
  });
724
726
 
725
727
  expect(res.status).toBe(409);
726
- const body = await res.json() as { error: string };
727
- expect(body.error).toContain('orchestrator');
728
+ const body = await res.json() as { error: { message: string; code?: string } };
729
+ expect(body.error.message).toContain('No active controller');
728
730
 
729
731
  await stopServer();
730
732
  });
@@ -11,6 +11,8 @@ let activeVoiceSession: {
11
11
 
12
12
  mock.module('../config/loader.js', () => ({
13
13
  getConfig: () => ({
14
+ ui: {},
15
+
14
16
  calls: { enabled: callsEnabled },
15
17
  }),
16
18
  }));
@@ -71,7 +71,7 @@ describe('channel-invite-transport', () => {
71
71
 
72
72
  describe('telegram buildShareableInvite', () => {
73
73
  test('produces a valid Telegram deep link', () => {
74
- const result = telegramInviteTransport.buildShareableInvite({
74
+ const result = telegramInviteTransport.buildShareableInvite!({
75
75
  rawToken: 'abc123_test-token',
76
76
  sourceChannel: 'telegram',
77
77
  });
@@ -81,15 +81,15 @@ describe('channel-invite-transport', () => {
81
81
  });
82
82
 
83
83
  test('deep link is deterministic for the same token', () => {
84
- const a = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
85
- const b = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
84
+ const a = telegramInviteTransport.buildShareableInvite!({ rawToken: 'tok1', sourceChannel: 'telegram' });
85
+ const b = telegramInviteTransport.buildShareableInvite!({ rawToken: 'tok1', sourceChannel: 'telegram' });
86
86
  expect(a.url).toBe(b.url);
87
87
  expect(a.displayText).toBe(b.displayText);
88
88
  });
89
89
 
90
90
  test('uses the configured bot username', () => {
91
91
  mockBotUsername = 'my_custom_bot';
92
- const result = telegramInviteTransport.buildShareableInvite({
92
+ const result = telegramInviteTransport.buildShareableInvite!({
93
93
  rawToken: 'token',
94
94
  sourceChannel: 'telegram',
95
95
  });
@@ -103,7 +103,7 @@ describe('channel-invite-transport', () => {
103
103
  delete process.env.TELEGRAM_BOT_USERNAME;
104
104
  try {
105
105
  expect(() =>
106
- telegramInviteTransport.buildShareableInvite({
106
+ telegramInviteTransport.buildShareableInvite!({
107
107
  rawToken: 'token',
108
108
  sourceChannel: 'telegram',
109
109
  }),
@@ -118,7 +118,7 @@ describe('channel-invite-transport', () => {
118
118
  const prev = process.env.TELEGRAM_BOT_USERNAME;
119
119
  process.env.TELEGRAM_BOT_USERNAME = 'env_bot';
120
120
  try {
121
- const result = telegramInviteTransport.buildShareableInvite({
121
+ const result = telegramInviteTransport.buildShareableInvite!({
122
122
  rawToken: 'token',
123
123
  sourceChannel: 'telegram',
124
124
  });
@@ -50,6 +50,25 @@ mock.module('../runtime/gateway-client.js', () => ({
50
50
  }));
51
51
 
52
52
  mock.module('../memory/conversation-store.js', () => ({
53
+ getConversationThreadType: () => 'default',
54
+ setConversationOriginChannelIfUnset: () => {},
55
+ updateConversationContextWindow: () => {},
56
+ deleteMessageById: () => {},
57
+ updateConversationTitle: () => {},
58
+ updateConversationUsage: () => {},
59
+ addMessage: () => ({ id: 'mock-msg-id' }),
60
+ getConversation: () => ({
61
+ id: 'conv-1',
62
+ contextSummary: null,
63
+ contextCompactedMessageCount: 0,
64
+ totalInputTokens: 0,
65
+ totalOutputTokens: 0,
66
+ totalEstimatedCost: 0,
67
+ title: null,
68
+ }),
69
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
70
+ getConversationOriginInterface: () => null,
71
+ getConversationOriginChannel: () => null,
53
72
  getMessages: () => conversationMessages,
54
73
  }));
55
74
 
@@ -0,0 +1,130 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
6
+ import { eq } from 'drizzle-orm';
7
+
8
+ const testDir = mkdtempSync(join(tmpdir(), 'channel-retry-sweep-test-'));
9
+
10
+ mock.module('../util/platform.js', () => ({
11
+ getDataDir: () => testDir,
12
+ isMacOS: () => process.platform === 'darwin',
13
+ isLinux: () => process.platform === 'linux',
14
+ isWindows: () => process.platform === 'win32',
15
+ getSocketPath: () => join(testDir, 'test.sock'),
16
+ getPidPath: () => join(testDir, 'test.pid'),
17
+ getDbPath: () => join(testDir, 'test.db'),
18
+ getLogPath: () => join(testDir, 'test.log'),
19
+ ensureDataDir: () => {},
20
+ }));
21
+
22
+ mock.module('../util/logger.js', () => ({
23
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
24
+ get: () => () => {},
25
+ }),
26
+ }));
27
+
28
+ import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
29
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
30
+ import { channelInboundEvents, messages } from '../memory/schema.js';
31
+ import { sweepFailedEvents } from '../runtime/channel-retry-sweep.js';
32
+
33
+ initializeDb();
34
+
35
+ afterAll(() => {
36
+ resetDb();
37
+ try {
38
+ rmSync(testDir, { recursive: true });
39
+ } catch {
40
+ // Best effort cleanup
41
+ }
42
+ });
43
+
44
+ function resetTables(): void {
45
+ const db = getDb();
46
+ db.run('DELETE FROM channel_inbound_events');
47
+ db.run('DELETE FROM conversation_keys');
48
+ db.run('DELETE FROM messages');
49
+ db.run('DELETE FROM conversations');
50
+ }
51
+
52
+ function seedFailedLegacyEvent(actorRole: 'guardian' | 'non-guardian' | 'unverified_channel'): string {
53
+ const inbound = channelDeliveryStore.recordInbound('telegram', 'chat-legacy', `msg-${actorRole}`);
54
+ channelDeliveryStore.storePayload(inbound.eventId, {
55
+ content: 'retry me',
56
+ sourceChannel: 'telegram',
57
+ interface: 'telegram',
58
+ guardianCtx: {
59
+ actorRole,
60
+ sourceChannel: 'telegram',
61
+ requesterExternalUserId: 'legacy-user',
62
+ requesterChatId: 'chat-legacy',
63
+ },
64
+ });
65
+
66
+ const db = getDb();
67
+ db.update(channelInboundEvents)
68
+ .set({
69
+ processingStatus: 'failed',
70
+ processingAttempts: 1,
71
+ retryAfter: Date.now() - 1,
72
+ })
73
+ .where(eq(channelInboundEvents.id, inbound.eventId))
74
+ .run();
75
+
76
+ return inbound.eventId;
77
+ }
78
+
79
+ describe('channel-retry-sweep', () => {
80
+ beforeEach(() => {
81
+ resetTables();
82
+ });
83
+
84
+ test('replays legacy guardianCtx.actorRole with preserved trust semantics', async () => {
85
+ const cases: Array<{
86
+ actorRole: 'guardian' | 'non-guardian' | 'unverified_channel';
87
+ expectedTrustClass: 'guardian' | 'trusted_contact' | 'unknown';
88
+ expectedInteractive: boolean;
89
+ }> = [
90
+ { actorRole: 'guardian', expectedTrustClass: 'guardian', expectedInteractive: true },
91
+ { actorRole: 'non-guardian', expectedTrustClass: 'trusted_contact', expectedInteractive: false },
92
+ { actorRole: 'unverified_channel', expectedTrustClass: 'unknown', expectedInteractive: false },
93
+ ];
94
+
95
+ for (const c of cases) {
96
+ const eventId = seedFailedLegacyEvent(c.actorRole);
97
+ let capturedOptions: {
98
+ guardianContext?: { trustClass?: string };
99
+ isInteractive?: boolean;
100
+ } | undefined;
101
+
102
+ await sweepFailedEvents(
103
+ async (conversationId, _content, _attachmentIds, options) => {
104
+ capturedOptions = options as {
105
+ guardianContext?: { trustClass?: string };
106
+ isInteractive?: boolean;
107
+ };
108
+ const messageId = `message-${c.actorRole}`;
109
+ const db = getDb();
110
+ db.insert(messages).values({
111
+ id: messageId,
112
+ conversationId,
113
+ role: 'user',
114
+ content: JSON.stringify([{ type: 'text', text: 'retry me' }]),
115
+ createdAt: Date.now(),
116
+ }).run();
117
+ return { messageId };
118
+ },
119
+ undefined,
120
+ );
121
+
122
+ expect(capturedOptions?.guardianContext?.trustClass).toBe(c.expectedTrustClass);
123
+ expect(capturedOptions?.isInteractive).toBe(c.expectedInteractive);
124
+
125
+ const db = getDb();
126
+ const row = db.select().from(channelInboundEvents).where(eq(channelInboundEvents.id, eventId)).get();
127
+ expect(row?.processingStatus).toBe('processed');
128
+ }
129
+ });
130
+ });
@@ -57,6 +57,8 @@ mock.module('../providers/provider-send-message.js', () => ({
57
57
 
58
58
  mock.module('../config/loader.js', () => ({
59
59
  getConfig: () => ({
60
+ ui: {},
61
+
60
62
  apiKeys: {
61
63
  anthropic: 'test-key',
62
64
  },
@@ -23,6 +23,8 @@ mock.module('../util/logger.js', () => ({
23
23
 
24
24
  mock.module('../config/loader.js', () => ({
25
25
  getConfig: () => ({
26
+ ui: {},
27
+
26
28
  apiKeys: { anthropic: 'test-key' },
27
29
  }),
28
30
  }));
@@ -31,6 +31,8 @@ mock.module('../util/logger.js', () => ({
31
31
  // Mock config
32
32
  mock.module('../config/loader.js', () => ({
33
33
  getConfig: () => ({
34
+ ui: {},
35
+
34
36
  apiKeys: { anthropic: 'test-key' },
35
37
  }),
36
38
  }));
@@ -3,11 +3,12 @@ import { existsSync,mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
 
6
- import { afterEach,beforeEach, describe, expect, test } from 'bun:test';
6
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test';
7
7
 
8
8
  import {
9
9
  _resetEnrichmentService,
10
10
  CommitEnrichmentService,
11
+ getEnrichmentService,
11
12
  } from '../workspace/commit-message-enrichment-service.js';
12
13
  import type { CommitContext } from '../workspace/commit-message-provider.js';
13
14
  import { _resetGitServiceRegistry,WorkspaceGitService } from '../workspace/git-service.js';
@@ -27,11 +28,18 @@ describe('CommitEnrichmentService', () => {
27
28
  });
28
29
 
29
30
  afterEach(async () => {
31
+ try { await getEnrichmentService().shutdown(); } catch { /* ignore */ }
32
+ _resetEnrichmentService();
30
33
  if (existsSync(testDir)) {
31
34
  rmSync(testDir, { recursive: true, force: true });
32
35
  }
33
36
  });
34
37
 
38
+ afterAll(async () => {
39
+ try { await getEnrichmentService().shutdown(); } catch { /* ignore */ }
40
+ _resetEnrichmentService();
41
+ });
42
+
35
43
  function makeContext(overrides?: Partial<CommitContext>): CommitContext {
36
44
  return {
37
45
  workspaceDir: testDir,
@@ -5,6 +5,8 @@ import { describe, expect, mock,test } from 'bun:test';
5
5
  // (which have no trust rules in test) don't trigger approval prompts.
6
6
  mock.module('../config/loader.js', () => ({
7
7
  getConfig: () => ({
8
+ ui: {},
9
+
8
10
  provider: 'mock-provider',
9
11
  permissions: { mode: 'legacy' },
10
12
  apiKeys: {},
@@ -47,6 +47,7 @@ mock.module('../util/platform.js', () => ({
47
47
  isLinux: () => true,
48
48
  isWindows: () => false,
49
49
  readHttpToken: () => null,
50
+ normalizeAssistantId: (id: string) => id,
50
51
  }));
51
52
 
52
53
  mock.module('../tools/executor.js', () => ({
@@ -3,6 +3,8 @@ import { beforeAll, describe, expect, mock,test } from 'bun:test';
3
3
  // Mock config before importing modules that depend on it.
4
4
  mock.module('../config/loader.js', () => ({
5
5
  getConfig: () => ({
6
+ ui: {},
7
+
6
8
  provider: 'mock-provider',
7
9
  permissions: { mode: 'legacy' },
8
10
  apiKeys: {},
@@ -79,7 +79,7 @@ describe('AssistantConfigSchema', () => {
79
79
  expect(result.thinking).toEqual({ enabled: false, budgetTokens: 10000, streamThinking: false });
80
80
  expect(result.contextWindow).toEqual({
81
81
  enabled: true,
82
- maxInputTokens: 180000,
82
+ maxInputTokens: 200000,
83
83
  targetInputTokens: 110000,
84
84
  compactThreshold: 0.8,
85
85
  preserveRecentUserTurns: 8,
@@ -1098,7 +1098,7 @@ describe('loadConfig with schema validation', () => {
1098
1098
  expect(config.thinking).toEqual({ enabled: false, budgetTokens: 10000, streamThinking: false });
1099
1099
  expect(config.contextWindow).toEqual({
1100
1100
  enabled: true,
1101
- maxInputTokens: 180000,
1101
+ maxInputTokens: 200000,
1102
1102
  targetInputTokens: 110000,
1103
1103
  compactThreshold: 0.8,
1104
1104
  preserveRecentUserTurns: 8,
@@ -1188,7 +1188,7 @@ describe('loadConfig with schema validation', () => {
1188
1188
  test('falls back for invalid contextWindow relationship', () => {
1189
1189
  writeConfig({ contextWindow: { maxInputTokens: 1000, targetInputTokens: 1000 } });
1190
1190
  const config = loadConfig();
1191
- expect(config.contextWindow.maxInputTokens).toBe(180000);
1191
+ expect(config.contextWindow.maxInputTokens).toBe(200000);
1192
1192
  expect(config.contextWindow.targetInputTokens).toBe(110000);
1193
1193
  });
1194
1194
 
@@ -1348,7 +1348,7 @@ describe('Call entrypoint gating', () => {
1348
1348
  const response = await handleStartCall(req);
1349
1349
  expect(response.status).toBe(403);
1350
1350
 
1351
- const body = await response.json() as { error: string };
1352
- expect(body.error).toContain('disabled');
1351
+ const body = await response.json() as { error: { message: string } };
1352
+ expect(body.error.message).toContain('disabled');
1353
1353
  });
1354
1354
  });
@@ -96,7 +96,9 @@ mock.module('node:fs', () => {
96
96
 
97
97
  // Mock config/loader and other dependencies that ConfigWatcher imports
98
98
  mock.module('../config/loader.js', () => ({
99
- getConfig: () => ({}),
99
+ getConfig: () => ({
100
+ ui: {},
101
+ }),
100
102
  invalidateConfigCache: () => {},
101
103
  }));
102
104
 
@@ -41,21 +41,30 @@ describe('hasNoAuthOverride', () => {
41
41
  expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'false' })).toBe(false);
42
42
  });
43
43
 
44
- test('returns true when VELLUM_DAEMON_NOAUTH is 1', () => {
45
- expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '1' })).toBe(true);
44
+ test('returns true when VELLUM_DAEMON_NOAUTH is 1 with safety gate', () => {
45
+ expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '1', VELLUM_UNSAFE_AUTH_BYPASS: '1' })).toBe(true);
46
46
  });
47
47
 
48
- test('returns true when VELLUM_DAEMON_NOAUTH is true', () => {
49
- expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'true' })).toBe(true);
48
+ test('returns false when VELLUM_DAEMON_NOAUTH is 1 without safety gate', () => {
49
+ expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '1' })).toBe(false);
50
+ });
51
+
52
+ test('returns true when VELLUM_DAEMON_NOAUTH is true with safety gate', () => {
53
+ expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'true', VELLUM_UNSAFE_AUTH_BYPASS: '1' })).toBe(true);
54
+ });
55
+
56
+ test('returns false when VELLUM_DAEMON_NOAUTH is true without safety gate', () => {
57
+ expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'true' })).toBe(false);
50
58
  });
51
59
 
52
60
  test('is independent of VELLUM_DAEMON_SOCKET', () => {
53
61
  // Socket override alone does NOT enable no-auth
54
62
  expect(hasNoAuthOverride({ VELLUM_DAEMON_SOCKET: '/tmp/custom.sock' })).toBe(false);
55
- // No-auth requires its own explicit flag
63
+ // No-auth requires its own explicit flag plus safety gate
56
64
  expect(hasNoAuthOverride({
57
65
  VELLUM_DAEMON_SOCKET: '/tmp/custom.sock',
58
66
  VELLUM_DAEMON_NOAUTH: '1',
67
+ VELLUM_UNSAFE_AUTH_BYPASS: '1',
59
68
  })).toBe(true);
60
69
  });
61
70
  });
@@ -27,7 +27,9 @@ mock.module('../util/logger.js', () => ({
27
27
  }));
28
28
 
29
29
  mock.module('../config/loader.js', () => ({
30
- getConfig: () => ({ memory: {} }),
30
+ getConfig: () => ({
31
+ ui: {},
32
+ memory: {} }),
31
33
  }));
32
34
 
33
35
  import type { Database } from 'bun:sqlite';
@@ -73,6 +73,8 @@ let mockConflictableKinds: string[] = [
73
73
 
74
74
  mock.module('../config/loader.js', () => ({
75
75
  getConfig: () => ({
76
+ ui: {},
77
+
76
78
  apiKeys: { anthropic: 'test-key' },
77
79
  memory: {
78
80
  conflicts: {
@@ -49,6 +49,16 @@ const getConversationMock = mock((id: string) => {
49
49
  });
50
50
 
51
51
  mock.module('../memory/conversation-store.js', () => ({
52
+ getConversationThreadType: () => 'default',
53
+ setConversationOriginChannelIfUnset: () => {},
54
+ updateConversationContextWindow: () => {},
55
+ deleteMessageById: () => {},
56
+ updateConversationTitle: () => {},
57
+ updateConversationUsage: () => {},
58
+ getMessages: () => [],
59
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
60
+ getConversationOriginInterface: () => null,
61
+ getConversationOriginChannel: () => null,
52
62
  createConversation: createConversationMock,
53
63
  addMessage: addMessageMock,
54
64
  getConversation: getConversationMock,
@@ -49,7 +49,7 @@ describe('handleSendMessage', () => {
49
49
  expect(body.messageId).toBe('msg-legacy-fallback');
50
50
  expect(capturedSourceChannel).toBe('telegram');
51
51
  expect(capturedOptions?.guardianContext).toEqual({
52
- actorRole: 'guardian',
52
+ trustClass: 'guardian',
53
53
  sourceChannel: 'telegram',
54
54
  });
55
55
  });
@@ -61,6 +61,7 @@ mock.module('../tools/registry.js', () => ({
61
61
  // Imports under test
62
62
  // ---------------------------------------------------------------------------
63
63
 
64
+ import { DEFAULT_CONFIG } from '../config/defaults.js';
64
65
  import { redactSensitiveFields } from '../security/redaction.js';
65
66
  import { setSecureKey } from '../security/secure-keys.js';
66
67
  import { CredentialBroker } from '../tools/credentials/broker.js';
@@ -125,7 +126,18 @@ describe('Invariant 1: secrets never enter LLM context', () => {
125
126
  test('user message containing secret is blocked from entering history', () => {
126
127
  // Mock config to enable block mode
127
128
  mock.module('../config/loader.js', () => ({
129
+ applyNestedDefaults: (config: unknown) => config,
128
130
  getConfig: () => ({
131
+ ui: {},
132
+ secretDetection: {
133
+ enabled: true,
134
+ action: 'block',
135
+ blockIngress: true,
136
+ },
137
+ }),
138
+ invalidateConfigCache: () => {},
139
+ loadConfig: () => ({
140
+ ui: {},
129
141
  secretDetection: {
130
142
  enabled: true,
131
143
  action: 'block',
@@ -204,6 +216,10 @@ describe('Invariant 2: no generic plaintext secret read API', () => {
204
216
  'messaging/providers/telegram-bot/adapter.ts', // Telegram bot token lookup for connectivity check
205
217
  'messaging/providers/sms/adapter.ts', // Twilio credential lookup for SMS connectivity check
206
218
  'runtime/channel-readiness-service.ts', // channel readiness probes for SMS/Telegram connectivity
219
+ 'messaging/providers/whatsapp/adapter.ts', // WhatsApp credential lookup for connectivity check
220
+ 'schedule/integration-status.ts', // integration status checks for scheduled reports
221
+ 'daemon/handlers/oauth-connect.ts', // OAuth connect handler for integration setup
222
+ 'daemon/handlers/config-slack-channel.ts', // Slack channel config credential management
207
223
  ]);
208
224
 
209
225
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -436,20 +452,14 @@ describe('One-time send override', () => {
436
452
  });
437
453
 
438
454
  test('allowOneTimeSend defaults to false in config', () => {
439
- // eslint-disable-next-line @typescript-eslint/no-require-imports
440
- const { DEFAULT_CONFIG } = require('../config/defaults.js');
441
455
  expect(DEFAULT_CONFIG.secretDetection.allowOneTimeSend).toBe(false);
442
456
  });
443
457
 
444
458
  test('default secretDetection.action is redact', () => {
445
- // eslint-disable-next-line @typescript-eslint/no-require-imports
446
- const { DEFAULT_CONFIG } = require('../config/defaults.js');
447
459
  expect(DEFAULT_CONFIG.secretDetection.action).toBe('redact');
448
460
  });
449
461
 
450
462
  test('default secretDetection.blockIngress is true', () => {
451
- // eslint-disable-next-line @typescript-eslint/no-require-imports
452
- const { DEFAULT_CONFIG } = require('../config/defaults.js');
453
463
  expect(DEFAULT_CONFIG.secretDetection.blockIngress).toBe(true);
454
464
  });
455
465
  });