@vellumai/assistant 0.5.7 → 0.5.8

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 (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
@@ -20,7 +20,10 @@ import { initializeProviders } from "../../providers/registry.js";
20
20
  import { credentialKey } from "../../security/credential-key.js";
21
21
  import {
22
22
  deleteSecureKeyAsync,
23
+ getActiveBackendName,
23
24
  getSecureKeyAsync,
25
+ getSecureKeyResultAsync,
26
+ listSecureKeysAsync,
24
27
  setSecureKeyAsync,
25
28
  } from "../../security/secure-keys.js";
26
29
  import {
@@ -103,11 +106,16 @@ export async function handleAddSecret(
103
106
  req: Request,
104
107
  getCesClient?: () => CesClient | undefined,
105
108
  ): Promise<Response> {
106
- const body = (await req.json()) as {
107
- type?: string;
108
- name?: string;
109
- value?: string;
110
- };
109
+ let body: { type?: string; name?: string; value?: string };
110
+ try {
111
+ body = (await req.json()) as {
112
+ type?: string;
113
+ name?: string;
114
+ value?: string;
115
+ };
116
+ } catch {
117
+ return httpError("BAD_REQUEST", "Request body must be valid JSON", 400);
118
+ }
111
119
 
112
120
  const { type, name, value } = body;
113
121
 
@@ -178,7 +186,7 @@ export async function handleAddSecret(
178
186
  if (!stored) {
179
187
  return httpError(
180
188
  "INTERNAL_ERROR",
181
- "Failed to store API key in secure storage",
189
+ `Failed to store API key in secure storage (backend: ${getActiveBackendName()})`,
182
190
  500,
183
191
  );
184
192
  }
@@ -190,7 +198,7 @@ export async function handleAddSecret(
190
198
  }
191
199
 
192
200
  if (type === "credential") {
193
- const colonIdx = name.indexOf(":");
201
+ const colonIdx = name.lastIndexOf(":");
194
202
  if (colonIdx < 1 || colonIdx === name.length - 1) {
195
203
  return httpError(
196
204
  "BAD_REQUEST",
@@ -243,7 +251,7 @@ export async function handleAddSecret(
243
251
  if (!stored) {
244
252
  return httpError(
245
253
  "INTERNAL_ERROR",
246
- "Failed to store credential in secure storage",
254
+ `Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
247
255
  500,
248
256
  );
249
257
  }
@@ -308,12 +316,90 @@ export async function handleAddSecret(
308
316
  }
309
317
  }
310
318
 
311
- export async function handleDeleteSecret(req: Request): Promise<Response> {
319
+ export async function handleReadSecret(req: Request): Promise<Response> {
312
320
  const body = (await req.json()) as {
313
321
  type?: string;
314
322
  name?: string;
323
+ reveal?: boolean;
315
324
  };
316
325
 
326
+ const { type, name, reveal } = body;
327
+
328
+ if (!type || typeof type !== "string") {
329
+ return httpError("BAD_REQUEST", "type is required", 400);
330
+ }
331
+ if (!name || typeof name !== "string") {
332
+ return httpError("BAD_REQUEST", "name is required", 400);
333
+ }
334
+
335
+ try {
336
+ let accountKey: string;
337
+
338
+ if (type === "api_key") {
339
+ if (
340
+ !API_KEY_PROVIDERS.includes(name as (typeof API_KEY_PROVIDERS)[number])
341
+ ) {
342
+ return httpError(
343
+ "BAD_REQUEST",
344
+ `Unknown API key provider: ${name}. Valid providers: ${API_KEY_PROVIDERS.join(
345
+ ", ",
346
+ )}`,
347
+ 400,
348
+ );
349
+ }
350
+ accountKey = name;
351
+ } else if (type === "credential") {
352
+ const colonIdx = name.lastIndexOf(":");
353
+ if (colonIdx < 1 || colonIdx === name.length - 1) {
354
+ return httpError(
355
+ "BAD_REQUEST",
356
+ 'For credential type, name must be in "service:field" format (e.g. "github:api_token")',
357
+ 400,
358
+ );
359
+ }
360
+ const service = name.slice(0, colonIdx);
361
+ const field = name.slice(colonIdx + 1);
362
+ accountKey = credentialKey(service, field);
363
+ } else {
364
+ return httpError(
365
+ "BAD_REQUEST",
366
+ `Unknown secret type: ${type}. Valid types: api_key, credential`,
367
+ 400,
368
+ );
369
+ }
370
+
371
+ const { value, unreachable } = await getSecureKeyResultAsync(accountKey);
372
+ if (value === undefined) {
373
+ return Response.json({ found: false, unreachable });
374
+ }
375
+
376
+ if (reveal) {
377
+ return Response.json({ found: true, value, unreachable: false });
378
+ }
379
+
380
+ // Mask the value: show first 10 chars and last 4, hiding at least 3
381
+ const minHidden = 3;
382
+ const maxVisible = Math.max(1, value.length - minHidden);
383
+ const prefixLen = Math.min(10, maxVisible);
384
+ const suffixLen = Math.min(4, Math.max(0, maxVisible - prefixLen));
385
+ const masked = `${value.slice(0, prefixLen)}...${suffixLen > 0 ? value.slice(-suffixLen) : ""}`;
386
+
387
+ return Response.json({ found: true, masked, unreachable: false });
388
+ } catch (err) {
389
+ const message = err instanceof Error ? err.message : String(err);
390
+ log.error({ err, type, name }, "Failed to read secret via HTTP");
391
+ return httpError("INTERNAL_ERROR", message, 500);
392
+ }
393
+ }
394
+
395
+ export async function handleDeleteSecret(req: Request): Promise<Response> {
396
+ let body: { type?: string; name?: string };
397
+ try {
398
+ body = (await req.json()) as { type?: string; name?: string };
399
+ } catch {
400
+ return httpError("BAD_REQUEST", "Request body must be valid JSON", 400);
401
+ }
402
+
317
403
  const { type, name } = body;
318
404
 
319
405
  if (!type || typeof type !== "string") {
@@ -358,7 +444,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
358
444
  }
359
445
 
360
446
  if (type === "credential") {
361
- const colonIdx = name.indexOf(":");
447
+ const colonIdx = name.lastIndexOf(":");
362
448
  if (colonIdx < 1 || colonIdx === name.length - 1) {
363
449
  return httpError(
364
450
  "BAD_REQUEST",
@@ -418,6 +504,41 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
418
504
  }
419
505
  }
420
506
 
507
+ const CREDENTIAL_KEY_PREFIX = "credential/";
508
+
509
+ export async function handleListSecrets(): Promise<Response> {
510
+ try {
511
+ const { accounts, unreachable } = await listSecureKeysAsync();
512
+ if (unreachable) {
513
+ return Response.json(
514
+ { error: "Credential store is unreachable" },
515
+ { status: 503 },
516
+ );
517
+ }
518
+
519
+ const secrets = accounts.map((account) => {
520
+ if (account.startsWith(CREDENTIAL_KEY_PREFIX)) {
521
+ // credential/{service}/{field} → service:field
522
+ const rest = account.slice(CREDENTIAL_KEY_PREFIX.length);
523
+ const slashIdx = rest.indexOf("/");
524
+ if (slashIdx > 0 && slashIdx < rest.length - 1) {
525
+ return {
526
+ type: "credential" as const,
527
+ name: `${rest.slice(0, slashIdx)}:${rest.slice(slashIdx + 1)}`,
528
+ };
529
+ }
530
+ }
531
+ // API key providers are stored with their raw provider name
532
+ return { type: "api_key" as const, name: account };
533
+ });
534
+
535
+ return Response.json({ secrets });
536
+ } catch (err) {
537
+ const message = err instanceof Error ? err.message : String(err);
538
+ return httpError("INTERNAL_ERROR", message, 500);
539
+ }
540
+ }
541
+
421
542
  // ---------------------------------------------------------------------------
422
543
  // Route definitions
423
544
  // ---------------------------------------------------------------------------
@@ -441,5 +562,15 @@ export function secretRouteDefinitions(
441
562
  method: "DELETE",
442
563
  handler: async ({ req }) => handleDeleteSecret(req),
443
564
  },
565
+ {
566
+ endpoint: "secrets",
567
+ method: "GET",
568
+ handler: async () => handleListSecrets(),
569
+ },
570
+ {
571
+ endpoint: "secrets/read",
572
+ method: "POST",
573
+ handler: async ({ req }) => handleReadSecret(req),
574
+ },
444
575
  ];
445
576
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { synthesizeWithFishAudio } from "../../calls/fish-audio-client.js";
11
+ import { sanitizeForTts } from "../../calls/tts-text-sanitizer.js";
11
12
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
12
13
  import { getConfig } from "../../config/loader.js";
13
14
  import { getMessageContent } from "../../daemon/handlers/conversation-history.js";
@@ -49,6 +50,15 @@ export function ttsRouteDefinitions(): RouteDefinition[] {
49
50
  return httpError("BAD_REQUEST", "Message has no text content", 400);
50
51
  }
51
52
 
53
+ const sanitizedText = sanitizeForTts(result.text);
54
+ if (!sanitizedText.trim()) {
55
+ return httpError(
56
+ "BAD_REQUEST",
57
+ "Message has no speakable text content",
58
+ 400,
59
+ );
60
+ }
61
+
52
62
  const { fishAudio } = config;
53
63
  if (!fishAudio?.referenceId) {
54
64
  return httpError(
@@ -60,7 +70,7 @@ export function ttsRouteDefinitions(): RouteDefinition[] {
60
70
 
61
71
  try {
62
72
  const audioBuffer = await synthesizeWithFishAudio(
63
- result.text,
73
+ sanitizedText,
64
74
  fishAudio,
65
75
  );
66
76
 
@@ -19,6 +19,7 @@ import { getLogger } from "../util/logger.js";
19
19
  import type {
20
20
  CredentialBackend,
21
21
  CredentialGetResult,
22
+ CredentialListResult,
22
23
  DeleteResult,
23
24
  } from "./credential-backend.js";
24
25
 
@@ -51,7 +52,13 @@ async function cesRequest(
51
52
  ): Promise<Response | null> {
52
53
  const baseUrl = getBaseUrl();
53
54
  const token = getServiceToken();
54
- if (!baseUrl || !token) return null;
55
+ if (!baseUrl || !token) {
56
+ log.warn(
57
+ { method, path, hasBaseUrl: !!baseUrl, hasToken: !!token },
58
+ "CES credential request skipped — missing config",
59
+ );
60
+ return null;
61
+ }
55
62
 
56
63
  const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
57
64
  const headers: Record<string, string> = {
@@ -128,16 +135,17 @@ export class CesCredentialBackend implements CredentialBackend {
128
135
  return false;
129
136
  }
130
137
  if (!res.ok) {
138
+ const detail = await res.text().catch(() => "");
131
139
  if (attempt < SET_MAX_RETRIES - 1) {
132
140
  log.warn(
133
- { account, status: res.status, attempt },
141
+ { account, status: res.status, detail, attempt },
134
142
  "CES credential set returned non-OK status, retrying",
135
143
  );
136
144
  await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
137
145
  continue;
138
146
  }
139
147
  log.warn(
140
- { account, status: res.status },
148
+ { account, status: res.status, detail },
141
149
  "CES credential set returned non-OK status",
142
150
  );
143
151
  return false;
@@ -168,8 +176,9 @@ export class CesCredentialBackend implements CredentialBackend {
168
176
  if (!res) return "error";
169
177
  if (res.status === 404) return "not-found";
170
178
  if (!res.ok) {
179
+ const detail = await res.text().catch(() => "");
171
180
  log.warn(
172
- { account, status: res.status },
181
+ { account, status: res.status, detail },
173
182
  "CES credential delete returned non-OK status",
174
183
  );
175
184
  return "error";
@@ -181,22 +190,22 @@ export class CesCredentialBackend implements CredentialBackend {
181
190
  }
182
191
  }
183
192
 
184
- async list(): Promise<string[]> {
193
+ async list(): Promise<CredentialListResult> {
185
194
  try {
186
195
  const res = await cesRequest("GET", "/v1/credentials");
187
- if (!res) return [];
196
+ if (!res) return { accounts: [], unreachable: true };
188
197
  if (!res.ok) {
189
198
  log.warn(
190
199
  { status: res.status },
191
200
  "CES credential list returned non-OK status",
192
201
  );
193
- return [];
202
+ return { accounts: [], unreachable: true };
194
203
  }
195
204
  const data = (await res.json()) as { accounts?: string[] };
196
- return data.accounts ?? [];
205
+ return { accounts: data.accounts ?? [], unreachable: false };
197
206
  } catch (err) {
198
207
  log.warn({ err }, "CES credential list threw unexpectedly");
199
- return [];
208
+ return { accounts: [], unreachable: true };
200
209
  }
201
210
  }
202
211
  }
@@ -14,6 +14,7 @@ import { getLogger } from "../util/logger.js";
14
14
  import type {
15
15
  CredentialBackend,
16
16
  CredentialGetResult,
17
+ CredentialListResult,
17
18
  DeleteResult,
18
19
  } from "./credential-backend.js";
19
20
 
@@ -70,16 +71,16 @@ export class CesRpcCredentialBackend implements CredentialBackend {
70
71
  }
71
72
  }
72
73
 
73
- async list(): Promise<string[]> {
74
+ async list(): Promise<CredentialListResult> {
74
75
  try {
75
76
  const result = await this.client.call(
76
77
  CesRpcMethod.ListCredentials,
77
78
  {},
78
79
  );
79
- return result.accounts;
80
+ return { accounts: result.accounts, unreachable: false };
80
81
  } catch (err) {
81
82
  log.warn({ err }, "CES RPC credential list failed");
82
- return [];
83
+ return { accounts: [], unreachable: true };
83
84
  }
84
85
  }
85
86
  }
@@ -19,6 +19,12 @@ export interface CredentialGetResult {
19
19
  unreachable: boolean;
20
20
  }
21
21
 
22
+ /** Result of a list operation — distinguishes unreachable from empty. */
23
+ export interface CredentialListResult {
24
+ accounts: string[];
25
+ unreachable: boolean;
26
+ }
27
+
22
28
  // ---------------------------------------------------------------------------
23
29
  // Interface
24
30
  // ---------------------------------------------------------------------------
@@ -40,7 +46,7 @@ export interface CredentialBackend {
40
46
  delete(account: string): Promise<DeleteResult>;
41
47
 
42
48
  /** List all account names. */
43
- list(): Promise<string[]>;
49
+ list(): Promise<CredentialListResult>;
44
50
  }
45
51
 
46
52
  // ---------------------------------------------------------------------------
@@ -78,11 +84,11 @@ export class EncryptedStoreBackend implements CredentialBackend {
78
84
  }
79
85
  }
80
86
 
81
- async list(): Promise<string[]> {
87
+ async list(): Promise<CredentialListResult> {
82
88
  try {
83
- return encryptedStore.listKeys();
89
+ return { accounts: encryptedStore.listKeys(), unreachable: false };
84
90
  } catch {
85
- return [];
91
+ return { accounts: [], unreachable: true };
86
92
  }
87
93
  }
88
94
  }
@@ -25,10 +25,17 @@ import type { CesClient } from "../credential-execution/client.js";
25
25
  import { getLogger } from "../util/logger.js";
26
26
  import { createCesCredentialBackend } from "./ces-credential-client.js";
27
27
  import { CesRpcCredentialBackend } from "./ces-rpc-credential-backend.js";
28
- import type { CredentialBackend, DeleteResult } from "./credential-backend.js";
28
+ import type {
29
+ CredentialBackend,
30
+ CredentialListResult,
31
+ DeleteResult,
32
+ } from "./credential-backend.js";
29
33
  import { createEncryptedStoreBackend } from "./credential-backend.js";
30
34
 
31
- export type { DeleteResult } from "./credential-backend.js";
35
+ export type {
36
+ CredentialListResult,
37
+ DeleteResult,
38
+ } from "./credential-backend.js";
32
39
 
33
40
  /**
34
41
  * Re-export shared-package secure-key abstractions so downstream consumers
@@ -89,7 +96,9 @@ async function doResolveBackend(): Promise<CredentialBackend> {
89
96
  _resolvedBackend = cesRpc;
90
97
  return cesRpc;
91
98
  }
92
- log.warn("CES RPC client is set but not ready — falling back to local credential store");
99
+ log.warn(
100
+ "CES RPC client is set but not ready — falling back to local credential store",
101
+ );
93
102
  }
94
103
 
95
104
  // 2. CES HTTP — containerized / Docker / managed mode
@@ -115,7 +124,7 @@ async function doResolveBackend(): Promise<CredentialBackend> {
115
124
  *
116
125
  * Queries exactly one backend — no cross-store merge.
117
126
  */
118
- export async function listSecureKeysAsync(): Promise<string[]> {
127
+ export async function listSecureKeysAsync(): Promise<CredentialListResult> {
119
128
  const backend = await resolveBackendAsync();
120
129
  return backend.list();
121
130
  }
@@ -241,6 +250,14 @@ export async function getMaskedProviderKey(
241
250
  // Test helpers
242
251
  // ---------------------------------------------------------------------------
243
252
 
253
+ /**
254
+ * Return the name of the currently resolved credential backend.
255
+ * Useful for diagnostic messages when credential operations fail.
256
+ */
257
+ export function getActiveBackendName(): string {
258
+ return _resolvedBackend?.name ?? "none";
259
+ }
260
+
244
261
  /** @internal Test-only: reset the cached backends so they're re-created. */
245
262
  export function _resetBackend(): void {
246
263
  _cesClient = undefined;
@@ -13,12 +13,9 @@ import { homedir } from "node:os";
13
13
  import { dirname, join, posix, resolve, sep } from "node:path";
14
14
  import { gunzipSync } from "node:zlib";
15
15
 
16
+ import { getPlatformBaseUrl } from "../config/env.js";
16
17
  import { getLogger } from "../util/logger.js";
17
- import {
18
- getWorkspaceConfigPath,
19
- getWorkspaceSkillsDir,
20
- readPlatformToken,
21
- } from "../util/platform.js";
18
+ import { getWorkspaceSkillsDir, readPlatformToken } from "../util/platform.js";
22
19
  import { deleteSkillCapabilityMemory } from "./skill-memory.js";
23
20
 
24
21
  const log = getLogger("catalog-install");
@@ -70,27 +67,6 @@ export function getRepoSkillsDir(): string | undefined {
70
67
 
71
68
  // ─── Platform API ────────────────────────────────────────────────────────────
72
69
 
73
- function getConfigPlatformUrl(): string | undefined {
74
- try {
75
- const configPath = getWorkspaceConfigPath();
76
- if (!existsSync(configPath)) return undefined;
77
- const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
78
- string,
79
- unknown
80
- >;
81
- const platform = raw.platform as Record<string, unknown> | undefined;
82
- const baseUrl = platform?.baseUrl;
83
- if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
84
- } catch {
85
- // ignore
86
- }
87
- return undefined;
88
- }
89
-
90
- function getPlatformUrl(): string {
91
- return process.env.VELLUM_PLATFORM_URL ?? getConfigPlatformUrl() ?? "";
92
- }
93
-
94
70
  function buildHeaders(): Record<string, string> {
95
71
  const headers: Record<string, string> = {};
96
72
  const token = readPlatformToken();
@@ -103,10 +79,7 @@ function buildHeaders(): Record<string, string> {
103
79
  // ─── Catalog operations ──────────────────────────────────────────────────────
104
80
 
105
81
  export async function fetchCatalog(): Promise<CatalogSkill[]> {
106
- const platformUrl = getPlatformUrl();
107
- if (!platformUrl) {
108
- return [];
109
- }
82
+ const platformUrl = getPlatformBaseUrl();
110
83
  const url = `${platformUrl}/v1/skills/`;
111
84
  const response = await fetch(url, {
112
85
  headers: buildHeaders(),
@@ -214,12 +187,7 @@ export async function fetchAndExtractSkill(
214
187
  skillId: string,
215
188
  destDir: string,
216
189
  ): Promise<void> {
217
- const platformUrl = getPlatformUrl();
218
- if (!platformUrl) {
219
- throw new Error(
220
- `Cannot fetch skill "${skillId}": VELLUM_PLATFORM_URL is not configured.`,
221
- );
222
- }
190
+ const platformUrl = getPlatformBaseUrl();
223
191
  const url = `${platformUrl}/v1/skills/${encodeURIComponent(skillId)}/`;
224
192
  const response = await fetch(url, {
225
193
  headers: buildHeaders(),
@@ -123,6 +123,7 @@ export function upsertSkillCapabilityMemory(
123
123
  confidence,
124
124
  importance,
125
125
  fingerprint,
126
+ sourceType: "extraction",
126
127
  scopeId,
127
128
  firstSeenAt: now,
128
129
  lastSeenAt: now,
@@ -18,7 +18,7 @@ import {
18
18
  import type { ServerMessage } from "../daemon/message-protocol.js";
19
19
  import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
20
20
  import { RateLimitProvider } from "../providers/ratelimit.js";
21
- import { getFailoverProvider } from "../providers/registry.js";
21
+ import { getProvider } from "../providers/registry.js";
22
22
  import { getLogger } from "../util/logger.js";
23
23
  import { getSandboxWorkingDir } from "../util/platform.js";
24
24
  import {
@@ -129,10 +129,7 @@ export class SubagentManager {
129
129
 
130
130
  // ── Build conversation dependencies ─────────────────────────────
131
131
  const appConfig = getConfig();
132
- let provider = getFailoverProvider(
133
- appConfig.services.inference.provider,
134
- appConfig.providerOrder,
135
- );
132
+ let provider = getProvider(appConfig.services.inference.provider);
136
133
  const { rateLimit } = appConfig;
137
134
  if (rateLimit.maxRequestsPerMinute > 0) {
138
135
  provider = new RateLimitProvider(
@@ -1,7 +1,68 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
1
4
  import { getAcpSessionManager } from "../../acp/index.js";
2
5
  import { getConfig } from "../../config/loader.js";
6
+ import { getLogger } from "../../util/logger.js";
3
7
  import type { ToolContext, ToolExecutionResult } from "../types.js";
4
8
 
9
+ const execFileAsync = promisify(execFile);
10
+ const log = getLogger("acp:spawn");
11
+
12
+ /** Cache so we only check once per process lifetime. */
13
+ let adapterVersionChecked = false;
14
+
15
+ interface AdapterVersionInfo {
16
+ outdated: true;
17
+ installed: string;
18
+ latest: string;
19
+ }
20
+
21
+ /**
22
+ * Checks if the globally-installed claude-agent-acp adapter is outdated.
23
+ * Runs at most once per process lifetime. Does NOT auto-update — returns
24
+ * version info so the caller can ask the user first.
25
+ */
26
+ async function checkAdapterVersion(
27
+ command: string,
28
+ ): Promise<AdapterVersionInfo | null> {
29
+ if (adapterVersionChecked || command !== "claude-agent-acp") {
30
+ return null;
31
+ }
32
+
33
+ try {
34
+ const { stdout: installedRaw } = await execFileAsync("npm", [
35
+ "ls",
36
+ "-g",
37
+ "--json",
38
+ "@zed-industries/claude-agent-acp",
39
+ ]);
40
+ const { stdout: latestRaw } = await execFileAsync("npm", [
41
+ "view",
42
+ "@zed-industries/claude-agent-acp",
43
+ "version",
44
+ ]);
45
+
46
+ const installed =
47
+ JSON.parse(installedRaw)?.dependencies?.[
48
+ "@zed-industries/claude-agent-acp"
49
+ ]?.version;
50
+ const latest = latestRaw.trim();
51
+
52
+ adapterVersionChecked = true;
53
+
54
+ if (!installed || !latest || installed === latest) {
55
+ return null;
56
+ }
57
+
58
+ log.info({ installed, latest }, "claude-agent-acp is outdated");
59
+ return { outdated: true, installed, latest };
60
+ } catch (err) {
61
+ log.warn({ err }, "Failed to check claude-agent-acp version");
62
+ return null;
63
+ }
64
+ }
65
+
5
66
  export async function executeAcpSpawn(
6
67
  input: Record<string, unknown>,
7
68
  context: ToolContext,
@@ -37,6 +98,17 @@ export async function executeAcpSpawn(
37
98
  };
38
99
  }
39
100
 
101
+ // Check if the ACP adapter is outdated before spawning
102
+ const versionInfo = await checkAdapterVersion(agentConfig.command);
103
+ if (versionInfo) {
104
+ return {
105
+ content:
106
+ `claude-agent-acp is outdated (installed: ${versionInfo.installed}, latest: ${versionInfo.latest}). ` +
107
+ `Ask the user if they'd like to update. If yes, run: npm install -g @zed-industries/claude-agent-acp@${versionInfo.latest} — then retry acp_spawn.`,
108
+ isError: true,
109
+ };
110
+ }
111
+
40
112
  try {
41
113
  const manager = getAcpSessionManager();
42
114
  const cwd = (input.cwd as string) || context.workingDir;
@@ -64,7 +136,12 @@ export async function executeAcpSpawn(
64
136
  isError: false,
65
137
  };
66
138
  } catch (err) {
67
- const msg = err instanceof Error ? err.message : String(err);
139
+ const msg =
140
+ err instanceof Error
141
+ ? err.message
142
+ : typeof err === "object" && err !== undefined
143
+ ? JSON.stringify(err)
144
+ : String(err);
68
145
  return { content: `Failed to spawn ACP agent: ${msg}`, isError: true };
69
146
  }
70
147
  }
@@ -400,8 +400,9 @@ class CredentialStoreTool implements Tool {
400
400
  const credIdSuffix = metadata
401
401
  ? ` (credential_id: ${metadata.credentialId})`
402
402
  : "";
403
+ const retrieveHint = ` Retrieve with: \`assistant credentials reveal --service ${service} --field ${field}\``;
403
404
  return {
404
- content: `Stored credential for ${service}/${field}.${credIdSuffix}${
405
+ content: `Stored credential for ${service}/${field}.${credIdSuffix}${retrieveHint}${
405
406
  slackChannelResult
406
407
  ? formatSlackChannelStatus(slackChannelResult)
407
408
  : ""
@@ -428,7 +429,7 @@ class CredentialStoreTool implements Tool {
428
429
  // (e.g. keychain) and the encrypted store (legacy keys).
429
430
  let secureKeySet: Set<string> | undefined;
430
431
  try {
431
- secureKeySet = new Set(await listSecureKeysAsync());
432
+ secureKeySet = new Set((await listSecureKeysAsync()).accounts);
432
433
  } catch (err) {
433
434
  log.error(
434
435
  { err },
@@ -810,8 +811,9 @@ class CredentialStoreTool implements Tool {
810
811
  const promptCredIdSuffix = promptMeta
811
812
  ? ` (credential_id: ${promptMeta.credentialId})`
812
813
  : "";
814
+ const promptRetrieveHint = ` Retrieve with: \`assistant credentials reveal --service ${service} --field ${field}\``;
813
815
  return {
814
- content: `Credential stored for ${service}/${field}.${promptCredIdSuffix}${
816
+ content: `Credential stored for ${service}/${field}.${promptCredIdSuffix}${promptRetrieveHint}${
815
817
  slackChannelResult
816
818
  ? formatSlackChannelStatus(slackChannelResult)
817
819
  : ""