@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
@@ -69,7 +69,10 @@ import {
69
69
  verifySymlinkEntry,
70
70
  } from "./vbundle-streaming-validator.js";
71
71
  import { parseVBundleStream } from "./vbundle-tar-stream.js";
72
- import type { ManifestType } from "./vbundle-validator.js";
72
+ import {
73
+ MANIFEST_MAX_ENTRIES,
74
+ type ManifestType,
75
+ } from "./vbundle-validator.js";
73
76
 
74
77
  const log = getLogger("vbundle-streaming-importer");
75
78
 
@@ -91,11 +94,11 @@ const log = getLogger("vbundle-streaming-importer");
91
94
  const DEFAULT_MAX_BUNDLE_BYTES = 16 * 1024 * 1024 * 1024;
92
95
 
93
96
  /**
94
- * Entry-count ceiling for the bundle. 100k is well above the largest
95
- * workspace we ship; anything past that is almost certainly an attack or
96
- * a corrupted archive.
97
+ * Entry-count ceiling for the bundle, shared with manifest validation so
98
+ * preflight and import agree. Also applied to tar-level entries so padding
99
+ * extras cannot exceed what the manifest declared.
97
100
  */
98
- const DEFAULT_MAX_BUNDLE_ENTRIES = 100_000;
101
+ const DEFAULT_MAX_BUNDLE_ENTRIES = MANIFEST_MAX_ENTRIES;
99
102
 
100
103
  /**
101
104
  * Prefixes used for scratch dirs the streaming importer creates INSIDE the
@@ -153,7 +156,7 @@ export interface StreamCommitArgs {
153
156
  maxBundleBytes?: number;
154
157
  /**
155
158
  * Test-only override for the entry-count ceiling. Production callers
156
- * should omit this and rely on the 100_000 default.
159
+ * should omit this and rely on the `MANIFEST_MAX_ENTRIES` default.
157
160
  */
158
161
  maxBundleEntries?: number;
159
162
  }
@@ -313,7 +316,9 @@ export async function streamCommitImport(
313
316
  if (entryIndex === 0) {
314
317
  // First entry MUST be manifest.json — readAndValidateManifest
315
318
  // enforces that and throws StreamingValidationError otherwise.
316
- const manifestResult = await readAndValidateManifest(entry);
319
+ const manifestResult = await readAndValidateManifest(entry, {
320
+ maxEntries: bundleEntryCap,
321
+ });
317
322
  manifest = manifestResult.manifest;
318
323
  expected = manifestResult.expected;
319
324
 
@@ -340,18 +345,8 @@ export async function streamCommitImport(
340
345
  );
341
346
  }
342
347
 
343
- // Entry-count ceiling check. The manifest declares every file the
344
- // bundle claims to contain, so one check here bounds the work the
345
- // importer is willing to do for this bundle.
346
- if (manifest.contents.length > bundleEntryCap) {
347
- throw new StreamingValidationError(
348
- "bundle_too_many_entries",
349
- `bundle contains more than ${bundleEntryCap} entries (declared: ${manifest.contents.length})`,
350
- );
351
- }
352
-
353
- // Only NOW — after the manifest is parsed, the version gate passes,
354
- // and the entry-count ceiling is enforced — do we materialize the
348
+ // Only NOW, after the manifest is parsed (including its entry-count
349
+ // ceiling) and the version gate passes, do we materialize the
355
350
  // temp staging dir on disk. Doing this lazily preserves the plan
356
351
  // invariant that importers gate on runtime-version compat BEFORE
357
352
  // any state mutation. If this throws, the outer catch runs
@@ -28,6 +28,8 @@ import {
28
28
  computeLegacyManifestSha256,
29
29
  computeManifestChecksum,
30
30
  LegacyManifestSchema,
31
+ MANIFEST_MAX_BYTES,
32
+ MANIFEST_MAX_ENTRIES,
31
33
  ManifestSchema,
32
34
  type ManifestType,
33
35
  translateLegacyManifest,
@@ -74,21 +76,17 @@ export class StreamingValidationError extends Error {
74
76
  // Manifest validation
75
77
  // ---------------------------------------------------------------------------
76
78
 
77
- // Manifests are metadata only — typically tens to hundreds of KB even for
78
- // huge bundles. A 1 MiB cap is comfortably above realistic sizes and
79
- // protects against a malicious archive whose "manifest" is actually a
80
- // multi-GB stream intended to OOM the validator.
81
- const MANIFEST_MAX_BYTES = 1 * 1024 * 1024;
82
-
83
79
  /**
84
80
  * Drain the first tar entry — which MUST be `manifest.json` — and run the
85
81
  * full manifest-level validation pipeline:
86
82
  * 1. Entry name check.
87
- * 2. Size cap (1 MiB).
83
+ * 2. Size cap (`MANIFEST_MAX_BYTES`).
88
84
  * 3. JSON parse.
89
85
  * 4. Zod schema validation.
90
86
  * 5. Self-referencing `checksum` verification against the
91
87
  * canonicalized JSON (minus that field).
88
+ * 6. Entry-count ceiling on `contents` (`maxEntries`, default
89
+ * `MANIFEST_MAX_ENTRIES`).
92
90
  *
93
91
  * On success, returns the parsed manifest plus a `Map` keyed by archive
94
92
  * path that callers consult as each subsequent entry streams past.
@@ -98,7 +96,9 @@ const MANIFEST_MAX_BYTES = 1 * 1024 * 1024;
98
96
  */
99
97
  export async function readAndValidateManifest(
100
98
  first: StreamedTarEntry,
99
+ options: { maxEntries?: number } = {},
101
100
  ): Promise<ManifestReadResult> {
101
+ const maxEntries = options.maxEntries ?? MANIFEST_MAX_ENTRIES;
102
102
  if (first.header.name !== "manifest.json") {
103
103
  // Drain the body so the underlying tar extractor isn't left dangling
104
104
  // on backpressure before the caller reports the error.
@@ -186,6 +186,13 @@ export async function readAndValidateManifest(
186
186
  manifest = translateLegacyManifest(legacy);
187
187
  }
188
188
 
189
+ if (manifest.contents.length > maxEntries) {
190
+ throw new StreamingValidationError(
191
+ "bundle_too_many_entries",
192
+ `bundle contains more than ${maxEntries} entries (declared: ${manifest.contents.length})`,
193
+ );
194
+ }
195
+
189
196
  const expected = new Map<
190
197
  string,
191
198
  { sha256: string; size: number; linkTarget: string | null }
@@ -381,6 +381,33 @@ export function computeManifestChecksum(manifest: unknown): string {
381
381
  // Core validation
382
382
  // ---------------------------------------------------------------------------
383
383
 
384
+ /**
385
+ * Hard caps on `manifest.json`, shared by the buffered and streaming
386
+ * validators so preflight and import agree on what is acceptable. The two
387
+ * are sized together: the manifest carries one `contents` entry (path,
388
+ * sha256, size) per bundled file at roughly 150 bytes each, so 200,000
389
+ * entries serialize to about 32 MiB.
390
+ *
391
+ * That absorbs a workspace with a stray dependency install or checked-out
392
+ * repo while still bounding the transient memory a hostile "manifest" can
393
+ * force the importer to hold (buffer, decoded string, parsed object,
394
+ * canonical re-serialization for the self-checksum) and the per-entry work
395
+ * it commits to, on a memory-constrained pod.
396
+ */
397
+ export const MANIFEST_MAX_BYTES = 32 * 1024 * 1024;
398
+ export const MANIFEST_MAX_ENTRIES = 200_000;
399
+
400
+ export interface ValidateVBundleOptions {
401
+ /**
402
+ * Reject manifests over `MANIFEST_MAX_BYTES` or `MANIFEST_MAX_ENTRIES`.
403
+ * Preflights that precede a streaming import set this so they never
404
+ * approve a bundle the import will refuse. The buffered import path
405
+ * (`commitImport`) has no such ceilings, so it stays off there to keep
406
+ * existing large backups restorable.
407
+ */
408
+ enforceStreamingLimits?: boolean;
409
+ }
410
+
384
411
  // Only manifest.json is structurally required. The DB and config live under
385
412
  // workspace/ (new format) or data/db/ + config/ (old format) — both are valid.
386
413
  const REQUIRED_ENTRIES = ["manifest.json"];
@@ -397,7 +424,10 @@ const MAX_DECOMPRESSED_SIZE = 2 * 1024 * 1024 * 1024;
397
424
  * 3. Manifest checksum (SHA-256 of canonicalized JSON with the `checksum` field set to empty string)
398
425
  * 4. Per-file content integrity (SHA-256 of each file vs manifest declaration)
399
426
  */
400
- export function validateVBundle(data: Uint8Array): VBundleValidationResult {
427
+ export function validateVBundle(
428
+ data: Uint8Array,
429
+ { enforceStreamingLimits = false }: ValidateVBundleOptions = {},
430
+ ): VBundleValidationResult {
401
431
  const errors: ValidationError[] = [];
402
432
 
403
433
  // Step 1: Decompress gzip with size cap to prevent zip-bomb DoS
@@ -455,6 +485,17 @@ export function validateVBundle(data: Uint8Array): VBundleValidationResult {
455
485
  }
456
486
 
457
487
  // Step 4: Parse and validate manifest schema
488
+ if (
489
+ enforceStreamingLimits &&
490
+ manifestEntry.data.length > MANIFEST_MAX_BYTES
491
+ ) {
492
+ errors.push({
493
+ code: "MANIFEST_TOO_LARGE",
494
+ message: `manifest.json exceeds ${MANIFEST_MAX_BYTES} byte limit (${manifestEntry.data.length} bytes)`,
495
+ path: "manifest.json",
496
+ });
497
+ return { is_valid: false, errors };
498
+ }
458
499
  let manifestRaw: unknown;
459
500
  try {
460
501
  manifestRaw = JSON.parse(new TextDecoder().decode(manifestEntry.data));
@@ -523,6 +564,18 @@ export function validateVBundle(data: Uint8Array): VBundleValidationResult {
523
564
  manifest = translateLegacyManifest(legacy);
524
565
  }
525
566
 
567
+ if (
568
+ enforceStreamingLimits &&
569
+ manifest.contents.length > MANIFEST_MAX_ENTRIES
570
+ ) {
571
+ errors.push({
572
+ code: "MANIFEST_TOO_MANY_ENTRIES",
573
+ message: `manifest.json declares more than ${MANIFEST_MAX_ENTRIES} entries (${manifest.contents.length})`,
574
+ path: "manifest.json",
575
+ });
576
+ return { is_valid: false, errors };
577
+ }
578
+
526
579
  // Step 6: Verify per-file content integrity
527
580
  const manifestFilePaths = new Set(manifest.contents.map((f) => f.path));
528
581
 
@@ -23,7 +23,10 @@ import type { QuestionEntry } from "../api/events/question-request.js";
23
23
  import { findConversationOrSubagent } from "../daemon/conversation-registry.js";
24
24
  import type { UserDecision } from "../permissions/types.js";
25
25
  import { getLogger } from "../util/logger.js";
26
- import { broadcastMessage } from "./assistant-event-hub.js";
26
+ import {
27
+ assistantEventHub,
28
+ broadcastMessage,
29
+ } from "./assistant-event-hub.js";
27
30
 
28
31
  const log = getLogger("pending-interactions");
29
32
 
@@ -110,6 +113,12 @@ export interface PendingInteraction {
110
113
  directResolve?: (decision: UserDecision) => void;
111
114
  /** When set, the host_bash request should be routed to this specific client. */
112
115
  targetClientId?: string;
116
+ /**
117
+ * Hub connection that was active for `targetClientId` at dispatch. Results
118
+ * must arrive while this connection is still subscribed so a reconnect
119
+ * cannot submit under the same client id.
120
+ */
121
+ targetConnectionId?: string;
113
122
  /**
114
123
  * Actor principal captured at registration. Targeted requests snapshot the
115
124
  * target client's actor so a brief SSE reconnect does not 403 a legitimate
@@ -167,7 +176,16 @@ export function register(
167
176
  requestId: string,
168
177
  interaction: PendingInteraction,
169
178
  ): void {
170
- pending.set(requestId, interaction);
179
+ const targetConnectionId =
180
+ interaction.targetConnectionId ??
181
+ (interaction.targetClientId
182
+ ? assistantEventHub.getClientById?.(interaction.targetClientId)
183
+ ?.connectionId
184
+ : undefined);
185
+ pending.set(requestId, {
186
+ ...interaction,
187
+ ...(targetConnectionId != null ? { targetConnectionId } : {}),
188
+ });
171
189
  }
172
190
 
173
191
  /**
@@ -275,7 +275,7 @@ describe("contacts read API relays from the gateway", () => {
275
275
  });
276
276
 
277
277
  test("get surfaces a clean gateway not-found as a 404", async () => {
278
- ipcResult = { ok: true };
278
+ ipcResult = null;
279
279
 
280
280
  await expect(handleGetContact("missing")).rejects.toMatchObject({
281
281
  statusCode: 404,
@@ -391,7 +391,8 @@ describe("filtered/native contact reads: daemon filters, gateway hydrates teleme
391
391
  expect(contact.interactionCount).toBe(11);
392
392
  expect(contact.lastInteraction).toBe(9200);
393
393
  expect(
394
- (contact as { autoApproveThreshold?: string | null }).autoApproveThreshold,
394
+ (contact as { autoApproveThreshold?: string | null })
395
+ .autoApproveThreshold,
395
396
  ).toBe("high");
396
397
  const channel = contact.channels[0] as Record<string, unknown>;
397
398
  expect(channel.interactionCount).toBe(11);
@@ -422,7 +423,8 @@ describe("filtered/native contact reads: daemon filters, gateway hydrates teleme
422
423
  expect(contact.interactionCount).toBe(0);
423
424
  expect(contact.lastInteraction).toBeNull();
424
425
  expect(
425
- (contact as { autoApproveThreshold?: string | null }).autoApproveThreshold,
426
+ (contact as { autoApproveThreshold?: string | null })
427
+ .autoApproveThreshold,
426
428
  ).toBeNull();
427
429
  const channel = contact.channels[0] as Record<string, unknown>;
428
430
  expect(channel.interactionCount).toBe(0);
@@ -71,6 +71,7 @@ interface ListResponse {
71
71
  nextOffset: number;
72
72
  hasMore: boolean;
73
73
  groups?: unknown[];
74
+ orderedBy: string;
74
75
  }
75
76
 
76
77
  function findHandler(routes: RouteDefinition[], operationId: string) {
@@ -1380,6 +1381,41 @@ describe("GET /v1/conversations, conversationType=all", () => {
1380
1381
  ]);
1381
1382
  });
1382
1383
 
1384
+ test("files a done row by when it was marked done when that is later than its last message", async () => {
1385
+ const recent = createConversation("recent-foreground");
1386
+ const doneLater = createConversation("old-chat-marked-done-later");
1387
+ const doneEarlier = createConversation("old-chat-done-before-a-reply");
1388
+ setLastMessageAt(recent.id, 2000);
1389
+ setLastMessageAt(doneLater.id, 1000);
1390
+ setLastMessageAt(doneEarlier.id, 1500);
1391
+ rawRun(
1392
+ "test:markDone",
1393
+ "UPDATE conversations SET archived_at = ? WHERE id = ?",
1394
+ 3000,
1395
+ doneLater.id,
1396
+ );
1397
+ rawRun(
1398
+ "test:markDone",
1399
+ "UPDATE conversations SET archived_at = ? WHERE id = ?",
1400
+ 500,
1401
+ doneEarlier.id,
1402
+ );
1403
+
1404
+ const result = (await invoke({
1405
+ conversationType: "all",
1406
+ archiveStatus: "all",
1407
+ })) as ListResponse;
1408
+
1409
+ expect(result.conversations.map((c) => c.title)).toEqual([
1410
+ "old-chat-marked-done-later",
1411
+ "recent-foreground",
1412
+ "old-chat-done-before-a-reply",
1413
+ ]);
1414
+ // A client paging done rows relies on this to know the order is not
1415
+ // message recency.
1416
+ expect(result.orderedBy).toBe("lastActivity");
1417
+ });
1418
+
1383
1419
  test("excludes legacy private rows and subagent runs", async () => {
1384
1420
  createConversation("foreground-1");
1385
1421
  const legacyPrivate = createConversation("legacy-private");