@vellumai/assistant 0.3.19 → 0.3.21

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 +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -0,0 +1,306 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
6
+
7
+ const testDir = mkdtempSync(join(tmpdir(), 'invite-redemption-service-test-'));
8
+
9
+ mock.module('../util/platform.js', () => ({
10
+ getDataDir: () => testDir,
11
+ isMacOS: () => process.platform === 'darwin',
12
+ isLinux: () => process.platform === 'linux',
13
+ isWindows: () => process.platform === 'win32',
14
+ getSocketPath: () => join(testDir, 'test.sock'),
15
+ getPidPath: () => join(testDir, 'test.pid'),
16
+ getDbPath: () => join(testDir, 'test.db'),
17
+ getLogPath: () => join(testDir, 'test.log'),
18
+ ensureDataDir: () => {},
19
+ }));
20
+
21
+ mock.module('../util/logger.js', () => ({
22
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
23
+ get: () => () => {},
24
+ }),
25
+ }));
26
+
27
+ import { getSqlite, initializeDb, resetDb } from '../memory/db.js';
28
+ import { createInvite, revokeInvite as revokeStoreFn } from '../memory/ingress-invite-store.js';
29
+ import { upsertMember } from '../memory/ingress-member-store.js';
30
+ import { type InviteRedemptionOutcome,redeemInvite } from '../runtime/invite-redemption-service.js';
31
+
32
+ initializeDb();
33
+
34
+ afterAll(() => {
35
+ resetDb();
36
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
37
+ });
38
+
39
+ function resetTables() {
40
+ getSqlite().run('DELETE FROM assistant_ingress_members');
41
+ getSqlite().run('DELETE FROM assistant_ingress_invites');
42
+ }
43
+
44
+ describe('invite-redemption-service', () => {
45
+ beforeEach(resetTables);
46
+
47
+ test('redeems a valid invite and returns typed outcome', () => {
48
+ const { rawToken, invite } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
49
+
50
+ const outcome = redeemInvite({
51
+ rawToken,
52
+ sourceChannel: 'telegram',
53
+ externalUserId: 'user-1',
54
+ });
55
+
56
+ expect(outcome.ok).toBe(true);
57
+ expect(outcome).toEqual({
58
+ ok: true,
59
+ type: 'redeemed',
60
+ memberId: expect.any(String),
61
+ inviteId: invite.id,
62
+ });
63
+ });
64
+
65
+ test('returns invalid_token for a bogus token', () => {
66
+ const outcome = redeemInvite({
67
+ rawToken: 'totally-bogus-token',
68
+ sourceChannel: 'telegram',
69
+ externalUserId: 'user-1',
70
+ });
71
+
72
+ expect(outcome).toEqual({ ok: false, reason: 'invalid_token' });
73
+ });
74
+
75
+ test('returns expired for an expired invite', () => {
76
+ // Create an invite that expired 1 ms ago
77
+ const { rawToken } = createInvite({
78
+ sourceChannel: 'telegram',
79
+ maxUses: 1,
80
+ expiresInMs: -1,
81
+ });
82
+
83
+ const outcome = redeemInvite({
84
+ rawToken,
85
+ sourceChannel: 'telegram',
86
+ externalUserId: 'user-1',
87
+ });
88
+
89
+ expect(outcome).toEqual({ ok: false, reason: 'expired' });
90
+ });
91
+
92
+ test('returns revoked for a revoked invite', () => {
93
+ const { rawToken, invite } = createInvite({
94
+ sourceChannel: 'telegram',
95
+ maxUses: 1,
96
+ });
97
+ revokeStoreFn(invite.id);
98
+
99
+ const outcome = redeemInvite({
100
+ rawToken,
101
+ sourceChannel: 'telegram',
102
+ externalUserId: 'user-1',
103
+ });
104
+
105
+ expect(outcome).toEqual({ ok: false, reason: 'revoked' });
106
+ });
107
+
108
+ test('returns max_uses_reached when invite is fully consumed', () => {
109
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
110
+
111
+ // First redemption should succeed
112
+ const first = redeemInvite({
113
+ rawToken,
114
+ sourceChannel: 'telegram',
115
+ externalUserId: 'user-1',
116
+ });
117
+ expect(first.ok).toBe(true);
118
+
119
+ // Second attempt should fail — the invite is now fully redeemed
120
+ const second = redeemInvite({
121
+ rawToken,
122
+ sourceChannel: 'telegram',
123
+ externalUserId: 'user-2',
124
+ });
125
+
126
+ expect(second).toEqual({ ok: false, reason: 'max_uses_reached' });
127
+ });
128
+
129
+ test('returns channel_mismatch when redeeming on wrong channel', () => {
130
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
131
+
132
+ const outcome = redeemInvite({
133
+ rawToken,
134
+ sourceChannel: 'sms',
135
+ externalUserId: 'user-1',
136
+ });
137
+
138
+ expect(outcome).toEqual({ ok: false, reason: 'channel_mismatch' });
139
+ });
140
+
141
+ test('returns missing_identity when no externalUserId or externalChatId', () => {
142
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
143
+
144
+ const outcome = redeemInvite({
145
+ rawToken,
146
+ sourceChannel: 'telegram',
147
+ });
148
+
149
+ expect(outcome).toEqual({ ok: false, reason: 'missing_identity' });
150
+ });
151
+
152
+ test('returns already_member when user is already an active member', () => {
153
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
154
+
155
+ // Pre-create an active member
156
+ upsertMember({
157
+ sourceChannel: 'telegram',
158
+ externalUserId: 'existing-user',
159
+ status: 'active',
160
+ });
161
+
162
+ const outcome = redeemInvite({
163
+ rawToken,
164
+ sourceChannel: 'telegram',
165
+ externalUserId: 'existing-user',
166
+ });
167
+
168
+ expect(outcome.ok).toBe(true);
169
+ expect((outcome as Extract<InviteRedemptionOutcome, { type: 'already_member' }>).type).toBe('already_member');
170
+ expect((outcome as Extract<InviteRedemptionOutcome, { type: 'already_member' }>).memberId).toEqual(expect.any(String));
171
+ });
172
+
173
+ test('returns invalid_token for a blocked member to avoid leaking membership status', () => {
174
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
175
+
176
+ // Pre-create a blocked member — simulates a guardian-initiated block
177
+ upsertMember({
178
+ sourceChannel: 'telegram',
179
+ externalUserId: 'blocked-user',
180
+ status: 'blocked',
181
+ });
182
+
183
+ const outcome = redeemInvite({
184
+ rawToken,
185
+ sourceChannel: 'telegram',
186
+ externalUserId: 'blocked-user',
187
+ });
188
+
189
+ expect(outcome).toEqual({ ok: false, reason: 'invalid_token' });
190
+ });
191
+
192
+ test('does not return already_member for a revoked member', () => {
193
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
194
+
195
+ // Pre-create a revoked member
196
+ const member = upsertMember({
197
+ sourceChannel: 'telegram',
198
+ externalUserId: 'revoked-user',
199
+ status: 'revoked',
200
+ });
201
+ expect(member.status).toBe('revoked');
202
+
203
+ const outcome = redeemInvite({
204
+ rawToken,
205
+ sourceChannel: 'telegram',
206
+ externalUserId: 'revoked-user',
207
+ });
208
+
209
+ // Should redeem, not return already_member
210
+ expect(outcome.ok).toBe(true);
211
+ expect((outcome as Extract<InviteRedemptionOutcome, { type: 'redeemed' }>).type).toBe('redeemed');
212
+ });
213
+
214
+ test('raw token is not present in the outcome object', () => {
215
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
216
+
217
+ const outcome = redeemInvite({
218
+ rawToken,
219
+ sourceChannel: 'telegram',
220
+ externalUserId: 'user-1',
221
+ });
222
+
223
+ // Verify the raw token does not appear anywhere in the serialized outcome
224
+ const serialized = JSON.stringify(outcome);
225
+ expect(serialized).not.toContain(rawToken);
226
+ });
227
+
228
+ test('channel enforcement blocks cross-channel redemption (voice invite via slack)', () => {
229
+ const { rawToken } = createInvite({ sourceChannel: 'voice', maxUses: 1 });
230
+
231
+ const outcome = redeemInvite({
232
+ rawToken,
233
+ sourceChannel: 'slack',
234
+ externalUserId: 'user-1',
235
+ });
236
+
237
+ expect(outcome).toEqual({ ok: false, reason: 'channel_mismatch' });
238
+ });
239
+
240
+ test('returns invalid_token for an active member with a bogus token (no membership probing)', () => {
241
+ // Pre-create an active member
242
+ upsertMember({
243
+ sourceChannel: 'telegram',
244
+ externalUserId: 'probed-user',
245
+ status: 'active',
246
+ });
247
+
248
+ // Attempt to redeem with a bogus token — must NOT leak membership status
249
+ const outcome = redeemInvite({
250
+ rawToken: 'completely-bogus-token',
251
+ sourceChannel: 'telegram',
252
+ externalUserId: 'probed-user',
253
+ });
254
+
255
+ expect(outcome).toEqual({ ok: false, reason: 'invalid_token' });
256
+ });
257
+
258
+ test('returns expired for an active member with an expired invite token', () => {
259
+ // Create an expired invite
260
+ const { rawToken } = createInvite({
261
+ sourceChannel: 'telegram',
262
+ maxUses: 5,
263
+ expiresInMs: -1,
264
+ });
265
+
266
+ // Pre-create an active member
267
+ upsertMember({
268
+ sourceChannel: 'telegram',
269
+ externalUserId: 'expired-token-user',
270
+ status: 'active',
271
+ });
272
+
273
+ // Expired token must return expired, not already_member
274
+ const outcome = redeemInvite({
275
+ rawToken,
276
+ sourceChannel: 'telegram',
277
+ externalUserId: 'expired-token-user',
278
+ });
279
+
280
+ expect(outcome).toEqual({ ok: false, reason: 'expired' });
281
+ });
282
+
283
+ test('returns channel_mismatch for an active member with a valid token for a different channel', () => {
284
+ // Create an invite for SMS
285
+ const { rawToken } = createInvite({
286
+ sourceChannel: 'sms',
287
+ maxUses: 5,
288
+ });
289
+
290
+ // Pre-create an active member on telegram
291
+ upsertMember({
292
+ sourceChannel: 'telegram',
293
+ externalUserId: 'cross-channel-user',
294
+ status: 'active',
295
+ });
296
+
297
+ // Valid token for wrong channel must return channel_mismatch, not already_member
298
+ const outcome = redeemInvite({
299
+ rawToken,
300
+ sourceChannel: 'telegram',
301
+ externalUserId: 'cross-channel-user',
302
+ });
303
+
304
+ expect(outcome).toEqual({ ok: false, reason: 'channel_mismatch' });
305
+ });
306
+ });
@@ -731,6 +731,27 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
731
731
  type: 'voice_config_update',
732
732
  activationKey: 'fn',
733
733
  },
734
+ generate_avatar: {
735
+ type: 'generate_avatar',
736
+ description: 'a friendly purple cat with green eyes wearing a tiny hat',
737
+ },
738
+ guardian_actions_pending_request: {
739
+ type: 'guardian_actions_pending_request',
740
+ conversationId: 'conv-guardian-001',
741
+ },
742
+ guardian_action_decision: {
743
+ type: 'guardian_action_decision',
744
+ requestId: 'req-guardian-001',
745
+ action: 'approve_once',
746
+ conversationId: 'conv-guardian-001',
747
+ },
748
+ reorder_threads: {
749
+ type: 'reorder_threads',
750
+ updates: [
751
+ { sessionId: 'sess-001', displayOrder: 0, isPinned: false },
752
+ { sessionId: 'sess-002', displayOrder: 1, isPinned: true },
753
+ ],
754
+ },
734
755
  };
735
756
 
736
757
  // ---------------------------------------------------------------------------
@@ -2020,6 +2041,42 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
2020
2041
  emoji: '',
2021
2042
  home: '',
2022
2043
  },
2044
+ avatar_updated: {
2045
+ type: 'avatar_updated',
2046
+ avatarPath: '/Users/test/.vellum/workspace/data/avatar/custom-avatar.png',
2047
+ },
2048
+ generate_avatar_response: {
2049
+ type: 'generate_avatar_response',
2050
+ success: true,
2051
+ error: undefined,
2052
+ },
2053
+ guardian_actions_pending_response: {
2054
+ type: 'guardian_actions_pending_response',
2055
+ conversationId: 'conv-guardian-001',
2056
+ prompts: [
2057
+ {
2058
+ requestId: 'req-guardian-001',
2059
+ requestCode: 'REQ-GU',
2060
+ state: 'pending',
2061
+ questionText: 'Approve tool: bash',
2062
+ toolName: 'bash',
2063
+ actions: [
2064
+ { action: 'approve_once', label: 'Approve once' },
2065
+ { action: 'reject', label: 'Reject' },
2066
+ ],
2067
+ expiresAt: 1700100000000,
2068
+ conversationId: 'conv-guardian-001',
2069
+ callSessionId: null,
2070
+ },
2071
+ ],
2072
+ },
2073
+ guardian_action_decision_response: {
2074
+ type: 'guardian_action_decision_response',
2075
+ applied: true,
2076
+ reason: undefined,
2077
+ requestId: 'req-guardian-001',
2078
+ userText: undefined,
2079
+ },
2023
2080
  };
2024
2081
 
2025
2082
  // ---------------------------------------------------------------------------
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Regression tests for notification decision fallback copy.
3
+ *
4
+ * Ensures fallback decisions still produce human-friendly copy when the
5
+ * decision-model call is unavailable.
6
+ */
7
+
8
+ import { describe, expect, mock, test } from 'bun:test';
9
+
10
+ mock.module('../channels/config.js', () => ({
11
+ getDeliverableChannels: () => ['vellum', 'telegram', 'sms'],
12
+ }));
13
+
14
+ mock.module('../config/loader.js', () => ({
15
+ getConfig: () => ({
16
+ notifications: {
17
+ decisionModelIntent: 'latency-optimized',
18
+ },
19
+ }),
20
+ }));
21
+
22
+ mock.module('../notifications/decisions-store.js', () => ({
23
+ createDecision: () => {},
24
+ }));
25
+
26
+ mock.module('../notifications/preference-summary.js', () => ({
27
+ getPreferenceSummary: () => undefined,
28
+ }));
29
+
30
+ mock.module('../notifications/thread-candidates.js', () => ({
31
+ buildThreadCandidates: () => undefined,
32
+ serializeCandidatesForPrompt: () => undefined,
33
+ }));
34
+
35
+ mock.module('../providers/provider-send-message.js', () => ({
36
+ getConfiguredProvider: () => null,
37
+ createTimeout: () => ({
38
+ signal: new AbortController().signal,
39
+ cleanup: () => {},
40
+ }),
41
+ extractToolUse: () => null,
42
+ userMessage: (text: string) => ({ role: 'user', content: text }),
43
+ }));
44
+
45
+ mock.module('../util/logger.js', () => ({
46
+ getLogger: () =>
47
+ new Proxy({} as Record<string, unknown>, {
48
+ get: () => () => {},
49
+ }),
50
+ }));
51
+
52
+ import { evaluateSignal } from '../notifications/decision-engine.js';
53
+ import type { NotificationSignal } from '../notifications/signal.js';
54
+ import type { NotificationChannel } from '../notifications/types.js';
55
+
56
+ function makeSignal(overrides?: Partial<NotificationSignal>): NotificationSignal {
57
+ return {
58
+ signalId: 'sig-fallback-guardian-1',
59
+ assistantId: 'self',
60
+ createdAt: Date.now(),
61
+ sourceChannel: 'voice',
62
+ sourceSessionId: 'call-session-1',
63
+ sourceEventName: 'guardian.question',
64
+ contextPayload: {
65
+ questionText: 'What is the gate code?',
66
+ },
67
+ attentionHints: {
68
+ requiresAction: true,
69
+ urgency: 'high',
70
+ isAsyncBackground: false,
71
+ visibleInSourceNow: false,
72
+ },
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ describe('notification decision fallback copy', () => {
78
+ test('uses human-friendly template copy for guardian.question', async () => {
79
+ const signal = makeSignal();
80
+ const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
81
+
82
+ expect(decision.fallbackUsed).toBe(true);
83
+ expect(decision.renderedCopy.vellum?.title).toBe('Guardian Question');
84
+ expect(decision.renderedCopy.vellum?.body).toBe('What is the gate code?');
85
+ expect(decision.renderedCopy.vellum?.title).not.toBe('guardian.question');
86
+ expect(decision.renderedCopy.vellum?.body).not.toContain('Action required: guardian.question');
87
+ });
88
+ });