@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
@@ -19,6 +19,8 @@ mock.module('../util/logger.js', () => ({
19
19
 
20
20
  mock.module('../config/loader.js', () => ({
21
21
  getConfig: () => ({
22
+ ui: {},
23
+
22
24
  daemon: { standaloneRecording: true },
23
25
  provider: 'mock-provider',
24
26
  model: 'mock-model',
@@ -233,6 +235,14 @@ mock.module('../daemon/handlers/recording.js', () => ({
233
235
  // ── Mock conversation store ────────────────────────────────────────────────
234
236
 
235
237
  mock.module('../memory/conversation-store.js', () => ({
238
+ getConversationThreadType: () => 'default',
239
+ setConversationOriginChannelIfUnset: () => {},
240
+ updateConversationContextWindow: () => {},
241
+ deleteMessageById: () => {},
242
+ updateConversationUsage: () => {},
243
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
244
+ getConversationOriginInterface: () => null,
245
+ getConversationOriginChannel: () => null,
236
246
  getMessages: () => [],
237
247
  addMessage: () => ({ id: 'msg-mock', role: 'assistant', content: '' }),
238
248
  createConversation: (titleOrOpts?: string | { title?: string }) => {
@@ -269,6 +279,7 @@ mock.module('../security/secret-ingress.js', () => ({
269
279
 
270
280
  mock.module('../security/secret-scanner.js', () => ({
271
281
  redactSecrets: (text: string) => text,
282
+ compileCustomPatterns: () => [],
272
283
  }));
273
284
 
274
285
  // ── Mock classifier (for task_submit fallthrough) ──────────────────────────
@@ -307,6 +318,7 @@ mock.module('../providers/provider-send-message.js', () => ({
307
318
 
308
319
  mock.module('../memory/external-conversation-store.js', () => ({
309
320
  getBindingsForConversations: () => new Map(),
321
+ upsertBinding: () => {},
310
322
  }));
311
323
 
312
324
  // ── Mock subagent manager ──────────────────────────────────────────────────
@@ -376,6 +388,7 @@ function createCtx(overrides?: Partial<HandlerContext>): {
376
388
  setTurnChannelContext: noop,
377
389
  setTurnInterfaceContext: noop,
378
390
  setAssistantId: noop,
391
+ setChannelCapabilities: noop,
379
392
  setGuardianContext: noop,
380
393
  setCommandIntent: noop,
381
394
  processMessage: async () => {},
@@ -386,6 +399,8 @@ function createCtx(overrides?: Partial<HandlerContext>): {
386
399
  dispose: noop,
387
400
  hasPendingConfirmation: () => false,
388
401
  hasPendingSecret: () => false,
402
+ isProcessing: () => false,
403
+ messages: [] as any[],
389
404
  };
390
405
 
391
406
  const sessions = new Map<string, any>();
@@ -16,6 +16,8 @@ mock.module('../util/logger.js', () => ({
16
16
 
17
17
  mock.module('../config/loader.js', () => ({
18
18
  getConfig: () => ({
19
+ ui: {},
20
+
19
21
  daemon: { standaloneRecording: true },
20
22
  provider: 'mock-provider',
21
23
  permissions: { mode: 'legacy' },
@@ -48,6 +50,15 @@ const mockMessages: Array<{ id: string; role: string; content: string }> = [];
48
50
  let mockMessageIdCounter = 0;
49
51
 
50
52
  mock.module('../memory/conversation-store.js', () => ({
53
+ getConversationThreadType: () => 'default',
54
+ setConversationOriginChannelIfUnset: () => {},
55
+ updateConversationContextWindow: () => {},
56
+ deleteMessageById: () => {},
57
+ updateConversationTitle: () => {},
58
+ updateConversationUsage: () => {},
59
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
60
+ getConversationOriginInterface: () => null,
61
+ getConversationOriginChannel: () => null,
51
62
  getMessages: () => mockMessages,
52
63
  addMessage: (_convId: string, role: string, content: string) => {
53
64
  const msg = { id: `msg-${++mockMessageIdCounter}`, role, content };
@@ -417,7 +428,7 @@ describe('stale completion guard (operation token)', () => {
417
428
  expect(getActiveRestartToken()).toBeNull();
418
429
  });
419
430
 
420
- test('allows tokenless recording_status during active restart (old recording ack)', () => {
431
+ test('allows tokenless recording_status during active restart (old recording ack)', async () => {
421
432
  const { ctx, sent, fakeSocket } = createCtx();
422
433
  const conversationId = 'conv-tokenless-1';
423
434
  ctx.socketToSession.set(fakeSocket, conversationId);
@@ -442,7 +453,7 @@ describe('stale completion guard (operation token)', () => {
442
453
  attachToConversationId: conversationId,
443
454
  // No operationToken — from old recording, should be allowed
444
455
  };
445
- recordingHandlers.recording_status(tokenlessStatus, fakeSocket, ctx);
456
+ await recordingHandlers.recording_status(tokenlessStatus, fakeSocket, ctx);
446
457
 
447
458
  // Should have triggered the deferred restart start
448
459
  const newStartMsgs = sent.filter((m) => m.type === 'recording_start');
@@ -160,15 +160,19 @@ describe('tool manifest', () => {
160
160
  });
161
161
 
162
162
  test('manifest declares expected core lazy tools', () => {
163
+ // bash and swarm_delegate moved from lazy to eager registration
163
164
  const lazyNames = new Set(lazyTools.map((t) => t.name));
164
- expect(lazyNames.has('bash')).toBe(true);
165
+ expect(lazyNames.has('bash')).toBe(false);
165
166
  expect(lazyNames.has('evaluate_typescript_code')).toBe(false);
166
167
  expect(lazyNames.has('claude_code')).toBe(false);
167
- expect(lazyNames.has('swarm_delegate')).toBe(true);
168
+ expect(lazyNames.has('swarm_delegate')).toBe(false);
169
+ // Verify they are in eager tools instead
170
+ expect(eagerModuleToolNames).toContain('bash');
171
+ expect(eagerModuleToolNames).toContain('swarm_delegate');
168
172
  });
169
173
 
170
174
  test('eager module tool names list contains expected count', () => {
171
- expect(eagerModuleToolNames.length).toBe(16);
175
+ expect(eagerModuleToolNames.length).toBe(15);
172
176
  });
173
177
 
174
178
  test('explicit tools list includes memory, credential, watch, and catalog tools', () => {
@@ -13,6 +13,7 @@
13
13
  * - destroy cleanup
14
14
  * - Malformed message resilience
15
15
  */
16
+ import { createHash, randomUUID } from 'node:crypto';
16
17
  import { mkdtempSync, rmSync } from 'node:fs';
17
18
  import { tmpdir } from 'node:os';
18
19
  import { join } from 'node:path';
@@ -49,6 +50,7 @@ const mockConfig = {
49
50
  provider: 'anthropic',
50
51
  providerOrder: ['anthropic'],
51
52
  apiKeys: { anthropic: 'test-key' },
53
+ secretDetection: { enabled: false },
52
54
  calls: {
53
55
  enabled: true,
54
56
  provider: 'twilio',
@@ -132,12 +134,14 @@ import {
132
134
  } from '../calls/call-store.js';
133
135
  import type { RelayWebSocketData } from '../calls/relay-server.js';
134
136
  import { activeRelayConnections,RelayConnection } from '../calls/relay-server.js';
135
- import { createBinding } from '../memory/channel-guardian-store.js';
136
- import { getMessages } from '../memory/conversation-store.js';
137
+ import { setVoiceBridgeDeps } from '../calls/voice-session-bridge.js';
138
+ import { createBinding, createChallenge } from '../memory/channel-guardian-store.js';
139
+ import { addMessage, getMessages } from '../memory/conversation-store.js';
137
140
  import { getDb, initializeDb, resetDb } from '../memory/db.js';
141
+ import { upsertMember } from '../memory/ingress-member-store.js';
138
142
  import { conversations } from '../memory/schema.js';
139
143
  import {
140
- createVerificationChallenge,
144
+ createOutboundSession,
141
145
  getGuardianBinding,
142
146
  } from '../runtime/channel-guardian-service.js';
143
147
 
@@ -200,12 +204,51 @@ function resetTables() {
200
204
  db.run('DELETE FROM tool_invocations');
201
205
  db.run('DELETE FROM messages');
202
206
  db.run('DELETE FROM conversations');
207
+ db.run('DELETE FROM assistant_ingress_members');
203
208
  db.run('DELETE FROM channel_guardian_verification_challenges');
204
209
  db.run('DELETE FROM channel_guardian_bindings');
205
210
  db.run('DELETE FROM channel_guardian_rate_limits');
206
211
  ensuredConvIds = new Set();
207
212
  }
208
213
 
214
+ function addTrustedVoiceContact(phoneNumber: string, assistantId: string = 'self'): void {
215
+ upsertMember({
216
+ assistantId,
217
+ sourceChannel: 'voice',
218
+ externalUserId: phoneNumber,
219
+ externalChatId: phoneNumber,
220
+ status: 'active',
221
+ policy: 'allow',
222
+ });
223
+ }
224
+
225
+ function createVoiceVerificationSession(
226
+ assistantId: string,
227
+ expectedPhoneE164: string,
228
+ sessionId?: string,
229
+ ): string {
230
+ const { secret } = createOutboundSession({
231
+ assistantId,
232
+ channel: 'voice',
233
+ expectedExternalUserId: expectedPhoneE164,
234
+ expectedChatId: expectedPhoneE164,
235
+ expectedPhoneE164,
236
+ sessionId,
237
+ });
238
+ return secret;
239
+ }
240
+
241
+ function createPendingVoiceGuardianChallenge(assistantId: string, secret: string = '123456'): string {
242
+ createChallenge({
243
+ id: randomUUID(),
244
+ assistantId,
245
+ channel: 'voice',
246
+ challengeHash: createHash('sha256').update(secret).digest('hex'),
247
+ expiresAt: Date.now() + 10 * 60 * 1000,
248
+ });
249
+ return secret;
250
+ }
251
+
209
252
  function getLatestAssistantText(conversationId: string): string | null {
210
253
  const messages = getMessages(conversationId).filter((m) => m.role === 'assistant');
211
254
  if (messages.length === 0) return null;
@@ -235,17 +278,88 @@ describe('relay-server', () => {
235
278
  mockConfig.calls.verification.maxAttempts = 3;
236
279
  mockConfig.calls.verification.codeLength = 6;
237
280
  mockConfig.calls.callerIdentity.userNumber = undefined;
281
+ setVoiceBridgeDeps({
282
+ getOrCreateSession: async (conversationId) => {
283
+ const session = {
284
+ callSessionId: undefined as string | undefined,
285
+ currentRequestId: undefined as string | undefined,
286
+ memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
287
+ isProcessing: () => false,
288
+ persistUserMessage: async (content: string, _attachments: unknown[], requestId?: string) => {
289
+ session.currentRequestId = requestId;
290
+ const message = await addMessage(
291
+ conversationId,
292
+ 'user',
293
+ JSON.stringify([{ type: 'text', text: content }]),
294
+ {
295
+ userMessageChannel: 'voice',
296
+ assistantMessageChannel: 'voice',
297
+ userMessageInterface: 'voice',
298
+ assistantMessageInterface: 'voice',
299
+ },
300
+ );
301
+ return message.id;
302
+ },
303
+ setChannelCapabilities: () => {},
304
+ setAssistantId: () => {},
305
+ setGuardianContext: () => {},
306
+ setCommandIntent: () => {},
307
+ setTurnChannelContext: () => {},
308
+ setVoiceCallControlPrompt: () => {},
309
+ updateClient: () => {},
310
+ handleConfirmationResponse: () => {},
311
+ handleSecretResponse: () => {},
312
+ abort: () => {},
313
+ runAgentLoop: async (
314
+ _content: string,
315
+ _messageId: string,
316
+ onEvent: (event: { type: string; sessionId?: string; text?: string }) => void,
317
+ ) => {
318
+ const tokens: string[] = [];
319
+ await mockSendMessage([], [], '', {
320
+ onEvent: (event: { type: string; text?: string }) => {
321
+ if (event.type !== 'text_delta' || typeof event.text !== 'string') return;
322
+ tokens.push(event.text);
323
+ onEvent({ type: 'assistant_text_delta', sessionId: conversationId, text: event.text });
324
+ },
325
+ });
326
+
327
+ const fullText = tokens.join('');
328
+ if (fullText.length > 0) {
329
+ await addMessage(
330
+ conversationId,
331
+ 'assistant',
332
+ JSON.stringify([{ type: 'text', text: fullText }]),
333
+ {
334
+ userMessageChannel: 'voice',
335
+ assistantMessageChannel: 'voice',
336
+ userMessageInterface: 'voice',
337
+ assistantMessageInterface: 'voice',
338
+ },
339
+ );
340
+ }
341
+
342
+ onEvent({ type: 'message_complete', sessionId: conversationId });
343
+ },
344
+ };
345
+ return session as unknown as import('../daemon/session.js').Session;
346
+ },
347
+ resolveAttachments: () => [],
348
+ deriveDefaultStrictSideEffects: () => false,
349
+ });
238
350
  });
239
351
 
240
352
  // ── Setup message handling ──────────────────────────────────────
241
353
 
242
354
  test('handleMessage: setup message associates callSid and records event', async () => {
243
355
  ensureConversation('conv-relay-1');
356
+ ensureConversation('conv-relay-1-origin');
244
357
  const session = createCallSession({
245
358
  conversationId: 'conv-relay-1',
246
359
  provider: 'twilio',
247
360
  fromNumber: '+15551111111',
248
361
  toNumber: '+15552222222',
362
+ initiatedFromConversationId: 'conv-relay-1-origin',
249
363
  });
250
364
 
251
365
  const { relay } = createMockWs(session.id);
@@ -277,12 +391,14 @@ describe('relay-server', () => {
277
391
 
278
392
  test('handleMessage: setup triggers initial assistant greeting turn', async () => {
279
393
  ensureConversation('conv-relay-setup-greet');
394
+ ensureConversation('conv-relay-setup-greet-origin');
280
395
  const session = createCallSession({
281
396
  conversationId: 'conv-relay-setup-greet',
282
397
  provider: 'twilio',
283
398
  fromNumber: '+15551111111',
284
399
  toNumber: '+15552222222',
285
400
  task: 'Confirm appointment time',
401
+ initiatedFromConversationId: 'conv-relay-setup-greet-origin',
286
402
  });
287
403
 
288
404
  mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, I am calling to confirm your appointment.']));
@@ -398,11 +514,13 @@ describe('relay-server', () => {
398
514
 
399
515
  test('handleMessage: final prompt routes to orchestrator and records event', async () => {
400
516
  ensureConversation('conv-relay-prompt');
517
+ ensureConversation('conv-relay-prompt-origin');
401
518
  const session = createCallSession({
402
519
  conversationId: 'conv-relay-prompt',
403
520
  provider: 'twilio',
404
521
  fromNumber: '+15551111111',
405
522
  toNumber: '+15552222222',
523
+ initiatedFromConversationId: 'conv-relay-prompt-origin',
406
524
  });
407
525
 
408
526
  const { ws, relay } = createMockWs(session.id);
@@ -858,6 +976,7 @@ describe('relay-server', () => {
858
976
 
859
977
  // Enable verification to prove inbound calls skip it
860
978
  mockConfig.calls.verification.enabled = true;
979
+ addTrustedVoiceContact('+15559999999');
861
980
 
862
981
  mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, how can I help you today?']));
863
982
 
@@ -896,6 +1015,7 @@ describe('relay-server', () => {
896
1015
  });
897
1016
 
898
1017
  mockSendMessage.mockImplementation(createMockProviderResponse(['Sure, let me help with that.']));
1018
+ addTrustedVoiceContact('+15559999999');
899
1019
 
900
1020
  const { relay } = createMockWs(session.id);
901
1021
 
@@ -941,6 +1061,7 @@ describe('relay-server', () => {
941
1061
  });
942
1062
 
943
1063
  let turnCount = 0;
1064
+ addTrustedVoiceContact('+15559999999');
944
1065
  mockSendMessage.mockImplementation(async (_messages: unknown[], _tools: unknown[], _systemPrompt: unknown, options?: { onEvent?: (event: { type: string; text?: string }) => void }) => {
945
1066
  turnCount++;
946
1067
  let tokens: string[];
@@ -1016,8 +1137,7 @@ describe('relay-server', () => {
1016
1137
  });
1017
1138
 
1018
1139
  // Create a pending voice guardian challenge
1019
- const challenge = createVerificationChallenge('test-assistant', 'voice');
1020
- const secret = challenge.secret;
1140
+ const secret = createPendingVoiceGuardianChallenge('test-assistant');
1021
1141
 
1022
1142
  mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, how can I help you?']));
1023
1143
 
@@ -1079,8 +1199,7 @@ describe('relay-server', () => {
1079
1199
  assistantId: 'test-assistant',
1080
1200
  });
1081
1201
 
1082
- const challenge = createVerificationChallenge('test-assistant', 'voice');
1083
- const secret = challenge.secret;
1202
+ const secret = createPendingVoiceGuardianChallenge('test-assistant');
1084
1203
 
1085
1204
  mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, verified caller!']));
1086
1205
 
@@ -1151,15 +1270,15 @@ describe('relay-server', () => {
1151
1270
  to: '+15551111111',
1152
1271
  }));
1153
1272
 
1154
- const runtimeContext = (relay.getController() as unknown as { guardianContext?: { sourceChannel?: string; actorRole?: string; guardianExternalUserId?: string } })?.guardianContext;
1273
+ const runtimeContext = (relay.getController() as unknown as { guardianContext?: { sourceChannel?: string; trustClass?: string; guardianExternalUserId?: string } })?.guardianContext;
1155
1274
  expect(runtimeContext?.sourceChannel).toBe('voice');
1156
- expect(runtimeContext?.actorRole).toBe('guardian');
1275
+ expect(runtimeContext?.trustClass).toBe('guardian');
1157
1276
  expect(runtimeContext?.guardianExternalUserId).toBe('+15550001111');
1158
1277
 
1159
1278
  relay.destroy();
1160
1279
  });
1161
1280
 
1162
- test('inbound call: caller not matching voice guardian binding is classified as non-guardian', async () => {
1281
+ test('inbound call: caller not matching voice guardian binding is classified as trusted contact', async () => {
1163
1282
  ensureConversation('conv-guardian-role-mismatch');
1164
1283
  const session = createCallSession({
1165
1284
  conversationId: 'conv-guardian-role-mismatch',
@@ -1175,6 +1294,7 @@ describe('relay-server', () => {
1175
1294
  guardianExternalUserId: '+15550009999',
1176
1295
  guardianDeliveryChatId: '+15550009999',
1177
1296
  });
1297
+ addTrustedVoiceContact('+15550002222', 'test-assistant');
1178
1298
 
1179
1299
  mockSendMessage.mockImplementation(createMockProviderResponse(['Hello there.']));
1180
1300
 
@@ -1190,13 +1310,13 @@ describe('relay-server', () => {
1190
1310
  const runtimeContext = (relay.getController() as unknown as {
1191
1311
  guardianContext?: {
1192
1312
  sourceChannel?: string;
1193
- actorRole?: string;
1313
+ trustClass?: string;
1194
1314
  guardianExternalUserId?: string;
1195
1315
  requesterExternalUserId?: string;
1196
1316
  };
1197
1317
  })?.guardianContext;
1198
1318
  expect(runtimeContext?.sourceChannel).toBe('voice');
1199
- expect(runtimeContext?.actorRole).toBe('non-guardian');
1319
+ expect(runtimeContext?.trustClass).toBe('trusted_contact');
1200
1320
  expect(runtimeContext?.guardianExternalUserId).toBe('+15550009999');
1201
1321
  expect(runtimeContext?.requesterExternalUserId).toBe('+15550002222');
1202
1322
 
@@ -1236,12 +1356,12 @@ describe('relay-server', () => {
1236
1356
  const runtimeContext = (relay.getController() as unknown as {
1237
1357
  guardianContext?: {
1238
1358
  sourceChannel?: string;
1239
- actorRole?: string;
1359
+ trustClass?: string;
1240
1360
  guardianExternalUserId?: string;
1241
1361
  };
1242
1362
  })?.guardianContext;
1243
1363
  expect(runtimeContext?.sourceChannel).toBe('voice');
1244
- expect(runtimeContext?.actorRole).toBe('guardian');
1364
+ expect(runtimeContext?.trustClass).toBe('guardian');
1245
1365
  expect(runtimeContext?.guardianExternalUserId).toBe('+15550001111');
1246
1366
 
1247
1367
  relay.destroy();
@@ -1283,11 +1403,11 @@ describe('relay-server', () => {
1283
1403
  const runtimeContext = (relay.getController() as unknown as {
1284
1404
  guardianContext?: {
1285
1405
  sourceChannel?: string;
1286
- actorRole?: string;
1406
+ trustClass?: string;
1287
1407
  };
1288
1408
  })?.guardianContext;
1289
1409
  expect(runtimeContext?.sourceChannel).toBe('voice');
1290
- expect(runtimeContext?.actorRole).toBe('unverified_channel');
1410
+ expect(runtimeContext?.trustClass).toBe('unknown');
1291
1411
 
1292
1412
  relay.destroy();
1293
1413
  });
@@ -1302,8 +1422,8 @@ describe('relay-server', () => {
1302
1422
  assistantId: 'test-assistant',
1303
1423
  });
1304
1424
 
1305
- const challenge = createVerificationChallenge('test-assistant', 'voice');
1306
- const spokenCode = challenge.secret.split('').join(' ');
1425
+ const secret = createPendingVoiceGuardianChallenge('test-assistant');
1426
+ const spokenCode = secret.split('').join(' ');
1307
1427
 
1308
1428
  const { relay } = createMockWs(session.id);
1309
1429
 
@@ -1315,9 +1435,9 @@ describe('relay-server', () => {
1315
1435
  }));
1316
1436
 
1317
1437
  const preVerify = (relay.getController() as unknown as {
1318
- guardianContext?: { actorRole?: string };
1438
+ guardianContext?: { trustClass?: string };
1319
1439
  })?.guardianContext;
1320
- expect(preVerify?.actorRole).toBe('unverified_channel');
1440
+ expect(preVerify?.trustClass).toBe('unknown');
1321
1441
 
1322
1442
  await relay.handleMessage(JSON.stringify({
1323
1443
  type: 'prompt',
@@ -1329,10 +1449,10 @@ describe('relay-server', () => {
1329
1449
  await new Promise((resolve) => setTimeout(resolve, 10));
1330
1450
 
1331
1451
  const postVerify = (relay.getController() as unknown as {
1332
- guardianContext?: { sourceChannel?: string; actorRole?: string; guardianExternalUserId?: string };
1452
+ guardianContext?: { sourceChannel?: string; trustClass?: string; guardianExternalUserId?: string };
1333
1453
  })?.guardianContext;
1334
1454
  expect(postVerify?.sourceChannel).toBe('voice');
1335
- expect(postVerify?.actorRole).toBe('guardian');
1455
+ expect(postVerify?.trustClass).toBe('guardian');
1336
1456
  expect(postVerify?.guardianExternalUserId).toBe(session.fromNumber);
1337
1457
 
1338
1458
  relay.destroy();
@@ -1348,7 +1468,7 @@ describe('relay-server', () => {
1348
1468
  assistantId: 'test-assistant',
1349
1469
  });
1350
1470
 
1351
- createVerificationChallenge('test-assistant', 'voice');
1471
+ createPendingVoiceGuardianChallenge('test-assistant');
1352
1472
 
1353
1473
  const { ws, relay } = createMockWs(session.id);
1354
1474
 
@@ -1389,7 +1509,7 @@ describe('relay-server', () => {
1389
1509
  assistantId: 'test-assistant',
1390
1510
  });
1391
1511
 
1392
- createVerificationChallenge('test-assistant', 'voice');
1512
+ createPendingVoiceGuardianChallenge('test-assistant');
1393
1513
 
1394
1514
  const { ws, relay } = createMockWs(session.id);
1395
1515
 
@@ -1451,6 +1571,7 @@ describe('relay-server', () => {
1451
1571
  // Do NOT create any pending challenge
1452
1572
 
1453
1573
  mockSendMessage.mockImplementation(createMockProviderResponse(['Welcome to the line.']));
1574
+ addTrustedVoiceContact('+15559999999', 'test-assistant');
1454
1575
 
1455
1576
  const { ws, relay } = createMockWs(session.id);
1456
1577
 
@@ -1486,7 +1607,7 @@ describe('relay-server', () => {
1486
1607
  assistantId: 'test-assistant',
1487
1608
  });
1488
1609
 
1489
- createVerificationChallenge('test-assistant', 'voice');
1610
+ createPendingVoiceGuardianChallenge('test-assistant');
1490
1611
 
1491
1612
  const { ws, relay } = createMockWs(session.id);
1492
1613
 
@@ -1536,8 +1657,7 @@ describe('relay-server', () => {
1536
1657
  initiatedFromConversationId: 'conv-gv-pointer-success-origin',
1537
1658
  });
1538
1659
 
1539
- const challenge = createVerificationChallenge('test-assistant', 'voice');
1540
- const secret = challenge.secret;
1660
+ const secret = createVoiceVerificationSession('test-assistant', '+15559999999', 'gv-session-ptr-success');
1541
1661
 
1542
1662
  const { relay } = createMockWs(session.id);
1543
1663
 
@@ -1586,7 +1706,7 @@ describe('relay-server', () => {
1586
1706
  initiatedFromConversationId: 'conv-gv-pointer-fail-origin',
1587
1707
  });
1588
1708
 
1589
- createVerificationChallenge('test-assistant', 'voice');
1709
+ createVoiceVerificationSession('test-assistant', '+15559999999', 'gv-session-ptr-fail');
1590
1710
 
1591
1711
  const { relay } = createMockWs(session.id);
1592
1712
 
@@ -27,6 +27,8 @@ mock.module('../util/logger.js', () => ({
27
27
 
28
28
  mock.module('../config/loader.js', () => ({
29
29
  getConfig: () => ({
30
+ ui: {},
31
+
30
32
  model: 'test',
31
33
  provider: 'test',
32
34
  apiKeys: {},
@@ -182,9 +184,9 @@ describe('Runtime attachment metadata', () => {
182
184
  `http://127.0.0.1:${port}/v1/attachments/nonexistent-id`,
183
185
  { headers: AUTH_HEADERS },
184
186
  );
185
- const body = await res.json() as { error: string };
187
+ const body = await res.json() as { error: { message: string; code?: string } };
186
188
 
187
189
  expect(res.status).toBe(404);
188
- expect(body.error).toBe('Attachment not found');
190
+ expect(body.error.message).toBe('Attachment not found');
189
191
  });
190
192
  });
@@ -10,6 +10,7 @@
10
10
  * - tool_input_delta
11
11
  * - tool_output_chunk
12
12
  * - tool_result
13
+ * - message_request_complete (request-level terminal)
13
14
  * - message_complete (terminal)
14
15
  * - generation_handoff (terminal)
15
16
  * - generation_cancelled (terminal)
@@ -43,6 +44,8 @@ mock.module('../util/logger.js', () => ({
43
44
 
44
45
  mock.module('../config/loader.js', () => ({
45
46
  getConfig: () => ({
47
+ ui: {},
48
+
46
49
  model: 'test',
47
50
  provider: 'test',
48
51
  apiKeys: {},
@@ -274,6 +277,24 @@ describe('SSE IPC parity — streaming/delta message types', () => {
274
277
  expect(event.message.type).toBe('message_complete');
275
278
  });
276
279
 
280
+ // ── message_request_complete (request-level terminal) ───────────────────
281
+
282
+ test('preserves message_request_complete payload', async () => {
283
+ const msg = {
284
+ type: 'message_request_complete' as const,
285
+ sessionId: 'conv-msg-request-complete',
286
+ requestId: 'req-123',
287
+ runStillActive: true,
288
+ };
289
+ const event = await publishAndReadFrame('parity-message-request-complete', msg);
290
+
291
+ expect(event.message.type).toBe('message_request_complete');
292
+ const m = event.message as typeof msg;
293
+ expect(m.sessionId).toBe('conv-msg-request-complete');
294
+ expect(m.requestId).toBe('req-123');
295
+ expect(m.runStillActive).toBe(true);
296
+ });
297
+
277
298
  // ── generation_handoff (terminal) ────────────────────────────────────────
278
299
 
279
300
  test('preserves generation_handoff payload', async () => {
@@ -35,6 +35,8 @@ mock.module('../util/logger.js', () => ({
35
35
 
36
36
  mock.module('../config/loader.js', () => ({
37
37
  getConfig: () => ({
38
+ ui: {},
39
+
38
40
  model: 'test',
39
41
  provider: 'test',
40
42
  apiKeys: {},
@@ -116,8 +118,8 @@ describe('SSE assistant-events endpoint', () => {
116
118
 
117
119
  const res = await fetch(eventsUrl(), { headers: AUTH_HEADERS });
118
120
  expect(res.status).toBe(400);
119
- const body = await res.json() as { error: string };
120
- expect(body.error).toContain('conversationKey');
121
+ const body = await res.json() as { error: { message: string; code?: string } };
122
+ expect(body.error.message).toContain('conversationKey');
121
123
 
122
124
  await stopServer();
123
125
  });
@@ -38,6 +38,8 @@ let mockSandboxConfig: {
38
38
 
39
39
  mock.module('../config/loader.js', () => ({
40
40
  getConfig: () => ({
41
+ ui: {},
42
+
41
43
  sandbox: mockSandboxConfig,
42
44
  }),
43
45
  loadRawConfig: () => ({}),
@@ -27,7 +27,9 @@ mock.module('../util/logger.js', () => ({
27
27
  }));
28
28
 
29
29
  mock.module('../config/loader.js', () => ({
30
- getConfig: () => ({ memory: {} }),
30
+ getConfig: () => ({
31
+ ui: {},
32
+ memory: {} }),
31
33
  }));
32
34
 
33
35
  import type { Database } from 'bun:sqlite';
@@ -38,6 +38,8 @@ mock.module('../util/logger.js', () => ({
38
38
 
39
39
  mock.module('../config/loader.js', () => ({
40
40
  getConfig: () => ({
41
+ ui: {},
42
+
41
43
  model: 'test',
42
44
  provider: 'test',
43
45
  apiKeys: {},
@@ -83,6 +85,7 @@ function makeCompletingSession(): Session {
83
85
  },
84
86
  handleConfirmationResponse: () => {},
85
87
  handleSecretResponse: () => {},
88
+ hasAnyPendingConfirmation: () => false,
86
89
  } as unknown as Session;
87
90
  }
88
91
 
@@ -114,6 +117,7 @@ function makeHangingSession(): Session {
114
117
  },
115
118
  handleConfirmationResponse: () => {},
116
119
  handleSecretResponse: () => {},
120
+ hasAnyPendingConfirmation: () => false,
117
121
  _enqueuedMessages: enqueuedMessages,
118
122
  } as unknown as Session;
119
123
  }