@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -51,10 +51,10 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
51
51
  },
52
52
  fireworks: {
53
53
  balanced: "accounts/fireworks/models/minimax-m3",
54
- "cost-optimized": "accounts/fireworks/models/deepseek-v4-flash-0731",
55
- "latency-optimized": "accounts/fireworks/models/deepseek-v4-flash-0731",
56
- "quality-optimized": "accounts/fireworks/models/kimi-k2p6",
57
- "vision-optimized": "accounts/fireworks/models/kimi-k2p6",
54
+ "cost-optimized": "accounts/fireworks/models/deepseek-v4p1-flash",
55
+ "latency-optimized": "accounts/fireworks/models/deepseek-v4p1-flash",
56
+ "quality-optimized": "accounts/fireworks/models/kimi-k3",
57
+ "vision-optimized": "accounts/fireworks/models/kimi-k3",
58
58
  },
59
59
  openrouter: {
60
60
  balanced: "anthropic/claude-sonnet-4.6",
@@ -39,6 +39,7 @@ import {
39
39
  extractOverflowTokensFromMessage,
40
40
  } from "../types.js";
41
41
  import {
42
+ boundUnparseableToolArgsRaw,
42
43
  isUnparseableToolArgs,
43
44
  wrapUnparseableToolArgs,
44
45
  } from "../unparseable-tool-args.js";
@@ -212,6 +213,12 @@ export interface OpenAIChatCompletionsProviderOptions {
212
213
  * (Fireworks, Together) keep sending `none` / forced choices. Enabled for
213
214
  * the generic `openai-compatible` adapter, whose upstream is unknown. */
214
215
  omitToolChoiceWhenReasoning?: boolean;
216
+ /** Send `tool_choice: "auto"` whenever tools are offered and the caller
217
+ * left the choice unspecified. Strict OpenAI-compatible upstreams default
218
+ * an unspecified `tool_choice` to "none" (io.net documents exactly that),
219
+ * which would leave the model unable to call any tool; OpenAI's own
220
+ * default is "auto", so this option only exists for upstreams that differ. */
221
+ defaultToolChoiceAuto?: boolean;
215
222
  /** Wire field for the output-token limit. OpenAI and OpenAI-compatible
216
223
  * backends use `max_completion_tokens`. OpenRouter defaults to
217
224
  * `max_tokens` because its parameter router matches that key on
@@ -864,6 +871,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
864
871
  private coerceObjectArgsToJsonString: boolean;
865
872
  private salvageXmlToolCalls: boolean;
866
873
  private omitToolChoiceWhenReasoning: boolean;
874
+ private defaultToolChoiceAuto: boolean;
867
875
  private outputTokenLimitField: "max_completion_tokens" | "max_tokens";
868
876
 
869
877
  constructor(
@@ -896,6 +904,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
896
904
  options.salvageXmlToolCalls ?? shouldSalvageXmlToolCalls(model);
897
905
  this.omitToolChoiceWhenReasoning =
898
906
  options.omitToolChoiceWhenReasoning ?? false;
907
+ this.defaultToolChoiceAuto = options.defaultToolChoiceAuto ?? false;
899
908
  this.outputTokenLimitField =
900
909
  options.outputTokenLimitField ?? "max_completion_tokens";
901
910
  }
@@ -958,6 +967,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
958
967
  maxTokens;
959
968
  }
960
969
 
970
+ // Per-conversation prompt-cache key (OpenAI Chat Completions
971
+ // `prompt_cache_key`). `RetryProvider` stamps it from the durable
972
+ // conversation id for providers in PROMPT_CACHE_KEY_PROVIDERS; omitted
973
+ // from the wire when absent.
974
+ const promptCacheKey = configObj?.promptCacheKey;
975
+ if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) {
976
+ params.prompt_cache_key = promptCacheKey;
977
+ }
978
+
961
979
  // Profile-scoped token biasing (e.g. the `suppress-cjk` preset). Resolved
962
980
  // and gated to this provider family upstream in `RetryProvider`.
963
981
  if (logitBias) {
@@ -1028,7 +1046,12 @@ export class OpenAIChatCompletionsProvider implements Provider {
1028
1046
  // wire. Catalog providers that honor `none` / forced choices still
1029
1047
  // receive them; the generic openai-compatible adapter drops every
1030
1048
  // explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
1031
- const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
1049
+ const callerToolChoice = mapNeutralToolChoice(configObj?.tool_choice);
1050
+ const toolChoice =
1051
+ callerToolChoice ??
1052
+ (this.defaultToolChoiceAuto
1053
+ ? ("auto" as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption)
1054
+ : undefined);
1032
1055
  xmlToolCallSalvageEnabled =
1033
1056
  this.salvageXmlToolCalls && toolChoice !== "none";
1034
1057
  if (toolChoice !== undefined) {
@@ -1149,6 +1172,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
1149
1172
  id: string;
1150
1173
  name: string;
1151
1174
  args: string;
1175
+ inspectableArgs?: string;
1152
1176
  providerMetadata?: ToolUseContent["providerMetadata"];
1153
1177
  }
1154
1178
  >();
@@ -1450,6 +1474,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
1450
1474
  input = JSON.parse(tc.args);
1451
1475
  } catch {
1452
1476
  input = wrapUnparseableToolArgs(tc.args);
1477
+ tc.inspectableArgs = boundUnparseableToolArgsRaw(tc.args);
1453
1478
  }
1454
1479
  const objectKeys = coercedObjectKeys.get(tc.name);
1455
1480
  if (objectKeys && !isUnparseableToolArgs(input)) {
@@ -1457,6 +1482,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
1457
1482
  input = decoded.failedKey
1458
1483
  ? wrapUnparseableToolArgs(tc.args)
1459
1484
  : decoded.input;
1485
+ if (decoded.failedKey) {
1486
+ tc.inspectableArgs = boundUnparseableToolArgsRaw(tc.args);
1487
+ }
1460
1488
  }
1461
1489
  content.push({
1462
1490
  type: "tool_use",
@@ -1484,7 +1512,10 @@ export class OpenAIChatCompletionsProvider implements Provider {
1484
1512
  {
1485
1513
  id: tc.id,
1486
1514
  type: "function",
1487
- function: { name: tc.name, arguments: tc.args },
1515
+ function: {
1516
+ name: tc.name,
1517
+ arguments: tc.inspectableArgs ?? tc.args,
1518
+ },
1488
1519
  },
1489
1520
  thoughtSignatureFromToolUseMetadata(
1490
1521
  tc.providerMetadata,
@@ -35,6 +35,7 @@ import {
35
35
  shouldSkipPrimary,
36
36
  tryAcquireRecoveryProbe,
37
37
  } from "./fallback-breaker.js";
38
+ import { resolveFireworksRequestHeaders } from "./fireworks/client.js";
38
39
  import { resolveLogitBiasPreset } from "./inference/logit-bias.js";
39
40
  import { MALFORMED_TOOL_CALL_MESSAGE } from "./malformed-tool-call.js";
40
41
  import {
@@ -77,10 +78,16 @@ const USAGE_ATTRIBUTION_HEADER_NAMES = {
77
78
  subagentSpawnMode: "X-Vellum-Subagent-Spawn-Mode",
78
79
  } as const;
79
80
 
80
- /** Providers whose transports consume `promptCacheKey` (OpenAI Responses
81
- * `prompt_cache_key`); `RetryProvider` derives it from `selectionSeed` for
82
- * these only. */
83
- const PROMPT_CACHE_KEY_PROVIDERS = new Set(["openai", "openrouter"]);
81
+ /** Providers whose transports consume `promptCacheKey` (OpenAI Responses /
82
+ * Chat Completions `prompt_cache_key`); `RetryProvider` derives it from
83
+ * `selectionSeed` for these only. `openai-compatible` is included so
84
+ * self-hosted bridges (e.g. a Claude Code shim) receive a stable
85
+ * per-conversation key and can pin server-side sessions / prompt caches to it. */
86
+ const PROMPT_CACHE_KEY_PROVIDERS = new Set([
87
+ "openai",
88
+ "openrouter",
89
+ "openai-compatible",
90
+ ]);
84
91
 
85
92
  /** Providers that support the `effort` config (extended thinking / reasoning). */
86
93
  const EFFORT_SUPPORTED_PROVIDERS = new Set([
@@ -681,6 +688,19 @@ function normalizeSendMessageOptions(
681
688
  nextConfig.requestHeaders = resolveOpenCodeRequestHeaders(conversationId);
682
689
  }
683
690
 
691
+ if (providerName === "fireworks") {
692
+ // Covers BYOK Fireworks and the Vellum-managed Fireworks upstream, which
693
+ // both dispatch through `FireworksProvider`. Keyless calls send no header.
694
+ const sessionKey = [config.selectionSeed, config.conversationId].find(
695
+ (value): value is string =>
696
+ typeof value === "string" && value.trim().length > 0,
697
+ );
698
+ const headers = resolveFireworksRequestHeaders(sessionKey);
699
+ if (Object.keys(headers).length > 0) {
700
+ nextConfig.requestHeaders = headers;
701
+ }
702
+ }
703
+
684
704
  // `overrideProfile`, `forceOverrideProfile`, `selectionSeed`,
685
705
  // `conversationId`, and `nativeWebSearchSentinel` are routing/resolution-time
686
706
  // concerns (consumed by the resolver below, `CallSiteRoutingProvider`'s
@@ -689,6 +709,7 @@ function normalizeSendMessageOptions(
689
709
  // (after the `openai` promptCacheKey copy above) so they never leak into
690
710
  // provider request bodies even when callers set them without a `callSite`.
691
711
  delete nextConfig.overrideProfile;
712
+ delete nextConfig.overrideProfileOrigin;
692
713
  delete nextConfig.forceOverrideProfile;
693
714
  delete nextConfig.selectionSeed;
694
715
  delete nextConfig.conversationId;
@@ -703,6 +724,7 @@ function normalizeSendMessageOptions(
703
724
  const attribution = resolveUsageAttribution({
704
725
  callSite: config.callSite,
705
726
  overrideProfile: config.overrideProfile,
727
+ overrideProfileOrigin: config.overrideProfileOrigin,
706
728
  forceOverrideProfile: config.forceOverrideProfile,
707
729
  selectionSeed: config.selectionSeed,
708
730
  });
@@ -912,6 +934,36 @@ function normalizeSendMessageOptions(
912
934
  }
913
935
  }
914
936
 
937
+ // Adaptive-thinking-only Claude models (Fable, Opus 5.5, Sonnet 5.5) cannot
938
+ // turn reasoning off, so stripping `thinking` below does not clear the
939
+ // forced-tool conflict and Anthropic 400s the request. Downgrade the forced
940
+ // choice to `auto` so the call goes out. The tool is no longer guaranteed:
941
+ // forced-tool call sites already treat a missing tool call as a failed
942
+ // result, which is what the 400 gave them on every request.
943
+ const forcedToolModel =
944
+ typeof nextConfig.model === "string" ? nextConfig.model : "";
945
+ const forcedToolChoice = nextConfig.tool_choice as
946
+ | Record<string, unknown>
947
+ | undefined;
948
+ if (
949
+ forcedToolChoice != null &&
950
+ (forcedToolChoice.type === "tool" || forcedToolChoice.type === "any") &&
951
+ isAdaptiveThinkingOnlyModel(forcedToolModel) &&
952
+ targetsAnthropicWire(providerName, forcedToolModel)
953
+ ) {
954
+ log.warn(
955
+ {
956
+ providerName,
957
+ callSite: config.callSite,
958
+ model: forcedToolModel,
959
+ droppedToolChoice: forcedToolChoice,
960
+ },
961
+ "Downgrading forced `tool_choice` to `auto` because this model always " +
962
+ "reasons and rejects forced tool use.",
963
+ );
964
+ nextConfig.tool_choice = { type: "auto" };
965
+ }
966
+
915
967
  // Anthropic (and the gateways fronting Anthropic) rejects requests that
916
968
  // combine extended thinking with forced tool use (`tool_choice.type` of
917
969
  // `"tool"` or `"any"`). Strip thinking when both are present so the
@@ -197,6 +197,16 @@ export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [
197
197
  fallbackOrder: 9,
198
198
  privacyPolicyUrl: "https://exa.ai/privacy-policy",
199
199
  },
200
+ {
201
+ id: "serply",
202
+ displayName: "Serply",
203
+ kind: "byok",
204
+ apiKeyPrefix: "Serply API key...",
205
+ envVar: "SERPLY_API_KEY",
206
+ secretKey: "serply",
207
+ fallbackOrder: 10,
208
+ privacyPolicyUrl: "https://serply.io/privacy",
209
+ },
200
210
  ];
201
211
 
202
212
  /** Provider ids accepted by the web-search config schema. */
@@ -346,6 +346,13 @@ export interface SendMessageConfig {
346
346
  * silently fall through.
347
347
  */
348
348
  overrideProfile?: string;
349
+ /**
350
+ * Who chose `overrideProfile`: `"auto"` when the Auto profile's router
351
+ * picked it for the turn. Usage attribution reports it as the profile
352
+ * source so a routed turn is not counted as a user pin. A routing-time
353
+ * concern only; stripped before any provider wire request.
354
+ */
355
+ overrideProfileOrigin?: "auto";
349
356
  /**
350
357
  * When true, the resolver floats `overrideProfile` above the call-site
351
358
  * layers (named site profile + call-site override) for non-main-agent call
@@ -405,8 +412,9 @@ export interface SendMessageConfig {
405
412
  usageAttributionHeaders?: Record<string, string>;
406
413
  /**
407
414
  * Per-request HTTP headers merged onto the transport. `RetryProvider`
408
- * stamps these for providers that need support-lookup headers (OpenCode
409
- * `x-opencode-session` / `x-opencode-request`). Provider clients pass
415
+ * stamps these for providers that need per-conversation routing headers
416
+ * (OpenCode `x-opencode-session` / `x-opencode-request`, Fireworks
417
+ * `x-session-affinity`). Provider clients pass
410
418
  * them through SDK request options only and must never include this
411
419
  * object in provider JSON request bodies.
412
420
  */
@@ -14,7 +14,9 @@
14
14
  */
15
15
 
16
16
  import { safeStringSlice } from "../util/unicode.js";
17
+
17
18
  const UNPARSEABLE_TOOL_ARGS_KEY = "_raw";
19
+ const UNPARSEABLE_TOOL_ARGS_PREVIEW_LIMIT = 200;
18
20
 
19
21
  /**
20
22
  * Message emitted by the Anthropic SDK's stream accumulator when a tool_use
@@ -30,6 +32,32 @@ export function wrapUnparseableToolArgs(raw: string): Record<string, unknown> {
30
32
  return { [UNPARSEABLE_TOOL_ARGS_KEY]: raw };
31
33
  }
32
34
 
35
+ /**
36
+ * Bound an unparseable marker before it joins history, events, or audit logs.
37
+ * The prefix is enough to diagnose where JSON broke without retaining a
38
+ * multi-megabyte partial tool call in every downstream copy.
39
+ */
40
+ export function boundUnparseableToolArgs(
41
+ input: Record<string, unknown>,
42
+ ): Record<string, unknown> {
43
+ if (!isUnparseableToolArgs(input)) {
44
+ return input;
45
+ }
46
+ const raw = input[UNPARSEABLE_TOOL_ARGS_KEY];
47
+ const bounded = boundUnparseableToolArgsRaw(raw);
48
+ return bounded === raw ? input : wrapUnparseableToolArgs(bounded);
49
+ }
50
+
51
+ /**
52
+ * Bound raw malformed arguments for diagnostic copies that retain the provider
53
+ * response shape rather than the internal `_raw` marker shape.
54
+ */
55
+ export function boundUnparseableToolArgsRaw(raw: string): string {
56
+ return raw.length > UNPARSEABLE_TOOL_ARGS_PREVIEW_LIMIT
57
+ ? `${safeStringSlice(raw, 0, UNPARSEABLE_TOOL_ARGS_PREVIEW_LIMIT)}…`
58
+ : raw;
59
+ }
60
+
33
61
  /**
34
62
  * Detect input produced by {@link wrapUnparseableToolArgs}: exactly one key,
35
63
  * the marker key, holding the raw argument string. The exact-shape check
@@ -56,11 +84,7 @@ export function unparseableToolArgsMessage(
56
84
  toolName: string,
57
85
  raw: string,
58
86
  ): string {
59
- const PREVIEW_LIMIT = 200;
60
- const preview =
61
- raw.length > PREVIEW_LIMIT
62
- ? `${safeStringSlice(raw, 0, PREVIEW_LIMIT)}…`
63
- : raw;
87
+ const preview = boundUnparseableToolArgsRaw(raw);
64
88
  return (
65
89
  `Error: the arguments for "${toolName}" were not valid JSON — the argument stream was malformed or truncated, so the tool was NOT executed. ` +
66
90
  `Received: ${preview || "(empty)"}\n` +
@@ -77,9 +77,11 @@ export class UsageTrackingProvider implements Provider {
77
77
  const attribution = resolveUsageAttribution({
78
78
  callSite: config.callSite,
79
79
  overrideProfile: appliedOverride,
80
- ...(response.actualInferenceProfile !== undefined
81
- ? { forceOverrideProfile: true }
82
- : {}),
80
+ // A rerouted serve ran on the backup the wrapper chose, not on the
81
+ // router's pick, so the origin applies only to the original route.
82
+ ...(response.actualInferenceProfile === undefined
83
+ ? { overrideProfileOrigin: config.overrideProfileOrigin }
84
+ : { forceOverrideProfile: true }),
83
85
  });
84
86
  const providerName = response.actualProvider ?? this.inner.name;
85
87
  const pricingUsage = buildPricingUsageFromResponse(
@@ -438,6 +438,29 @@ export class AssistantEventHub {
438
438
  return undefined;
439
439
  }
440
440
 
441
+ /**
442
+ * Whether `connectionId` is still an active client subscription, optionally
443
+ * required to belong to `clientId`.
444
+ */
445
+ hasActiveClientConnection(
446
+ connectionId: string,
447
+ clientId?: string,
448
+ ): boolean {
449
+ for (const entry of this.subscribers) {
450
+ if (entry.connectionId !== connectionId) {
451
+ continue;
452
+ }
453
+ if (!entry.active || entry.type !== "client") {
454
+ return false;
455
+ }
456
+ if (clientId != null && entry.clientId !== clientId) {
457
+ return false;
458
+ }
459
+ return true;
460
+ }
461
+ return false;
462
+ }
463
+
441
464
  /**
442
465
  * Return the verified actor principal id captured at SSE subscription time
443
466
  * for the given client, or `undefined` if the client is unknown or
@@ -68,11 +68,6 @@ describe("route policy coverage", () => {
68
68
  // handler calls `assertPlaygroundEnabled()`, so the surface
69
69
  // is invisible in prod regardless of policy.
70
70
  //
71
- // GROUP C — pre-existing latent unprotected on main (the prior
72
- // registry had no entry for these endpoints, so `enforcePolicy`
73
- // returned allowed). Migration preserves behavior. Triage these
74
- // and assign real policies in a follow-up PR:
75
- // - integrations/a2a/invite/accept
76
71
  const INTENTIONALLY_UNPROTECTED = new Set([
77
72
  // A — design-intentional
78
73
  "health",
@@ -87,8 +82,6 @@ describe("route policy coverage", () => {
87
82
  "playground/seed-conversation",
88
83
  "playground/seeded-conversations",
89
84
  "playground/seeded-conversations/:id",
90
- // C — pre-existing latent unprotected (follow-up audit owed)
91
- "integrations/a2a/invite/accept",
92
85
  ]);
93
86
 
94
87
  const unprotectedFound: string[] = [];
@@ -223,7 +223,7 @@ describe("enforcePolicy", () => {
223
223
  // An oauth_proxy_v1 grant is printed for a user to export into a stock
224
224
  // third-party CLI's environment, so it sits outside this install's trust
225
225
  // boundary. `policy: null` admits any valid token, which for this grant means
226
- // mutating routes it was never minted for (integrations/a2a/invite/accept).
226
+ // mutating routes it was never minted for.
227
227
  // ---------------------------------------------------------------------------
228
228
 
229
229
  /** The context an exported OAuth proxy grant produces. */
@@ -3,6 +3,7 @@ import type { MessageAudience } from "@vellumai/gateway-client";
3
3
  import { stripVellumLinks } from "../daemon/assistant-attachments.js";
4
4
  import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
5
5
  import { renderHistoryContent } from "../daemon/handlers/shared.js";
6
+ import { DEFAULT_LOCALE } from "../i18n/index.js";
6
7
  import { editChannelMessage } from "../messaging/providers/index.js";
7
8
  import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
8
9
  import {
@@ -344,6 +345,7 @@ function readPersistedAssistantReply(msg: PersistedMessage): {
344
345
  undefined,
345
346
  undefined,
346
347
  msg.metadata,
348
+ DEFAULT_LOCALE,
347
349
  );
348
350
 
349
351
  const linked = getAttachmentMetadataForMessage(msg.id);
@@ -75,9 +75,8 @@ function parseTrustRuntimeContext(value: unknown): TrustContext | undefined {
75
75
  if (!rawSourceChannel || !isChannelId(rawSourceChannel)) {
76
76
  return undefined;
77
77
  }
78
- const sourceChannel = rawSourceChannel;
79
78
  return {
80
- sourceChannel,
79
+ sourceChannel: rawSourceChannel,
81
80
  trustClass,
82
81
  guardianChatId:
83
82
  typeof raw.guardianChatId === "string" ? raw.guardianChatId : undefined,
@@ -12,6 +12,7 @@ import type {
12
12
  ConversationCreateOptions,
13
13
  SlackInboundMessageMetadata,
14
14
  } from "../daemon/handlers/shared.js";
15
+ import type { SupportedLocale } from "../i18n/index.js";
15
16
 
16
17
  // Re-export so route modules (background-dispatch, etc.) can pull the type
17
18
  // from the runtime barrel without reaching into daemon internals.
@@ -110,6 +111,8 @@ export interface RuntimeMessageConversationOptions {
110
111
  sourceChannel?: ChannelId;
111
112
  /** Originating interface (e.g. "cli", "web"). Defaults to "web". */
112
113
  sourceInterface?: InterfaceId;
114
+ /** Locale for daemon-authored text emitted during this turn. */
115
+ locale?: SupportedLocale;
113
116
  }
114
117
 
115
118
  export type MessageProcessor = (
@@ -30,6 +30,7 @@ import {
30
30
  import {
31
31
  computeLegacyManifestSha256,
32
32
  computeManifestChecksum,
33
+ MANIFEST_MAX_BYTES,
33
34
  } from "../vbundle-validator.js";
34
35
  import { defaultV1Options } from "./v1-test-helpers.js";
35
36
 
@@ -202,15 +203,16 @@ describe("readAndValidateManifest — negative paths", () => {
202
203
  });
203
204
 
204
205
  test("throws manifest_too_large and fails fast before draining the whole body", async () => {
205
- // Fake a tar entry whose body would emit 5 MiB if fully drained. The
206
- // validator must destroy() the stream and throw the moment the running
207
- // byte count crosses the 1 MiB cap — it must NOT keep pulling chunks.
206
+ // Fake a tar entry whose body would emit 10x the cap if fully drained.
207
+ // The validator must destroy() the stream and throw the moment the
208
+ // running byte count crosses MANIFEST_MAX_BYTES; it must NOT keep
209
+ // pulling chunks.
208
210
  //
209
211
  // We count bytes emitted via a _read implementation, and after the
210
- // throw assert both that destroy() fired and that far fewer than 5 MiB
211
- // were ever pulled out of the stream.
212
- const CHUNK = 512 * 1024; // 512 KiB
213
- const TOTAL_CHUNKS = 10; // 5 MiB worth — way past the 1 MiB cap
212
+ // throw assert both that destroy() fired and that far less than the
213
+ // full body was ever pulled out of the stream.
214
+ const CHUNK = MANIFEST_MAX_BYTES / 2;
215
+ const TOTAL_CHUNKS = 20;
214
216
  let chunksEmitted = 0;
215
217
  let bytesEmitted = 0;
216
218
  const body = new Readable({
@@ -245,11 +247,34 @@ describe("readAndValidateManifest — negative paths", () => {
245
247
  expect(err).toBeInstanceOf(StreamingValidationError);
246
248
  expect(err?.code).toBe("manifest_too_large");
247
249
  // Fail-fast assertions: destroy() was called, and we didn't drain past
248
- // ~1 MiB + one chunk. If the validator had drained to EOF we'd see the
249
- // full 5 MiB / 10 chunks here.
250
+ // the cap + one chunk. If the validator had drained to EOF we'd see all
251
+ // 20 chunks here.
250
252
  expect(body.destroyed).toBe(true);
251
253
  expect(chunksEmitted).toBeLessThanOrEqual(3);
252
- expect(bytesEmitted).toBeLessThan(2 * 1024 * 1024);
254
+ expect(bytesEmitted).toBeLessThan(2 * MANIFEST_MAX_BYTES);
255
+ });
256
+
257
+ test("throws bundle_too_many_entries when manifest.contents exceeds maxEntries", async () => {
258
+ const { archive } = buildVBundle({
259
+ files: [
260
+ { path: "data/db/assistant.db", data: new Uint8Array() },
261
+ { path: "workspace/a.txt", data: new TextEncoder().encode("a") },
262
+ { path: "workspace/b.txt", data: new TextEncoder().encode("b") },
263
+ ],
264
+ ...defaultV1Options(),
265
+ });
266
+ const { entry, drainRest } = await firstEntryOf(archive);
267
+
268
+ let err: StreamingValidationError | null = null;
269
+ try {
270
+ await readAndValidateManifest(entry, { maxEntries: 2 });
271
+ } catch (e) {
272
+ err = e as StreamingValidationError;
273
+ }
274
+ await drainRest();
275
+
276
+ expect(err).toBeInstanceOf(StreamingValidationError);
277
+ expect(err?.code).toBe("bundle_too_many_entries");
253
278
  });
254
279
 
255
280
  test("throws manifest_malformed when manifest body is not valid JSON", async () => {
@@ -19,6 +19,8 @@ import {
19
19
  canonicalizeJson,
20
20
  computeLegacyManifestSha256,
21
21
  computeManifestChecksum,
22
+ MANIFEST_MAX_BYTES,
23
+ MANIFEST_MAX_ENTRIES,
22
24
  ManifestSchema,
23
25
  validateVBundle,
24
26
  } from "../vbundle-validator.js";
@@ -174,6 +176,52 @@ describe("ManifestSchema — v1 acceptance", () => {
174
176
  });
175
177
  });
176
178
 
179
+ describe("validateVBundle: streaming manifest limits", () => {
180
+ test("rejects a manifest over MANIFEST_MAX_BYTES with MANIFEST_TOO_LARGE", () => {
181
+ const oversized = new Uint8Array(MANIFEST_MAX_BYTES + 1).fill(0x20);
182
+ const archive = gzipSync(
183
+ tarArchive([{ name: "manifest.json", data: oversized }]),
184
+ );
185
+ const result = validateVBundle(archive, { enforceStreamingLimits: true });
186
+ expect(result.is_valid).toBe(false);
187
+ expect(result.errors.map((e) => e.code)).toEqual(["MANIFEST_TOO_LARGE"]);
188
+ });
189
+
190
+ test("rejects a manifest declaring more than MANIFEST_MAX_ENTRIES with MANIFEST_TOO_MANY_ENTRIES", () => {
191
+ const skeleton = v1Skeleton();
192
+ const dbEntry = (skeleton.contents as Array<Record<string, unknown>>)[0]!;
193
+ const extra = Array.from({ length: MANIFEST_MAX_ENTRIES }, (_, i) => ({
194
+ ...dbEntry,
195
+ path: `workspace/${i}`,
196
+ }));
197
+ const archive = gzipTarOf(
198
+ withChecksum({ ...skeleton, contents: [dbEntry, ...extra] }),
199
+ );
200
+ const result = validateVBundle(archive, { enforceStreamingLimits: true });
201
+ expect(result.is_valid).toBe(false);
202
+ expect(result.errors.map((e) => e.code)).toEqual([
203
+ "MANIFEST_TOO_MANY_ENTRIES",
204
+ ]);
205
+ });
206
+
207
+ test("does not apply the limits unless enforceStreamingLimits is set", () => {
208
+ const skeleton = v1Skeleton();
209
+ const dbEntry = (skeleton.contents as Array<Record<string, unknown>>)[0]!;
210
+ const extra = Array.from({ length: MANIFEST_MAX_ENTRIES }, (_, i) => ({
211
+ ...dbEntry,
212
+ path: `workspace/${i}`,
213
+ }));
214
+ const archive = gzipTarOf(
215
+ withChecksum({ ...skeleton, contents: [dbEntry, ...extra] }),
216
+ [{ name: "data/db/assistant.db", data: DB_BYTES }],
217
+ );
218
+ const result = validateVBundle(archive);
219
+ expect(result.errors.map((e) => e.code)).not.toContain(
220
+ "MANIFEST_TOO_MANY_ENTRIES",
221
+ );
222
+ });
223
+ });
224
+
177
225
  describe("ManifestSchema — v1 rejection", () => {
178
226
  test("rejects schema_version !== 1", () => {
179
227
  const manifest = withChecksum({ ...v1Skeleton(), schema_version: 2 });
@@ -31,9 +31,11 @@ import { sanitizeConfigForTransfer } from "../../config/sanitize-for-transfer.js
31
31
  import { trackDaemonActivity } from "../../daemon/activity-trail.js";
32
32
  import { getLogger } from "../../util/logger.js";
33
33
  import type { VBundleOriginMode } from "./origin-mode.js";
34
- import type {
35
- ManifestFileEntryType,
36
- ManifestType,
34
+ import {
35
+ MANIFEST_MAX_BYTES,
36
+ MANIFEST_MAX_ENTRIES,
37
+ type ManifestFileEntryType,
38
+ type ManifestType,
37
39
  } from "./vbundle-validator.js";
38
40
 
39
41
  // ---------------------------------------------------------------------------
@@ -400,6 +402,20 @@ function buildManifestObject(input: {
400
402
  const checksum = sha256Hex(canonicalizeJson(manifestWithEmptyChecksum));
401
403
  const manifest: ManifestType = { ...manifestWithEmptyChecksum, checksum };
402
404
  const manifestData = new TextEncoder().encode(JSON.stringify(manifest));
405
+ if (
406
+ manifest.contents.length > MANIFEST_MAX_ENTRIES ||
407
+ manifestData.length > MANIFEST_MAX_BYTES
408
+ ) {
409
+ getLogger("vbundle-builder").warn(
410
+ {
411
+ entries: manifest.contents.length,
412
+ manifestBytes: manifestData.length,
413
+ maxEntries: MANIFEST_MAX_ENTRIES,
414
+ maxBytes: MANIFEST_MAX_BYTES,
415
+ },
416
+ "Bundle manifest exceeds the streaming import limits; teleport and staged-path imports will reject this bundle",
417
+ );
418
+ }
403
419
  return { manifest, manifestData };
404
420
  }
405
421