@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,107 @@
1
+ import { mkdirSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
6
+
7
+ import { resolveBundledDir } from '../util/bundled-asset.js';
8
+
9
+ let tempDir: string;
10
+
11
+ beforeEach(() => {
12
+ tempDir = join(tmpdir(), `bundled-asset-test-${crypto.randomUUID()}`);
13
+ mkdirSync(tempDir, { recursive: true });
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(tempDir, { recursive: true, force: true });
18
+ });
19
+
20
+ describe('resolveBundledDir', () => {
21
+ test('source mode: returns join(callerDir, relativePath) when callerDir is a normal path', () => {
22
+ const result = resolveBundledDir('/some/source/path', 'templates', 'templates');
23
+ expect(result).toBe(join('/some/source/path', 'templates'));
24
+ });
25
+
26
+ test('source mode: does not check existsSync for the source path', () => {
27
+ // Even if the resolved path does not exist, it returns it as-is
28
+ const result = resolveBundledDir('/nonexistent/path', 'templates', 'templates');
29
+ expect(result).toBe(join('/nonexistent/path', 'templates'));
30
+ });
31
+
32
+ describe('compiled mode (/$bunfs/ prefix)', () => {
33
+ // In compiled mode, process.execPath determines fallback locations.
34
+ // We simulate by creating real directories at the expected fallback paths.
35
+
36
+ let savedExecPath: string;
37
+
38
+ beforeEach(() => {
39
+ savedExecPath = process.execPath;
40
+ });
41
+
42
+ afterEach(() => {
43
+ process.execPath = savedExecPath;
44
+ });
45
+
46
+ test('prefers Contents/Resources/<bundleName> when it exists', () => {
47
+ // Simulate macOS .app bundle: binary at Contents/MacOS/vellum-daemon
48
+ const macosDir = join(tempDir, 'Contents', 'MacOS');
49
+ const resourcesDir = join(tempDir, 'Contents', 'Resources');
50
+ mkdirSync(macosDir, { recursive: true });
51
+ mkdirSync(join(resourcesDir, 'templates'), { recursive: true });
52
+
53
+ process.execPath = join(macosDir, 'vellum-daemon');
54
+
55
+ const result = resolveBundledDir('/$bunfs/root/src/config', 'templates', 'templates');
56
+ expect(result).toBe(join(resourcesDir, 'templates'));
57
+ });
58
+
59
+ test('falls back to <execDir>/<bundleName> when Resources does not exist', () => {
60
+ // Simulate standalone binary deployment (no .app bundle)
61
+ const binDir = join(tempDir, 'bin');
62
+ mkdirSync(join(binDir, 'templates'), { recursive: true });
63
+
64
+ process.execPath = join(binDir, 'vellum-daemon');
65
+
66
+ const result = resolveBundledDir('/$bunfs/root/src/config', 'templates', 'templates');
67
+ expect(result).toBe(join(binDir, 'templates'));
68
+ });
69
+
70
+ test('falls back to source path when neither Resources nor execDir have the asset', () => {
71
+ const binDir = join(tempDir, 'bin');
72
+ mkdirSync(binDir, { recursive: true });
73
+ // Don't create any asset directories
74
+
75
+ process.execPath = join(binDir, 'vellum-daemon');
76
+
77
+ const result = resolveBundledDir('/$bunfs/root/src/config', 'templates', 'templates');
78
+ expect(result).toBe(join('/$bunfs/root/src/config', 'templates'));
79
+ });
80
+
81
+ test('Resources path takes priority over execDir path when both exist', () => {
82
+ const macosDir = join(tempDir, 'Contents', 'MacOS');
83
+ const resourcesDir = join(tempDir, 'Contents', 'Resources');
84
+ mkdirSync(macosDir, { recursive: true });
85
+ mkdirSync(join(resourcesDir, 'hook-templates'), { recursive: true });
86
+ // Also create at execDir level
87
+ mkdirSync(join(macosDir, 'hook-templates'), { recursive: true });
88
+
89
+ process.execPath = join(macosDir, 'vellum-daemon');
90
+
91
+ const result = resolveBundledDir('/$bunfs/root/src/hooks', '../../hook-templates', 'hook-templates');
92
+ expect(result).toBe(join(resourcesDir, 'hook-templates'));
93
+ });
94
+
95
+ test('works with different bundleName values', () => {
96
+ const macosDir = join(tempDir, 'Contents', 'MacOS');
97
+ const resourcesDir = join(tempDir, 'Contents', 'Resources');
98
+ mkdirSync(macosDir, { recursive: true });
99
+ mkdirSync(join(resourcesDir, 'prebuilt'), { recursive: true });
100
+
101
+ process.execPath = join(macosDir, 'vellum-daemon');
102
+
103
+ const result = resolveBundledDir('/$bunfs/root/src/home-base/prebuilt', '.', 'prebuilt');
104
+ expect(result).toBe(join(resourcesDir, 'prebuilt'));
105
+ });
106
+ });
107
+ });
@@ -32,6 +32,8 @@ mock.module('../util/logger.js', () => ({
32
32
 
33
33
  mock.module('../config/loader.js', () => ({
34
34
  getConfig: () => ({
35
+ ui: {},
36
+
35
37
  provider: 'anthropic',
36
38
  providerOrder: ['anthropic'],
37
39
  apiKeys: { anthropic: 'test-key' },
@@ -47,6 +49,7 @@ mock.module('../config/loader.js', () => ({
47
49
  model: undefined,
48
50
  },
49
51
  memory: { enabled: false },
52
+ notifications: { decisionModelIntent: 'latency-optimized' },
50
53
  }),
51
54
  }));
52
55
 
@@ -127,11 +130,11 @@ import {
127
130
  updateCallSession,
128
131
  } from '../calls/call-store.js';
129
132
  import type { RelayConnection } from '../calls/relay-server.js';
130
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
131
133
  import {
132
- getGuardianActionRequest,
133
- getPendingRequestByCallSessionId,
134
- } from '../memory/guardian-action-store.js';
134
+ getCanonicalGuardianRequest,
135
+ getPendingCanonicalRequestByCallSessionId,
136
+ } from '../memory/canonical-guardian-store.js';
137
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
135
138
  import { conversations } from '../memory/schema.js';
136
139
 
137
140
  initializeDb();
@@ -192,6 +195,8 @@ function ensureConversation(id: string): void {
192
195
 
193
196
  function resetTables() {
194
197
  const db = getDb();
198
+ db.run('DELETE FROM canonical_guardian_deliveries');
199
+ db.run('DELETE FROM canonical_guardian_requests');
195
200
  db.run('DELETE FROM guardian_action_deliveries');
196
201
  db.run('DELETE FROM guardian_action_requests');
197
202
  db.run('DELETE FROM call_pending_questions');
@@ -631,7 +636,7 @@ describe('call-controller', () => {
631
636
  test('handleCallerUtterance: passes guardian context to startVoiceTurn', async () => {
632
637
  const guardianCtx = {
633
638
  sourceChannel: 'voice' as const,
634
- actorRole: 'non-guardian' as const,
639
+ trustClass: 'trusted_contact' as const,
635
640
  guardianExternalUserId: '+15550009999',
636
641
  guardianChatId: '+15550009999',
637
642
  requesterExternalUserId: '+15550002222',
@@ -683,13 +688,13 @@ describe('call-controller', () => {
683
688
  test('setGuardianContext: subsequent turns use updated guardian context', async () => {
684
689
  const initialCtx = {
685
690
  sourceChannel: 'voice' as const,
686
- actorRole: 'unverified_channel' as const,
691
+ trustClass: 'unknown' as const,
687
692
  denialReason: 'no_binding' as const,
688
693
  };
689
694
 
690
695
  const upgradedCtx = {
691
696
  sourceChannel: 'voice' as const,
692
- actorRole: 'guardian' as const,
697
+ trustClass: 'guardian' as const,
693
698
  guardianExternalUserId: '+15550003333',
694
699
  guardianChatId: '+15550003333',
695
700
  };
@@ -1163,7 +1168,7 @@ describe('call-controller', () => {
1163
1168
  await new Promise((r) => setTimeout(r, 10));
1164
1169
 
1165
1170
  // Verify a guardian action request was created
1166
- const pendingRequest = getPendingRequestByCallSessionId(session.id);
1171
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1167
1172
  expect(pendingRequest).not.toBeNull();
1168
1173
  expect(pendingRequest!.status).toBe('pending');
1169
1174
 
@@ -1175,11 +1180,10 @@ describe('call-controller', () => {
1175
1180
  // Wait for the consultation timeout
1176
1181
  await new Promise((r) => setTimeout(r, 200));
1177
1182
 
1178
- // The guardian action request should now be expired with call_timeout reason
1179
- const timedOutRequest = getGuardianActionRequest(pendingRequest!.id);
1183
+ // The canonical guardian request should now be expired
1184
+ const timedOutRequest = getCanonicalGuardianRequest(pendingRequest!.id);
1180
1185
  expect(timedOutRequest).not.toBeNull();
1181
1186
  expect(timedOutRequest!.status).toBe('expired');
1182
- expect(timedOutRequest!.expiredReason).toBe('call_timeout');
1183
1187
 
1184
1188
  // Event should be recorded
1185
1189
  const events = getCallEvents(session.id);
@@ -1277,7 +1281,7 @@ describe('call-controller', () => {
1277
1281
  expect(question!.questionText).toBe('Allow send_email to bob@example.com?');
1278
1282
 
1279
1283
  // Verify the guardian action request has tool metadata
1280
- const pendingRequest = getPendingRequestByCallSessionId(session.id);
1284
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1281
1285
  expect(pendingRequest).not.toBeNull();
1282
1286
  expect(pendingRequest!.toolName).toBe('send_email');
1283
1287
  expect(pendingRequest!.inputDigest).not.toBeNull();
@@ -1305,7 +1309,7 @@ describe('call-controller', () => {
1305
1309
  await controller.handleCallerUtterance('Send it');
1306
1310
  await new Promise((r) => setTimeout(r, 50));
1307
1311
 
1308
- const request1 = getPendingRequestByCallSessionId(session.id);
1312
+ const request1 = getPendingCanonicalRequestByCallSessionId(session.id);
1309
1313
  expect(request1).not.toBeNull();
1310
1314
 
1311
1315
  // Compute expected digest independently using the same utility
@@ -1326,7 +1330,7 @@ describe('call-controller', () => {
1326
1330
  await new Promise((r) => setTimeout(r, 50));
1327
1331
 
1328
1332
  // Verify the guardian action request has NO tool metadata
1329
- const pendingRequest = getPendingRequestByCallSessionId(session.id);
1333
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1330
1334
  expect(pendingRequest).not.toBeNull();
1331
1335
  expect(pendingRequest!.toolName).toBeNull();
1332
1336
  expect(pendingRequest!.inputDigest).toBeNull();
@@ -1384,7 +1388,7 @@ describe('call-controller', () => {
1384
1388
  expect(question!.questionText).toBe('Allow send_message?');
1385
1389
 
1386
1390
  // Verify tool metadata was parsed correctly
1387
- const pendingRequest = getPendingRequestByCallSessionId(session.id);
1391
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1388
1392
  expect(pendingRequest).not.toBeNull();
1389
1393
  expect(pendingRequest!.toolName).toBe('send_message');
1390
1394
  expect(pendingRequest!.inputDigest).not.toBeNull();
@@ -1411,7 +1415,7 @@ describe('call-controller', () => {
1411
1415
  await controller.handleCallerUtterance('Do something');
1412
1416
  await new Promise((r) => setTimeout(r, 50));
1413
1417
 
1414
- const pendingRequest = getPendingRequestByCallSessionId(session.id);
1418
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1415
1419
  expect(pendingRequest).not.toBeNull();
1416
1420
  expect(pendingRequest!.questionText).toBe('Fallback question?');
1417
1421
  // Tool metadata should be null since the approval marker was malformed
@@ -1547,7 +1551,7 @@ describe('call-controller', () => {
1547
1551
 
1548
1552
  const firstQuestionId = controller.getPendingConsultationQuestionId();
1549
1553
  expect(firstQuestionId).not.toBeNull();
1550
- const firstRequest = getPendingRequestByCallSessionId(session.id);
1554
+ const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1551
1555
  expect(firstRequest).not.toBeNull();
1552
1556
 
1553
1557
  // Repeated ASK_GUARDIAN with same informational question (no tool metadata)
@@ -1559,7 +1563,7 @@ describe('call-controller', () => {
1559
1563
 
1560
1564
  // Should coalesce: same consultation ID, same request
1561
1565
  expect(controller.getPendingConsultationQuestionId()).toBe(firstQuestionId);
1562
- const currentRequest = getPendingRequestByCallSessionId(session.id);
1566
+ const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1563
1567
  expect(currentRequest).not.toBeNull();
1564
1568
  expect(currentRequest!.id).toBe(firstRequest!.id);
1565
1569
  expect(currentRequest!.status).toBe('pending');
@@ -1589,7 +1593,7 @@ describe('call-controller', () => {
1589
1593
 
1590
1594
  const firstQuestionId = controller.getPendingConsultationQuestionId();
1591
1595
  expect(firstQuestionId).not.toBeNull();
1592
- const firstRequest = getPendingRequestByCallSessionId(session.id);
1596
+ const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1593
1597
  expect(firstRequest).not.toBeNull();
1594
1598
 
1595
1599
  // Repeated ASK_GUARDIAN_APPROVAL with same tool/input
@@ -1601,7 +1605,7 @@ describe('call-controller', () => {
1601
1605
 
1602
1606
  // Should coalesce: same consultation, same request
1603
1607
  expect(controller.getPendingConsultationQuestionId()).toBe(firstQuestionId);
1604
- const currentRequest = getPendingRequestByCallSessionId(session.id);
1608
+ const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1605
1609
  expect(currentRequest!.id).toBe(firstRequest!.id);
1606
1610
  expect(currentRequest!.status).toBe('pending');
1607
1611
 
@@ -1623,7 +1627,7 @@ describe('call-controller', () => {
1623
1627
  await controller.handleCallerUtterance('Send email');
1624
1628
  await new Promise((r) => setTimeout(r, 50));
1625
1629
 
1626
- const firstRequest = getPendingRequestByCallSessionId(session.id);
1630
+ const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1627
1631
  expect(firstRequest).not.toBeNull();
1628
1632
  expect(firstRequest!.toolName).toBe('send_email');
1629
1633
 
@@ -1640,18 +1644,15 @@ describe('call-controller', () => {
1640
1644
  await new Promise((r) => setTimeout(r, 100));
1641
1645
 
1642
1646
  // New consultation should be active
1643
- const secondRequest = getPendingRequestByCallSessionId(session.id);
1647
+ const secondRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1644
1648
  expect(secondRequest).not.toBeNull();
1645
1649
  expect(secondRequest!.id).not.toBe(firstRequest!.id);
1646
1650
  expect(secondRequest!.toolName).toBe('calendar_create');
1647
1651
 
1648
- // Old request should be expired with 'superseded' reason
1649
- const expiredRequest = getGuardianActionRequest(firstRequest!.id);
1652
+ // Old request should be expired (superseded by the new one)
1653
+ const expiredRequest = getCanonicalGuardianRequest(firstRequest!.id);
1650
1654
  expect(expiredRequest).not.toBeNull();
1651
1655
  expect(expiredRequest!.status).toBe('expired');
1652
- expect(expiredRequest!.expiredReason).toBe('superseded');
1653
- expect(expiredRequest!.supersededByRequestId).toBe(secondRequest!.id);
1654
- expect(expiredRequest!.supersededAt).not.toBeNull();
1655
1656
 
1656
1657
  controller.destroy();
1657
1658
  });
@@ -1671,7 +1672,7 @@ describe('call-controller', () => {
1671
1672
  await controller.handleCallerUtterance('Send email to Bob');
1672
1673
  await new Promise((r) => setTimeout(r, 50));
1673
1674
 
1674
- const firstRequest = getPendingRequestByCallSessionId(session.id);
1675
+ const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1675
1676
  expect(firstRequest).not.toBeNull();
1676
1677
  expect(firstRequest!.toolName).toBe('send_email');
1677
1678
 
@@ -1685,7 +1686,7 @@ describe('call-controller', () => {
1685
1686
  await new Promise((r) => setTimeout(r, 50));
1686
1687
 
1687
1688
  // Should coalesce: the inherited tool metadata matches the existing consultation
1688
- const currentRequest = getPendingRequestByCallSessionId(session.id);
1689
+ const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1689
1690
  expect(currentRequest!.id).toBe(firstRequest!.id);
1690
1691
  expect(currentRequest!.status).toBe('pending');
1691
1692
 
@@ -47,6 +47,8 @@ const mockCallsConfig = {
47
47
 
48
48
  mock.module('../config/loader.js', () => ({
49
49
  getConfig: () => ({
50
+ ui: {},
51
+
50
52
  model: 'test',
51
53
  provider: 'test',
52
54
  apiKeys: {},
@@ -227,8 +229,8 @@ describe('runtime call routes — HTTP layer', () => {
227
229
  });
228
230
 
229
231
  expect(res.status).toBe(400);
230
- const body = await res.json() as { error: string };
231
- expect(body.error).toContain('conversationId');
232
+ const body = await res.json() as { error: { message: string; code?: string } };
233
+ expect(body.error.message).toContain('conversationId');
232
234
 
233
235
  await stopServer();
234
236
  });
@@ -269,8 +271,8 @@ describe('runtime call routes — HTTP layer', () => {
269
271
  });
270
272
 
271
273
  expect(res.status).toBe(400);
272
- const body = await res.json() as { error: string };
273
- expect(body.error).toContain('E.164');
274
+ const body = await res.json() as { error: { message: string; code?: string } };
275
+ expect(body.error.message).toContain('E.164');
274
276
 
275
277
  await stopServer();
276
278
  });
@@ -285,8 +287,8 @@ describe('runtime call routes — HTTP layer', () => {
285
287
  });
286
288
 
287
289
  expect(res.status).toBe(400);
288
- const body = await res.json() as { error: string };
289
- expect(body.error).toContain('Invalid JSON');
290
+ const body = await res.json() as { error: { message: string; code?: string } };
291
+ expect(body.error.message).toContain('Invalid JSON');
290
292
 
291
293
  await stopServer();
292
294
  });
@@ -309,8 +311,8 @@ describe('runtime call routes — HTTP layer', () => {
309
311
  // user_number mode requires a configured user phone number;
310
312
  // since we haven't set one, this should return a 400 explaining why
311
313
  expect(res.status).toBe(400);
312
- const body = await res.json() as { error: string };
313
- expect(body.error).toContain('user_number');
314
+ const body = await res.json() as { error: { message: string; code?: string } };
315
+ expect(body.error.message).toContain('user_number');
314
316
 
315
317
  await stopServer();
316
318
  });
@@ -364,11 +366,11 @@ describe('runtime call routes — HTTP layer', () => {
364
366
  });
365
367
 
366
368
  expect(res.status).toBe(400);
367
- const body = await res.json() as { error: string };
368
- expect(body.error).toContain('Invalid callerIdentityMode');
369
- expect(body.error).toContain('bogus');
370
- expect(body.error).toContain('assistant_number');
371
- expect(body.error).toContain('user_number');
369
+ const body = await res.json() as { error: { message: string; code?: string } };
370
+ expect(body.error.message).toContain('Invalid callerIdentityMode');
371
+ expect(body.error.message).toContain('bogus');
372
+ expect(body.error.message).toContain('assistant_number');
373
+ expect(body.error.message).toContain('user_number');
372
374
 
373
375
  await stopServer();
374
376
  });
@@ -510,8 +512,8 @@ describe('runtime call routes — HTTP layer', () => {
510
512
  });
511
513
 
512
514
  expect(res.status).toBe(400);
513
- const body = await res.json() as { error: string };
514
- expect(body.error).toContain('Invalid JSON');
515
+ const body = await res.json() as { error: { message: string; code?: string } };
516
+ expect(body.error.message).toContain('Invalid JSON');
515
517
 
516
518
  await stopServer();
517
519
  });
@@ -533,9 +535,9 @@ describe('runtime call routes — HTTP layer', () => {
533
535
  body: JSON.stringify({ answer: 'Yes, please' }),
534
536
  });
535
537
 
536
- expect(res.status).toBe(404);
537
- const body = await res.json() as { error: string };
538
- expect(body.error).toContain('pending question');
538
+ expect(res.status).toBe(409);
539
+ const body = await res.json() as { error: { message: string; code?: string } };
540
+ expect(body.error.message).toContain('No active controller');
539
541
 
540
542
  await stopServer();
541
543
  });
@@ -583,8 +585,8 @@ describe('runtime call routes — HTTP layer', () => {
583
585
  });
584
586
 
585
587
  expect(res.status).toBe(409);
586
- const body = await res.json() as { error: string };
587
- expect(body.error).toContain('orchestrator');
588
+ const body = await res.json() as { error: { message: string; code?: string } };
589
+ expect(body.error.message).toContain('No active controller');
588
590
 
589
591
  await stopServer();
590
592
  });
@@ -609,8 +611,8 @@ describe('runtime call routes — HTTP layer', () => {
609
611
  });
610
612
 
611
613
  expect(res.status).toBe(400);
612
- const body = await res.json() as { error: string };
613
- expect(body.error).toContain('Invalid JSON');
614
+ const body = await res.json() as { error: { message: string; code?: string } };
615
+ expect(body.error.message).toContain('Invalid JSON');
614
616
 
615
617
  await stopServer();
616
618
  });
@@ -633,8 +635,8 @@ describe('runtime call routes — HTTP layer', () => {
633
635
  });
634
636
 
635
637
  expect(res.status).toBe(400);
636
- const body = await res.json() as { error: string };
637
- expect(body.error).toContain('instructionText');
638
+ const body = await res.json() as { error: { message: string; code?: string } };
639
+ expect(body.error.message).toContain('instructionText');
638
640
 
639
641
  await stopServer();
640
642
  });
@@ -657,8 +659,8 @@ describe('runtime call routes — HTTP layer', () => {
657
659
  });
658
660
 
659
661
  expect(res.status).toBe(400);
660
- const body = await res.json() as { error: string };
661
- expect(body.error).toContain('instructionText');
662
+ const body = await res.json() as { error: { message: string; code?: string } };
663
+ expect(body.error.message).toContain('instructionText');
662
664
 
663
665
  await stopServer();
664
666
  });
@@ -673,8 +675,8 @@ describe('runtime call routes — HTTP layer', () => {
673
675
  });
674
676
 
675
677
  expect(res.status).toBe(404);
676
- const body = await res.json() as { error: string };
677
- expect(body.error).toContain('No call session found');
678
+ const body = await res.json() as { error: { message: string; code?: string } };
679
+ expect(body.error.message).toContain('No call session found');
678
680
 
679
681
  await stopServer();
680
682
  });
@@ -699,8 +701,8 @@ describe('runtime call routes — HTTP layer', () => {
699
701
  });
700
702
 
701
703
  expect(res.status).toBe(409);
702
- const body = await res.json() as { error: string };
703
- expect(body.error).toContain('not active');
704
+ const body = await res.json() as { error: { message: string; code?: string } };
705
+ expect(body.error.message).toContain('not active');
704
706
 
705
707
  await stopServer();
706
708
  });
@@ -723,8 +725,8 @@ describe('runtime call routes — HTTP layer', () => {
723
725
  });
724
726
 
725
727
  expect(res.status).toBe(409);
726
- const body = await res.json() as { error: string };
727
- expect(body.error).toContain('orchestrator');
728
+ const body = await res.json() as { error: { message: string; code?: string } };
729
+ expect(body.error.message).toContain('No active controller');
728
730
 
729
731
  await stopServer();
730
732
  });
@@ -11,6 +11,8 @@ let activeVoiceSession: {
11
11
 
12
12
  mock.module('../config/loader.js', () => ({
13
13
  getConfig: () => ({
14
+ ui: {},
15
+
14
16
  calls: { enabled: callsEnabled },
15
17
  }),
16
18
  }));