@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
@@ -13,6 +13,7 @@ import {
13
13
  } from "../config/loader.js";
14
14
  import { seedInferenceProfiles } from "../config/seed-inference-profiles.js";
15
15
  import { reconcileFlagGatedProfiles } from "../config/sync-gated-profiles.js";
16
+ import { startConversationAutoArchive } from "../conversations/auto-archive.js";
16
17
  import { startCes } from "../credential-execution/ces-runtime.js";
17
18
  import { refreshManagedConnectionCache } from "../credential-execution/managed-catalog.js";
18
19
  import { startHeartbeatService } from "../heartbeat/heartbeat-service.js";
@@ -57,6 +58,7 @@ import { startScheduler } from "../schedule/scheduler.js";
57
58
  import { getSubagentManager } from "../subagent/index.js";
58
59
  import { startUsageTelemetryReporter } from "../telemetry/usage-telemetry-reporter.js";
59
60
  import { getLogger, initLogger } from "../util/logger.js";
61
+ import { DAEMON_OOM_SCORE_ADJ, setOomScoreAdj } from "../util/oom-priority.js";
60
62
  import {
61
63
  ensureDataDir,
62
64
  getDotEnvPath,
@@ -114,6 +116,9 @@ export async function runDaemon(): Promise<void> {
114
116
  // the event hub real clients subscribe to, so plugin-facing publishes made
115
117
  // here fan out locally rather than routing to a daemon over IPC.
116
118
  markCurrentProcessAsMainDaemon();
119
+ // Before the first spawn: every child inherits this value and resets its
120
+ // own, so the kernel OOM killer takes a tool or worker before the daemon.
121
+ const oomProtected = setOomScoreAdj(DAEMON_OOM_SCORE_ADJ);
117
122
 
118
123
  const startupStartedAt = Date.now();
119
124
  // dotenv loads before the first log call so the lazy root logger
@@ -121,7 +126,13 @@ export async function runDaemon(): Promise<void> {
121
126
  // whatever was in the live environment at process spawn.
122
127
  loadDotEnv();
123
128
  validateEnv();
124
- log.info({ version: APP_VERSION }, "Daemon starting");
129
+ log.info(
130
+ {
131
+ version: APP_VERSION,
132
+ oomScoreAdj: oomProtected ? DAEMON_OOM_SCORE_ADJ : undefined,
133
+ },
134
+ "Daemon starting",
135
+ );
125
136
 
126
137
  // Signal handlers install before any blocking startup work — a boot that
127
138
  // inherits a large WAL can spend minutes inside `initializeDb()`, and
@@ -860,13 +871,15 @@ export async function runDaemon(): Promise<void> {
860
871
  // CES, which the startup sequence above just brought up. Fire-and-forget:
861
872
  // the resumes run sequentially in the background while the daemon serves
862
873
  // requests; per-conversation failures are logged inside.
874
+ let startupRecovery = Promise.resolve();
863
875
  if (conversationsToResume.length > 0) {
864
876
  log.info(
865
877
  { count: conversationsToResume.length },
866
878
  "Resuming conversations interrupted by the previous process",
867
879
  );
868
- void resumeInterruptedConversations(conversationsToResume);
880
+ startupRecovery = resumeInterruptedConversations(conversationsToResume);
869
881
  }
882
+ startConversationAutoArchive(startupRecovery);
870
883
 
871
884
  log.info(
872
885
  {
@@ -19,6 +19,7 @@ import {
19
19
  parseChannelId,
20
20
  parseInterfaceId,
21
21
  } from "../channels/types.js";
22
+ import type { SupportedLocale } from "../i18n/index.js";
22
23
  import { resolveAttachmentsForPersist } from "../persistence/attachments-store.js";
23
24
  import {
24
25
  addMessage,
@@ -88,6 +89,8 @@ type ProcessMessageOptions = ConversationCreateOptions & {
88
89
  sourceChannel?: string;
89
90
  /** Originating interface (e.g. "cli", "web"). Defaults to "web". */
90
91
  sourceInterface?: string;
92
+ /** Locale for daemon-authored text emitted during this turn. */
93
+ locale?: SupportedLocale;
91
94
  /**
92
95
  * Origin tag of the turn (the conversation's `TitleOrigin`, e.g.
93
96
  * "memory_consolidation"), threaded from `runBackgroundJob`. Propagated
@@ -780,6 +783,7 @@ export async function processMessage(
780
783
  onEvent: emitEvent,
781
784
  isInteractive: options?.isInteractive ?? false,
782
785
  isUserMessage: true,
786
+ locale: options?.locale,
783
787
  ...(options?.callSite ? { callSite: options.callSite } : {}),
784
788
  ...(options?.overrideProfile
785
789
  ? { overrideProfile: options.overrideProfile }
@@ -854,6 +858,7 @@ export async function processMessageInBackground(
854
858
  onEvent: emitEvent,
855
859
  isInteractive: options?.isInteractive ?? false,
856
860
  isUserMessage: true,
861
+ locale: options?.locale,
857
862
  ...(options?.callSite ? { callSite: options.callSite } : {}),
858
863
  ...(options?.overrideProfile
859
864
  ? { overrideProfile: options.overrideProfile }
@@ -1,4 +1,5 @@
1
1
  import { disposeAcpSessionManager } from "../acp/index.js";
2
+ import { stopConversationAutoArchive } from "../conversations/auto-archive.js";
2
3
  import { stopCes } from "../credential-execution/ces-runtime.js";
3
4
  import { stopHeartbeatService } from "../heartbeat/heartbeat-service.js";
4
5
  import { stopCliIpcServer } from "../ipc/assistant-server.js";
@@ -43,6 +44,7 @@ const log = getLogger("lifecycle");
43
44
  * leaves a stale PID file or orphaned timers behind.
44
45
  */
45
46
  function stopBackgroundServicesAndCleanupPidFile(): void {
47
+ stopConversationAutoArchive();
46
48
  stopGatewayFlagListener();
47
49
  stopDiskPressureGuardForLifecycle();
48
50
  stopResourcePressureGuardForLifecycle();
@@ -63,6 +65,7 @@ async function shutdown(): Promise<void> {
63
65
  return;
64
66
  }
65
67
  shuttingDown = true;
68
+ stopConversationAutoArchive();
66
69
 
67
70
  // Force exit if graceful shutdown takes too long.
68
71
  // Set this BEFORE awaiting heartbeat stop so it covers all
@@ -23,6 +23,10 @@ const pendingFinalizations = new Map<
23
23
  { promise: Promise<void>; settle: () => void }
24
24
  >();
25
25
 
26
+ export function hasPendingTurnFinalization(conversationId: string): boolean {
27
+ return pendingFinalizations.has(conversationId);
28
+ }
29
+
26
30
  /**
27
31
  * Open the barrier for a turn that is starting. Returns the closer, which the
28
32
  * turn calls once everything the next turn must not overlap with is done.
@@ -2,9 +2,14 @@ import { readdir, readFile, readlink, realpath } from "node:fs/promises";
2
2
  import { createServer } from "node:net";
3
3
  import { join } from "node:path";
4
4
 
5
+ import { z } from "zod";
6
+
5
7
  import { shouldRestoreDesktopChromeSession } from "./desktop-chrome-session.js";
6
8
 
7
9
  export const DESKTOP_CHROME_WINDOW_CLASS = "vellum-desktop-chrome";
10
+ const DesktopVersionResponseSchema = z.object({
11
+ webSocketDebuggerUrl: z.unknown().optional(),
12
+ });
8
13
 
9
14
  export async function allocateDesktopDebugPort(): Promise<number> {
10
15
  const server = createServer();
@@ -119,9 +124,12 @@ export async function discoverDesktopBrowser(
119
124
  if (body.length > 16_384) {
120
125
  throw new Error("Desktop browser discovery exceeded its size limit");
121
126
  }
122
- const data = JSON.parse(body) as { webSocketDebuggerUrl?: unknown };
127
+ const parsed = DesktopVersionResponseSchema.safeParse(JSON.parse(body));
123
128
  await assertDesktopListener(pid, port);
124
- return validateDesktopWebSocket(data.webSocketDebuggerUrl, port);
129
+ return validateDesktopWebSocket(
130
+ parsed.success ? parsed.data.webSocketDebuggerUrl : undefined,
131
+ port,
132
+ );
125
133
  }
126
134
 
127
135
  export function desktopChromeArguments(
@@ -134,6 +142,7 @@ export function desktopChromeArguments(
134
142
  `--class=${DESKTOP_CHROME_WINDOW_CLASS}`,
135
143
  "--force-renderer-accessibility",
136
144
  "--disable-dev-shm-usage",
145
+ `--disk-cache-size=${250 * 1024 * 1024}`,
137
146
  "--disable-features=Prerender2",
138
147
  ...(shouldRestoreDesktopChromeSession(profileDir)
139
148
  ? ["--restore-last-session", "--hide-crash-restore-bubble"]
@@ -35,6 +35,7 @@ test("preserves unrelated policies and does not rewrite on repeated startup", ()
35
35
  path,
36
36
  JSON.stringify({
37
37
  CommandLineFlagSecurityWarningsEnabled: false,
38
+ PasswordManagerEnabled: false,
38
39
  DownloadRestrictions: 1,
39
40
  }),
40
41
  );
@@ -23,16 +23,22 @@ export function writeDesktopChromePolicy(policyDir: string): void {
23
23
  }
24
24
  }
25
25
  if (
26
+ policy.NetworkPredictionOptions === 2 &&
26
27
  policy.CommandLineFlagSecurityWarningsEnabled === false &&
27
- policy.PasswordManagerEnabled === false
28
+ policy.PasswordManagerEnabled === false &&
29
+ policy.HighEfficiencyModeEnabled === true &&
30
+ policy.MemorySaverModeSavings === 1
28
31
  ) {
29
32
  return;
30
33
  }
31
34
 
35
+ policy.NetworkPredictionOptions = 2;
32
36
  // This suppresses command-line warnings; Chrome's sandbox stays unchanged.
33
37
  policy.CommandLineFlagSecurityWarningsEnabled = false;
34
38
  // Suppress save prompts while keeping previously saved passwords usable.
35
39
  policy.PasswordManagerEnabled = false;
40
+ policy.HighEfficiencyModeEnabled = true;
41
+ policy.MemorySaverModeSavings = 1;
36
42
  mkdirSync(policyDir, { recursive: true });
37
43
  const staging = mkdtempSync(join(policyDir, ".vellum-desktop-"));
38
44
  try {
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, mock, spyOn, test } from "bun:test";
2
2
 
3
+ import sharp from "sharp";
4
+
3
5
  import { run as click } from "../config/bundled-skills/computer-use/tools/computer-use-click.js";
4
6
  import * as configLoader from "../config/loader.js";
5
7
  import { HostCuProxy } from "../daemon/host-cu-proxy.js";
@@ -14,12 +16,20 @@ import {
14
16
  } from "./desktop-computer-use.js";
15
17
  import * as sessionManager from "./desktop-session-manager.js";
16
18
 
19
+ const SCREENSHOT = (
20
+ await sharp({
21
+ create: { width: 1440, height: 810, channels: 3, background: "white" },
22
+ })
23
+ .jpeg()
24
+ .toBuffer()
25
+ ).toString("base64");
26
+
17
27
  function driver(userGuidance?: string) {
18
28
  return {
19
29
  input: mock(async (_args: string[], _signal?: AbortSignal) => {}),
20
30
  capture: mock(async (_signal: AbortSignal) => ({
21
31
  userGuidance,
22
- screenshot: "anBlZw==",
32
+ screenshot: SCREENSHOT,
23
33
  screenshotWidthPx: 1440,
24
34
  screenshotHeightPx: 810,
25
35
  screenWidthPt: 1440,
@@ -52,7 +62,7 @@ describe("virtual desktop computer use", () => {
52
62
  signal(),
53
63
  backend,
54
64
  );
55
- expect(result.screenshot).toBe("anBlZw==");
65
+ expect(result.screenshot).toBe(SCREENSHOT);
56
66
  }
57
67
  expect(backend.input).not.toHaveBeenCalled();
58
68
  expect(backend.capture).toHaveBeenCalledTimes(2);
@@ -161,7 +171,7 @@ describe("virtual desktop computer use", () => {
161
171
  );
162
172
  expect(backend.input).toHaveBeenCalledTimes(1);
163
173
  expect(result.executionError).toBe("Input failed");
164
- expect(result.screenshot).toBe("anBlZw==");
174
+ expect(result.screenshot).toBe(SCREENSHOT);
165
175
  });
166
176
 
167
177
  test("done releases only its conversation without starting the desktop", async () => {
@@ -316,7 +326,7 @@ describe("virtual desktop computer use", () => {
316
326
  source: {
317
327
  type: "base64",
318
328
  media_type: "image/jpeg",
319
- data: "anBlZw==",
329
+ data: SCREENSHOT,
320
330
  },
321
331
  },
322
332
  ]);
@@ -15,6 +15,7 @@ import { getIsContainerized } from "../config/env-registry.js";
15
15
  import { connectCdpWsTransport } from "../tools/browser/cdp-client/cdp-inspect/ws-transport.js";
16
16
  import { terminateProcessTree } from "../util/host-process.js";
17
17
  import { getLogger } from "../util/logger.js";
18
+ import { CHILD_OOM_SCORE_ADJ, withOomScoreAdj } from "../util/oom-priority.js";
18
19
  import { getDataDir, getWorkspaceDir } from "../util/platform.js";
19
20
  import { sleep } from "../util/retry.js";
20
21
  import { DesktopBrowserClient } from "./desktop-browser-client.js";
@@ -46,7 +47,7 @@ const log = getLogger("desktop-session");
46
47
 
47
48
  export const DESKTOP_VNC_PORT = 5999;
48
49
  const DESKTOP_GEOMETRY = `${DESKTOP_WIDTH}x${DESKTOP_HEIGHT}`;
49
- const DESKTOP_LINGER_MS = 24 * 60 * 60_000;
50
+ const DESKTOP_LINGER_MS = 8 * 60 * 60_000;
50
51
  const VNC_READY_DEADLINE_MS = 10_000;
51
52
  const VNC_PROBE_INTERVAL_MS = 100;
52
53
  const KILL_GRACE_MS = 2_000;
@@ -928,8 +929,9 @@ function spawnDetached(
928
929
  request: DesktopSpawnRequest,
929
930
  ): DesktopChild {
930
931
  // Each child leads its own process group so teardown can kill everything
931
- // it forked (Chrome's renderers, openbox's autostart) in one signal.
932
- return Bun.spawn([...request.cmd], {
932
+ // it forked (Chrome's renderers, openbox's autostart) in one signal. The
933
+ // OOM wrapper execs in place, so pid and process group are unchanged.
934
+ return Bun.spawn(withOomScoreAdj(request.cmd, CHILD_OOM_SCORE_ADJ), {
933
935
  env: request.env,
934
936
  detached: true,
935
937
  stdio: ["ignore", "ignore", "ignore"],
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Document revision history: snapshots of a document's title and body as they
3
+ * stood at a revision, taken just before a write replaces that state.
4
+ *
5
+ * Snapshot writes run inside the caller's transaction, next to the document
6
+ * write they precede, so a snapshot exists exactly when its write landed.
7
+ */
8
+ import { rawAll, rawGet, rawRun } from "../persistence/raw-query.js";
9
+
10
+ /** Who made the write a snapshot was taken ahead of. */
11
+ export type DocumentRevisionAuthor = "user" | "assistant";
12
+
13
+ /** Snapshots kept per document; older ones are pruned on each insert. */
14
+ export const MAX_REVISIONS_PER_DOCUMENT = 100;
15
+
16
+ /**
17
+ * A user save snapshots the prior state at most this often per document,
18
+ * unless the latest snapshot preceded an assistant edit.
19
+ */
20
+ export const USER_SNAPSHOT_INTERVAL_MS = 10 * 60 * 1000;
21
+
22
+ export interface DocumentRevisionSummary {
23
+ revision: number;
24
+ author: DocumentRevisionAuthor;
25
+ createdAt: number;
26
+ wordCount: number;
27
+ title: string;
28
+ }
29
+
30
+ export interface DocumentRevisionRecord extends DocumentRevisionSummary {
31
+ content: string;
32
+ }
33
+
34
+ /** The document state a snapshot preserves. */
35
+ export interface DocumentSnapshotSource {
36
+ surfaceId: string;
37
+ revision: number;
38
+ title: string;
39
+ content: string;
40
+ wordCount: number;
41
+ }
42
+
43
+ interface RevisionRow {
44
+ revision: number;
45
+ author: DocumentRevisionAuthor;
46
+ created_at: number;
47
+ word_count: number;
48
+ title: string;
49
+ content: string;
50
+ }
51
+
52
+ function mapRowToSummary(
53
+ row: Omit<RevisionRow, "content">,
54
+ ): DocumentRevisionSummary {
55
+ return {
56
+ revision: row.revision,
57
+ author: row.author,
58
+ createdAt: row.created_at,
59
+ wordCount: row.word_count,
60
+ title: row.title,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Store `state` as the snapshot for its revision and prune the document's
66
+ * history to the newest {@link MAX_REVISIONS_PER_DOCUMENT}. A revision that is
67
+ * already snapshotted keeps its first snapshot.
68
+ */
69
+ export function recordDocumentSnapshot(
70
+ state: DocumentSnapshotSource,
71
+ author: DocumentRevisionAuthor,
72
+ ): void {
73
+ rawRun(
74
+ "documentRevisions:record",
75
+ /*sql*/ `INSERT OR IGNORE INTO document_revisions (surface_id, revision, title, content, word_count, author, created_at)
76
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
77
+ state.surfaceId,
78
+ state.revision,
79
+ state.title,
80
+ state.content,
81
+ state.wordCount,
82
+ author,
83
+ Date.now(),
84
+ );
85
+ rawRun(
86
+ "documentRevisions:prune",
87
+ /*sql*/ `DELETE FROM document_revisions
88
+ WHERE surface_id = ?
89
+ AND revision NOT IN (
90
+ SELECT revision FROM document_revisions
91
+ WHERE surface_id = ?
92
+ ORDER BY revision DESC
93
+ LIMIT ?
94
+ )`,
95
+ state.surfaceId,
96
+ state.surfaceId,
97
+ MAX_REVISIONS_PER_DOCUMENT,
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Whether a user save should snapshot the state it replaces: when the document
103
+ * has no snapshot yet, when the latest one is older than
104
+ * {@link USER_SNAPSHOT_INTERVAL_MS}, or when the latest one preceded an
105
+ * assistant edit, so the assistant's result survives the user's next edits.
106
+ */
107
+ export function shouldSnapshotBeforeUserSave(surfaceId: string): boolean {
108
+ const latest = rawGet<{ author: string; created_at: number }>(
109
+ "documentRevisions:latest",
110
+ /*sql*/ `SELECT author, created_at FROM document_revisions
111
+ WHERE surface_id = ?
112
+ ORDER BY revision DESC
113
+ LIMIT 1`,
114
+ surfaceId,
115
+ );
116
+ if (!latest) {
117
+ return true;
118
+ }
119
+ return (
120
+ latest.author === "assistant" ||
121
+ Date.now() - latest.created_at >= USER_SNAPSHOT_INTERVAL_MS
122
+ );
123
+ }
124
+
125
+ /** A document's snapshots, newest first, without their bodies. */
126
+ export function listDocumentRevisions(
127
+ surfaceId: string,
128
+ ): DocumentRevisionSummary[] {
129
+ return rawAll<Omit<RevisionRow, "content">>(
130
+ "documentRevisions:list",
131
+ /*sql*/ `SELECT revision, author, created_at, word_count, title
132
+ FROM document_revisions
133
+ WHERE surface_id = ?
134
+ ORDER BY revision DESC`,
135
+ surfaceId,
136
+ ).map(mapRowToSummary);
137
+ }
138
+
139
+ /** One snapshot with its body, or `null` when it does not exist. */
140
+ export function getDocumentRevision(
141
+ surfaceId: string,
142
+ revision: number,
143
+ ): DocumentRevisionRecord | null {
144
+ const row = rawGet<RevisionRow>(
145
+ "documentRevisions:get",
146
+ /*sql*/ `SELECT revision, author, created_at, word_count, title, content
147
+ FROM document_revisions
148
+ WHERE surface_id = ? AND revision = ?`,
149
+ surfaceId,
150
+ revision,
151
+ );
152
+ return row ? { ...mapRowToSummary(row), content: row.content } : null;
153
+ }