@vellumai/assistant 0.3.27 → 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 (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -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
  });
@@ -470,7 +470,7 @@ describe('credential_store tool — oauth2_connect error paths', () => {
470
470
  client_id: 'test-client-id',
471
471
  }, { ..._ctx, isInteractive: false });
472
472
  expect(result.isError).toBe(true);
473
- expect(result.content).toContain('interactive client session');
473
+ expect(result.content).toContain('non-interactive session');
474
474
  });
475
475
 
476
476
  test('resolves gmail alias to integration:gmail', async () => {
@@ -672,7 +672,7 @@ describe('credential_store tool — tool definition', () => {
672
672
  expect(schema.required).toContain('action');
673
673
  const props = schema.properties as Record<string, Record<string, unknown>>;
674
674
  expect(props.action.enum).toEqual(
675
- ['store', 'list', 'delete', 'prompt', 'oauth2_connect'],
675
+ ['store', 'list', 'delete', 'prompt', 'oauth2_connect', 'describe'],
676
676
  );
677
677
  });
678
678
 
@@ -406,18 +406,19 @@ describe('credential_store tool', () => {
406
406
  expect(entries[0].service).toBe('svc-b');
407
407
  });
408
408
 
409
- test('returns error when secure storage is corrupt/unreadable', async () => {
409
+ test('recovers from corrupt secure storage by resetting and returning empty list', async () => {
410
410
  // Store a credential so metadata exists
411
411
  await credentialStoreTool.execute({
412
412
  action: 'store', service: 'svc-x', field: 'key', value: 'val-x',
413
413
  }, _ctx);
414
414
 
415
- // Corrupt the encrypted store file so listKeys() throws
415
+ // Corrupt the encrypted store file the store auto-recovers by
416
+ // backing up the corrupt file and creating a fresh store
416
417
  writeFileSync(STORE_PATH, 'not-valid-json!!!', 'utf-8');
417
418
 
418
419
  const result = await credentialStoreTool.execute({ action: 'list' }, _ctx);
419
- expect(result.isError).toBe(true);
420
- expect(result.content).toContain('failed to read secure storage');
420
+ // Store auto-recovers: list succeeds but the corrupted credentials are lost
421
+ expect(result.isError).toBe(false);
421
422
  });
422
423
  });
423
424
 
@@ -94,6 +94,15 @@ const conversation = {
94
94
  };
95
95
 
96
96
  mock.module('../memory/conversation-store.js', () => ({
97
+ setConversationOriginChannelIfUnset: () => {},
98
+ updateConversationContextWindow: () => {},
99
+ deleteMessageById: () => {},
100
+ updateConversationTitle: () => {},
101
+ updateConversationUsage: () => {},
102
+ addMessage: () => ({ id: 'mock-msg-id' }),
103
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
104
+ getConversationOriginInterface: () => null,
105
+ getConversationOriginChannel: () => null,
97
106
  getLatestConversation: () => conversation,
98
107
  createConversation: () => conversation,
99
108
  getConversation: (id: string) => (id === conversation.id ? conversation : null),
@@ -203,6 +203,8 @@ mock.module('../providers/ratelimit.js', () => ({
203
203
 
204
204
  mock.module('../config/loader.js', () => ({
205
205
  getConfig: () => ({
206
+ ui: {},
207
+
206
208
  provider: 'mock-provider',
207
209
  providerOrder: ['mock-provider'],
208
210
  maxTokens: 4096,
@@ -243,7 +245,31 @@ mock.module('../memory/external-conversation-store.js', () => ({
243
245
  getBindingsForConversations: () => new Map(),
244
246
  }));
245
247
 
248
+ mock.module('../memory/conversation-attention-store.js', () => ({
249
+ getAttentionStateByConversationIds: () => new Map(),
250
+ recordAttentionSignal: () => {},
251
+ recordConversationSeenSignal: () => {},
252
+ }));
253
+
254
+ mock.module('../memory/canonical-guardian-store.js', () => ({
255
+ generateCanonicalRequestCode: () => 'mock-code-0000',
256
+ createCanonicalGuardianRequest: () => ({ requestCode: 'mock-code-0000', status: 'pending' }),
257
+ submitCanonicalRequest: () => ({ requestCode: 'mock-code-0000', status: 'pending' }),
258
+ getCanonicalRequest: () => null,
259
+ resolveCanonicalRequest: () => false,
260
+ listPendingCanonicalRequests: () => [],
261
+ }));
262
+
246
263
  mock.module('../memory/conversation-store.js', () => ({
264
+ setConversationOriginChannelIfUnset: () => {},
265
+ updateConversationContextWindow: () => {},
266
+ deleteMessageById: () => {},
267
+ updateConversationTitle: () => {},
268
+ updateConversationUsage: () => {},
269
+ addMessage: () => ({ id: 'mock-msg-id' }),
270
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
271
+ getConversationOriginInterface: () => null,
272
+ getConversationOriginChannel: () => null,
247
273
  getLatestConversation: () => conversation,
248
274
  createConversation: (titleOrOpts?: string | { title?: string; threadType?: string }) => {
249
275
  lastCreateConversationArgs = titleOrOpts;
@@ -266,6 +292,7 @@ mock.module('../memory/conversation-store.js', () => ({
266
292
  getMessages: () => [],
267
293
  listConversations: () => [conversation],
268
294
  countConversations: () => 1,
295
+ getDisplayMetaForConversations: () => new Map(),
269
296
  }));
270
297
 
271
298
  mock.module('../daemon/session.js', () => ({
@@ -27,6 +27,8 @@ let mockVoiceConfig = {
27
27
 
28
28
  mock.module('../config/loader.js', () => ({
29
29
  getConfig: () => ({
30
+ ui: {},
31
+
30
32
  calls: { voice: mockVoiceConfig },
31
33
  }),
32
34
  }));
@@ -7,6 +7,7 @@ const runDeterministicChecksMock = mock();
7
7
  const createEventMock = mock();
8
8
  const updateEventDedupeKeyMock = mock();
9
9
  const dispatchDecisionMock = mock();
10
+ const activeBindingChannels = new Set<string>(['telegram']);
10
11
 
11
12
  mock.module('../util/logger.js', () => ({
12
13
  getLogger: () =>
@@ -21,7 +22,7 @@ mock.module('../channels/config.js', () => ({
21
22
 
22
23
  mock.module('../memory/channel-guardian-store.js', () => ({
23
24
  getActiveBinding: (_assistantId: string, channel: string) =>
24
- channel === 'telegram'
25
+ activeBindingChannels.has(channel)
25
26
  ? {
26
27
  guardianDeliveryChatId: 'guardian-chat-123',
27
28
  guardianExternalUserId: 'guardian-user-123',
@@ -83,6 +84,8 @@ describe('emitNotificationSignal routing intent re-persistence', () => {
83
84
  createEventMock.mockReset();
84
85
  updateEventDedupeKeyMock.mockReset();
85
86
  dispatchDecisionMock.mockReset();
87
+ activeBindingChannels.clear();
88
+ activeBindingChannels.add('telegram');
86
89
 
87
90
  createEventMock.mockReturnValue({ id: 'evt-1' });
88
91
  runDeterministicChecksMock.mockResolvedValue({ passed: true });
@@ -176,4 +179,43 @@ describe('emitNotificationSignal routing intent re-persistence', () => {
176
179
 
177
180
  expect(updateDecisionMock).not.toHaveBeenCalled();
178
181
  });
182
+
183
+ test('excludes unverified binding channels from connected channel candidates', async () => {
184
+ activeBindingChannels.clear();
185
+
186
+ const decision = {
187
+ shouldNotify: true,
188
+ selectedChannels: ['vellum'],
189
+ reasoningSummary: 'Local only',
190
+ renderedCopy: {
191
+ vellum: { title: 'Reminder', body: 'Check this' },
192
+ },
193
+ dedupeKey: 'dedupe-rem-3',
194
+ confidence: 0.8,
195
+ fallbackUsed: false,
196
+ persistedDecisionId: 'dec-3',
197
+ };
198
+
199
+ evaluateSignalMock.mockResolvedValue(decision);
200
+ enforceRoutingIntentMock.mockImplementation((inputDecision: unknown) => inputDecision);
201
+
202
+ await emitNotificationSignal({
203
+ sourceEventName: 'reminder.fired',
204
+ sourceChannel: 'scheduler',
205
+ sourceSessionId: 'rem-3',
206
+ attentionHints: {
207
+ requiresAction: false,
208
+ urgency: 'medium',
209
+ isAsyncBackground: false,
210
+ visibleInSourceNow: false,
211
+ },
212
+ contextPayload: { reminderId: 'rem-3' },
213
+ routingIntent: 'single_channel',
214
+ });
215
+
216
+ expect(evaluateSignalMock).toHaveBeenCalled();
217
+ const callArgs = evaluateSignalMock.mock.calls[0];
218
+ expect(callArgs).toBeDefined();
219
+ expect(callArgs?.[1]).toEqual(['vellum']);
220
+ });
179
221
  });
@@ -253,24 +253,29 @@ describe('encrypted-store', () => {
253
253
  expect(getKey('test')).toBe('value');
254
254
  });
255
255
 
256
- test('setKey refuses to overwrite a corrupt store file', () => {
256
+ test('setKey recovers from a corrupt store file by backing up and creating fresh store', () => {
257
257
  // Write a valid store first
258
258
  setKey('existing', 'old-secret');
259
259
  // Corrupt the store
260
260
  writeFileSync(STORE_PATH, 'corrupted data');
261
- // setKey should fail rather than overwrite with new salt
261
+ // setKey should recover by backing up corrupt file and creating fresh store
262
262
  const result = setKey('new-key', 'new-value');
263
- expect(result).toBe(false);
263
+ expect(result).toBe(true);
264
+ // Old key is lost but new key works
265
+ expect(getKey('new-key')).toBe('new-value');
266
+ expect(getKey('existing')).toBeUndefined();
264
267
  });
265
268
 
266
- test('setKey refuses to overwrite a store with invalid version', () => {
269
+ test('setKey recovers from a store with invalid version', () => {
267
270
  writeFileSync(STORE_PATH, JSON.stringify({
268
271
  version: 99,
269
272
  salt: 'abc',
270
273
  entries: {},
271
274
  }));
275
+ // setKey should recover by backing up invalid store and creating fresh store
272
276
  const result = setKey('test', 'value');
273
- expect(result).toBe(false);
277
+ expect(result).toBe(true);
278
+ expect(getKey('test')).toBe('value');
274
279
  });
275
280
 
276
281
  test('writeStore enforces 0600 permissions on existing files', () => {
@@ -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';
@@ -243,9 +243,9 @@ describe('gateway-only ingress enforcement', () => {
243
243
  body: makeFormBody({ CallSid: 'CA123', AccountSid: 'AC_test' }),
244
244
  });
245
245
  expect(res.status).toBe(410);
246
- const body = await res.json() as { error: string; code: string };
247
- expect(body.code).toBe('GATEWAY_ONLY');
248
- expect(body.error).toContain('Direct webhook access disabled');
246
+ const body = await res.json() as { error: { code: string; message: string } };
247
+ expect(body.error.code).toBe('GONE');
248
+ expect(body.error.message).toContain('Direct webhook access disabled');
249
249
  });
250
250
 
251
251
  test('POST /webhooks/twilio/status returns 410', async () => {
@@ -255,8 +255,8 @@ describe('gateway-only ingress enforcement', () => {
255
255
  body: makeFormBody({ CallSid: 'CA123', CallStatus: 'completed' }),
256
256
  });
257
257
  expect(res.status).toBe(410);
258
- const body = await res.json() as { error: string; code: string };
259
- expect(body.code).toBe('GATEWAY_ONLY');
258
+ const body = await res.json() as { error: { code: string; message: string } };
259
+ expect(body.error.code).toBe('GONE');
260
260
  });
261
261
 
262
262
  test('POST /webhooks/twilio/connect-action returns 410', async () => {
@@ -266,8 +266,8 @@ describe('gateway-only ingress enforcement', () => {
266
266
  body: makeFormBody({ CallSid: 'CA123' }),
267
267
  });
268
268
  expect(res.status).toBe(410);
269
- const body = await res.json() as { error: string; code: string };
270
- expect(body.code).toBe('GATEWAY_ONLY');
269
+ const body = await res.json() as { error: { code: string; message: string } };
270
+ expect(body.error.code).toBe('GONE');
271
271
  });
272
272
 
273
273
  test('POST /v1/calls/twilio/voice-webhook returns 410', async () => {
@@ -277,8 +277,8 @@ describe('gateway-only ingress enforcement', () => {
277
277
  body: makeFormBody({ CallSid: 'CA123' }),
278
278
  });
279
279
  expect(res.status).toBe(410);
280
- const body = await res.json() as { error: string; code: string };
281
- expect(body.code).toBe('GATEWAY_ONLY');
280
+ const body = await res.json() as { error: { code: string; message: string } };
281
+ expect(body.error.code).toBe('GONE');
282
282
  });
283
283
 
284
284
  test('POST /v1/calls/twilio/status returns 410', async () => {
@@ -288,8 +288,8 @@ describe('gateway-only ingress enforcement', () => {
288
288
  body: makeFormBody({ CallSid: 'CA123', CallStatus: 'completed' }),
289
289
  });
290
290
  expect(res.status).toBe(410);
291
- const body = await res.json() as { error: string; code: string };
292
- expect(body.code).toBe('GATEWAY_ONLY');
291
+ const body = await res.json() as { error: { code: string; message: string } };
292
+ expect(body.error.code).toBe('GONE');
293
293
  });
294
294
  });
295
295
 
@@ -304,9 +304,9 @@ describe('gateway-only ingress enforcement', () => {
304
304
  body: makeFormBody({ Body: 'hello', From: '+15551234567', To: '+15559876543', MessageSid: 'SM123' }),
305
305
  });
306
306
  expect(res.status).toBe(410);
307
- const body = await res.json() as { error: string; code: string };
308
- expect(body.code).toBe('GATEWAY_ONLY');
309
- expect(body.error).toContain('Direct webhook access disabled');
307
+ const body = await res.json() as { error: { code: string; message: string } };
308
+ expect(body.error.code).toBe('GONE');
309
+ expect(body.error.message).toContain('Direct webhook access disabled');
310
310
  });
311
311
 
312
312
  test('POST /v1/calls/twilio/sms returns 410 (legacy path also blocked)', async () => {
@@ -316,8 +316,8 @@ describe('gateway-only ingress enforcement', () => {
316
316
  body: makeFormBody({ Body: 'hello', From: '+15551234567', MessageSid: 'SM456' }),
317
317
  });
318
318
  expect(res.status).toBe(410);
319
- const body = await res.json() as { error: string; code: string };
320
- expect(body.code).toBe('GATEWAY_ONLY');
319
+ const body = await res.json() as { error: { code: string; message: string } };
320
+ expect(body.error.code).toBe('GONE');
321
321
  });
322
322
 
323
323
  test('POST /webhooks/twilio/sms with valid auth still returns 410 (auth does not bypass gateway-only)', async () => {
@@ -331,8 +331,8 @@ describe('gateway-only ingress enforcement', () => {
331
331
  });
332
332
  // The gateway-only guard runs before auth for Twilio webhook paths
333
333
  expect(res.status).toBe(410);
334
- const body = await res.json() as { error: string; code: string };
335
- expect(body.code).toBe('GATEWAY_ONLY');
334
+ const body = await res.json() as { error: { code: string; message: string } };
335
+ expect(body.error.code).toBe('GONE');
336
336
  });
337
337
  });
338
338
 
@@ -407,9 +407,9 @@ describe('gateway-only ingress enforcement', () => {
407
407
  },
408
408
  });
409
409
  expect(res.status).toBe(403);
410
- const body = await res.json() as { error: string; code: string };
411
- expect(body.code).toBe('GATEWAY_ONLY');
412
- expect(body.error).toContain('Direct relay access disabled');
410
+ const body = await res.json() as { error: { code: string; message: string } };
411
+ expect(body.error.code).toBe('FORBIDDEN');
412
+ expect(body.error.message).toContain('Direct relay access disabled');
413
413
  });
414
414
 
415
415
  test('allows request with no origin header (private network peer)', async () => {
@@ -41,7 +41,6 @@ describe('Messaging tool contract', () => {
41
41
  'messaging_read',
42
42
  'messaging_search',
43
43
  'messaging_send',
44
- 'send_notification',
45
44
  'messaging_reply',
46
45
  'messaging_mark_read',
47
46
  'messaging_analyze_activity',