@vellumai/assistant 0.12.2 → 0.12.3-staging.1

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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -27,7 +27,10 @@ import {
27
27
  type InstalledPluginInfo,
28
28
  readInstalledPlugin,
29
29
  } from "./list-installed-plugins.js";
30
- import { readBundledLocalPluginCatalog } from "./plugin-catalog-local.js";
30
+ import {
31
+ arePlatformFeaturesEnabled,
32
+ readBundledLocalPluginCatalog,
33
+ } from "./plugin-catalog-local.js";
31
34
  import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
32
35
  import {
33
36
  compareFingerprint,
@@ -338,17 +341,21 @@ export async function inspectPlugin(
338
341
  let remote: PluginRemoteInfo | null = null;
339
342
  let remoteError: string | null = null;
340
343
  const installedLocalSource = local?.source?.kind === "local";
344
+ const useBundledLocalCatalog =
345
+ installedLocalSource || (!installed && !arePlatformFeaturesEnabled());
341
346
  try {
342
- if (installedLocalSource) {
347
+ if (useBundledLocalCatalog) {
343
348
  const catalog = deps.localCatalog ?? readBundledLocalPluginCatalog();
344
349
  const match = catalog.matches.find(
345
350
  (candidate) =>
346
351
  candidate.name === name &&
347
352
  candidate.source.kind === "local" &&
348
- candidate.source.path === local.source?.path,
353
+ (!installedLocalSource ||
354
+ candidate.source.path === local.source?.path),
349
355
  );
350
356
  remote = match ? readLocalPackageRemote(match, catalog.ref) : null;
351
- } else {
357
+ }
358
+ if (!remote && !installedLocalSource) {
352
359
  const entries = await fetchMarketplaceEntries(
353
360
  { fetch: deps.fetch },
354
361
  { ref: marketplaceRef },
@@ -412,6 +412,24 @@ export async function upgradePlugin(
412
412
  };
413
413
  }
414
414
 
415
+ const usesMergeStrategy =
416
+ strategy === "ours" || strategy === "theirs" || strategy === "assistant";
417
+ const canOverwriteCleanBundledInstall =
418
+ remote.kind === "local" &&
419
+ strategy === "theirs" &&
420
+ local.localChanges?.clean === true;
421
+
422
+ if (
423
+ remote.kind === "local" &&
424
+ usesMergeStrategy &&
425
+ !canOverwriteCleanBundledInstall
426
+ ) {
427
+ throw new PluginMergeBaselineError(
428
+ name,
429
+ "bundled packages do not retain the previous package version needed for a three-way merge",
430
+ );
431
+ }
432
+
415
433
  if (dryRun) {
416
434
  return {
417
435
  name,
@@ -433,17 +451,7 @@ export async function upgradePlugin(
433
451
  // `ours`/`theirs`/`assistant` carry local edits forward via a three-way
434
452
  // merge; the default `overwrite` discards them and re-installs the pin
435
453
  // wholesale.
436
- if (
437
- strategy === "ours" ||
438
- strategy === "theirs" ||
439
- strategy === "assistant"
440
- ) {
441
- if (remote.kind === "local") {
442
- throw new PluginMergeBaselineError(
443
- name,
444
- "bundled packages do not retain the previous package version needed for a three-way merge",
445
- );
446
- }
454
+ if (usesMergeStrategy && remote.kind !== "local") {
447
455
  const [remoteOwner, remoteRepo] = remote.repo.split("/");
448
456
  return mergeUpgrade(
449
457
  {
package/src/cli/output.ts CHANGED
@@ -11,20 +11,34 @@ export function shouldOutputJson(cmd: Command): boolean {
11
11
  return false;
12
12
  }
13
13
 
14
- export function writeOutput(cmd: Command, payload: unknown): void {
14
+ /**
15
+ * Write a JSON payload to stdout. `onFlushed` runs once the bytes have left
16
+ * the process, for a command that must exit itself and would otherwise cut
17
+ * a piped payload short.
18
+ */
19
+ export function writeOutput(
20
+ cmd: Command,
21
+ payload: unknown,
22
+ onFlushed?: () => void,
23
+ ): void {
15
24
  const compact = shouldOutputJson(cmd);
16
25
  process.stdout.write(
17
26
  compact
18
27
  ? JSON.stringify(payload) + "\n"
19
28
  : JSON.stringify(payload, null, 2) + "\n",
29
+ onFlushed,
20
30
  );
21
31
  }
22
32
 
23
33
  /** Format-aware error output: JSON envelope with --json, stderr otherwise. */
24
- export function writeError(cmd: Command, message: string): void {
34
+ export function writeError(
35
+ cmd: Command,
36
+ message: string,
37
+ onFlushed?: () => void,
38
+ ): void {
25
39
  if (shouldOutputJson(cmd)) {
26
- writeOutput(cmd, { ok: false, error: message });
40
+ writeOutput(cmd, { ok: false, error: message }, onFlushed);
27
41
  } else {
28
- process.stderr.write(`Error: ${message}\n`);
42
+ process.stderr.write(`Error: ${message}\n`, onFlushed);
29
43
  }
30
44
  }
@@ -72,6 +72,10 @@ describe("LLMSchema.defaultProvider", () => {
72
72
  ).toThrow();
73
73
  });
74
74
 
75
+ test("rejects a structured-decision catalog provider", () => {
76
+ expect(() => DefaultProviderSchema.parse({ provider: "jev" })).toThrow();
77
+ });
78
+
75
79
  test("rejects an empty connectionName", () => {
76
80
  expect(() =>
77
81
  DefaultProviderSchema.parse({
@@ -249,18 +249,18 @@ If any required capability is missing:
249
249
 
250
250
  Scheduled messages run without user interaction, in a conversation nobody has open. If the task produces output the user should see (a digest, summary, report, or a check whose answer is "nothing changed"), the scheduled message **must** end with an explicit instruction to deliver it. Without one, the output lives in a conversation log the user never opens.
251
251
 
252
- Write the delivery step into the `message` when you create the schedule — not as a vague "let me know", but as the actual call, with a real title:
252
+ Write the delivery step into the `message` when you create the schedule, not as a vague "let me know", but as the actual call, with a real title:
253
253
 
254
- > "…then send the summary with `assistant notifications send --title \"Inbox digest\" --message \"<the summary>\"`."
254
+ > "…then send the summary with `assistant notifications send --source-channel scheduler --title \"Inbox digest\" --message \"<the summary>\"`."
255
255
 
256
256
  A schedule whose message has no delivery step is not finished. Before calling `schedule_create` in `execute` mode, read your own message back and check that it says where the output goes.
257
257
 
258
- There is a safety net, and it is not a substitute for the above. When an execute-mode run finishes with user-facing output and nothing delivered it, the assistant sends a notification carrying the run's final reply, so a schedule can no longer run and leave no trace. `messaging_send` counts as a delivery only when the call reported success, and `assistant notifications send` once the notification pipeline has taken it: a send the channel refused, or one that never reported at all, leaves the run eligible for the net, so the failure reaches you instead of vanishing into a conversation nobody has open. A Slack `chat.postMessage` never counts, because Slack refuses a call with a successful HTTP status and `ok: false` in the body, so the command succeeding proves nothing; a run that posts Block Kit that way also gets the net's notification. It fires on the raw reply, whatever the run happened to end on, at whatever length. An authored delivery step gets a title and body you chose, sent at the moment you chose. Rely on the net and you get the machine's guess instead.
258
+ There is a safety net, and it is not a substitute for the above. When an execute-mode run finishes with user-facing output and nothing delivered it, the assistant sends a notification carrying the run's final reply, so a schedule can no longer run and leave no trace. `messaging_send` counts as a delivery only when the call reported success, and `assistant notifications send` once the notification pipeline has taken it: a send the channel refused, or one that never reported at all, leaves the run eligible for the net, so the failure reaches you instead of vanishing into a conversation nobody has open. A Slack `chat.postMessage` never counts, because the net recognizes only the recorded routes; a run that posts Block Kit that way also gets the net's notification. It fires on the raw reply, whatever the run happened to end on, at whatever length. An authored delivery step gets a title and body you chose, sent at the moment you chose. Rely on the net and you get the machine's guess instead.
259
259
 
260
- Choose the right delivery tool based on the content:
260
+ Choose the right delivery tool based on the destination:
261
261
 
262
- - **Rich content** (digests, summaries, reports): Use `messaging_send` with the target platform and conversation ID. It reaches Gmail or Outlook as a draft, and posts to a Slack, Telegram, or Discord chat through that channel's own transport, where the post is recorded and can be found again afterwards. Reach for `chat.postMessage` through `assistant oauth request` only for a Block Kit shape `messaging_send` cannot carry, with the provider the **slack** skill says to pass for posting on this workspace (`slack_channel` when the bot is set up; where only the `slack` integration exists, the skill says to tell the user before posting as them), never through `curl` or a revealed token. Either way the full content posts directly.
263
- - **Short alerts** (status updates, completion notices): Use `assistant notifications send` via `bash` to let the notification router pick the best channel. Note: the router's decision engine rewrites content into short alerts, so it is not suitable for rich content.
262
+ - **A specific chat or mailbox** (a named Slack, Telegram, or Discord conversation, or an email draft): Use `messaging_send` with the target platform and conversation ID. It reaches Gmail or Outlook as a draft, and posts to a Slack, Telegram, or Discord chat through that channel's own transport, where the post is recorded and can be found again afterwards. Reach for `chat.postMessage` through `assistant oauth request` only for a Block Kit shape `messaging_send` cannot carry, with the provider the **slack** skill says to pass for posting on this workspace (`slack_channel` when the bot is set up; where only the `slack` integration exists, the skill says to tell the user before posting as them), never through `curl` or a revealed token. Either way the full content posts directly.
263
+ - **The user's notification surfaces** (Vellum inbox, Telegram, platform push): Use `assistant notifications send` via `bash`. Pass `--source-channel scheduler` and the complete authored body as `--message`. The pipeline keeps that body. Do not summarize the report before sending.
264
264
 
265
265
  Example schedule message for a Slack digest:
266
266
 
@@ -58,14 +58,6 @@ type ConfigFileSignature =
58
58
  exists: false;
59
59
  };
60
60
 
61
- function getConfigPath(): string {
62
- return getWorkspaceConfigPath();
63
- }
64
-
65
- function ensureMigratedDataDir(): void {
66
- ensureDataDir();
67
- }
68
-
69
61
  function readConfigFileSignature(configPath: string): ConfigFileSignature {
70
62
  const stats = safeStatSync(configPath);
71
63
  if (!stats) {
@@ -104,7 +96,7 @@ function getCachedConfigIfFresh(): AssistantConfig | null {
104
96
  return null;
105
97
  }
106
98
 
107
- const currentSignature = readConfigFileSignature(getConfigPath());
99
+ const currentSignature = readConfigFileSignature(getWorkspaceConfigPath());
108
100
  if (configFileSignaturesEqual(cachedFileSignature, currentSignature)) {
109
101
  return cached;
110
102
  }
@@ -1002,7 +994,7 @@ export function mergeDefaultWorkspaceConfig(): DefaultWorkspaceConfigMergeResult
1002
994
  Object.prototype.hasOwnProperty.call(llmDefaults, "activeProfile"),
1003
995
  };
1004
996
 
1005
- const configPath = getConfigPath();
997
+ const configPath = getWorkspaceConfigPath();
1006
998
  let existing: Record<string, unknown> = {};
1007
999
  if (existsSync(configPath)) {
1008
1000
  try {
@@ -1070,8 +1062,8 @@ export function loadConfig(): AssistantConfig {
1070
1062
  loading = true;
1071
1063
 
1072
1064
  try {
1073
- ensureMigratedDataDir();
1074
- const configPath = getConfigPath();
1065
+ ensureDataDir();
1066
+ const configPath = getWorkspaceConfigPath();
1075
1067
 
1076
1068
  let fileConfig: Record<string, unknown> = {};
1077
1069
  let configFileExisted = true;
@@ -1259,7 +1251,7 @@ export function getConfigReadOnly(): AssistantConfig {
1259
1251
  return freshCached;
1260
1252
  }
1261
1253
 
1262
- const configPath = getConfigPath();
1254
+ const configPath = getWorkspaceConfigPath();
1263
1255
  let fileConfig: Record<string, unknown> = {};
1264
1256
  if (existsSync(configPath)) {
1265
1257
  try {
@@ -1312,8 +1304,8 @@ export function withSuppressedConfigDiskWritesSync<T>(fn: () => T): T {
1312
1304
  * type without runtime shape-checking — the boundary check happens here.
1313
1305
  */
1314
1306
  export function loadRawConfig(): Record<string, unknown> {
1315
- ensureMigratedDataDir();
1316
- const configPath = getConfigPath();
1307
+ ensureDataDir();
1308
+ const configPath = getWorkspaceConfigPath();
1317
1309
  if (!existsSync(configPath)) {
1318
1310
  return {};
1319
1311
  }
@@ -1372,8 +1364,8 @@ function describeJsonShape(value: unknown): string {
1372
1364
  }
1373
1365
 
1374
1366
  export function saveRawConfig(config: Record<string, unknown>): void {
1375
- ensureMigratedDataDir();
1376
- const configPath = getConfigPath();
1367
+ ensureDataDir();
1368
+ const configPath = getWorkspaceConfigPath();
1377
1369
 
1378
1370
  // Strip legacy apiKeys — provider keys belong in secure storage, not plaintext config
1379
1371
  delete config.apiKeys;
@@ -136,7 +136,7 @@ export function _resetPlatformIdentityEnsureForTests(): void {
136
136
  * Concurrent callers share one in-flight request.
137
137
  */
138
138
  export async function ensurePlatformIdentityIds(): Promise<void> {
139
- if (getPlatformAssistantId().trim()) {
139
+ if (getPlatformAssistantId()?.trim()) {
140
140
  return;
141
141
  }
142
142
  if (Date.now() < nextEnsureAttemptAt) {
@@ -146,7 +146,7 @@ export async function ensurePlatformIdentityIds(): Promise<void> {
146
146
  ensureInFlight = (async () => {
147
147
  try {
148
148
  const apiKey = await readAssistantApiKey();
149
- const baseUrl = getPlatformBaseUrl();
149
+ const baseUrl = (getPlatformBaseUrl() ?? "").replace(/\/+$/, "");
150
150
  if (!apiKey || !baseUrl) {
151
151
  return;
152
152
  }
@@ -167,12 +167,12 @@ export async function ensurePlatformIdentityIds(): Promise<void> {
167
167
  }
168
168
 
169
169
  export async function resolvePlatformAssistantId(): Promise<string> {
170
- const existing = getPlatformAssistantId().trim();
170
+ const existing = getPlatformAssistantId()?.trim() ?? "";
171
171
  if (existing) {
172
172
  return existing;
173
173
  }
174
174
  await ensurePlatformIdentityIds();
175
- return getPlatformAssistantId().trim();
175
+ return getPlatformAssistantId()?.trim() ?? "";
176
176
  }
177
177
 
178
178
  export async function resolvePlatformAssistantIdOrNull(): Promise<
@@ -183,19 +183,19 @@ export async function resolvePlatformAssistantIdOrNull(): Promise<
183
183
  }
184
184
 
185
185
  export async function resolvePlatformOrganizationId(): Promise<string> {
186
- const existing = getPlatformOrganizationId().trim();
186
+ const existing = getPlatformOrganizationId()?.trim() ?? "";
187
187
  if (existing) {
188
188
  return existing;
189
189
  }
190
190
  await ensurePlatformIdentityIds();
191
- return getPlatformOrganizationId().trim();
191
+ return getPlatformOrganizationId()?.trim() ?? "";
192
192
  }
193
193
 
194
194
  export async function resolvePlatformUserId(): Promise<string> {
195
- const existing = getPlatformUserId().trim();
195
+ const existing = getPlatformUserId()?.trim() ?? "";
196
196
  if (existing) {
197
197
  return existing;
198
198
  }
199
199
  await ensurePlatformIdentityIds();
200
- return getPlatformUserId().trim();
200
+ return getPlatformUserId()?.trim() ?? "";
201
201
  }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ nonTextConversationProfileMessage,
5
+ profileSupportsTextGeneration,
6
+ } from "./profile-text-generation.js";
7
+
8
+ describe("profileSupportsTextGeneration", () => {
9
+ test("false for a structured-decision catalog model", () => {
10
+ expect(
11
+ profileSupportsTextGeneration(
12
+ { provider: "jev", model: "jev-latest" },
13
+ {},
14
+ ),
15
+ ).toBe(false);
16
+ });
17
+
18
+ test("true for a chat model and for unlisted custom ids", () => {
19
+ expect(
20
+ profileSupportsTextGeneration(
21
+ { provider: "anthropic", model: "claude-opus-4-8" },
22
+ {},
23
+ ),
24
+ ).toBe(true);
25
+ expect(
26
+ profileSupportsTextGeneration(
27
+ { provider: "openai-compatible", model: "local-mixtral" },
28
+ {},
29
+ ),
30
+ ).toBe(true);
31
+ });
32
+
33
+ test("a mix is false when any arm is non-text", () => {
34
+ expect(
35
+ profileSupportsTextGeneration(
36
+ { mix: [{ profile: "jev" }, { profile: "balanced" }] },
37
+ {
38
+ jev: { provider: "jev", model: "jev-latest" },
39
+ balanced: { provider: "anthropic", model: "claude-opus-4-8" },
40
+ },
41
+ ),
42
+ ).toBe(false);
43
+ });
44
+
45
+ test("names the profile in the conversation-model error", () => {
46
+ expect(nonTextConversationProfileMessage("jev")).toContain("jev");
47
+ expect(nonTextConversationProfileMessage("jev")).toContain(
48
+ "structured answers rather than chat text",
49
+ );
50
+ });
51
+ });
@@ -0,0 +1,51 @@
1
+ import { catalogModelSupportsText } from "../providers/model-catalog.js";
2
+
3
+ /**
4
+ * Whether a profile can back the conversation model (active profile or a
5
+ * per-conversation pin). Mix arms are judged individually: a mix that can
6
+ * land on a non-text model is not a conversation profile.
7
+ *
8
+ * Missing arms and unlisted models default to true so other validators
9
+ * (existence, availability) own those failures.
10
+ */
11
+ export function profileSupportsTextGeneration(
12
+ entry: {
13
+ provider?: unknown;
14
+ model?: unknown;
15
+ mix?: unknown;
16
+ },
17
+ siblings: Record<string, unknown>,
18
+ ): boolean {
19
+ const mix = entry.mix;
20
+ if (Array.isArray(mix) && mix.length > 0) {
21
+ return mix.every((arm) => {
22
+ const name =
23
+ arm !== null &&
24
+ typeof arm === "object" &&
25
+ "profile" in arm &&
26
+ typeof arm.profile === "string"
27
+ ? arm.profile
28
+ : undefined;
29
+ if (name === undefined) {
30
+ return true;
31
+ }
32
+ const target = siblings[name];
33
+ if (target === null || typeof target !== "object") {
34
+ return true;
35
+ }
36
+ const nested = target as { provider?: unknown; model?: unknown };
37
+ return catalogModelSupportsText(
38
+ typeof nested.provider === "string" ? nested.provider : undefined,
39
+ typeof nested.model === "string" ? nested.model : undefined,
40
+ );
41
+ });
42
+ }
43
+ return catalogModelSupportsText(
44
+ typeof entry.provider === "string" ? entry.provider : undefined,
45
+ typeof entry.model === "string" ? entry.model : undefined,
46
+ );
47
+ }
48
+
49
+ export function nonTextConversationProfileMessage(profileName: string): string {
50
+ return `Profile "${profileName}" uses a model that returns structured answers rather than chat text, so it cannot be the conversation model.`;
51
+ }
@@ -45,6 +45,7 @@ describe("MemoryV3ConfigSchema", () => {
45
45
  bm25NormK: null,
46
46
  bypassForCore: false,
47
47
  },
48
+ poolLog: { captureInput: false },
48
49
  });
49
50
  });
50
51
 
@@ -204,6 +205,17 @@ describe("MemoryV3ConfigSchema", () => {
204
205
  ).toThrow();
205
206
  });
206
207
 
208
+ test("poolLog.captureInput defaults false, accepts true, rejects non-booleans", () => {
209
+ expect(MemoryV3ConfigSchema.parse({}).poolLog.captureInput).toBe(false);
210
+ expect(
211
+ MemoryV3ConfigSchema.parse({ poolLog: { captureInput: true } }).poolLog
212
+ .captureInput,
213
+ ).toBe(true);
214
+ expect(() =>
215
+ MemoryV3ConfigSchema.parse({ poolLog: { captureInput: "yes" } }),
216
+ ).toThrow();
217
+ });
218
+
207
219
  test("gate.topK rejects zero and non-integers", () => {
208
220
  expect(() => MemoryV3ConfigSchema.parse({ gate: { topK: 0 } })).toThrow();
209
221
  expect(() => MemoryV3ConfigSchema.parse({ gate: { topK: 2.5 } })).toThrow();
@@ -4,7 +4,10 @@ import {
4
4
  PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
5
5
  ROUTING_IDENTITY_PROVIDERS,
6
6
  } from "../../providers/inference/auth.js";
7
- import { PROVIDER_CATALOG } from "../../providers/model-catalog.js";
7
+ import {
8
+ catalogModelSupportsText,
9
+ PROVIDER_CATALOG,
10
+ } from "../../providers/model-catalog.js";
8
11
  import { isCodexSubscriptionModel } from "../../providers/openai/codex-models.js";
9
12
  import {
10
13
  getManagedUpstream,
@@ -59,6 +62,7 @@ export const KNOWN_LLM_PROVIDERS = [
59
62
  "opencode",
60
63
  "baseten",
61
64
  "poolside",
65
+ "jev",
62
66
  // Routing identities: "vellum" = the platform-managed route (upstream
63
67
  // derived from the model at dispatch) and the catalog owner of
64
68
  // Vellum-hosted GPU models; "chatgpt" = the subscription route to OpenAI.
@@ -99,7 +103,8 @@ export const DEFAULT_PROVIDER_CHOICES: readonly LLMProvider[] = [
99
103
  (entry) =>
100
104
  entry.setupMode === "api-key" &&
101
105
  !PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(entry.id) &&
102
- entry.defaultModel !== "",
106
+ entry.defaultModel !== "" &&
107
+ catalogModelSupportsText(entry.id, entry.defaultModel),
103
108
  )
104
109
  .map((entry) => entry.id)
105
110
  // A catalog provider outside the known provider set cannot be
@@ -17,7 +17,11 @@ export const PLUGIN_MCP_RISK_LEVEL = "low" as const;
17
17
  /** Per-server cap on tools registered from one MCP server. */
18
18
  export const MCP_MAX_TOOLS_PER_SERVER = 20;
19
19
 
20
- /** Cap on tools registered across every MCP server. */
20
+ /**
21
+ * Cap on tools registered across every MCP server. Selection is a
22
+ * deterministic round-robin by server id (see `mcp/tool-caps.ts`), so a
23
+ * later server is not emptied just because earlier ones filled the budget.
24
+ */
21
25
  export const MCP_GLOBAL_MAX_TOOLS = 50;
22
26
 
23
27
  export type McpRiskLevel =
@@ -352,6 +352,17 @@ export const MemoryV3GateSchema = z
352
352
  "Memory v3 per-turn injection gate tuning (thresholds; the gate runs when `enabled` is on).",
353
353
  );
354
354
 
355
+ const MemoryV3PoolLogSchema = z
356
+ .object({
357
+ captureInput: z
358
+ .boolean({ error: "memory.v3.poolLog.captureInput must be a boolean" })
359
+ .default(false)
360
+ .describe(
361
+ "Persist the selector's exact input beside each turn's pool audit: the rendered text of every pooled candidate, content-addressed in `memory_v3_pool_texts`, and the turn's context strings in `memory_v3_pool_inputs`, for offline selector evaluation and training data. Off by default; the pool audit itself (`memory_v3_pools`) is always written.",
362
+ ),
363
+ })
364
+ .describe("Memory v3 selector pool audit options.");
365
+
355
366
  // Persisted config files can carry unsupported tuning keys this object does
356
367
  // not declare; zod's default unknown-key stripping accepts and drops them, so
357
368
  // such a config keeps parsing. Do not make this object `.strict()`.
@@ -434,6 +445,7 @@ export const MemoryV3ConfigSchema = z
434
445
  entity: MemoryV3EntitySchema.default(MemoryV3EntitySchema.parse({})),
435
446
  rareTerm: MemoryV3RareTermSchema.default(MemoryV3RareTermSchema.parse({})),
436
447
  gate: MemoryV3GateSchema.default(MemoryV3GateSchema.parse({})),
448
+ poolLog: MemoryV3PoolLogSchema.default(MemoryV3PoolLogSchema.parse({})),
437
449
  })
438
450
  .describe("Memory v3 — section-grain lane retrieval");
439
451
 
@@ -49,8 +49,8 @@ interface CesStartupResult {
49
49
  * handles to the in-flight initialization — callers don't need to await this
50
50
  * for startup to continue.
51
51
  *
52
- * The managed sidecar accepts exactly one bootstrap connection, so this must be
53
- * called at the process level (not per-conversation).
52
+ * CES serves a multi-connection Unix socket, so this is called at the
53
+ * process level and child processes may open their own connections.
54
54
  */
55
55
  function startCesProcess(config: AssistantConfig): CesStartupResult {
56
56
  const pm = createCesProcessManager({ assistantConfig: config });