@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
@@ -46,6 +46,10 @@ const FIRECRAWL_API_URL = "https://api.firecrawl.dev/v2/search";
46
46
  const FASTCRW_SEARCH_PATH = "/v1/search";
47
47
  const TINYFISH_DEFAULT_SEARCH_API_BASE = "https://api.search.tinyfish.ai";
48
48
  const EXA_API_URL = "https://api.exa.ai/search";
49
+ const SERPLY_API_URL = "https://api.serply.io/v1/search";
50
+ // Serply returns at most 10 organic results per call; larger counts are
51
+ // clamped on request and trimmed on response.
52
+ const SERPLY_MAX_RESULTS = 10;
49
53
  // Keenable is keyless by default: the public path needs no key (rate-limited);
50
54
  // a key switches to the authenticated path and lifts the cap.
51
55
  const KEENABLE_API_BASE_URL = "https://api.keenable.ai";
@@ -61,7 +65,8 @@ type WebSearchProvider =
61
65
  | "fastcrw"
62
66
  | "searxng"
63
67
  | "tinyfish"
64
- | "exa";
68
+ | "exa"
69
+ | "serply";
65
70
 
66
71
  /**
67
72
  * Arguments passed to every {@link WebSearchAdapter}. The full superset is
@@ -213,6 +218,19 @@ interface ExaSearchResponse {
213
218
  results?: ExaSearchResult[];
214
219
  }
215
220
 
221
+ interface SerplySearchResult {
222
+ title?: string | null;
223
+ link?: string;
224
+ description?: string | null;
225
+ position?: number;
226
+ }
227
+
228
+ interface SerplySearchResponse {
229
+ query?: string;
230
+ total?: number;
231
+ results?: SerplySearchResult[];
232
+ }
233
+
216
234
  const SEARXNG_MISSING_INSTANCE_MESSAGE =
217
235
  "SearXNG needs an instance URL. Set API Base in Settings under Web Search.";
218
236
  const SEARXNG_JSON_DISABLED_MESSAGE =
@@ -236,8 +254,11 @@ function getWebSearchProvider(): WebSearchProvider {
236
254
  async function getApiKey(
237
255
  provider: WebSearchProvider,
238
256
  ): Promise<string | undefined> {
239
- const adapter = WEB_SEARCH_ADAPTERS[provider];
240
- return (await getProviderKeyAsync(adapter.providerKeyName)) ?? undefined;
257
+ return (
258
+ (await getProviderKeyAsync(
259
+ WEB_SEARCH_ADAPTERS[provider].providerKeyName,
260
+ )) ?? undefined
261
+ );
241
262
  }
242
263
 
243
264
  function fallbackProvidersFor(
@@ -810,6 +831,81 @@ function exaStartPublishedDateForFreshness(
810
831
  return new Date(now - days * 24 * 60 * 60 * 1000).toISOString();
811
832
  }
812
833
 
834
+ function formatSerplyResults(
835
+ data: SerplySearchResponse,
836
+ query: string,
837
+ ): string {
838
+ const results = data.results ?? [];
839
+ if (results.length === 0) {
840
+ return `No results found for "${query}".`;
841
+ }
842
+
843
+ const lines: string[] = [`Web search results for "${query}":\n`];
844
+ for (let i = 0; i < results.length; i++) {
845
+ const result = results[i];
846
+ const title =
847
+ result.title?.trim() || result.link?.trim() || "Untitled result";
848
+ lines.push(`${i + 1}. ${title}`);
849
+ if (result.link) {
850
+ lines.push(` URL: ${result.link}`);
851
+ }
852
+ const snippet = result.description?.trim();
853
+ if (snippet) {
854
+ lines.push(` ${snippet}`);
855
+ }
856
+ lines.push("");
857
+ }
858
+ return lines.join("\n");
859
+ }
860
+
861
+ function buildSerplyMetadata(
862
+ data: SerplySearchResponse,
863
+ query: string,
864
+ durationMs: number,
865
+ ): WebSearchMetadata {
866
+ const results = data.results ?? [];
867
+ const items: WebSearchResultItem[] = results.map((result, index) => {
868
+ const url = result.link ?? "";
869
+ const domain = extractDomain(url);
870
+ return {
871
+ rank: index + 1,
872
+ title: result.title?.trim() || url.trim() || "Untitled result",
873
+ url,
874
+ domain,
875
+ faviconUrl: faviconUrlForDomain(domain),
876
+ snippet: result.description?.trim() || undefined,
877
+ };
878
+ });
879
+ return {
880
+ query,
881
+ provider: "serply",
882
+ resultCount: items.length,
883
+ durationMs,
884
+ results: items,
885
+ };
886
+ }
887
+
888
+ /**
889
+ * Serply passes Google's `tbs` time filter through unchanged, so the tool's
890
+ * freshness buckets map onto the `qdr:` values Google understands.
891
+ */
892
+ function serplyTbsForFreshness(
893
+ freshness: string | undefined,
894
+ ): string | undefined {
895
+ switch (freshness) {
896
+ case "pd":
897
+ return "qdr:d";
898
+ case "pw":
899
+ return "qdr:w";
900
+ case "pm":
901
+ return "qdr:m";
902
+ case "py":
903
+ return "qdr:y";
904
+ default:
905
+ return undefined;
906
+ }
907
+ }
908
+
813
909
  function tinyfishRecencyMinutesForFreshness(
814
910
  freshness: string | undefined,
815
911
  ): number | undefined {
@@ -1517,9 +1613,8 @@ function executeFastcrwSearch(
1517
1613
  apiKey: string,
1518
1614
  signal?: AbortSignal,
1519
1615
  ): Promise<ToolExecutionResult> {
1520
- const apiBase = getConfig().services["web-search"]?.apiBase;
1521
1616
  const endpoint = resolveProviderApiUrl(
1522
- apiBase,
1617
+ getConfig().services["web-search"]?.apiBase,
1523
1618
  FASTCRW_SEARCH_PATH,
1524
1619
  FASTCRW_DEFAULT_API_BASE,
1525
1620
  );
@@ -1779,9 +1874,8 @@ async function executeTinyfishSearch(
1779
1874
  signal?: AbortSignal,
1780
1875
  ): Promise<ToolExecutionResult> {
1781
1876
  const startedAt = Date.now();
1782
- const apiBase = getConfig().services["web-search"]?.apiBase;
1783
1877
  const endpoint = resolveProviderApiUrl(
1784
- apiBase,
1878
+ getConfig().services["web-search"]?.apiBase,
1785
1879
  "/",
1786
1880
  TINYFISH_DEFAULT_SEARCH_API_BASE,
1787
1881
  );
@@ -2023,6 +2117,128 @@ async function executeExaSearch(
2023
2117
  );
2024
2118
  }
2025
2119
 
2120
+ async function executeSerplySearch(
2121
+ query: string,
2122
+ count: number,
2123
+ freshness: string | undefined,
2124
+ apiKey: string,
2125
+ signal?: AbortSignal,
2126
+ ): Promise<ToolExecutionResult> {
2127
+ const startedAt = Date.now();
2128
+ const params = new URLSearchParams({
2129
+ q: query,
2130
+ num: String(Math.min(count, SERPLY_MAX_RESULTS)),
2131
+ });
2132
+ const tbs = serplyTbsForFreshness(freshness);
2133
+ if (tbs !== undefined) {
2134
+ params.set("tbs", tbs);
2135
+ }
2136
+ const url = `${SERPLY_API_URL}?${params.toString()}`;
2137
+
2138
+ const headers = {
2139
+ Accept: "application/json",
2140
+ "X-Api-Key": apiKey.trim(),
2141
+ "User-Agent": "vellum-assistant",
2142
+ };
2143
+
2144
+ for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
2145
+ let response: Response;
2146
+ try {
2147
+ response = await fetch(url, { method: "GET", headers, signal });
2148
+ } catch (err) {
2149
+ return networkFailureResult(query, "serply", startedAt, err, signal);
2150
+ }
2151
+
2152
+ const bodyText = await response.text();
2153
+ if (response.ok) {
2154
+ let data: SerplySearchResponse;
2155
+ try {
2156
+ data = JSON.parse(bodyText) as SerplySearchResponse;
2157
+ } catch {
2158
+ return errorResult(
2159
+ query,
2160
+ "serply",
2161
+ startedAt,
2162
+ "Serply Search returned an invalid JSON payload.",
2163
+ );
2164
+ }
2165
+ if (data.results && data.results.length > count) {
2166
+ data.results = data.results.slice(0, count);
2167
+ }
2168
+ const durationMs = Date.now() - startedAt;
2169
+ return {
2170
+ content:
2171
+ wrapUntrustedContent(formatSerplyResults(data, query), {
2172
+ source: "search",
2173
+ sourceDetail: "serply",
2174
+ }) + CITATION_INSTRUCTION,
2175
+ isError: false,
2176
+ activityMetadata: {
2177
+ webSearch: buildSerplyMetadata(data, query, durationMs),
2178
+ },
2179
+ };
2180
+ }
2181
+
2182
+ if (response.status === 401) {
2183
+ return errorResult(
2184
+ query,
2185
+ "serply",
2186
+ startedAt,
2187
+ "Invalid or expired Serply API key",
2188
+ );
2189
+ }
2190
+ if (response.status === 402) {
2191
+ return errorResult(
2192
+ query,
2193
+ "serply",
2194
+ startedAt,
2195
+ "Serply account has no remaining credits.",
2196
+ );
2197
+ }
2198
+ if (response.status === 403) {
2199
+ return errorResult(
2200
+ query,
2201
+ "serply",
2202
+ startedAt,
2203
+ "Serply Search request was forbidden by the upstream service.",
2204
+ );
2205
+ }
2206
+
2207
+ if (response.status === 429 && attempt < DEFAULT_MAX_RETRIES) {
2208
+ const delayMs = getHttpRetryDelay(
2209
+ response,
2210
+ attempt,
2211
+ DEFAULT_BASE_DELAY_MS,
2212
+ );
2213
+ log.warn(
2214
+ { attempt: attempt + 1, delayMs },
2215
+ "Serply Search rate limited, retrying",
2216
+ );
2217
+ await sleep(delayMs);
2218
+ continue;
2219
+ }
2220
+
2221
+ log.warn({ status: response.status }, "Serply Search API error");
2222
+ return backendFailureResult(
2223
+ query,
2224
+ "serply",
2225
+ startedAt,
2226
+ { statusCode: response.status, error: rawBodyDetail(bodyText) },
2227
+ response.status === 429
2228
+ ? "Serply Search rate limit exceeded after retries. Try again shortly."
2229
+ : `Serply Search API returned status ${response.status}`,
2230
+ );
2231
+ }
2232
+
2233
+ return backendFailureResult(
2234
+ query,
2235
+ "serply",
2236
+ startedAt,
2237
+ { statusCode: 429 },
2238
+ "Serply Search rate limit exceeded after retries. Try again shortly.",
2239
+ );
2240
+ }
2241
+
2026
2242
  // ----------------------------------------------------------------------------
2027
2243
  // Adapter registry
2028
2244
  //
@@ -2114,6 +2330,14 @@ const exaSearchAdapter: WebSearchAdapter = {
2114
2330
  executeExaSearch(query, count, freshness, apiKey, signal),
2115
2331
  };
2116
2332
 
2333
+ const serplySearchAdapter: WebSearchAdapter = {
2334
+ id: "serply",
2335
+ providerKeyName: "serply",
2336
+ fallbackOrder: 10,
2337
+ execute: ({ query, count, freshness, apiKey, signal }) =>
2338
+ executeSerplySearch(query, count, freshness, apiKey, signal),
2339
+ };
2340
+
2117
2341
  /**
2118
2342
  * All built-in web-search adapters keyed by provider id. The
2119
2343
  * `Record<WebSearchProvider, ...>` shape forces TypeScript to flag any
@@ -2129,6 +2353,7 @@ const WEB_SEARCH_ADAPTERS: Record<WebSearchProvider, WebSearchAdapter> = {
2129
2353
  searxng: searxngSearchAdapter,
2130
2354
  tinyfish: tinyfishSearchAdapter,
2131
2355
  exa: exaSearchAdapter,
2356
+ serply: serplySearchAdapter,
2132
2357
  };
2133
2358
 
2134
2359
  /**
@@ -2160,7 +2385,7 @@ export const webSearchTool = {
2160
2385
  count: {
2161
2386
  type: "number",
2162
2387
  description:
2163
- "Number of results to return (1-20, default 10). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, TinyFish, and Exa providers.",
2388
+ "Number of results to return (1-20, default 10). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, TinyFish, Exa, and Serply providers.",
2164
2389
  },
2165
2390
  offset: {
2166
2391
  type: "number",
@@ -2170,7 +2395,7 @@ export const webSearchTool = {
2170
2395
  freshness: {
2171
2396
  type: "string",
2172
2397
  description:
2173
- 'Filter by recency: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, TinyFish, and Exa providers. SearXNG maps day/month/year and omits week.',
2398
+ 'Filter by recency: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, TinyFish, Exa, and Serply providers. SearXNG maps day/month/year and omits week.',
2174
2399
  },
2175
2400
  },
2176
2401
  required: ["query"],
@@ -2274,8 +2499,10 @@ export const webSearchTool = {
2274
2499
  // `mode: "managed"` did for installs that never configured a search
2275
2500
  // key. Read the configured provider off config — the local `provider`
2276
2501
  // has already been coerced to "perplexity".
2277
- const configured = getConfig().services["web-search"].provider;
2278
- if (configured === "inference-provider-native") {
2502
+ if (
2503
+ getConfig().services["web-search"].provider ===
2504
+ "inference-provider-native"
2505
+ ) {
2279
2506
  const { managedSearchAvailable } =
2280
2507
  await import("./managed-search-proxy.js");
2281
2508
  if (await managedSearchAvailable()) {
@@ -2286,7 +2513,7 @@ export const webSearchTool = {
2286
2513
  query,
2287
2514
  provider,
2288
2515
  startedAt,
2289
- "No web search API key configured. Set it via `keys set perplexity <key>`, `keys set brave <key>`, `keys set tavily <key>`, `keys set firecrawl <key>`, or `keys set fastcrw <key>`, or `keys set tinyfish <key>`, or `keys set exa <key>`, or configure it from the Settings page under API Keys. Or switch the web-search provider to Keenable, which works without a key, or SearXNG with your instance URL.",
2516
+ "No web search API key configured. Set it via `keys set perplexity <key>`, `keys set brave <key>`, `keys set tavily <key>`, `keys set firecrawl <key>`, or `keys set fastcrw <key>`, or `keys set tinyfish <key>`, or `keys set exa <key>`, or `keys set serply <key>`, or configure it from the Settings page under API Keys. Or switch the web-search provider to Keenable, which works without a key, or SearXNG with your instance URL.",
2290
2517
  );
2291
2518
  }
2292
2519
  }
@@ -23,6 +23,5 @@ export function formatWriteSummary(
23
23
  if (isNewFile) {
24
24
  return `(new file, ${newLineCount} line${newLineCount !== 1 ? "s" : ""})`;
25
25
  }
26
- const oldLineCount = oldContent.split("\n").length;
27
- return `(${oldLineCount} → ${newLineCount} lines)`;
26
+ return `(${oldContent.split("\n").length} → ${newLineCount} lines)`;
28
27
  }
@@ -112,3 +112,45 @@ describe("formatShellOutput truncation", () => {
112
112
  expect(result.content).toContain(OUTPUT_TRUNCATED_TAG);
113
113
  });
114
114
  });
115
+
116
+ describe("formatShellOutput kill reporting", () => {
117
+ test("names the signal and hints at memory for a SIGKILL", () => {
118
+ const result = formatShellOutput("", "", null, false, 120, {
119
+ signal: "SIGKILL",
120
+ });
121
+ expect(result.isError).toBe(true);
122
+ expect(result.content).toContain('<command_killed signal="SIGKILL" />');
123
+ expect(result.content).toContain("out of memory");
124
+ expect(result.content).not.toContain('code="null"');
125
+ expect(result.status).toBe('<command_killed signal="SIGKILL" />');
126
+ });
127
+
128
+ test("keeps the output produced before the kill", () => {
129
+ const result = formatShellOutput("partial", "", null, false, 120, {
130
+ signal: "SIGTERM",
131
+ });
132
+ expect(result.content.startsWith("partial\n")).toBe(true);
133
+ expect(result.content).toContain("killed by SIGTERM");
134
+ expect(result.content).not.toContain("out of memory");
135
+ });
136
+
137
+ test("does not infer a kill from a 128 + n exit code", () => {
138
+ const result = formatShellOutput("", "Killed", 137, false, 120);
139
+ expect(result.content).toBe("Killed");
140
+ expect(result.status).toBe('<command_exit code="137" />');
141
+ });
142
+
143
+ test("leaves a timeout framed as a timeout", () => {
144
+ const result = formatShellOutput("", "", null, true, 120, {
145
+ signal: "SIGKILL",
146
+ });
147
+ expect(result.content).toContain("<command_timeout");
148
+ expect(result.content).not.toContain("command_killed");
149
+ });
150
+
151
+ test("leaves an ordinary failure unchanged", () => {
152
+ const result = formatShellOutput("", "boom", 1, false, 120);
153
+ expect(result.content).toBe("boom");
154
+ expect(result.status).toBe('<command_exit code="1" />');
155
+ });
156
+ });
@@ -13,6 +13,15 @@ export interface ShellOutputResult {
13
13
  isError: boolean;
14
14
  }
15
15
 
16
+ function describeKill(signal: NodeJS.Signals): string {
17
+ const tag = `<command_killed signal="${signal}" />`;
18
+ const hint =
19
+ signal === "SIGKILL"
20
+ ? " A SIGKILL the command did not send itself usually means the system ran out of memory."
21
+ : "";
22
+ return `${tag}\nCommand was killed by ${signal} before it finished.${hint}`;
23
+ }
24
+
16
25
  type StdioStream = "stdout" | "stderr";
17
26
 
18
27
  type DataListener = (data: Buffer | string) => void;
@@ -34,7 +43,11 @@ export function formatShellOutput(
34
43
  code: number | null,
35
44
  timedOut: boolean,
36
45
  timeoutSec: number,
37
- options?: { truncated?: boolean; started?: boolean },
46
+ options?: {
47
+ truncated?: boolean;
48
+ started?: boolean;
49
+ signal?: NodeJS.Signals | null;
50
+ },
38
51
  ): ShellOutputResult {
39
52
  if (options?.started === false) {
40
53
  return {
@@ -66,6 +79,12 @@ export function formatShellOutput(
66
79
  statusParts.push(OUTPUT_TRUNCATED_TAG);
67
80
  }
68
81
 
82
+ const killSignal = timedOut ? null : (options?.signal ?? null);
83
+ if (killSignal) {
84
+ output += (output ? "\n" : "") + describeKill(killSignal);
85
+ statusParts.push(`<command_killed signal="${killSignal}" />`);
86
+ }
87
+
69
88
  if (!output.trim()) {
70
89
  if (code === 0) {
71
90
  output = "<command_completed />";
@@ -74,7 +93,7 @@ export function formatShellOutput(
74
93
  output = `${exitTag}\nCommand failed with exit code ${code}. No stdout or stderr output was produced.`;
75
94
  statusParts.push(exitTag);
76
95
  }
77
- } else if (code !== 0 && !timedOut) {
96
+ } else if (code !== 0 && !timedOut && !killSignal) {
78
97
  statusParts.push(`<command_exit code="${code}" />`);
79
98
  }
80
99
 
@@ -148,7 +167,7 @@ export class BoundedStdioCollector {
148
167
  code: number | null,
149
168
  timedOut: boolean,
150
169
  timeoutSec: number,
151
- options?: { started?: boolean },
170
+ options?: { started?: boolean; signal?: NodeJS.Signals | null },
152
171
  ): ShellOutputResult {
153
172
  return formatShellOutput(
154
173
  Buffer.concat(this.stdoutParts).toString(),
@@ -156,7 +175,11 @@ export class BoundedStdioCollector {
156
175
  code,
157
176
  timedOut,
158
177
  timeoutSec,
159
- { truncated: this.truncated, started: options?.started },
178
+ {
179
+ truncated: this.truncated,
180
+ started: options?.started,
181
+ signal: options?.signal,
182
+ },
160
183
  );
161
184
  }
162
185
  }
@@ -1,3 +1,5 @@
1
+ import { z } from "zod";
2
+
1
3
  import { RiskLevel } from "../../permissions/types.js";
2
4
  import { isUnparseableToolArgs } from "../../providers/unparseable-tool-args.js";
3
5
  import { declareDaemonActivityField } from "../schema-transforms.js";
@@ -9,6 +11,9 @@ import type {
9
11
 
10
12
  /** Envelope keys consumed by `skill_execute` itself, never inner-tool params. */
11
13
  const SKILL_EXECUTE_ENVELOPE_KEYS = new Set(["tool", "input", "activity"]);
14
+ const recoveredSkillExecuteEnvelopeSchema = z
15
+ .object({ tool: z.string() })
16
+ .passthrough();
12
17
 
13
18
  /**
14
19
  * Recover a `skill_execute` envelope that the provider layer wrapped under the
@@ -30,13 +35,11 @@ export function recoverSkillExecuteEnvelope(
30
35
  return envelope;
31
36
  }
32
37
  try {
33
- const parsed: unknown = JSON.parse(envelope._raw);
34
- if (
35
- parsed != null &&
36
- typeof parsed === "object" &&
37
- !Array.isArray(parsed)
38
- ) {
39
- return parsed as Record<string, unknown>;
38
+ const recovered = recoveredSkillExecuteEnvelopeSchema.safeParse(
39
+ JSON.parse(envelope._raw),
40
+ );
41
+ if (recovered.success) {
42
+ return recovered.data;
40
43
  }
41
44
  } catch {
42
45
  // Genuinely malformed/truncated — leave wrapped for the retryable error.
@@ -374,6 +374,7 @@ export const shellTool = {
374
374
 
375
375
  const fmtResult = collector.format(code, timedOut, timeoutSec, {
376
376
  started: launch.didStart(),
377
+ signal: aborted ? null : signal,
377
378
  });
378
379
 
379
380
  const status: BackgroundToolCompletedEvent["status"] = aborted
@@ -531,7 +532,7 @@ export const shellTool = {
531
532
  // -----------------------------------------------------------------------
532
533
  // Foreground mode: await the process and return its output.
533
534
  // -----------------------------------------------------------------------
534
- const result = await new Promise<ToolExecutionResult>((resolve) => {
535
+ return await new Promise<ToolExecutionResult>((resolve) => {
535
536
  let timedOut = false;
536
537
  const startedAt = Date.now();
537
538
 
@@ -585,6 +586,7 @@ export const shellTool = {
585
586
 
586
587
  const fmtResult = collector.format(code, timedOut, timeoutSec, {
587
588
  started: launch.didStart(),
589
+ signal: context.signal?.aborted ? null : signal,
588
590
  });
589
591
 
590
592
  resolve({
@@ -620,8 +622,6 @@ export const shellTool = {
620
622
  });
621
623
  });
622
624
  });
623
-
624
- return result;
625
625
  },
626
626
  } satisfies ToolDefinition;
627
627
 
@@ -27,6 +27,7 @@ import {
27
27
  isWorkspaceWriteTool,
28
28
  } from "../permissions/workspace-policy.js";
29
29
  import {
30
+ boundUnparseableToolArgs,
30
31
  isUnparseableToolArgs,
31
32
  unparseableToolArgsMessage,
32
33
  } from "../providers/unparseable-tool-args.js";
@@ -797,7 +798,14 @@ export class ToolApprovalHandler {
797
798
  // mangled. Fail loudly instead so the model retries.
798
799
  if (isUnparseableToolArgs(input)) {
799
800
  const msg = unparseableToolArgsMessage(name, input._raw);
800
- this.auditGateError(context, name, input, riskLevel, startTime, msg);
801
+ this.auditGateError(
802
+ context,
803
+ name,
804
+ boundUnparseableToolArgs(input),
805
+ riskLevel,
806
+ startTime,
807
+ msg,
808
+ );
801
809
  return { allowed: false, result: { content: msg, isError: true } };
802
810
  }
803
811
 
@@ -302,8 +302,7 @@ function collectSurfaceFillOnTextProblems(html: string): string[] {
302
302
  const offenders: string[] = [];
303
303
  for (const rule of html.matchAll(/([^{}<>]{1,120})\{([^{}]*)\}/g)) {
304
304
  const selector = rule[1].trim();
305
- const body = rule[2];
306
- if (!/fill\s*:\s*var\(--surface-/.test(body)) {
305
+ if (!/fill\s*:\s*var\(--surface-/.test(rule[2])) {
307
306
  continue;
308
307
  }
309
308
  if (SHAPE_SCOPED_SELECTOR_PATTERN.test(selector)) {
@@ -10,11 +10,15 @@ import { safeStringSlice } from "../util/unicode.js";
10
10
 
11
11
  const MAX_METADATA_VALUE_LENGTH = 128;
12
12
 
13
+ /** `profileSource` reported for a turn whose profile the Auto router chose. */
14
+ export const AUTO_PROFILE_SOURCE = "auto";
15
+
13
16
  export type UsageAttributionProfileSource =
14
17
  | "call_site"
15
18
  | "conversation"
16
19
  | "active"
17
20
  | "default"
21
+ | typeof AUTO_PROFILE_SOURCE
18
22
  | "unknown";
19
23
 
20
24
  export interface UsageAttributionInput {
@@ -27,6 +31,12 @@ export interface UsageAttributionInput {
27
31
  */
28
32
  profileResolutionCallSite?: LLMCallSite | null;
29
33
  overrideProfile?: string | null;
34
+ /**
35
+ * Who chose `overrideProfile`. `"auto"` marks a profile the Auto profile's
36
+ * router picked for the turn, so usage rows can tell an Auto pick from a
37
+ * user pin that landed on the same profile. Absent for a user pin.
38
+ */
39
+ overrideProfileOrigin?: "auto" | null;
30
40
  /**
31
41
  * Mirrors `ResolveCallSiteOpts.forceOverrideProfile`: the override profile
32
42
  * was floated above the call-site layers for this request, so attribution
@@ -227,9 +237,16 @@ function appliedProfileFromWinnerSelection(
227
237
  call_site: "call_site",
228
238
  default: "default",
229
239
  } as const;
240
+ // An override the Auto router chose is reported as its own source: the
241
+ // applied profile is the routed arm, so without this a routed turn would
242
+ // be indistinguishable from a user who pinned that arm.
243
+ const profileSource =
244
+ selection.source === "override" && input.overrideProfileOrigin === "auto"
245
+ ? AUTO_PROFILE_SOURCE
246
+ : sourceBySelection[selection.source];
230
247
  return {
231
248
  appliedProfile: selection.profileName,
232
- profileSource: sourceBySelection[selection.source],
249
+ profileSource,
233
250
  };
234
251
  }
235
252
 
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, test } from "bun:test";
5
+
6
+ import { setOomScoreAdj, withOomScoreAdj } from "../oom-priority.js";
7
+
8
+ function fakeProc(pid: number): string {
9
+ const procRoot = mkdtempSync(join(tmpdir(), "oom-priority-"));
10
+ mkdirSync(join(procRoot, String(pid)));
11
+ return procRoot;
12
+ }
13
+
14
+ describe("setOomScoreAdj", () => {
15
+ test("writes the value for a pid on Linux", () => {
16
+ const procRoot = fakeProc(42);
17
+ expect(setOomScoreAdj(1000, 42, { procRoot, platform: "linux" })).toBe(
18
+ true,
19
+ );
20
+ expect(readFileSync(join(procRoot, "42", "oom_score_adj"), "utf-8")).toBe(
21
+ "1000",
22
+ );
23
+ });
24
+
25
+ test("is a no-op off Linux", () => {
26
+ const procRoot = fakeProc(42);
27
+ expect(setOomScoreAdj(1000, 42, { procRoot, platform: "darwin" })).toBe(
28
+ false,
29
+ );
30
+ });
31
+
32
+ test("reports failure for a pid that is gone", () => {
33
+ const procRoot = fakeProc(42);
34
+ expect(setOomScoreAdj(0, 43, { procRoot, platform: "linux" })).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe("withOomScoreAdj", () => {
39
+ test("execs the command through a shell that raises its own score on Linux", () => {
40
+ expect(withOomScoreAdj(["chrome", "--flag"], 1000, "linux")).toEqual([
41
+ "sh",
42
+ "-c",
43
+ 'echo 1000 2>/dev/null >/proc/self/oom_score_adj; exec "$0" "$@"',
44
+ "chrome",
45
+ "--flag",
46
+ ]);
47
+ });
48
+
49
+ test("returns the command unchanged off Linux", () => {
50
+ expect(withOomScoreAdj(["chrome", "--flag"], 1000, "darwin")).toEqual([
51
+ "chrome",
52
+ "--flag",
53
+ ]);
54
+ });
55
+ });