@vellumai/assistant 0.5.7 → 0.5.9

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 (205) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/eslint.config.mjs +0 -31
  5. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  9. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/approval-cascade.test.ts +0 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  13. package/src/__tests__/call-controller.test.ts +0 -1
  14. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  15. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  16. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  17. package/src/__tests__/config-schema.test.ts +2 -0
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  19. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  20. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  21. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  22. package/src/__tests__/conversation-error.test.ts +15 -1
  23. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  24. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  26. package/src/__tests__/conversation-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
  28. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  29. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  30. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  32. package/src/__tests__/credential-execution-client.test.ts +5 -2
  33. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  34. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  35. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  37. package/src/__tests__/credentials-cli.test.ts +4 -3
  38. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  39. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  40. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  41. package/src/__tests__/journal-context.test.ts +335 -0
  42. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  43. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  44. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  45. package/src/__tests__/memory-regressions.test.ts +408 -363
  46. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  47. package/src/__tests__/non-member-access-request.test.ts +2 -2
  48. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  49. package/src/__tests__/oauth-cli.test.ts +5 -1
  50. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  51. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  52. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  53. package/src/__tests__/relay-server.test.ts +1 -2
  54. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  55. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  56. package/src/__tests__/secure-keys.test.ts +18 -15
  57. package/src/__tests__/skill-memory.test.ts +17 -3
  58. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  59. package/src/__tests__/stt-hints.test.ts +437 -0
  60. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  61. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  62. package/src/__tests__/voice-quality.test.ts +58 -0
  63. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  64. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  65. package/src/acp/agent-process.ts +9 -1
  66. package/src/agent/loop.ts +1 -1
  67. package/src/approvals/guardian-request-resolvers.ts +164 -38
  68. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  69. package/src/calls/call-controller.ts +9 -5
  70. package/src/calls/fish-audio-client.ts +26 -14
  71. package/src/calls/stt-hints.ts +189 -0
  72. package/src/calls/tts-text-sanitizer.ts +61 -0
  73. package/src/calls/twilio-routes.ts +32 -4
  74. package/src/calls/voice-quality.ts +15 -3
  75. package/src/calls/voice-session-bridge.ts +1 -0
  76. package/src/cli/commands/avatar.ts +2 -2
  77. package/src/cli/commands/credentials.ts +110 -94
  78. package/src/cli/commands/doctor.ts +2 -2
  79. package/src/cli/commands/keys.ts +7 -7
  80. package/src/cli/commands/memory.ts +1 -1
  81. package/src/cli/commands/oauth/connections.ts +11 -29
  82. package/src/cli/commands/oauth/platform.ts +389 -43
  83. package/src/cli/lib/daemon-credential-client.ts +284 -0
  84. package/src/cli.ts +1 -1
  85. package/src/config/bundled-skills/AGENTS.md +34 -0
  86. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  87. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  88. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  89. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  90. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  91. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  92. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  93. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  94. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  95. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  96. package/src/config/bundled-tool-registry.ts +4 -0
  97. package/src/config/defaults.ts +0 -2
  98. package/src/config/env-registry.ts +4 -4
  99. package/src/config/env.ts +14 -1
  100. package/src/config/feature-flag-registry.json +1 -1
  101. package/src/config/loader.ts +8 -11
  102. package/src/config/schema.ts +5 -16
  103. package/src/config/schemas/calls.ts +17 -0
  104. package/src/config/schemas/inference.ts +2 -2
  105. package/src/config/schemas/journal.ts +16 -0
  106. package/src/config/schemas/memory-processing.ts +2 -2
  107. package/src/config/types.ts +1 -0
  108. package/src/contacts/contact-store.ts +2 -2
  109. package/src/credential-execution/executable-discovery.ts +1 -1
  110. package/src/credential-execution/startup-timeout.ts +36 -0
  111. package/src/daemon/approval-generators.ts +3 -9
  112. package/src/daemon/conversation-agent-loop.ts +6 -0
  113. package/src/daemon/conversation-error.ts +13 -1
  114. package/src/daemon/conversation-memory.ts +1 -2
  115. package/src/daemon/conversation-process.ts +18 -1
  116. package/src/daemon/conversation-runtime-assembly.ts +61 -1
  117. package/src/daemon/conversation-surfaces.ts +30 -1
  118. package/src/daemon/conversation.ts +20 -9
  119. package/src/daemon/guardian-action-generators.ts +3 -9
  120. package/src/daemon/lifecycle.ts +18 -11
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/server.ts +2 -3
  123. package/src/memory/app-store.ts +31 -0
  124. package/src/memory/db-init.ts +4 -0
  125. package/src/memory/indexer.ts +19 -10
  126. package/src/memory/items-extractor.ts +315 -322
  127. package/src/memory/job-handlers/summarization.ts +26 -16
  128. package/src/memory/jobs-store.ts +33 -1
  129. package/src/memory/journal-memory.ts +214 -0
  130. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  131. package/src/memory/migrations/index.ts +1 -0
  132. package/src/memory/migrations/registry.ts +8 -0
  133. package/src/memory/retriever.test.ts +37 -25
  134. package/src/memory/retriever.ts +24 -49
  135. package/src/memory/schema/memory-core.ts +2 -0
  136. package/src/memory/search/formatting.ts +7 -44
  137. package/src/memory/search/staleness.ts +4 -0
  138. package/src/memory/search/tier-classifier.ts +10 -2
  139. package/src/memory/search/types.ts +2 -5
  140. package/src/memory/task-memory-cleanup.ts +4 -3
  141. package/src/notifications/adapters/slack.ts +168 -6
  142. package/src/notifications/broadcaster.ts +1 -0
  143. package/src/notifications/copy-composer.ts +59 -2
  144. package/src/notifications/signal.ts +2 -0
  145. package/src/notifications/types.ts +2 -0
  146. package/src/prompts/journal-context.ts +133 -0
  147. package/src/prompts/persona-resolver.ts +80 -24
  148. package/src/prompts/system-prompt.ts +30 -0
  149. package/src/prompts/templates/NOW.md +26 -0
  150. package/src/prompts/templates/SOUL.md +20 -0
  151. package/src/prompts/update-bulletin-format.ts +0 -2
  152. package/src/providers/provider-send-message.ts +3 -32
  153. package/src/providers/registry.ts +2 -139
  154. package/src/providers/types.ts +1 -1
  155. package/src/runtime/access-request-helper.ts +4 -0
  156. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  157. package/src/runtime/auth/route-policy.ts +2 -0
  158. package/src/runtime/gateway-client.ts +47 -4
  159. package/src/runtime/guardian-decision-types.ts +45 -4
  160. package/src/runtime/http-server.ts +5 -2
  161. package/src/runtime/routes/access-request-decision.ts +2 -2
  162. package/src/runtime/routes/app-management-routes.ts +2 -1
  163. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  164. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  165. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  166. package/src/runtime/routes/debug-routes.ts +12 -9
  167. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  168. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  169. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  170. package/src/runtime/routes/identity-routes.ts +1 -1
  171. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  172. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  173. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  174. package/src/runtime/routes/integrations/twilio.ts +52 -10
  175. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  176. package/src/runtime/routes/memory-item-routes.ts +25 -11
  177. package/src/runtime/routes/secret-routes.ts +141 -10
  178. package/src/runtime/routes/tts-routes.ts +11 -1
  179. package/src/security/ces-credential-client.ts +18 -9
  180. package/src/security/ces-rpc-credential-backend.ts +4 -3
  181. package/src/security/credential-backend.ts +10 -4
  182. package/src/security/secure-keys.ts +21 -4
  183. package/src/skills/catalog-install.ts +4 -36
  184. package/src/skills/inline-command-expansions.ts +7 -7
  185. package/src/skills/skill-memory.ts +1 -0
  186. package/src/subagent/manager.ts +2 -5
  187. package/src/tools/acp/spawn.ts +78 -1
  188. package/src/tools/credentials/vault.ts +5 -3
  189. package/src/tools/memory/definitions.ts +3 -2
  190. package/src/tools/memory/handlers.ts +10 -7
  191. package/src/tools/sensitive-output-placeholders.ts +2 -2
  192. package/src/tools/terminal/safe-env.ts +1 -0
  193. package/src/util/browser.ts +15 -0
  194. package/src/util/platform.ts +1 -1
  195. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  196. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  197. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  198. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  199. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  200. package/src/workspace/migrations/registry.ts +4 -0
  201. package/src/workspace/provider-commit-message-generator.ts +12 -21
  202. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  203. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  204. package/src/memory/search/lexical.ts +0 -48
  205. package/src/providers/failover.ts +0 -186
@@ -2,7 +2,8 @@
2
2
  * Unit tests for TwiML generation with voice quality profiles.
3
3
  *
4
4
  * Tests that generateTwiML correctly uses profile values for
5
- * ttsProvider, voice, language, and transcriptionProvider.
5
+ * ttsProvider, voice, language, transcriptionProvider,
6
+ * and interruptSensitivity.
6
7
  */
7
8
  import { describe, expect, mock, test } from "bun:test";
8
9
 
@@ -26,6 +27,7 @@ describe("generateTwiML with voice quality profile", () => {
26
27
  transcriptionProvider: "Deepgram",
27
28
  ttsProvider: "Google",
28
29
  voice: "Google.en-US-Journey-O",
30
+ interruptSensitivity: "low",
29
31
  });
30
32
 
31
33
  expect(twiml).toContain('ttsProvider="Google"');
@@ -40,6 +42,7 @@ describe("generateTwiML with voice quality profile", () => {
40
42
  transcriptionProvider: "Deepgram",
41
43
  ttsProvider: "ElevenLabs",
42
44
  voice: "voice123-turbo_v2_5-1_0.5_0.75",
45
+ interruptSensitivity: "low",
43
46
  });
44
47
 
45
48
  expect(twiml).toContain('ttsProvider="ElevenLabs"');
@@ -52,6 +55,7 @@ describe("generateTwiML with voice quality profile", () => {
52
55
  transcriptionProvider: "Deepgram",
53
56
  ttsProvider: "Google",
54
57
  voice: "Google.en-US-Journey-O",
58
+ interruptSensitivity: "low",
55
59
  });
56
60
 
57
61
  expect(twiml).toContain('voice="Google.en-US-Journey-O"');
@@ -63,6 +67,7 @@ describe("generateTwiML with voice quality profile", () => {
63
67
  transcriptionProvider: "Deepgram",
64
68
  ttsProvider: "ElevenLabs",
65
69
  voice: "abc123-turbo_v2_5-1_0.5_0.75",
70
+ interruptSensitivity: "low",
66
71
  });
67
72
 
68
73
  expect(twiml).toContain('voice="abc123-turbo_v2_5-1_0.5_0.75"');
@@ -74,6 +79,7 @@ describe("generateTwiML with voice quality profile", () => {
74
79
  transcriptionProvider: "Google",
75
80
  ttsProvider: "Google",
76
81
  voice: "Google.es-MX-Standard-A",
82
+ interruptSensitivity: "low",
77
83
  });
78
84
 
79
85
  expect(twiml).toContain('language="es-MX"');
@@ -85,6 +91,7 @@ describe("generateTwiML with voice quality profile", () => {
85
91
  transcriptionProvider: "Google",
86
92
  ttsProvider: "Google",
87
93
  voice: "Google.en-US-Journey-O",
94
+ interruptSensitivity: "low",
88
95
  });
89
96
 
90
97
  expect(twiml).toContain('transcriptionProvider="Google"');
@@ -96,6 +103,7 @@ describe("generateTwiML with voice quality profile", () => {
96
103
  transcriptionProvider: "Deepgram",
97
104
  ttsProvider: "Google",
98
105
  voice: 'voice<>&"test',
106
+ interruptSensitivity: "low",
99
107
  });
100
108
 
101
109
  expect(twiml).toContain('voice="voice&lt;&gt;&amp;&quot;test"');
@@ -108,6 +116,7 @@ describe("generateTwiML with voice quality profile", () => {
108
116
  transcriptionProvider: "Deepgram",
109
117
  ttsProvider: "Google",
110
118
  voice: "Google.en-US-Journey-O",
119
+ interruptSensitivity: "low",
111
120
  });
112
121
 
113
122
  expect(twiml).toContain(`callSessionId=${callSessionId}`);
@@ -119,6 +128,7 @@ describe("generateTwiML with voice quality profile", () => {
119
128
  transcriptionProvider: "Deepgram",
120
129
  ttsProvider: "Google",
121
130
  voice: "Google.en-US-Journey-O",
131
+ interruptSensitivity: "low",
122
132
  });
123
133
 
124
134
  expect(twiml).toContain('interruptible="true"');
@@ -131,8 +141,136 @@ describe("generateTwiML with voice quality profile", () => {
131
141
  transcriptionProvider: "Deepgram",
132
142
  ttsProvider: "Google",
133
143
  voice: "Google.en-US-Journey-O",
144
+ interruptSensitivity: "low",
134
145
  });
135
146
 
136
147
  expect(twiml).not.toContain("welcomeGreeting=");
137
148
  });
149
+
150
+ test('TwiML includes interruptSensitivity="low" when profile has low', () => {
151
+ const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
152
+ language: "en-US",
153
+ transcriptionProvider: "Deepgram",
154
+ ttsProvider: "Google",
155
+ voice: "Google.en-US-Journey-O",
156
+ interruptSensitivity: "low",
157
+ });
158
+
159
+ expect(twiml).toContain('interruptSensitivity="low"');
160
+ });
161
+
162
+ test("custom interruptSensitivity values are reflected correctly", () => {
163
+ const twimlMedium = generateTwiML(
164
+ callSessionId,
165
+ relayUrl,
166
+ welcomeGreeting,
167
+ {
168
+ language: "en-US",
169
+ transcriptionProvider: "Deepgram",
170
+ ttsProvider: "Google",
171
+ voice: "Google.en-US-Journey-O",
172
+ interruptSensitivity: "medium",
173
+ },
174
+ );
175
+
176
+ expect(twimlMedium).toContain('interruptSensitivity="medium"');
177
+
178
+ const twimlHigh = generateTwiML(
179
+ callSessionId,
180
+ relayUrl,
181
+ welcomeGreeting,
182
+ {
183
+ language: "en-US",
184
+ transcriptionProvider: "Deepgram",
185
+ ttsProvider: "Google",
186
+ voice: "Google.en-US-Journey-O",
187
+ interruptSensitivity: "high",
188
+ },
189
+ );
190
+
191
+ expect(twimlHigh).toContain('interruptSensitivity="high"');
192
+ });
193
+
194
+ test("hints attribute present when hints string is non-empty", () => {
195
+ const twiml = generateTwiML(
196
+ callSessionId,
197
+ relayUrl,
198
+ welcomeGreeting,
199
+ {
200
+ language: "en-US",
201
+ transcriptionProvider: "Deepgram",
202
+ ttsProvider: "ElevenLabs",
203
+ voice: "voice123",
204
+ interruptSensitivity: "low",
205
+ },
206
+ undefined,
207
+ undefined,
208
+ "Alice,Bob,Vellum",
209
+ );
210
+
211
+ expect(twiml).toContain('hints="Alice,Bob,Vellum"');
212
+ });
213
+
214
+ test("hints attribute omitted when hints string is empty", () => {
215
+ const twiml = generateTwiML(
216
+ callSessionId,
217
+ relayUrl,
218
+ welcomeGreeting,
219
+ {
220
+ language: "en-US",
221
+ transcriptionProvider: "Deepgram",
222
+ ttsProvider: "ElevenLabs",
223
+ voice: "voice123",
224
+ interruptSensitivity: "low",
225
+ },
226
+ undefined,
227
+ undefined,
228
+ "",
229
+ );
230
+
231
+ expect(twiml).not.toContain("hints=");
232
+ });
233
+
234
+ test("hints attribute omitted when hints parameter is undefined", () => {
235
+ const twiml = generateTwiML(
236
+ callSessionId,
237
+ relayUrl,
238
+ welcomeGreeting,
239
+ {
240
+ language: "en-US",
241
+ transcriptionProvider: "Deepgram",
242
+ ttsProvider: "ElevenLabs",
243
+ voice: "voice123",
244
+ interruptSensitivity: "low",
245
+ },
246
+ undefined,
247
+ undefined,
248
+ undefined,
249
+ );
250
+
251
+ expect(twiml).not.toContain("hints=");
252
+ });
253
+
254
+ test("XML special characters in hints are escaped properly", () => {
255
+ const twiml = generateTwiML(
256
+ callSessionId,
257
+ relayUrl,
258
+ welcomeGreeting,
259
+ {
260
+ language: "en-US",
261
+ transcriptionProvider: "Deepgram",
262
+ ttsProvider: "ElevenLabs",
263
+ voice: "voice123",
264
+ interruptSensitivity: "low",
265
+ },
266
+ undefined,
267
+ undefined,
268
+ 'O\'Brien,Smith & Jones,"Dr. Lee"',
269
+ );
270
+
271
+ expect(twiml).toContain(
272
+ 'hints="O&apos;Brien,Smith &amp; Jones,&quot;Dr. Lee&quot;"',
273
+ );
274
+ expect(twiml).not.toContain("hints=\"O'Brien");
275
+ });
138
276
  });
@@ -124,4 +124,62 @@ describe("resolveVoiceQualityProfile", () => {
124
124
  const profile = resolveVoiceQualityProfile();
125
125
  expect(profile.voice).toBe("voice1-turbo_v2_5-0.9_0.8_0.9");
126
126
  });
127
+
128
+ test("interruptSensitivity defaults to 'low' when not configured", () => {
129
+ mockConfig = {
130
+ elevenlabs: { voiceId: "abc" },
131
+ calls: {
132
+ voice: {
133
+ language: "en-US",
134
+ transcriptionProvider: "Deepgram",
135
+ },
136
+ },
137
+ };
138
+ const profile = resolveVoiceQualityProfile();
139
+ expect(profile.interruptSensitivity).toBe("low");
140
+ });
141
+
142
+ test("interruptSensitivity reflects configured value", () => {
143
+ mockConfig = {
144
+ elevenlabs: { voiceId: "abc" },
145
+ calls: {
146
+ voice: {
147
+ language: "en-US",
148
+ transcriptionProvider: "Deepgram",
149
+ interruptSensitivity: "high",
150
+ },
151
+ },
152
+ };
153
+ const profile = resolveVoiceQualityProfile();
154
+ expect(profile.interruptSensitivity).toBe("high");
155
+ });
156
+
157
+ test("hints defaults to empty array when not configured", () => {
158
+ mockConfig = {
159
+ elevenlabs: { voiceId: "abc" },
160
+ calls: {
161
+ voice: {
162
+ language: "en-US",
163
+ transcriptionProvider: "Deepgram",
164
+ },
165
+ },
166
+ };
167
+ const profile = resolveVoiceQualityProfile();
168
+ expect(profile.hints).toEqual([]);
169
+ });
170
+
171
+ test("hints reflects configured values", () => {
172
+ mockConfig = {
173
+ elevenlabs: { voiceId: "abc" },
174
+ calls: {
175
+ voice: {
176
+ language: "en-US",
177
+ transcriptionProvider: "Deepgram",
178
+ hints: ["Vellum", "Nova", "AI assistant"],
179
+ },
180
+ },
181
+ };
182
+ const profile = resolveVoiceQualityProfile();
183
+ expect(profile.hints).toEqual(["Vellum", "Nova", "AI assistant"]);
184
+ });
127
185
  });
@@ -54,7 +54,6 @@ mock.module("../config/loader.js", () => ({
54
54
  ui: {},
55
55
 
56
56
  provider: "anthropic",
57
- providerOrder: ["anthropic"],
58
57
  calls: {
59
58
  enabled: true,
60
59
  provider: "twilio",
@@ -115,12 +115,14 @@ describe("016-migrate-credentials-from-keychain migration", () => {
115
115
  });
116
116
 
117
117
  test(
118
- "returns silently when broker is not available (does not throw)",
118
+ "throws when broker is not available (skips checkpoint for retry)",
119
119
  async () => {
120
120
  isAvailableFn.mockReturnValue(false);
121
121
 
122
- // Migration 016 returns silently (unlike 015 which throws)
123
- await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
122
+ // Throwing skips the checkpoint so the migration retries on next startup
123
+ await expect(
124
+ migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR),
125
+ ).rejects.toThrow("Keychain broker not available after waiting");
124
126
 
125
127
  // Should not proceed to list or migrate keys
126
128
  expect(brokerListFn).not.toHaveBeenCalled();
@@ -52,7 +52,7 @@ export class AcpAgentProcess {
52
52
 
53
53
  this.proc = spawn(this.config.command, this.config.args, {
54
54
  cwd,
55
- stdio: ["pipe", "pipe", "inherit"],
55
+ stdio: ["pipe", "pipe", "pipe"],
56
56
  env: { ...process.env, ...this.config.env },
57
57
  });
58
58
 
@@ -68,6 +68,14 @@ export class AcpAgentProcess {
68
68
  stream,
69
69
  );
70
70
 
71
+ // Capture stderr so agent crash details appear in logs
72
+ this.proc.stderr?.on("data", (chunk: Buffer) => {
73
+ const text = chunk.toString().trim();
74
+ if (text) {
75
+ log.error({ agentId: this.agentId, stderr: text }, "ACP agent stderr");
76
+ }
77
+ });
78
+
71
79
  // Handle process exit
72
80
  this.proc.on("exit", (code) => {
73
81
  this.handleProcessExit(code);
package/src/agent/loop.ts CHANGED
@@ -23,7 +23,7 @@ export interface AgentLoopConfig {
23
23
  maxTokens: number;
24
24
  maxInputTokens?: number; // context window size for tool result truncation
25
25
  thinking?: { enabled: boolean };
26
- effort: "low" | "medium" | "high";
26
+ effort: "low" | "medium" | "high" | "max";
27
27
  toolChoice?:
28
28
  | { type: "auto" }
29
29
  | { type: "any" }
@@ -40,6 +40,22 @@ const log = getLogger("guardian-request-resolvers");
40
40
  // Helpers
41
41
  // ---------------------------------------------------------------------------
42
42
 
43
+ /**
44
+ * Determines whether a Slack delivery should use ephemeral mode.
45
+ *
46
+ * Ephemeral messages (`chat.postEphemeral`) require a real channel ID
47
+ * (starts with `C` for public/private channels, or `D` for DM conversations).
48
+ * When the chat ID is a user ID (starts with `U`), `chat.postEphemeral` fails
49
+ * with `channel_not_found`. In that case the message is already going to a DM
50
+ * opened by `chat.postMessage`, so ephemeral isn't needed.
51
+ *
52
+ * Returns `true` only when the source channel is Slack AND the chatId is a
53
+ * shared channel (starts with `C`), meaning other users could see the message.
54
+ */
55
+ function shouldUseEphemeral(sourceChannel: string, chatId: string): boolean {
56
+ return sourceChannel === "slack" && chatId.startsWith("C");
57
+ }
58
+
43
59
  // ---------------------------------------------------------------------------
44
60
  // Types
45
61
  // ---------------------------------------------------------------------------
@@ -372,13 +388,22 @@ const accessRequestResolver: GuardianRequestResolver = {
372
388
  // Deliver denial notification and lifecycle signals when channel context is available
373
389
  if (channelDeliveryContext) {
374
390
  try {
391
+ const denialPayload: Parameters<typeof deliverChannelReply>[1] = {
392
+ chatId: requesterChatId,
393
+ text: "Your access request has been denied.",
394
+ assistantId,
395
+ };
396
+ // On Slack shared channels, deliver as ephemeral so only the requester sees the denial
397
+ if (
398
+ shouldUseEphemeral(channel, requesterChatId) &&
399
+ requesterExternalUserId
400
+ ) {
401
+ denialPayload.ephemeral = true;
402
+ denialPayload.user = requesterExternalUserId;
403
+ }
375
404
  await deliverChannelReply(
376
405
  channelDeliveryContext.replyCallbackUrl,
377
- {
378
- chatId: requesterChatId,
379
- text: "Your access request has been denied by the guardian.",
380
- assistantId,
381
- },
406
+ denialPayload,
382
407
  channelDeliveryContext.bearerToken,
383
408
  );
384
409
  } catch (err) {
@@ -435,7 +460,7 @@ const accessRequestResolver: GuardianRequestResolver = {
435
460
  desktopDeliverUrl,
436
461
  {
437
462
  chatId: targetChatId,
438
- text: "Your access request has been denied by the guardian.",
463
+ text: "Your access request has been denied.",
439
464
  assistantId,
440
465
  },
441
466
  desktopBearerToken,
@@ -520,18 +545,28 @@ const accessRequestResolver: GuardianRequestResolver = {
520
545
  let codeDelivered = true;
521
546
 
522
547
  // Deliver verification code to guardian
548
+ const codeText =
549
+ `You approved access for ${requesterExternalUserId}. ` +
550
+ `Give them this verification code: \`${session.secret}\`. ` +
551
+ `The code expires in 10 minutes.`;
523
552
  try {
524
- const codeText =
525
- `You approved access for ${requesterExternalUserId}. ` +
526
- `Give them this verification code: ${session.secret}. ` +
527
- `The code expires in 10 minutes.`;
553
+ const codePayload: Parameters<typeof deliverChannelReply>[1] = {
554
+ chatId: channelDeliveryContext.guardianChatId,
555
+ text: codeText,
556
+ assistantId,
557
+ };
558
+ // On Slack shared channels, deliver the verification code as ephemeral
559
+ // so only the guardian sees the secret — not all channel members.
560
+ if (
561
+ shouldUseEphemeral(channel, channelDeliveryContext.guardianChatId) &&
562
+ ctx.actor.actorExternalUserId
563
+ ) {
564
+ codePayload.ephemeral = true;
565
+ codePayload.user = ctx.actor.actorExternalUserId;
566
+ }
528
567
  await deliverChannelReply(
529
568
  channelDeliveryContext.replyCallbackUrl,
530
- {
531
- chatId: channelDeliveryContext.guardianChatId,
532
- text: codeText,
533
- assistantId,
534
- },
569
+ codePayload,
535
570
  channelDeliveryContext.bearerToken,
536
571
  );
537
572
  } catch (err) {
@@ -542,20 +577,85 @@ const accessRequestResolver: GuardianRequestResolver = {
542
577
  codeDelivered = false;
543
578
  }
544
579
 
545
- // Notify the requester
546
- if (codeDelivered) {
580
+ // If the guardian approved in a shared channel (not a DM), also send
581
+ // them a DM with the verification code for better privacy and
582
+ // discoverability. On Slack, posting to a user ID opens a DM.
583
+ const guardianUserId = ctx.actor.actorExternalUserId;
584
+ if (
585
+ codeDelivered &&
586
+ channel === "slack" &&
587
+ guardianUserId &&
588
+ !channelDeliveryContext.guardianChatId.startsWith("D")
589
+ ) {
590
+ // Strip threadTs from the callback URL — it belongs to the shared
591
+ // channel thread and would cause thread_not_found errors in the DM.
592
+ let dmCallbackUrl = channelDeliveryContext.replyCallbackUrl;
593
+ try {
594
+ const url = new URL(channelDeliveryContext.replyCallbackUrl);
595
+ url.searchParams.delete("threadTs");
596
+ dmCallbackUrl = url.toString();
597
+ } catch {
598
+ // Malformed URL — use as-is
599
+ }
600
+
547
601
  try {
548
602
  await deliverChannelReply(
549
- channelDeliveryContext.replyCallbackUrl,
603
+ dmCallbackUrl,
550
604
  {
551
- chatId: requesterChatId,
552
- text:
553
- "Your access request has been approved! " +
554
- "Please enter the 6-digit verification code you receive from the guardian.",
605
+ chatId: guardianUserId,
606
+ text: codeText,
555
607
  assistantId,
556
608
  },
557
609
  channelDeliveryContext.bearerToken,
558
610
  );
611
+ } catch (err) {
612
+ // Best-effort: the code was already delivered in the shared channel
613
+ log.warn(
614
+ { err, guardianUserId },
615
+ "Failed to send guardian DM confirmation with verification code",
616
+ );
617
+ }
618
+ }
619
+
620
+ // Notify the requester. For Slack, route to DM via the user ID and
621
+ // strip threadTs (which belongs to the guardian's channel thread).
622
+ const requesterTargetChatId =
623
+ channel === "slack" && requesterExternalUserId
624
+ ? requesterExternalUserId
625
+ : requesterChatId;
626
+ let requesterCallbackUrl = channelDeliveryContext.replyCallbackUrl;
627
+ if (channel === "slack" && requesterExternalUserId) {
628
+ try {
629
+ const url = new URL(channelDeliveryContext.replyCallbackUrl);
630
+ url.searchParams.delete("threadTs");
631
+ requesterCallbackUrl = url.toString();
632
+ } catch {
633
+ // Malformed URL — use as-is
634
+ }
635
+ }
636
+
637
+ if (codeDelivered) {
638
+ try {
639
+ const approvalPayload: Parameters<typeof deliverChannelReply>[1] = {
640
+ chatId: requesterTargetChatId,
641
+ text:
642
+ "Your access request has been approved! " +
643
+ "Please enter the 6-digit verification code you receive from the guardian.",
644
+ assistantId,
645
+ };
646
+ // On Slack shared channels, deliver as ephemeral so only the requester sees
647
+ if (
648
+ shouldUseEphemeral(channel, requesterChatId) &&
649
+ requesterExternalUserId
650
+ ) {
651
+ approvalPayload.ephemeral = true;
652
+ approvalPayload.user = requesterExternalUserId;
653
+ }
654
+ await deliverChannelReply(
655
+ requesterCallbackUrl,
656
+ approvalPayload,
657
+ channelDeliveryContext.bearerToken,
658
+ );
559
659
  requesterNotified = true;
560
660
  } catch (err) {
561
661
  log.error(
@@ -565,15 +665,23 @@ const accessRequestResolver: GuardianRequestResolver = {
565
665
  }
566
666
  } else {
567
667
  try {
668
+ const failurePayload: Parameters<typeof deliverChannelReply>[1] = {
669
+ chatId: requesterTargetChatId,
670
+ text:
671
+ "Your access request was approved, but we were unable to " +
672
+ "deliver the verification code. Please try again later.",
673
+ assistantId,
674
+ };
675
+ if (
676
+ shouldUseEphemeral(channel, requesterChatId) &&
677
+ requesterExternalUserId
678
+ ) {
679
+ failurePayload.ephemeral = true;
680
+ failurePayload.user = requesterExternalUserId;
681
+ }
568
682
  await deliverChannelReply(
569
- channelDeliveryContext.replyCallbackUrl,
570
- {
571
- chatId: requesterChatId,
572
- text:
573
- "Your access request was approved, but we were unable to " +
574
- "deliver the verification code. Please try again later.",
575
- assistantId,
576
- },
683
+ requesterCallbackUrl,
684
+ failurePayload,
577
685
  channelDeliveryContext.bearerToken,
578
686
  );
579
687
  } catch (err) {
@@ -635,8 +743,8 @@ const accessRequestResolver: GuardianRequestResolver = {
635
743
  }
636
744
 
637
745
  const verificationReplyText = requesterNotified
638
- ? `Access approved for ${requesterLabel}. Give them this verification code: ${session.secret}. The code expires in 10 minutes.`
639
- : `Access approved for ${requesterLabel}. Give them this verification code: ${session.secret}. The code expires in 10 minutes. I could not notify them automatically, so please tell them to send the code manually.`;
746
+ ? `Access approved for ${requesterLabel}. Give them this verification code: \`${session.secret}\`. The code expires in 10 minutes.`
747
+ : `Access approved for ${requesterLabel}. Give them this verification code: \`${session.secret}\`. The code expires in 10 minutes. I could not notify them automatically, so please tell them to send the code manually.`;
640
748
 
641
749
  return {
642
750
  ok: true,
@@ -686,13 +794,22 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
686
794
 
687
795
  if (channelDeliveryContext && requesterChatId) {
688
796
  try {
689
- await deliverChannelReply(
690
- channelDeliveryContext.replyCallbackUrl,
797
+ const grantDenialPayload: Parameters<typeof deliverChannelReply>[1] =
691
798
  {
692
799
  chatId: requesterChatId,
693
800
  text: `Your request to use "${request.toolName}" has been denied by the guardian.`,
694
801
  assistantId,
695
- },
802
+ };
803
+ if (
804
+ shouldUseEphemeral(request.sourceChannel ?? "", requesterChatId) &&
805
+ request.requesterExternalUserId
806
+ ) {
807
+ grantDenialPayload.ephemeral = true;
808
+ grantDenialPayload.user = request.requesterExternalUserId;
809
+ }
810
+ await deliverChannelReply(
811
+ channelDeliveryContext.replyCallbackUrl,
812
+ grantDenialPayload,
696
813
  channelDeliveryContext.bearerToken,
697
814
  );
698
815
  } catch (err) {
@@ -775,13 +892,22 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
775
892
  );
776
893
  } else if (channelDeliveryContext && requesterChatId) {
777
894
  try {
778
- await deliverChannelReply(
779
- channelDeliveryContext.replyCallbackUrl,
895
+ const grantApprovalPayload: Parameters<typeof deliverChannelReply>[1] =
780
896
  {
781
897
  chatId: requesterChatId,
782
898
  text: `Your request to use "${request.toolName}" has been approved. Please retry your request.`,
783
899
  assistantId,
784
- },
900
+ };
901
+ if (
902
+ shouldUseEphemeral(request.sourceChannel ?? "", requesterChatId) &&
903
+ request.requesterExternalUserId
904
+ ) {
905
+ grantApprovalPayload.ephemeral = true;
906
+ grantApprovalPayload.user = request.requesterExternalUserId;
907
+ }
908
+ await deliverChannelReply(
909
+ channelDeliveryContext.replyCallbackUrl,
910
+ grantApprovalPayload,
785
911
  channelDeliveryContext.bearerToken,
786
912
  );
787
913
  } catch (err) {