@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
@@ -36,6 +36,8 @@ mock.module('../providers/registry.js', () => ({
36
36
 
37
37
  mock.module('../config/loader.js', () => ({
38
38
  getConfig: () => ({
39
+ ui: {},
40
+
39
41
  provider: 'mock-provider',
40
42
  maxTokens: 4096,
41
43
  thinking: false,
@@ -111,6 +113,11 @@ mock.module('../security/secret-allowlist.js', () => ({
111
113
  }));
112
114
 
113
115
  mock.module('../memory/conversation-store.js', () => ({
116
+ getConversationThreadType: () => 'default',
117
+ setConversationOriginChannelIfUnset: () => {},
118
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
119
+ getConversationOriginInterface: () => null,
120
+ getConversationOriginChannel: () => null,
114
121
  getMessages: () => persistedMessages,
115
122
  getConversation: () => ({
116
123
  id: 'conv-1',
@@ -235,6 +242,20 @@ mock.module('../agent/loop.js', () => ({
235
242
  }
236
243
  },
237
244
  }));
245
+ mock.module('../memory/canonical-guardian-store.js', () => ({
246
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
247
+ listCanonicalGuardianRequests: () => [],
248
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
249
+ getCanonicalGuardianRequest: () => null,
250
+ getCanonicalGuardianRequestByCode: () => null,
251
+ updateCanonicalGuardianRequest: () => {},
252
+ resolveCanonicalGuardianRequest: () => {},
253
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
254
+ listCanonicalGuardianDeliveries: () => [],
255
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
256
+ updateCanonicalGuardianDelivery: () => {},
257
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
258
+ }));
238
259
 
239
260
  import type { SessionMemoryPolicy } from '../daemon/session.js';
240
261
  import { DEFAULT_MEMORY_POLICY,Session } from '../daemon/session.js';
@@ -27,6 +27,8 @@ mock.module('../providers/registry.js', () => ({
27
27
 
28
28
  mock.module('../config/loader.js', () => ({
29
29
  getConfig: () => ({
30
+ ui: {},
31
+
30
32
  provider: 'mock-provider',
31
33
  maxTokens: 4096,
32
34
  thinking: false,
@@ -67,6 +69,9 @@ mock.module('../memory/admin.js', () => ({
67
69
  }));
68
70
 
69
71
  mock.module('../memory/conversation-store.js', () => ({
72
+ getConversationThreadType: () => 'default',
73
+ setConversationOriginChannelIfUnset: () => {},
74
+ deleteMessageById: () => {},
70
75
  getMessages: () => [],
71
76
  getConversation: () => ({
72
77
  id: 'conv-1',
@@ -202,6 +207,21 @@ mock.module('../agent/loop.js', () => ({
202
207
  },
203
208
  }));
204
209
 
210
+ mock.module('../memory/canonical-guardian-store.js', () => ({
211
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
212
+ listCanonicalGuardianRequests: () => [],
213
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
214
+ getCanonicalGuardianRequest: () => null,
215
+ getCanonicalGuardianRequestByCode: () => null,
216
+ updateCanonicalGuardianRequest: () => {},
217
+ resolveCanonicalGuardianRequest: () => {},
218
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
219
+ listCanonicalGuardianDeliveries: () => [],
220
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
221
+ updateCanonicalGuardianDelivery: () => {},
222
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
223
+ }));
224
+
205
225
  import { Session } from '../daemon/session.js';
206
226
 
207
227
  function makeSession(): Session {
@@ -40,6 +40,8 @@ mock.module('../providers/registry.js', () => ({
40
40
 
41
41
  mock.module('../config/loader.js', () => ({
42
42
  getConfig: () => ({
43
+ ui: {},
44
+
43
45
  provider: 'mock-provider',
44
46
  maxTokens: 4096,
45
47
  thinking: false,
@@ -101,6 +103,13 @@ mock.module('../memory/admin.js', () => ({
101
103
  }));
102
104
 
103
105
  mock.module('../memory/conversation-store.js', () => ({
106
+ getConversationThreadType: () => 'default',
107
+ setConversationOriginChannelIfUnset: () => {},
108
+ updateConversationContextWindow: () => {},
109
+ deleteMessageById: () => {},
110
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
111
+ getConversationOriginInterface: () => null,
112
+ getConversationOriginChannel: () => null,
104
113
  getMessages: () => [],
105
114
  getConversation: () => ({
106
115
  id: 'conv-1',
@@ -215,6 +224,20 @@ mock.module('../agent/loop.js', () => ({
215
224
  }
216
225
  },
217
226
  }));
227
+ mock.module('../memory/canonical-guardian-store.js', () => ({
228
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
229
+ listCanonicalGuardianRequests: () => [],
230
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
231
+ getCanonicalGuardianRequest: () => null,
232
+ getCanonicalGuardianRequestByCode: () => null,
233
+ updateCanonicalGuardianRequest: () => {},
234
+ resolveCanonicalGuardianRequest: () => {},
235
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
236
+ listCanonicalGuardianDeliveries: () => [],
237
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
238
+ updateCanonicalGuardianDelivery: () => {},
239
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
240
+ }));
218
241
 
219
242
  // ---------------------------------------------------------------------------
220
243
  // Import Session AFTER mocks are registered.
@@ -26,6 +26,15 @@ describe('resolveChannelCapabilities', () => {
26
26
  test('defaults to vellum when no source channel is provided', () => {
27
27
  const caps = resolveChannelCapabilities();
28
28
  expect(caps.channel).toBe('vellum');
29
+ // Without a sourceInterface, desktop UI capabilities are false
30
+ expect(caps.dashboardCapable).toBe(false);
31
+ expect(caps.supportsDynamicUi).toBe(false);
32
+ expect(caps.supportsVoiceInput).toBe(false);
33
+ });
34
+
35
+ test('vellum channel with macos interface has full desktop capabilities', () => {
36
+ const caps = resolveChannelCapabilities(undefined, 'macos');
37
+ expect(caps.channel).toBe('vellum');
29
38
  expect(caps.dashboardCapable).toBe(true);
30
39
  expect(caps.supportsDynamicUi).toBe(true);
31
40
  expect(caps.supportsVoiceInput).toBe(true);
@@ -34,41 +43,42 @@ describe('resolveChannelCapabilities', () => {
34
43
  test('defaults to vellum for null source channel', () => {
35
44
  const caps = resolveChannelCapabilities(null);
36
45
  expect(caps.channel).toBe('vellum');
37
- expect(caps.dashboardCapable).toBe(true);
46
+ expect(caps.dashboardCapable).toBe(false);
38
47
  });
39
48
 
40
49
  test('normalises "dashboard" to "vellum"', () => {
41
50
  const caps = resolveChannelCapabilities('dashboard');
42
51
  expect(caps.channel).toBe('vellum');
43
- expect(caps.dashboardCapable).toBe(true);
44
- expect(caps.supportsDynamicUi).toBe(true);
45
- expect(caps.supportsVoiceInput).toBe(true);
52
+ // Without macos interface, capabilities are false
53
+ expect(caps.dashboardCapable).toBe(false);
54
+ expect(caps.supportsDynamicUi).toBe(false);
55
+ expect(caps.supportsVoiceInput).toBe(false);
46
56
  });
47
57
 
48
58
  test('normalises "http-api" to "vellum"', () => {
49
59
  const caps = resolveChannelCapabilities('http-api');
50
60
  expect(caps.channel).toBe('vellum');
51
- expect(caps.dashboardCapable).toBe(true);
52
- expect(caps.supportsDynamicUi).toBe(true);
53
- expect(caps.supportsVoiceInput).toBe(true);
61
+ expect(caps.dashboardCapable).toBe(false);
62
+ expect(caps.supportsDynamicUi).toBe(false);
63
+ expect(caps.supportsVoiceInput).toBe(false);
54
64
  });
55
65
 
56
66
  test('normalises "mac" to "vellum"', () => {
57
67
  const caps = resolveChannelCapabilities('mac');
58
68
  expect(caps.channel).toBe('vellum');
59
- expect(caps.dashboardCapable).toBe(true);
69
+ expect(caps.dashboardCapable).toBe(false);
60
70
  });
61
71
 
62
72
  test('normalises "macos" to "vellum"', () => {
63
73
  const caps = resolveChannelCapabilities('macos');
64
74
  expect(caps.channel).toBe('vellum');
65
- expect(caps.dashboardCapable).toBe(true);
75
+ expect(caps.dashboardCapable).toBe(false);
66
76
  });
67
77
 
68
78
  test('normalises "ios" to "vellum"', () => {
69
79
  const caps = resolveChannelCapabilities('ios');
70
80
  expect(caps.channel).toBe('vellum');
71
- expect(caps.dashboardCapable).toBe(true);
81
+ expect(caps.dashboardCapable).toBe(false);
72
82
  });
73
83
 
74
84
  test('resolves "telegram" as non-dashboard-capable', () => {
@@ -342,8 +352,8 @@ describe('buildChannelAwarenessSection', () => {
342
352
  // ---------------------------------------------------------------------------
343
353
 
344
354
  describe('trust-gating via channel capabilities', () => {
345
- test('vellum channel does not add constraint rules', () => {
346
- const caps = resolveChannelCapabilities('vellum');
355
+ test('vellum channel with macos interface does not add constraint rules', () => {
356
+ const caps = resolveChannelCapabilities('vellum', 'macos');
347
357
  const message: Message = {
348
358
  role: 'user',
349
359
  content: [{ type: 'text', text: 'Enable my microphone' }],
@@ -548,6 +558,9 @@ describe('injectInboundActorContext', () => {
548
558
  sourceChannel: 'sms',
549
559
  canonicalActorIdentity: 'guardian-user-1',
550
560
  actorIdentifier: '+15550001111',
561
+ actorDisplayName: 'Guardian Name',
562
+ actorSenderDisplayName: 'Guardian Name',
563
+ actorMemberDisplayName: 'Guardian Name',
551
564
  trustClass: 'guardian',
552
565
  guardianIdentity: 'guardian-user-1',
553
566
  };
@@ -561,9 +574,34 @@ describe('injectInboundActorContext', () => {
561
574
  expect(text).toContain('trust_class: guardian');
562
575
  expect(text).toContain('source_channel: sms');
563
576
  expect(text).toContain('canonical_actor_identity: guardian-user-1');
577
+ expect(text).toContain('actor_display_name: Guardian Name');
578
+ expect(text).toContain('actor_sender_display_name: Guardian Name');
579
+ expect(text).toContain('actor_member_display_name: Guardian Name');
564
580
  expect(text).toContain('</inbound_actor_context>');
565
581
  });
566
582
 
583
+ test('adds nickname guidance when member and sender display names differ', () => {
584
+ const ctx: InboundActorContext = {
585
+ sourceChannel: 'telegram',
586
+ canonicalActorIdentity: 'trusted-user-1',
587
+ actorIdentifier: '@jeff_handle',
588
+ actorDisplayName: 'Jeff',
589
+ actorSenderDisplayName: 'Jeffrey',
590
+ actorMemberDisplayName: 'Jeff',
591
+ trustClass: 'trusted_contact',
592
+ guardianIdentity: 'guardian-user-1',
593
+ memberStatus: 'active',
594
+ memberPolicy: 'allow',
595
+ };
596
+
597
+ const result = injectInboundActorContext(baseUserMessage, ctx);
598
+ const text = (result.content[0] as { type: 'text'; text: string }).text;
599
+ expect(text).toContain('actor_display_name: Jeff');
600
+ expect(text).toContain('actor_sender_display_name: Jeffrey');
601
+ expect(text).toContain('actor_member_display_name: Jeff');
602
+ expect(text).toContain('name_preference_note: actor_member_display_name is the guardian-preferred nickname');
603
+ });
604
+
567
605
  test('includes behavioral guidance for trusted_contact actors', () => {
568
606
  const ctx: InboundActorContext = {
569
607
  sourceChannel: 'telegram',
@@ -33,6 +33,7 @@ let mockVersionHashErrors: Set<string> = new Set();
33
33
 
34
34
  mock.module('../config/skills.js', () => ({
35
35
  loadSkillCatalog: () => mockCatalog,
36
+ checkSkillRequirements: () => ({ eligible: true, missing: {} }),
36
37
  }));
37
38
 
38
39
  mock.module('../skills/active-skill-tools.js', () => {
@@ -204,6 +205,27 @@ mock.module('../util/logger.js', () => ({
204
205
  }),
205
206
  }));
206
207
 
208
+ mock.module('../config/loader.js', () => ({
209
+ getConfig: () => ({
210
+ skills: { entries: {}, allowBundled: null },
211
+ assistantFeatureFlagValues: {},
212
+ }),
213
+ loadConfig: () => ({
214
+ skills: { entries: {}, allowBundled: null },
215
+ assistantFeatureFlagValues: {},
216
+ }),
217
+ invalidateConfigCache: () => {},
218
+ }));
219
+
220
+ mock.module('../config/assistant-feature-flags.js', () => ({
221
+ isAssistantFeatureFlagEnabled: () => true,
222
+ loadDefaultsRegistry: () => ({}),
223
+ }));
224
+
225
+ mock.module('../config/skill-state.js', () => ({
226
+ skillFlagKey: (skillId: string) => `feature_flags.${skillId}.enabled`,
227
+ }));
228
+
207
229
  // ---------------------------------------------------------------------------
208
230
  // Import module under test (after mocks)
209
231
  // ---------------------------------------------------------------------------
@@ -1673,7 +1695,7 @@ describe('bundled skill: browser', () => {
1673
1695
  sessionState = new Map<string, string>();
1674
1696
  });
1675
1697
 
1676
- test('browser skill activation via loaded_skill marker projects all 10 tool definitions', () => {
1698
+ test('browser skill activation via loaded_skill marker projects all 14 tool definitions', () => {
1677
1699
  mockCatalog = [makeSkill('browser', '/path/to/bundled-skills/browser')];
1678
1700
  mockManifests = { browser: makeManifest([...BROWSER_TOOL_NAMES]) };
1679
1701
 
@@ -1683,7 +1705,7 @@ describe('bundled skill: browser', () => {
1683
1705
 
1684
1706
  const result = projectSkillTools(history, { previouslyActiveSkillIds: sessionState });
1685
1707
 
1686
- expect(result.toolDefinitions).toHaveLength(10);
1708
+ expect(result.toolDefinitions).toHaveLength(14);
1687
1709
  expect(result.toolDefinitions.map((d) => d.name)).toEqual([...BROWSER_TOOL_NAMES]);
1688
1710
  expect(result.allowedToolNames).toEqual(new Set(BROWSER_TOOL_NAMES));
1689
1711
  });
@@ -1717,7 +1739,7 @@ describe('bundled skill: browser', () => {
1717
1739
 
1718
1740
  const tools = mockRegisteredTools.get('browser');
1719
1741
  expect(tools).toBeDefined();
1720
- expect(tools!.length).toBe(10);
1742
+ expect(tools!.length).toBe(14);
1721
1743
 
1722
1744
  for (const tool of tools!) {
1723
1745
  expect(tool.origin).toBe('skill');
@@ -2410,8 +2432,8 @@ describe('browser skill migration harness', () => {
2410
2432
  expect(id1).not.toBe(id2);
2411
2433
  });
2412
2434
 
2413
- test('BROWSER_TOOL_NAMES contains all 10 browser tools', () => {
2414
- expect(BROWSER_TOOL_NAMES).toHaveLength(10);
2435
+ test('BROWSER_TOOL_NAMES contains all 14 browser tools', () => {
2436
+ expect(BROWSER_TOOL_NAMES).toHaveLength(14);
2415
2437
  expect(BROWSER_TOOL_NAMES).toContain('browser_navigate');
2416
2438
  expect(BROWSER_TOOL_NAMES).toContain('browser_fill_credential');
2417
2439
  });
@@ -30,6 +30,8 @@ mock.module('../providers/registry.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'mock-provider',
34
36
  maxTokens: 4096,
35
37
  thinking: false,
@@ -62,6 +64,13 @@ mock.module('../security/secret-allowlist.js', () => ({
62
64
  }));
63
65
 
64
66
  mock.module('../memory/conversation-store.js', () => ({
67
+ getConversationThreadType: () => 'default',
68
+ setConversationOriginChannelIfUnset: () => {},
69
+ updateConversationContextWindow: () => {},
70
+ deleteMessageById: () => {},
71
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
72
+ getConversationOriginInterface: () => null,
73
+ getConversationOriginChannel: () => null,
65
74
  getMessages: () => [],
66
75
  getConversation: () => ({
67
76
  id: 'conv-1',
@@ -165,6 +174,20 @@ mock.module('../agent/loop.js', () => ({
165
174
  }
166
175
  },
167
176
  }));
177
+ mock.module('../memory/canonical-guardian-store.js', () => ({
178
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
179
+ listCanonicalGuardianRequests: () => [],
180
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
181
+ getCanonicalGuardianRequest: () => null,
182
+ getCanonicalGuardianRequestByCode: () => null,
183
+ updateCanonicalGuardianRequest: () => {},
184
+ resolveCanonicalGuardianRequest: () => {},
185
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
186
+ listCanonicalGuardianDeliveries: () => [],
187
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
188
+ updateCanonicalGuardianDelivery: () => {},
189
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
190
+ }));
168
191
 
169
192
  // ---------------------------------------------------------------------------
170
193
  // Import Session AFTER mocks are registered.
@@ -30,6 +30,8 @@ mock.module('../providers/registry.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'mock-provider',
34
36
  maxTokens: 4096,
35
37
  thinking: false,
@@ -62,6 +64,13 @@ mock.module('../security/secret-allowlist.js', () => ({
62
64
  }));
63
65
 
64
66
  mock.module('../memory/conversation-store.js', () => ({
67
+ getConversationThreadType: () => 'default',
68
+ setConversationOriginChannelIfUnset: () => {},
69
+ updateConversationContextWindow: () => {},
70
+ deleteMessageById: () => {},
71
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
72
+ getConversationOriginInterface: () => null,
73
+ getConversationOriginChannel: () => null,
65
74
  getMessages: () => [],
66
75
  getConversation: () => ({
67
76
  id: 'conv-1',
@@ -165,6 +174,20 @@ mock.module('../agent/loop.js', () => ({
165
174
  }
166
175
  },
167
176
  }));
177
+ mock.module('../memory/canonical-guardian-store.js', () => ({
178
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
179
+ listCanonicalGuardianRequests: () => [],
180
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
181
+ getCanonicalGuardianRequest: () => null,
182
+ getCanonicalGuardianRequestByCode: () => null,
183
+ updateCanonicalGuardianRequest: () => {},
184
+ resolveCanonicalGuardianRequest: () => {},
185
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
186
+ listCanonicalGuardianDeliveries: () => [],
187
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
188
+ updateCanonicalGuardianDelivery: () => {},
189
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
190
+ }));
168
191
 
169
192
  // ---------------------------------------------------------------------------
170
193
  // Import Session AFTER mocks are registered.
@@ -30,6 +30,8 @@ mock.module('../providers/registry.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'mock-provider',
34
36
  maxTokens: 4096,
35
37
  thinking: false,
@@ -64,6 +66,13 @@ mock.module('../security/secret-allowlist.js', () => ({
64
66
  const addMessageCalls: Array<{ convId: string; role: string; content: string }> = [];
65
67
 
66
68
  mock.module('../memory/conversation-store.js', () => ({
69
+ getConversationThreadType: () => 'default',
70
+ setConversationOriginChannelIfUnset: () => {},
71
+ updateConversationContextWindow: () => {},
72
+ deleteMessageById: () => {},
73
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
74
+ getConversationOriginInterface: () => null,
75
+ getConversationOriginChannel: () => null,
67
76
  getMessages: () => [],
68
77
  getConversation: () => ({
69
78
  id: 'conv-1',
@@ -177,6 +186,20 @@ mock.module('../agent/loop.js', () => ({
177
186
  }
178
187
  },
179
188
  }));
189
+ mock.module('../memory/canonical-guardian-store.js', () => ({
190
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
191
+ listCanonicalGuardianRequests: () => [],
192
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
193
+ getCanonicalGuardianRequest: () => null,
194
+ getCanonicalGuardianRequestByCode: () => null,
195
+ updateCanonicalGuardianRequest: () => {},
196
+ resolveCanonicalGuardianRequest: () => {},
197
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
198
+ listCanonicalGuardianDeliveries: () => [],
199
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
200
+ updateCanonicalGuardianDelivery: () => {},
201
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
202
+ }));
180
203
 
181
204
  // ---------------------------------------------------------------------------
182
205
  // Import Session AFTER mocks are registered.
@@ -23,6 +23,8 @@ mock.module('../providers/registry.js', () => ({
23
23
 
24
24
  mock.module('../config/loader.js', () => ({
25
25
  getConfig: () => ({
26
+ ui: {},
27
+
26
28
  provider: 'mock-provider',
27
29
  maxTokens: 4096,
28
30
  thinking: false,
@@ -76,6 +78,11 @@ mock.module('../security/secret-allowlist.js', () => ({
76
78
  }));
77
79
 
78
80
  mock.module('../memory/conversation-store.js', () => ({
81
+ getConversationThreadType: () => 'default',
82
+ setConversationOriginChannelIfUnset: () => {},
83
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
84
+ getConversationOriginInterface: () => null,
85
+ getConversationOriginChannel: () => null,
79
86
  getMessages: () => [],
80
87
  getConversation: () => ({
81
88
  id: 'conv-1',
@@ -37,6 +37,8 @@ mock.module('../providers/registry.js', () => ({
37
37
 
38
38
  mock.module('../config/loader.js', () => ({
39
39
  getConfig: () => ({
40
+ ui: {},
41
+
40
42
  provider: 'mock-provider',
41
43
  maxTokens: 4096,
42
44
  thinking: false,
@@ -67,6 +69,11 @@ mock.module('../permissions/trust-store.js', () => ({ addRule: () => {}, findHig
67
69
  mock.module('../security/secret-allowlist.js', () => ({ resetAllowlist: () => {} }));
68
70
 
69
71
  mock.module('../memory/conversation-store.js', () => ({
72
+ getConversationThreadType: () => 'default',
73
+ setConversationOriginChannelIfUnset: () => {},
74
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
75
+ getConversationOriginInterface: () => null,
76
+ getConversationOriginChannel: () => null,
70
77
  getMessages: () => [],
71
78
  getConversation: () => ({
72
79
  id: 'conv-1', contextSummary: null, contextCompactedMessageCount: 0,
@@ -134,6 +141,20 @@ mock.module('../agent/loop.js', () => ({
134
141
  }
135
142
  },
136
143
  }));
144
+ mock.module('../memory/canonical-guardian-store.js', () => ({
145
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
146
+ listCanonicalGuardianRequests: () => [],
147
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
148
+ getCanonicalGuardianRequest: () => null,
149
+ getCanonicalGuardianRequestByCode: () => null,
150
+ updateCanonicalGuardianRequest: () => {},
151
+ resolveCanonicalGuardianRequest: () => {},
152
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
153
+ listCanonicalGuardianDeliveries: () => [],
154
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
155
+ updateCanonicalGuardianDelivery: () => {},
156
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
157
+ }));
137
158
 
138
159
  import { Session } from '../daemon/session.js';
139
160
 
@@ -36,6 +36,8 @@ mock.module('../providers/registry.js', () => ({
36
36
 
37
37
  mock.module('../config/loader.js', () => ({
38
38
  getConfig: () => ({
39
+ ui: {},
40
+
39
41
  provider: 'mock-provider',
40
42
  maxTokens: 4096,
41
43
  thinking: false,
@@ -66,6 +68,11 @@ mock.module('../permissions/trust-store.js', () => ({ addRule: () => {}, findHig
66
68
  mock.module('../security/secret-allowlist.js', () => ({ resetAllowlist: () => {} }));
67
69
 
68
70
  mock.module('../memory/conversation-store.js', () => ({
71
+ getConversationThreadType: () => 'default',
72
+ setConversationOriginChannelIfUnset: () => {},
73
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
74
+ getConversationOriginInterface: () => null,
75
+ getConversationOriginChannel: () => null,
69
76
  getMessages: () => [],
70
77
  getConversation: () => ({
71
78
  id: 'conv-1', contextSummary: null, contextCompactedMessageCount: 0,
@@ -124,6 +131,20 @@ mock.module('../agent/loop.js', () => ({
124
131
  }
125
132
  },
126
133
  }));
134
+ mock.module('../memory/canonical-guardian-store.js', () => ({
135
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
136
+ listCanonicalGuardianRequests: () => [],
137
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
138
+ getCanonicalGuardianRequest: () => null,
139
+ getCanonicalGuardianRequestByCode: () => null,
140
+ updateCanonicalGuardianRequest: () => {},
141
+ resolveCanonicalGuardianRequest: () => {},
142
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
143
+ listCanonicalGuardianDeliveries: () => [],
144
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
145
+ updateCanonicalGuardianDelivery: () => {},
146
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
147
+ }));
127
148
 
128
149
  import { Session } from '../daemon/session.js';
129
150
 
@@ -20,6 +20,8 @@ mock.module('../tools/registry.js', () => ({
20
20
  // Mock config
21
21
  mock.module('../config/loader.js', () => ({
22
22
  getConfig: () => ({
23
+ ui: {},
24
+
23
25
  timeouts: { shellDefaultTimeoutSec: 120, shellMaxTimeoutSec: 600 },
24
26
  sandbox: { enabled: false, backend: 'none' },
25
27
  secretDetection: { allowOneTimeSend: false },
@@ -20,7 +20,7 @@ let currentConfig: Record<string, unknown> = {
20
20
  };
21
21
 
22
22
  const DECLARED_SKILL_ID = 'hatch-new-assistant';
23
- const DECLARED_LEGACY_KEY = 'skills.hatch-new-assistant.enabled';
23
+ const DECLARED_FLAG_KEY = 'feature_flags.hatch-new-assistant.enabled';
24
24
 
25
25
  mock.module('../util/platform.js', () => ({
26
26
  getRootDir: () => TEST_DIR,
@@ -121,7 +121,7 @@ describe('buildSystemPrompt feature flag filtering', () => {
121
121
 
122
122
  currentConfig = {
123
123
  sandbox: { enabled: false, backend: 'native' },
124
- featureFlags: { [DECLARED_LEGACY_KEY]: false },
124
+ assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
125
125
  };
126
126
 
127
127
  const result = buildSystemPrompt();
@@ -137,7 +137,7 @@ describe('buildSystemPrompt feature flag filtering', () => {
137
137
 
138
138
  currentConfig = {
139
139
  sandbox: { enabled: false, backend: 'native' },
140
- featureFlags: {},
140
+ assistantFeatureFlagValues: {},
141
141
  };
142
142
 
143
143
  const result = buildSystemPrompt();
@@ -152,9 +152,9 @@ describe('buildSystemPrompt feature flag filtering', () => {
152
152
 
153
153
  currentConfig = {
154
154
  sandbox: { enabled: false, backend: 'native' },
155
- featureFlags: {
156
- [DECLARED_LEGACY_KEY]: false,
157
- 'skills.twitter.enabled': false,
155
+ assistantFeatureFlagValues: {
156
+ [DECLARED_FLAG_KEY]: false,
157
+ 'feature_flags.twitter.enabled': false,
158
158
  },
159
159
  };
160
160
 
@@ -15,7 +15,7 @@ let currentConfig: Record<string, unknown> = {
15
15
  };
16
16
 
17
17
  const DECLARED_SKILL_ID = 'hatch-new-assistant';
18
- const DECLARED_LEGACY_KEY = 'skills.hatch-new-assistant.enabled';
18
+ const DECLARED_FLAG_KEY = 'feature_flags.hatch-new-assistant.enabled';
19
19
 
20
20
  const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
21
21
  getRootDir: () => TEST_DIR,
@@ -54,6 +54,7 @@ mock.module('../util/logger.js', () => ({
54
54
  getLogger: () => new Proxy({} as Record<string, unknown>, {
55
55
  get: () => () => {},
56
56
  }),
57
+ isDebug: () => false,
57
58
  }));
58
59
 
59
60
  mock.module('../config/loader.js', () => ({
@@ -101,7 +102,7 @@ describe('skill_load feature flag enforcement', () => {
101
102
  writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), `- ${DECLARED_SKILL_ID}\n`);
102
103
 
103
104
  currentConfig = {
104
- featureFlags: { [DECLARED_LEGACY_KEY]: false },
105
+ assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
105
106
  };
106
107
 
107
108
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });
@@ -116,7 +117,7 @@ describe('skill_load feature flag enforcement', () => {
116
117
  writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), `- ${DECLARED_SKILL_ID}\n`);
117
118
 
118
119
  currentConfig = {
119
- featureFlags: { [DECLARED_LEGACY_KEY]: true },
120
+ assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: true },
120
121
  };
121
122
 
122
123
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });
@@ -130,7 +131,7 @@ describe('skill_load feature flag enforcement', () => {
130
131
  writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), `- ${DECLARED_SKILL_ID}\n`);
131
132
 
132
133
  currentConfig = {
133
- featureFlags: {},
134
+ assistantFeatureFlagValues: {},
134
135
  };
135
136
 
136
137
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });