@vellumai/assistant 0.3.28 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/ARCHITECTURE.md +33 -3
  2. package/bun.lock +4 -1
  3. package/docs/trusted-contact-access.md +9 -2
  4. package/package.json +6 -3
  5. package/scripts/ipc/generate-swift.ts +3 -3
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  7. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +13 -5
  9. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  10. package/src/__tests__/asset-search-tool.test.ts +2 -0
  11. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  12. package/src/__tests__/attachments-store.test.ts +2 -0
  13. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  14. package/src/__tests__/call-controller.test.ts +30 -29
  15. package/src/__tests__/call-routes-http.test.ts +34 -32
  16. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  17. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  18. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  19. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  20. package/src/__tests__/clarification-resolver.test.ts +2 -0
  21. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  22. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  24. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  25. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  26. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  27. package/src/__tests__/config-schema.test.ts +5 -5
  28. package/src/__tests__/config-watcher.test.ts +3 -1
  29. package/src/__tests__/connection-policy.test.ts +14 -5
  30. package/src/__tests__/contacts-tools.test.ts +3 -1
  31. package/src/__tests__/contradiction-checker.test.ts +2 -0
  32. package/src/__tests__/conversation-pairing.test.ts +10 -0
  33. package/src/__tests__/conversation-routes.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  35. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  36. package/src/__tests__/credential-vault.test.ts +5 -4
  37. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  38. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  39. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  40. package/src/__tests__/encrypted-store.test.ts +10 -5
  41. package/src/__tests__/followup-tools.test.ts +3 -1
  42. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  43. package/src/__tests__/gmail-integration.test.ts +0 -1
  44. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  45. package/src/__tests__/guardian-dispatch.test.ts +2 -0
  46. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  47. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  48. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  49. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  50. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  51. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  52. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  53. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  55. package/src/__tests__/heartbeat-service.test.ts +20 -0
  56. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  57. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  58. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  59. package/src/__tests__/intent-routing.test.ts +2 -0
  60. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  61. package/src/__tests__/media-generate-image.test.ts +21 -0
  62. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  63. package/src/__tests__/memory-regressions.test.ts +20 -20
  64. package/src/__tests__/non-member-access-request.test.ts +183 -9
  65. package/src/__tests__/notification-decision-fallback.test.ts +2 -0
  66. package/src/__tests__/notification-decision-strategy.test.ts +61 -0
  67. package/src/__tests__/notification-guardian-path.test.ts +2 -0
  68. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  69. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  70. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  71. package/src/__tests__/pairing-routes.test.ts +171 -0
  72. package/src/__tests__/playbook-execution.test.ts +3 -1
  73. package/src/__tests__/playbook-tools.test.ts +3 -1
  74. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  75. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  76. package/src/__tests__/recording-handler.test.ts +11 -0
  77. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  78. package/src/__tests__/recording-state-machine.test.ts +13 -2
  79. package/src/__tests__/registry.test.ts +7 -3
  80. package/src/__tests__/relay-server.test.ts +148 -28
  81. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  82. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  83. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  84. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  85. package/src/__tests__/schedule-tools.test.ts +3 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  87. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  88. package/src/__tests__/session-agent-loop.test.ts +16 -0
  89. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  90. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  91. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  92. package/src/__tests__/session-profile-injection.test.ts +21 -0
  93. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  94. package/src/__tests__/session-queue.test.ts +23 -0
  95. package/src/__tests__/session-runtime-assembly.test.ts +50 -12
  96. package/src/__tests__/session-skill-tools.test.ts +27 -5
  97. package/src/__tests__/session-slash-known.test.ts +23 -0
  98. package/src/__tests__/session-slash-queue.test.ts +23 -0
  99. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  100. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  101. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  102. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  103. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  104. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  105. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  107. package/src/__tests__/skills.test.ts +8 -4
  108. package/src/__tests__/slack-channel-config.test.ts +3 -1
  109. package/src/__tests__/subagent-tools.test.ts +19 -0
  110. package/src/__tests__/swarm-recursion.test.ts +2 -0
  111. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  112. package/src/__tests__/swarm-tool.test.ts +2 -0
  113. package/src/__tests__/system-prompt.test.ts +3 -1
  114. package/src/__tests__/task-compiler.test.ts +3 -1
  115. package/src/__tests__/task-management-tools.test.ts +3 -1
  116. package/src/__tests__/task-tools.test.ts +3 -1
  117. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  118. package/src/__tests__/terminal-tools.test.ts +2 -0
  119. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  120. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  121. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  122. package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
  123. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  124. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  125. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  126. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  127. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  128. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  129. package/src/__tests__/view-image-tool.test.ts +3 -1
  130. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  131. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  132. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  133. package/src/__tests__/work-item-output.test.ts +3 -1
  134. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  135. package/src/calls/call-controller.ts +26 -23
  136. package/src/calls/guardian-action-sweep.ts +10 -2
  137. package/src/calls/relay-server.ts +216 -27
  138. package/src/calls/types.ts +1 -1
  139. package/src/calls/voice-session-bridge.ts +3 -3
  140. package/src/cli.ts +12 -0
  141. package/src/config/agent-schema.ts +14 -3
  142. package/src/config/calls-schema.ts +6 -6
  143. package/src/config/core-schema.ts +3 -3
  144. package/src/config/feature-flag-registry.json +8 -0
  145. package/src/config/mcp-schema.ts +1 -1
  146. package/src/config/memory-schema.ts +27 -19
  147. package/src/config/schema.ts +21 -21
  148. package/src/config/skills-schema.ts +7 -7
  149. package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
  150. package/src/daemon/handlers/config-inbox.ts +4 -4
  151. package/src/daemon/handlers/sessions.ts +148 -4
  152. package/src/daemon/ipc-contract/messages.ts +16 -0
  153. package/src/daemon/ipc-contract-inventory.json +1 -0
  154. package/src/daemon/lifecycle.ts +19 -0
  155. package/src/daemon/pairing-store.ts +86 -3
  156. package/src/daemon/session-agent-loop.ts +5 -5
  157. package/src/daemon/session-lifecycle.ts +25 -17
  158. package/src/daemon/session-memory.ts +2 -2
  159. package/src/daemon/session-process.ts +1 -20
  160. package/src/daemon/session-runtime-assembly.ts +28 -22
  161. package/src/daemon/session-tool-setup.ts +2 -2
  162. package/src/daemon/session.ts +3 -3
  163. package/src/memory/canonical-guardian-store.ts +63 -1
  164. package/src/memory/channel-guardian-store.ts +1 -0
  165. package/src/memory/conversation-crud.ts +7 -7
  166. package/src/memory/db-init.ts +4 -0
  167. package/src/memory/embedding-local.ts +257 -39
  168. package/src/memory/embedding-runtime-manager.ts +471 -0
  169. package/src/memory/guardian-bindings.ts +25 -1
  170. package/src/memory/indexer.ts +3 -3
  171. package/src/memory/ingress-invite-store.ts +45 -0
  172. package/src/memory/job-handlers/backfill.ts +16 -9
  173. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  174. package/src/memory/migrations/index.ts +1 -0
  175. package/src/memory/qdrant-client.ts +31 -22
  176. package/src/memory/schema.ts +4 -0
  177. package/src/notifications/copy-composer.ts +15 -0
  178. package/src/runtime/access-request-helper.ts +43 -7
  179. package/src/runtime/actor-trust-resolver.ts +46 -50
  180. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  181. package/src/runtime/channel-retry-sweep.ts +18 -6
  182. package/src/runtime/guardian-context-resolver.ts +38 -96
  183. package/src/runtime/guardian-reply-router.ts +31 -1
  184. package/src/runtime/ingress-service.ts +80 -3
  185. package/src/runtime/invite-redemption-service.ts +141 -2
  186. package/src/runtime/routes/channel-route-shared.ts +1 -1
  187. package/src/runtime/routes/channel-routes.ts +1 -1
  188. package/src/runtime/routes/conversation-routes.ts +2 -2
  189. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  190. package/src/runtime/routes/inbound-message-handler.ts +41 -10
  191. package/src/runtime/routes/ingress-routes.ts +52 -4
  192. package/src/runtime/routes/pairing-routes.ts +3 -0
  193. package/src/tools/guardian-control-plane-policy.ts +2 -2
  194. package/src/tools/tool-approval-handler.ts +11 -11
  195. package/src/tools/types.ts +2 -2
  196. package/src/util/logger.ts +20 -8
  197. package/src/util/platform.ts +10 -0
  198. package/src/util/voice-code.ts +29 -0
  199. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -25,6 +25,8 @@ mock.module('../providers/registry.js', () => ({
25
25
 
26
26
  mock.module('../config/loader.js', () => ({
27
27
  getConfig: () => ({
28
+ ui: {},
29
+
28
30
  provider: 'mock-provider',
29
31
  maxTokens: 4096,
30
32
  thinking: false,
@@ -66,6 +68,13 @@ mock.module('../memory/admin.js', () => ({
66
68
  let persistedMessages: Array<{ role: string; content: string }> = [];
67
69
 
68
70
  mock.module('../memory/conversation-store.js', () => ({
71
+ getConversationThreadType: () => 'default',
72
+ setConversationOriginChannelIfUnset: () => {},
73
+ updateConversationContextWindow: () => {},
74
+ deleteMessageById: () => {},
75
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
76
+ getConversationOriginInterface: () => null,
77
+ getConversationOriginChannel: () => null,
69
78
  getMessages: () => [],
70
79
  getConversation: () => ({
71
80
  id: 'conv-1',
@@ -148,6 +157,20 @@ mock.module('../agent/loop.js', () => ({
148
157
  }
149
158
  },
150
159
  }));
160
+ mock.module('../memory/canonical-guardian-store.js', () => ({
161
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
162
+ listCanonicalGuardianRequests: () => [],
163
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
164
+ getCanonicalGuardianRequest: () => null,
165
+ getCanonicalGuardianRequestByCode: () => null,
166
+ updateCanonicalGuardianRequest: () => {},
167
+ resolveCanonicalGuardianRequest: () => {},
168
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
169
+ listCanonicalGuardianDeliveries: () => [],
170
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
171
+ updateCanonicalGuardianDelivery: () => {},
172
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
173
+ }));
151
174
 
152
175
  import { Session } from '../daemon/session.js';
153
176
 
@@ -52,6 +52,21 @@ mock.module('../hooks/manager.js', () => ({
52
52
  }));
53
53
 
54
54
  mock.module('../memory/conversation-store.js', () => ({
55
+ getConversationThreadType: () => 'default',
56
+ setConversationOriginChannelIfUnset: () => {},
57
+ updateConversationUsage: () => {},
58
+ getMessages: () => [],
59
+ getConversation: () => ({
60
+ id: 'conv-1',
61
+ contextSummary: null,
62
+ contextCompactedMessageCount: 0,
63
+ totalInputTokens: 0,
64
+ totalOutputTokens: 0,
65
+ totalEstimatedCost: 0,
66
+ title: null,
67
+ }),
68
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
69
+ getConversationOriginInterface: () => null,
55
70
  addMessage: () => ({ id: 'mock-msg-id' }),
56
71
  deleteMessageById: () => {},
57
72
  updateConversationContextWindow: () => {},
@@ -300,6 +315,7 @@ function makeCtx(overrides?: Partial<AgentLoopSessionContext> & { agentLoopRun?:
300
315
  hasQueuedMessages: () => false,
301
316
  canHandoffAtCheckpoint: () => false,
302
317
  drainQueue: () => {},
318
+ getTurnInterfaceContext: () => null,
303
319
  getTurnChannelContext: () => ({
304
320
  userMessageChannel: 'vellum' as const,
305
321
  assistantMessageChannel: 'vellum' as const,
@@ -76,6 +76,8 @@ mock.module('../providers/registry.js', () => ({
76
76
 
77
77
  mock.module('../config/loader.js', () => ({
78
78
  getConfig: () => ({
79
+ ui: {},
80
+
79
81
  provider: 'mock-provider',
80
82
  maxTokens: 4096,
81
83
  thinking: false,
@@ -149,6 +151,11 @@ mock.module('../security/secret-allowlist.js', () => ({
149
151
  }));
150
152
 
151
153
  mock.module('../memory/conversation-store.js', () => ({
154
+ getConversationThreadType: () => 'default',
155
+ setConversationOriginChannelIfUnset: () => {},
156
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
157
+ getConversationOriginInterface: () => null,
158
+ getConversationOriginChannel: () => null,
152
159
  getMessages: () => persistedMessages,
153
160
  getConversation: () => ({
154
161
  id: 'conv-1',
@@ -270,6 +277,20 @@ mock.module('../agent/loop.js', () => ({
270
277
  }
271
278
  },
272
279
  }));
280
+ mock.module('../memory/canonical-guardian-store.js', () => ({
281
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
282
+ listCanonicalGuardianRequests: () => [],
283
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
284
+ getCanonicalGuardianRequest: () => null,
285
+ getCanonicalGuardianRequestByCode: () => null,
286
+ updateCanonicalGuardianRequest: () => {},
287
+ resolveCanonicalGuardianRequest: () => {},
288
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
289
+ listCanonicalGuardianDeliveries: () => [],
290
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
291
+ updateCanonicalGuardianDelivery: () => {},
292
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
293
+ }));
273
294
 
274
295
  import { Session, type SessionMemoryPolicy } from '../daemon/session.js';
275
296
  import { ConflictGate, looksLikeClarificationReply } from '../daemon/session-conflict-gate.js';
@@ -19,6 +19,8 @@ mock.module('../providers/registry.js', () => ({
19
19
 
20
20
  mock.module('../config/loader.js', () => ({
21
21
  getConfig: () => ({
22
+ ui: {},
23
+
22
24
  provider: 'mock-provider',
23
25
  maxTokens: 4096,
24
26
  thinking: false,
@@ -54,6 +56,14 @@ let mockConversation: Record<string, unknown> | null = null;
54
56
  let nextMockMessageId = 1;
55
57
 
56
58
  mock.module('../memory/conversation-store.js', () => ({
59
+ getConversationThreadType: () => 'default',
60
+ updateConversationContextWindow: () => {},
61
+ deleteMessageById: () => {},
62
+ updateConversationTitle: () => {},
63
+ updateConversationUsage: () => {},
64
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
65
+ getConversationOriginInterface: () => null,
66
+ getConversationOriginChannel: () => null,
57
67
  getMessages: () => mockDbMessages,
58
68
  getConversation: () => mockConversation,
59
69
  createConversation: () => ({ id: 'conv-1' }),
@@ -252,30 +262,30 @@ describe('loadFromDb history repair', () => {
252
262
  id: 'm1',
253
263
  role: 'user',
254
264
  content: JSON.stringify([{ type: 'text', text: 'Guardian secret question' }]),
255
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
265
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
256
266
  },
257
267
  {
258
268
  id: 'm2',
259
269
  role: 'assistant',
260
270
  content: JSON.stringify([{ type: 'text', text: 'Guardian-only answer' }]),
261
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
271
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
262
272
  },
263
273
  {
264
274
  id: 'm3',
265
275
  role: 'user',
266
276
  content: JSON.stringify([{ type: 'text', text: 'Untrusted follow-up' }]),
267
- metadata: JSON.stringify({ provenanceActorRole: 'unverified_channel', provenanceSourceChannel: 'telegram' }),
277
+ metadata: JSON.stringify({ provenanceTrustClass: 'unknown', provenanceSourceChannel: 'telegram' }),
268
278
  },
269
279
  {
270
280
  id: 'm4',
271
281
  role: 'assistant',
272
282
  content: JSON.stringify([{ type: 'text', text: 'Untrusted-safe reply' }]),
273
- metadata: JSON.stringify({ provenanceActorRole: 'unverified_channel', provenanceSourceChannel: 'telegram' }),
283
+ metadata: JSON.stringify({ provenanceTrustClass: 'unknown', provenanceSourceChannel: 'telegram' }),
274
284
  },
275
285
  ];
276
286
 
277
287
  const session = makeSession();
278
- session.setGuardianContext({ actorRole: 'unverified_channel', sourceChannel: 'telegram' });
288
+ session.setGuardianContext({ trustClass: 'unknown', sourceChannel: 'telegram' });
279
289
  await session.loadFromDb();
280
290
  const messages = session.getMessages();
281
291
 
@@ -300,35 +310,35 @@ describe('loadFromDb history repair', () => {
300
310
  id: 'm1',
301
311
  role: 'user',
302
312
  content: JSON.stringify([{ type: 'text', text: 'Guardian question' }]),
303
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
313
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
304
314
  },
305
315
  {
306
316
  id: 'm2',
307
317
  role: 'assistant',
308
318
  content: JSON.stringify([{ type: 'text', text: 'Guardian answer' }]),
309
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
319
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
310
320
  },
311
321
  {
312
322
  id: 'm3',
313
323
  role: 'user',
314
324
  content: JSON.stringify([{ type: 'text', text: 'Unverified ping' }]),
315
- metadata: JSON.stringify({ provenanceActorRole: 'unverified_channel', provenanceSourceChannel: 'telegram' }),
325
+ metadata: JSON.stringify({ provenanceTrustClass: 'unknown', provenanceSourceChannel: 'telegram' }),
316
326
  },
317
327
  {
318
328
  id: 'm4',
319
329
  role: 'assistant',
320
330
  content: JSON.stringify([{ type: 'text', text: 'Unverified reply' }]),
321
- metadata: JSON.stringify({ provenanceActorRole: 'unverified_channel', provenanceSourceChannel: 'telegram' }),
331
+ metadata: JSON.stringify({ provenanceTrustClass: 'unknown', provenanceSourceChannel: 'telegram' }),
322
332
  },
323
333
  ];
324
334
 
325
335
  const session = makeSession();
326
336
 
327
- session.setGuardianContext({ actorRole: 'guardian', sourceChannel: 'telegram' });
337
+ session.setGuardianContext({ trustClass: 'guardian', sourceChannel: 'telegram' });
328
338
  await session.ensureActorScopedHistory();
329
339
  expect(session.getMessages()).toHaveLength(4);
330
340
 
331
- session.setGuardianContext({ actorRole: 'unverified_channel', sourceChannel: 'telegram' });
341
+ session.setGuardianContext({ trustClass: 'unknown', sourceChannel: 'telegram' });
332
342
  await session.ensureActorScopedHistory();
333
343
  const downgradedMessages = session.getMessages();
334
344
  expect(downgradedMessages).toHaveLength(2);
@@ -350,35 +360,35 @@ describe('loadFromDb history repair', () => {
350
360
  id: 'm1',
351
361
  role: 'user',
352
362
  content: JSON.stringify([{ type: 'text', text: 'Guardian-only question' }]),
353
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
363
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
354
364
  },
355
365
  {
356
366
  id: 'm2',
357
367
  role: 'assistant',
358
368
  content: JSON.stringify([{ type: 'text', text: 'Guardian-only answer' }]),
359
- metadata: JSON.stringify({ provenanceActorRole: 'guardian', provenanceSourceChannel: 'telegram' }),
369
+ metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
360
370
  },
361
371
  {
362
372
  id: 'm3',
363
373
  role: 'user',
364
374
  content: JSON.stringify([{ type: 'text', text: 'Unverified ping' }]),
365
- metadata: JSON.stringify({ provenanceActorRole: 'unverified_channel', provenanceSourceChannel: 'telegram' }),
375
+ metadata: JSON.stringify({ provenanceTrustClass: 'unknown', provenanceSourceChannel: 'telegram' }),
366
376
  },
367
377
  {
368
378
  id: 'm4',
369
379
  role: 'assistant',
370
380
  content: JSON.stringify([{ type: 'text', text: 'Unverified reply' }]),
371
- metadata: JSON.stringify({ provenanceActorRole: 'unverified_channel', provenanceSourceChannel: 'telegram' }),
381
+ metadata: JSON.stringify({ provenanceTrustClass: 'unknown', provenanceSourceChannel: 'telegram' }),
372
382
  },
373
383
  ];
374
384
 
375
385
  const session = makeSession();
376
386
 
377
- session.setGuardianContext({ actorRole: 'unverified_channel', sourceChannel: 'telegram' });
387
+ session.setGuardianContext({ trustClass: 'unknown', sourceChannel: 'telegram' });
378
388
  await session.ensureActorScopedHistory();
379
389
  expect(session.getMessages()).toHaveLength(2);
380
390
 
381
- session.setGuardianContext({ actorRole: 'guardian', sourceChannel: 'telegram' });
391
+ session.setGuardianContext({ trustClass: 'guardian', sourceChannel: 'telegram' });
382
392
  await session.persistUserMessage('Guardian follow-up', []);
383
393
  const messagesAfterPersist = session.getMessages();
384
394
 
@@ -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,
@@ -69,6 +71,13 @@ let mockDbMessages: Array<{ id: string; role: string; content: string }> = [];
69
71
  let mockConversation: Record<string, unknown> | null = null;
70
72
 
71
73
  mock.module('../memory/conversation-store.js', () => ({
74
+ getConversationThreadType: () => 'default',
75
+ setConversationOriginChannelIfUnset: () => {},
76
+ updateConversationContextWindow: () => {},
77
+ deleteMessageById: () => {},
78
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
79
+ getConversationOriginInterface: () => null,
80
+ getConversationOriginChannel: () => null,
72
81
  getMessages: () => mockDbMessages,
73
82
  getConversation: () => mockConversation,
74
83
  createConversation: () => ({ id: 'conv-1' }),
@@ -120,6 +129,20 @@ mock.module('../context/window-manager.js', () => ({
120
129
  createContextSummaryMessage: () => ({ role: 'user', content: [{ type: 'text', text: 'summary' }] }),
121
130
  getSummaryFromContextMessage: () => null,
122
131
  }));
132
+ mock.module('../memory/canonical-guardian-store.js', () => ({
133
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
134
+ listCanonicalGuardianRequests: () => [],
135
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
136
+ getCanonicalGuardianRequest: () => null,
137
+ getCanonicalGuardianRequestByCode: () => null,
138
+ updateCanonicalGuardianRequest: () => {},
139
+ resolveCanonicalGuardianRequest: () => {},
140
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
141
+ listCanonicalGuardianDeliveries: () => [],
142
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
143
+ updateCanonicalGuardianDelivery: () => {},
144
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
145
+ }));
123
146
 
124
147
  import { Session } from '../daemon/session.js';
125
148
 
@@ -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
  });