@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
@@ -0,0 +1,208 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ applyStreamingSubstitution,
5
+ applySubstitutions,
6
+ extractAndSanitize,
7
+ } from '../tools/sensitive-output-placeholders.js';
8
+
9
+ describe('extractAndSanitize', () => {
10
+ test('parses a valid invite_code directive and replaces raw value with placeholder', () => {
11
+ const rawToken = 'abc123def456';
12
+ const content = `<vellum-sensitive-output kind="invite_code" value="${rawToken}" />\nhttps://t.me/bot?start=iv_${rawToken}`;
13
+
14
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
15
+
16
+ expect(bindings).toHaveLength(1);
17
+ expect(bindings[0].kind).toBe('invite_code');
18
+ expect(bindings[0].value).toBe(rawToken);
19
+ expect(bindings[0].placeholder).toMatch(/^VELLUM_ASSISTANT_INVITE_CODE_[A-Z0-9]{8}$/);
20
+
21
+ // Directive tag should be stripped
22
+ expect(sanitizedContent).not.toContain('<vellum-sensitive-output');
23
+ // Raw token should be replaced with placeholder
24
+ expect(sanitizedContent).not.toContain(rawToken);
25
+ expect(sanitizedContent).toContain(bindings[0].placeholder);
26
+ // The link structure should be preserved
27
+ expect(sanitizedContent).toContain(`https://t.me/bot?start=iv_${bindings[0].placeholder}`);
28
+ });
29
+
30
+ test('ignores malformed directives safely', () => {
31
+ const content = 'Some text <vellum-sensitive-output broken />';
32
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
33
+
34
+ expect(bindings).toHaveLength(0);
35
+ expect(sanitizedContent).toBe(content);
36
+ });
37
+
38
+ test('ignores unknown kind values', () => {
39
+ const content = '<vellum-sensitive-output kind="unknown_kind" value="secret123" />';
40
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
41
+
42
+ expect(bindings).toHaveLength(0);
43
+ expect(sanitizedContent).toBe(content);
44
+ });
45
+
46
+ test('drops empty values', () => {
47
+ const content = '<vellum-sensitive-output kind="invite_code" value="" />';
48
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
49
+
50
+ expect(bindings).toHaveLength(0);
51
+ // Directive should remain since no valid binding was extracted
52
+ expect(sanitizedContent).toBe(content);
53
+ });
54
+
55
+ test('deduplicates identical values into a single binding', () => {
56
+ const rawToken = 'token123';
57
+ const content = [
58
+ `<vellum-sensitive-output kind="invite_code" value="${rawToken}" />`,
59
+ `<vellum-sensitive-output kind="invite_code" value="${rawToken}" />`,
60
+ `Link1: https://t.me/bot?start=iv_${rawToken}`,
61
+ `Link2: https://t.me/bot?start=iv_${rawToken}`,
62
+ ].join('\n');
63
+
64
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
65
+
66
+ expect(bindings).toHaveLength(1);
67
+ expect(bindings[0].value).toBe(rawToken);
68
+
69
+ // Both occurrences of the raw token should be replaced with the same placeholder
70
+ const placeholderCount = sanitizedContent.split(bindings[0].placeholder).length - 1;
71
+ expect(placeholderCount).toBe(2);
72
+ expect(sanitizedContent).not.toContain(rawToken);
73
+ });
74
+
75
+ test('supports multiple distinct bindings', () => {
76
+ const token1 = 'firstToken123';
77
+ const token2 = 'secondToken456';
78
+ const content = [
79
+ `<vellum-sensitive-output kind="invite_code" value="${token1}" />`,
80
+ `<vellum-sensitive-output kind="invite_code" value="${token2}" />`,
81
+ `Link1: https://t.me/bot?start=iv_${token1}`,
82
+ `Link2: https://t.me/bot?start=iv_${token2}`,
83
+ ].join('\n');
84
+
85
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
86
+
87
+ expect(bindings).toHaveLength(2);
88
+ expect(bindings[0].value).toBe(token1);
89
+ expect(bindings[1].value).toBe(token2);
90
+
91
+ // Both raw tokens should be replaced
92
+ expect(sanitizedContent).not.toContain(token1);
93
+ expect(sanitizedContent).not.toContain(token2);
94
+ expect(sanitizedContent).toContain(bindings[0].placeholder);
95
+ expect(sanitizedContent).toContain(bindings[1].placeholder);
96
+ });
97
+
98
+ test('returns content unchanged when no directives are present', () => {
99
+ const content = 'Just a normal tool output with no directives.';
100
+ const { sanitizedContent, bindings } = extractAndSanitize(content);
101
+
102
+ expect(bindings).toHaveLength(0);
103
+ expect(sanitizedContent).toBe(content);
104
+ });
105
+
106
+ test('placeholder format matches required pattern', () => {
107
+ const content = '<vellum-sensitive-output kind="invite_code" value="tok123" />';
108
+ const { bindings } = extractAndSanitize(content);
109
+
110
+ expect(bindings).toHaveLength(1);
111
+ // Must be exactly: VELLUM_ASSISTANT_INVITE_CODE_ followed by 8 uppercase alphanumeric chars
112
+ expect(bindings[0].placeholder).toMatch(/^VELLUM_ASSISTANT_INVITE_CODE_[A-Z0-9]{8}$/);
113
+ });
114
+ });
115
+
116
+ describe('applySubstitutions', () => {
117
+ test('replaces placeholders with real values', () => {
118
+ const map = new Map([
119
+ ['VELLUM_ASSISTANT_INVITE_CODE_ABCD1234', 'realtoken123'],
120
+ ]);
121
+
122
+ const text = 'Your link: https://t.me/bot?start=iv_VELLUM_ASSISTANT_INVITE_CODE_ABCD1234';
123
+ const result = applySubstitutions(text, map);
124
+
125
+ expect(result).toBe('Your link: https://t.me/bot?start=iv_realtoken123');
126
+ });
127
+
128
+ test('replaces multiple placeholders', () => {
129
+ const map = new Map([
130
+ ['VELLUM_ASSISTANT_INVITE_CODE_AAAA1111', 'token1'],
131
+ ['VELLUM_ASSISTANT_INVITE_CODE_BBBB2222', 'token2'],
132
+ ]);
133
+
134
+ const text = 'Link1: VELLUM_ASSISTANT_INVITE_CODE_AAAA1111, Link2: VELLUM_ASSISTANT_INVITE_CODE_BBBB2222';
135
+ const result = applySubstitutions(text, map);
136
+
137
+ expect(result).toBe('Link1: token1, Link2: token2');
138
+ });
139
+
140
+ test('returns text unchanged when map is empty', () => {
141
+ const map = new Map<string, string>();
142
+ const text = 'No placeholders here.';
143
+ expect(applySubstitutions(text, map)).toBe(text);
144
+ });
145
+ });
146
+
147
+ describe('applyStreamingSubstitution', () => {
148
+ test('resolves complete placeholders in a single chunk', () => {
149
+ const map = new Map([
150
+ ['VELLUM_ASSISTANT_INVITE_CODE_ABCD1234', 'realtoken'],
151
+ ]);
152
+
153
+ const { emit, pending } = applyStreamingSubstitution(
154
+ 'Your code: VELLUM_ASSISTANT_INVITE_CODE_ABCD1234 is ready.',
155
+ map,
156
+ );
157
+
158
+ expect(emit).toContain('realtoken');
159
+ expect(emit).not.toContain('VELLUM_ASSISTANT_INVITE_CODE_ABCD1234');
160
+ // No pending text since the placeholder was complete
161
+ expect(pending).toBe('');
162
+ });
163
+
164
+ test('buffers text that could be an incomplete placeholder prefix', () => {
165
+ const map = new Map([
166
+ ['VELLUM_ASSISTANT_INVITE_CODE_ABCD1234', 'realtoken'],
167
+ ]);
168
+
169
+ // Chunk ends mid-placeholder
170
+ const { emit, pending } = applyStreamingSubstitution(
171
+ 'Your code: VELLUM_ASSISTANT_',
172
+ map,
173
+ );
174
+
175
+ // The ambiguous tail should be buffered
176
+ expect(pending.length).toBeGreaterThan(0);
177
+ // emit should not contain the partial placeholder
178
+ expect(emit).not.toContain('VELLUM_ASSISTANT_');
179
+ });
180
+
181
+ test('handles split-chunk placeholder by concatenating pending with next chunk', () => {
182
+ const map = new Map([
183
+ ['VELLUM_ASSISTANT_INVITE_CODE_ABCD1234', 'realtoken'],
184
+ ]);
185
+
186
+ // First chunk: partial placeholder
187
+ const result1 = applyStreamingSubstitution(
188
+ 'Link: VELLUM_ASSISTANT_',
189
+ map,
190
+ );
191
+
192
+ // Second chunk completes the placeholder
193
+ const combined = result1.pending + 'INVITE_CODE_ABCD1234 done.';
194
+ const result2 = applyStreamingSubstitution(combined, map);
195
+
196
+ expect(result2.emit).toContain('realtoken');
197
+ expect(result2.emit).toContain('done.');
198
+ expect(result2.pending).toBe('');
199
+ });
200
+
201
+ test('returns text unchanged when map is empty', () => {
202
+ const map = new Map<string, string>();
203
+ const { emit, pending } = applyStreamingSubstitution('Hello world', map);
204
+
205
+ expect(emit).toBe('Hello world');
206
+ expect(pending).toBe('');
207
+ });
208
+ });
@@ -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.