@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292019.a6f6d85

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 (264) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +23 -0
  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__/llm-catalog-parity.test.ts +4 -4
  46. package/src/__tests__/llm-schema.test.ts +1 -1
  47. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  48. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  49. package/src/__tests__/openai-provider.test.ts +11 -11
  50. package/src/__tests__/pricing.test.ts +11 -0
  51. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  52. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  53. package/src/__tests__/usage-attribution.test.ts +31 -0
  54. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  55. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  56. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  57. package/src/acp/agent-process.ts +6 -1
  58. package/src/acp/client-handler.ts +6 -1
  59. package/src/agent/loop.ts +29 -6
  60. package/src/api/events/document-editor-show.ts +5 -0
  61. package/src/api/events/document-editor-update.ts +10 -0
  62. package/src/api/events/tool-result.ts +1 -0
  63. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  64. package/src/calls/channel-admission-reader.ts +15 -9
  65. package/src/calls/twilio-provider.ts +14 -31
  66. package/src/calls/voice-session-bridge.ts +18 -1
  67. package/src/cli/commands/browser.ts +1 -2
  68. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  69. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  70. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  71. package/src/cli/lib/bundled-marketplace.json +41 -2
  72. package/src/cli/lib/inspect-plugin.ts +15 -15
  73. package/src/cli/lib/list-installed-plugins.ts +29 -28
  74. package/src/cli/lib/plugin-artifact.ts +74 -63
  75. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  76. package/src/cli/lib/plugin-details.ts +27 -42
  77. package/src/cli/lib/plugin-marketplace.ts +6 -8
  78. package/src/cli/lib/plugin-surfaces.ts +8 -7
  79. package/src/cli.ts +1 -3
  80. package/src/config/__tests__/chat-settings.test.ts +121 -0
  81. package/src/config/assistant-feature-flags.ts +28 -23
  82. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  83. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  84. package/src/config/chat-settings.ts +100 -0
  85. package/src/config/default-profile-catalog.ts +1 -1
  86. package/src/config/feature-flag-cache.ts +7 -25
  87. package/src/config/feature-flag-registry.json +1 -9
  88. package/src/config/loader.ts +5 -1
  89. package/src/config/profile-text-generation.ts +25 -21
  90. package/src/config/schemas/conversations.ts +14 -0
  91. package/src/config/schemas/llm.ts +1 -0
  92. package/src/config/schemas/notifications.ts +8 -1
  93. package/src/contacts/gateway-channel-read.ts +1 -1
  94. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  95. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  96. package/src/conversations/auto-archive-activity.ts +61 -0
  97. package/src/conversations/auto-archive.ts +201 -0
  98. package/src/daemon/computer-use-observation.test.ts +81 -0
  99. package/src/daemon/computer-use-observation.ts +23 -0
  100. package/src/daemon/conversation-agent-loop.ts +11 -6
  101. package/src/daemon/conversation-evictor.ts +50 -16
  102. package/src/daemon/doordash-steps.ts +1 -3
  103. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  104. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  105. package/src/daemon/handlers/config-a2a.ts +75 -2
  106. package/src/daemon/host-cu-proxy.ts +34 -3
  107. package/src/daemon/lifecycle.ts +15 -2
  108. package/src/daemon/shutdown-handlers.ts +3 -0
  109. package/src/daemon/turn-finalization.ts +4 -0
  110. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  111. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  112. package/src/desktop/desktop-chrome-policy.ts +7 -1
  113. package/src/desktop/desktop-computer-use.test.ts +14 -4
  114. package/src/desktop/desktop-session-manager.ts +5 -3
  115. package/src/documents/document-revisions-store.ts +153 -0
  116. package/src/documents/document-store.ts +471 -133
  117. package/src/followups/followup-store.ts +0 -4
  118. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  119. package/src/inbound/oauth-callback-url.ts +30 -1
  120. package/src/inbound/public-ingress-urls.ts +12 -2
  121. package/src/ipc/gateway-client.test.ts +40 -0
  122. package/src/ipc/gateway-client.ts +12 -29
  123. package/src/ipc/gateway-flag-listener.ts +7 -10
  124. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  125. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  126. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  127. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  128. package/src/live-voice/live-voice-session.ts +23 -6
  129. package/src/live-voice/protocol.ts +22 -0
  130. package/src/live-voice/share-targets.ts +225 -0
  131. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  132. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  133. package/src/mcp/client.ts +1 -19
  134. package/src/mcp/mcp-oauth-provider.ts +66 -49
  135. package/src/media/openrouter-image-service.ts +39 -22
  136. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  137. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  138. package/src/monitoring/control.ts +2 -1
  139. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  140. package/src/monitoring/oom-kill-reporter.ts +469 -0
  141. package/src/monitoring/resource-sampler.ts +30 -0
  142. package/src/notifications/README.md +53 -4
  143. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  144. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  145. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  146. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  147. package/src/notifications/adapters/platform.ts +14 -0
  148. package/src/notifications/assistant-initiated-thread.ts +101 -0
  149. package/src/notifications/broadcaster.ts +94 -16
  150. package/src/notifications/chat-reply-policy.ts +11 -0
  151. package/src/notifications/completion-policy.ts +12 -2
  152. package/src/notifications/conversation-pairing.ts +15 -70
  153. package/src/notifications/emit-signal.ts +34 -3
  154. package/src/notifications/types.ts +2 -0
  155. package/src/oauth/connect-orchestrator.ts +1 -2
  156. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  157. package/src/persistence/conversation-auto-archive.ts +170 -0
  158. package/src/persistence/conversation-crud.ts +2 -1
  159. package/src/persistence/conversation-queries.ts +15 -8
  160. package/src/persistence/embeddings/embed.test.ts +96 -0
  161. package/src/persistence/embeddings/embed.ts +20 -2
  162. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  163. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  164. package/src/persistence/embeddings/embedding-local.ts +6 -0
  165. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  166. package/src/persistence/job-utils.ts +10 -0
  167. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  168. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  169. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  170. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  171. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  172. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  173. package/src/persistence/schema/conversations.ts +1 -0
  174. package/src/persistence/schema/documents.ts +20 -0
  175. package/src/persistence/steps.ts +6 -0
  176. package/src/platform/managed-speech.ts +42 -46
  177. package/src/plugin-api/vision-support.test.ts +21 -21
  178. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  179. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  180. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  181. package/src/providers/anthropic/client.ts +1 -1
  182. package/src/providers/fireworks/client.test.ts +168 -0
  183. package/src/providers/fireworks/client.ts +24 -0
  184. package/src/providers/inference/adapter-factory.ts +6 -0
  185. package/src/providers/ionet/client.test.ts +143 -0
  186. package/src/providers/ionet/client.ts +37 -0
  187. package/src/providers/model-catalog.ts +141 -84
  188. package/src/providers/model-intents.ts +4 -4
  189. package/src/providers/openai/chat-completions-provider.ts +23 -1
  190. package/src/providers/retry.ts +56 -4
  191. package/src/providers/search-provider-catalog.ts +10 -0
  192. package/src/providers/types.ts +10 -2
  193. package/src/providers/usage-tracking.ts +5 -3
  194. package/src/runtime/assistant-event-hub.ts +23 -0
  195. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  196. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  197. package/src/runtime/channel-retry-sweep.ts +1 -2
  198. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  199. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  200. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  201. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  202. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  203. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  204. package/src/runtime/pending-interactions.ts +20 -2
  205. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  206. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  207. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  208. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  209. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  210. package/src/runtime/routes/contact-routes.ts +2 -2
  211. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  212. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  213. package/src/runtime/routes/documents-routes.ts +185 -47
  214. package/src/runtime/routes/events-routes.ts +1 -3
  215. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  216. package/src/runtime/routes/host-bash-routes.ts +1 -0
  217. package/src/runtime/routes/host-browser-routes.ts +15 -0
  218. package/src/runtime/routes/host-cu-routes.ts +4 -1
  219. package/src/runtime/routes/host-file-routes.ts +1 -0
  220. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  221. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  222. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  223. package/src/runtime/routes/identity-routes.ts +3 -3
  224. package/src/runtime/routes/integrations/a2a.ts +4 -1
  225. package/src/runtime/routes/migration-routes.ts +3 -1
  226. package/src/runtime/routes/notification-routes.ts +1 -2
  227. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  228. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  229. package/src/runtime/verification-templates.ts +1 -2
  230. package/src/sequence/analytics.ts +0 -4
  231. package/src/sequence/engine.ts +1 -11
  232. package/src/sequence/guardrails.ts +1 -6
  233. package/src/sequence/importer.ts +0 -8
  234. package/src/sequence/store.ts +0 -10
  235. package/src/signals/user-message.ts +0 -6
  236. package/src/skills/catalog-install.ts +52 -23
  237. package/src/tools/browser/browser-execution.ts +20 -23
  238. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  239. package/src/tools/document/document-tool.ts +13 -11
  240. package/src/tools/executor.ts +1 -2
  241. package/src/tools/host-terminal/host-shell.ts +4 -2
  242. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  243. package/src/tools/network/web-fetch.ts +2 -4
  244. package/src/tools/network/web-search.ts +239 -12
  245. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  246. package/src/tools/shared/shell-output.test.ts +42 -0
  247. package/src/tools/shared/shell-output.ts +27 -4
  248. package/src/tools/terminal/shell.ts +3 -3
  249. package/src/tools/ui-surface/visual-validation.ts +1 -2
  250. package/src/usage/attribution.ts +18 -1
  251. package/src/util/__tests__/oom-priority.test.ts +55 -0
  252. package/src/util/__tests__/worker-memory.test.ts +16 -0
  253. package/src/util/debounce.ts +0 -4
  254. package/src/util/oom-priority.ts +79 -0
  255. package/src/util/platform.ts +0 -2
  256. package/src/util/plugin-manifest.ts +5 -7
  257. package/src/util/pricing.ts +25 -0
  258. package/src/util/worker-memory.ts +9 -3
  259. package/src/util/worker-process.ts +8 -0
  260. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  261. package/src/workspace/hatched-date.ts +11 -4
  262. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  263. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  264. package/src/workspace/migrations/registry.ts +4 -0
@@ -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
+ });
@@ -38,6 +38,7 @@ describe("computeWorkerForceRamSizeBytes", () => {
38
38
 
39
39
  describe("workerMemoryEnv", () => {
40
40
  const savedForceRamSize = process.env.BUN_JSC_forceRAMSize;
41
+ const savedGcTimerDisable = process.env.BUN_GC_TIMER_DISABLE;
41
42
 
42
43
  afterEach(() => {
43
44
  if (savedForceRamSize === undefined) {
@@ -45,6 +46,11 @@ describe("workerMemoryEnv", () => {
45
46
  } else {
46
47
  process.env.BUN_JSC_forceRAMSize = savedForceRamSize;
47
48
  }
49
+ if (savedGcTimerDisable === undefined) {
50
+ delete process.env.BUN_GC_TIMER_DISABLE;
51
+ } else {
52
+ process.env.BUN_GC_TIMER_DISABLE = savedGcTimerDisable;
53
+ }
48
54
  });
49
55
 
50
56
  test("adds a clamped BUN_JSC_forceRAMSize on top of the parent environment", () => {
@@ -61,4 +67,14 @@ describe("workerMemoryEnv", () => {
61
67
  const env = workerMemoryEnv();
62
68
  expect(env.BUN_JSC_forceRAMSize).toBe("12345");
63
69
  });
70
+
71
+ test("disables the worker GC timer unless the operator set it", () => {
72
+ delete process.env.BUN_GC_TIMER_DISABLE;
73
+ expect(workerMemoryEnv().BUN_GC_TIMER_DISABLE).toBe("1");
74
+ });
75
+
76
+ test("keeps an operator-provided BUN_GC_TIMER_DISABLE", () => {
77
+ process.env.BUN_GC_TIMER_DISABLE = "0";
78
+ expect(workerMemoryEnv().BUN_GC_TIMER_DISABLE).toBe("0");
79
+ });
64
80
  });
@@ -1,7 +1,3 @@
1
- /**
2
- * Single-key debouncer. Delays execution until no new calls arrive
3
- * within the specified delay period.
4
- */
5
1
  /**
6
2
  * Multi-key debouncer. Each key gets its own independent timer.
7
3
  * Includes an optional entry limit with eviction of oldest non-protected entries.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Linux OOM-killer priority for the daemon and everything it spawns.
3
+ *
4
+ * When the container hits its memory limit the kernel kills the process with
5
+ * the highest oom_score: its share of memory plus `oom_score_adj`. The daemon
6
+ * is usually the largest process in the container, so by default it is the
7
+ * victim and the assistant goes down with the runaway tool. Setting the daemon
8
+ * strongly negative and every child non-negative makes the kernel take a tool,
9
+ * desktop, or worker process first and leave the assistant running.
10
+ *
11
+ * Children inherit the parent's value at fork, so every spawn path must reset
12
+ * its child: shell children through `buildShellInvocation`, desktop children
13
+ * through the desktop session's spawn wrapper, workers and Qdrant through a
14
+ * parent-side write right after spawn. Raising a value never needs a
15
+ * capability; lowering one below the inherited value needs CAP_SYS_RESOURCE,
16
+ * which the platform grants and self-hosted Docker (non-root) does not. There
17
+ * the daemon stays at 0, which children at +1000 still lose to.
18
+ */
19
+
20
+ import { writeFileSync } from "node:fs";
21
+
22
+ import { CHILD_OOM_SCORE_ADJ } from "@vellumai/environments/shell";
23
+
24
+ export { CHILD_OOM_SCORE_ADJ };
25
+
26
+ /** Strongly avoided but still killable as a last resort, unlike -1000. */
27
+ export const DAEMON_OOM_SCORE_ADJ = -700;
28
+ /** The monitor records the kill, so it outlives everything but the daemon. */
29
+ export const MONITOR_OOM_SCORE_ADJ = -500;
30
+ /** Workers and Qdrant: kernel default, picked by memory share alone. */
31
+ export const WORKER_OOM_SCORE_ADJ = 0;
32
+
33
+ /**
34
+ * Wrap a command so the child raises its own `oom_score_adj` before it execs.
35
+ * `exec` keeps the pid and process group, so callers that track or kill the
36
+ * child see no difference. Deterministic where a parent-side write after
37
+ * spawn is not: the child may already have forked (Chrome's zygote, a coding
38
+ * agent's tools) before the parent's write lands, and those grandchildren
39
+ * would keep the inherited value. Identity on non-Linux hosts.
40
+ */
41
+ export function withOomScoreAdj(
42
+ cmd: readonly string[],
43
+ value: number,
44
+ platform: NodeJS.Platform = process.platform,
45
+ ): string[] {
46
+ if (platform !== "linux") {
47
+ return [...cmd];
48
+ }
49
+ return [
50
+ "sh",
51
+ "-c",
52
+ `echo ${value} 2>/dev/null >/proc/self/oom_score_adj; exec "$0" "$@"`,
53
+ ...cmd,
54
+ ];
55
+ }
56
+
57
+ /**
58
+ * Write `oom_score_adj` for `pid` (or this process). Returns false where it
59
+ * cannot apply: non-Linux hosts, a process that already exited, or lowering
60
+ * without CAP_SYS_RESOURCE.
61
+ */
62
+ export function setOomScoreAdj(
63
+ value: number,
64
+ pid: number | "self" = "self",
65
+ options: { procRoot?: string; platform?: NodeJS.Platform } = {},
66
+ ): boolean {
67
+ if ((options.platform ?? process.platform) !== "linux") {
68
+ return false;
69
+ }
70
+ try {
71
+ writeFileSync(
72
+ `${options.procRoot ?? "/proc"}/${pid}/oom_score_adj`,
73
+ String(value),
74
+ );
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
@@ -297,7 +297,6 @@ export function getSignalsDir(): string {
297
297
  return join(getWorkspaceDir(), "signals");
298
298
  }
299
299
 
300
- // --- Root-level runtime path helpers ---
301
300
  // These expose specific root-level file paths so callers don't need to
302
301
  // import getRootDir() directly. getRootDir() is intentionally unexported.
303
302
 
@@ -567,7 +566,6 @@ export function getWorkspaceSystemPromptDir(): string {
567
566
  return join(getWorkspaceDir(), "prompts", "system");
568
567
  }
569
568
 
570
- // ── Profiler filesystem layout ──────────────────────────────────────────
571
569
  // Managed profiler runs live under <workspace>/data/profiler/. These
572
570
  // helpers enforce a single canonical layout so every runtime caller
573
571
  // resolves the same paths.
@@ -37,6 +37,10 @@ const LegacyPluginManifestSchema = z
37
37
  })
38
38
  .passthrough();
39
39
 
40
+ const StandardPluginManifestDiscriminatorSchema = z.object({
41
+ $schema: z.literal(AGENT_PLUGINS_MANIFEST_SCHEMA_URL),
42
+ });
43
+
40
44
  const StandardPluginManifestSchema = z
41
45
  .object({
42
46
  $schema: z.literal(AGENT_PLUGINS_MANIFEST_SCHEMA_URL),
@@ -129,13 +133,7 @@ export function getPluginManifestInstallAction(
129
133
  return "synthesize-legacy";
130
134
  }
131
135
 
132
- if (
133
- typeof json !== "object" ||
134
- json === null ||
135
- Array.isArray(json) ||
136
- (json as Record<string, unknown>).$schema !==
137
- AGENT_PLUGINS_MANIFEST_SCHEMA_URL
138
- ) {
136
+ if (!StandardPluginManifestDiscriminatorSchema.safeParse(json).success) {
139
137
  return "synthesize-legacy";
140
138
  }
141
139
 
@@ -63,6 +63,31 @@ const LEGACY_PRICING_FALLBACK: Record<string, Record<string, ModelPricing>> = {
63
63
  gemini: {
64
64
  "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 },
65
65
  },
66
+ // Models Fireworks no longer serves serverless. The catalog does not list
67
+ // them, but stored usage keeps their IDs, so request-log cost estimates
68
+ // still need their rates.
69
+ fireworks: {
70
+ "accounts/fireworks/models/glm-5p2": {
71
+ inputPer1M: 1.4,
72
+ outputPer1M: 4.4,
73
+ cacheReadPer1M: 0.26,
74
+ },
75
+ "accounts/fireworks/models/kimi-k2p6": {
76
+ inputPer1M: 0.95,
77
+ outputPer1M: 4.0,
78
+ cacheReadPer1M: 0.16,
79
+ },
80
+ "accounts/fireworks/models/deepseek-v4-pro-0813": {
81
+ inputPer1M: 1.32,
82
+ outputPer1M: 3.96,
83
+ cacheReadPer1M: 0.044,
84
+ },
85
+ "accounts/fireworks/models/deepseek-v4-flash-0731": {
86
+ inputPer1M: 0.14,
87
+ outputPer1M: 0.28,
88
+ cacheReadPer1M: 0.028,
89
+ },
90
+ },
66
91
  };
67
92
 
68
93
  /** Convert a catalog pricing entry to the internal pricing shape. */
@@ -60,9 +60,14 @@ export function computeWorkerForceRamSizeBytes(
60
60
 
61
61
  /**
62
62
  * Environment for spawning a background worker `bun` process: the parent
63
- * environment plus `BUN_JSC_forceRAMSize`. An operator-provided
64
- * `BUN_JSC_forceRAMSize` in the parent environment wins over the computed
65
- * value.
63
+ * environment plus `BUN_JSC_forceRAMSize` and `BUN_GC_TIMER_DISABLE`. An
64
+ * operator-provided value for either in the parent environment wins.
65
+ *
66
+ * `BUN_GC_TIMER_DISABLE=1` turns off Bun's repeating GC timer in the worker.
67
+ * Under a `forceRAMSize` heap that timer, together with the per-tick
68
+ * collection request, keeps a long-lived worker collecting continuously at
69
+ * roughly one full core; the workers' own allocation still triggers
70
+ * collection normally.
66
71
  *
67
72
  * Bun.spawn replaces (rather than merges) the child environment when `env` is
68
73
  * passed, so this spreads the full parent environment.
@@ -73,5 +78,6 @@ export function workerMemoryEnv(): Record<string, string | undefined> {
73
78
  BUN_JSC_forceRAMSize:
74
79
  process.env.BUN_JSC_forceRAMSize ??
75
80
  String(computeWorkerForceRamSizeBytes(getContainerMemoryLimitBytes())),
81
+ BUN_GC_TIMER_DISABLE: process.env.BUN_GC_TIMER_DISABLE ?? "1",
76
82
  };
77
83
  }
@@ -20,6 +20,7 @@ import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
 
22
22
  import { getCurrentLogFilePath, getLogger } from "./logger.js";
23
+ import { setOomScoreAdj, WORKER_OOM_SCORE_ADJ } from "./oom-priority.js";
23
24
  import { isProcessAlive } from "./process-liveness.js";
24
25
  import {
25
26
  listProcessTable,
@@ -128,6 +129,11 @@ const PID_FILE_WAIT_TIMEOUT_MS = 15_000;
128
129
  const PID_FILE_POLL_INTERVAL_MS = 100;
129
130
 
130
131
  export interface SpawnWorkerProcessOptions {
132
+ /**
133
+ * `oom_score_adj` for the worker. Defaults to {@link WORKER_OOM_SCORE_ADJ};
134
+ * the child would otherwise inherit the daemon's protected value.
135
+ */
136
+ oomScoreAdj?: number;
131
137
  /**
132
138
  * Override how long to wait for the worker's PID file, in ms. Defaults to
133
139
  * {@link PID_FILE_WAIT_TIMEOUT_MS}. Primarily a testing seam.
@@ -626,6 +632,8 @@ export async function spawnWorkerProcess(args: {
626
632
  windowsHide: true,
627
633
  });
628
634
 
635
+ setOomScoreAdj(opts.oomScoreAdj ?? WORKER_OOM_SCORE_ADJ, child.pid);
636
+
629
637
  // Close our copy of the log fd — the child has its own.
630
638
  if (typeof stderrFd === "number") {
631
639
  closeSync(stderrFd);
@@ -187,6 +187,8 @@ const HISTORICAL_INTENT_MODELS: Record<
187
187
  "accounts/fireworks/models/deepseek-v4-flash",
188
188
  // the ID migration 146 writes over stale deepseek-v4-flash pins.
189
189
  "accounts/fireworks/models/deepseek-v4-flash-0731",
190
+ // the ID migration 160 writes over stale deepseek-v4-flash-0731 pins.
191
+ "accounts/fireworks/models/deepseek-v4p1-flash",
190
192
  ],
191
193
  },
192
194
  "quality-optimized": {
@@ -209,10 +211,15 @@ const HISTORICAL_INTENT_MODELS: Record<
209
211
  fireworks: [
210
212
  // quality intent 2026-05-05 (#29755) to 2026-05-19 (#31068).
211
213
  "accounts/fireworks/models/kimi-k2p5",
214
+ // quality intent 2026-05-19 (#31068) to the 2026-09-28 kimi-k3
215
+ // repoint.
216
+ "accounts/fireworks/models/kimi-k2p6",
212
217
  // migration 136's in-place rewrite of a kimi-k2p5 pin.
213
218
  "accounts/fireworks/models/deepseek-v4-flash",
214
219
  // the ID migration 146 writes over stale deepseek-v4-flash pins.
215
220
  "accounts/fireworks/models/deepseek-v4-flash-0731",
221
+ // the ID migration 160 writes over stale deepseek-v4-flash-0731 pins.
222
+ "accounts/fireworks/models/deepseek-v4p1-flash",
216
223
  ],
217
224
  },
218
225
  "cost-optimized": {
@@ -234,6 +241,10 @@ const HISTORICAL_INTENT_MODELS: Record<
234
241
  // 146 rewrites it to the dated ID, so this survives only in pre-146
235
242
  // backups.
236
243
  "accounts/fireworks/models/deepseek-v4-flash",
244
+ // latency intent 2026-08-17 to the 2026-09-28 deepseek-v4p1-flash
245
+ // repoint. On live configs migration 160 rewrites it, so this
246
+ // survives only in pre-160 backups.
247
+ "accounts/fireworks/models/deepseek-v4-flash-0731",
237
248
  ],
238
249
  },
239
250
  };
@@ -1,9 +1,14 @@
1
1
  import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ import { z } from "zod";
5
+
4
6
  import { getDataDir } from "../util/platform.js";
5
7
 
6
8
  const HATCHED_SIDECAR_FILENAME = "hatched.json";
9
+ const HatchedSidecarSchema = z.object({
10
+ hatchedAt: z.unknown().optional(),
11
+ });
7
12
 
8
13
  export function getHatchedSidecarPath(): string {
9
14
  return join(getDataDir(), HATCHED_SIDECAR_FILENAME);
@@ -24,10 +29,12 @@ function normalizeHatchedAt(value: unknown): string | undefined {
24
29
 
25
30
  export function readHatchedAtSidecar(): string | undefined {
26
31
  try {
27
- const parsed = JSON.parse(
28
- readFileSync(getHatchedSidecarPath(), "utf-8"),
29
- ) as { hatchedAt?: unknown };
30
- return normalizeHatchedAt(parsed.hatchedAt);
32
+ const parsed = HatchedSidecarSchema.safeParse(
33
+ JSON.parse(readFileSync(getHatchedSidecarPath(), "utf-8")),
34
+ );
35
+ return parsed.success
36
+ ? normalizeHatchedAt(parsed.data.hatchedAt)
37
+ : undefined;
31
38
  } catch {
32
39
  return undefined;
33
40
  }
@@ -0,0 +1,209 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+
5
+ import type { WorkspaceMigration } from "./types.js";
6
+
7
+ /**
8
+ * Repair the retired Fireworks model IDs in workspace LLM config.
9
+ *
10
+ * Fireworks has no serverless deployment of DeepSeek V4 Flash 0731,
11
+ * DeepSeek V4 Pro 0813 or Kimi K2.6: chat/completions calls fail with a
12
+ * 404 "Model not found, inaccessible, and/or not deployed". Existing
13
+ * configs can pin any of these IDs in `llm.default`, `llm.callSites.*`, and `llm.profiles.*`.
14
+ *
15
+ * Repair those leaves only on an exact stale match, replacing with the
16
+ * successor Fireworks names (see `REPLACEMENT_BY_STALE_MODEL_ID`). Kimi
17
+ * K2.6 has two named successors; Kimi K3 is the one that accepts image
18
+ * input, which Kimi K2.6 does.
19
+ *
20
+ * A replacement can also be an intent template's model, so a repaired
21
+ * hand-edited `custom-*` profile can read as unedited to
22
+ * `ensureByokDefaultProfiles` and collapse into that default. That is
23
+ * acceptable: the collapsed copy routes to the same model this migration
24
+ * writes.
25
+ *
26
+ * Provider guard: the stale IDs belong to the `fireworks` provider and also
27
+ * appear in managed profiles stamped `provider: "vellum"` (which route
28
+ * Fireworks-account model IDs through the managed proxy). Under the entries
29
+ * model (migration 145) `provider` can also hold a `provider_connections`
30
+ * entry name whose row kind drives dispatch. A fragment is repaired when
31
+ * its `provider` is `"fireworks"`, `"vellum"`, absent, or an entry name
32
+ * whose row kind is one of those. Any other provider is left untouched: an
33
+ * `openai-compatible` endpoint may legitimately serve a model by the stale
34
+ * name.
35
+ */
36
+ export const repairRetiredFireworksModelIdsMigration: WorkspaceMigration = {
37
+ id: "160-repair-retired-fireworks-model-ids",
38
+ description: "Repair retired Fireworks model IDs in workspace LLM config",
39
+ run(workspaceDir: string): void {
40
+ const configPath = join(workspaceDir, "config.json");
41
+ if (!existsSync(configPath)) {
42
+ return;
43
+ }
44
+
45
+ // Read outside the parse catch: a transient filesystem error (EIO,
46
+ // EACCES) must reach the runner so the migration retries, while
47
+ // malformed JSON is a permanent state this migration cannot repair.
48
+ const rawText = readFileSync(configPath, "utf-8");
49
+
50
+ let config: Record<string, unknown>;
51
+ try {
52
+ const raw = JSON.parse(rawText);
53
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
54
+ return;
55
+ }
56
+ config = raw as Record<string, unknown>;
57
+ } catch {
58
+ return;
59
+ }
60
+
61
+ const llm = readObject(config.llm);
62
+ if (llm === null) {
63
+ return;
64
+ }
65
+
66
+ // Entry rows load lazily: only a stale fragment whose provider is
67
+ // neither a repairable vendor nor absent needs them. An unreadable DB
68
+ // then fails the run (retried next boot) rather than checkpointing a
69
+ // pass that skips entry-bound profiles.
70
+ let rows: Map<string, string> | null | undefined;
71
+ const isRepairableProvider = (provider: unknown): boolean => {
72
+ if (provider === undefined) {
73
+ return true;
74
+ }
75
+ if (typeof provider !== "string") {
76
+ return false;
77
+ }
78
+ if (REPAIRABLE_PROVIDERS.has(provider)) {
79
+ return true;
80
+ }
81
+ if (rows === undefined) {
82
+ rows = readConnectionRows(workspaceDir);
83
+ }
84
+ if (rows === null) {
85
+ throw new Error(
86
+ "provider_connections is not readable; retrying the model-ID repair on the next run",
87
+ );
88
+ }
89
+ const kind = rows.get(provider);
90
+ return kind !== undefined && REPAIRABLE_PROVIDERS.has(kind);
91
+ };
92
+
93
+ let changed = false;
94
+
95
+ changed =
96
+ repairFragment(readObject(llm.default), isRepairableProvider) || changed;
97
+
98
+ const callSites = readObject(llm.callSites);
99
+ if (callSites !== null) {
100
+ for (const rawConfig of Object.values(callSites)) {
101
+ changed =
102
+ repairFragment(readObject(rawConfig), isRepairableProvider) ||
103
+ changed;
104
+ }
105
+ }
106
+
107
+ const profiles = readObject(llm.profiles);
108
+ if (profiles !== null) {
109
+ for (const rawProfile of Object.values(profiles)) {
110
+ changed =
111
+ repairFragment(readObject(rawProfile), isRepairableProvider) ||
112
+ changed;
113
+ }
114
+ }
115
+
116
+ if (!changed) {
117
+ return;
118
+ }
119
+
120
+ // Write-then-rename so an interrupted write cannot leave config.json
121
+ // truncated: a torn in-place write would parse as invalid JSON on the
122
+ // retry, which the catch above treats as "nothing to do", letting the
123
+ // runner checkpoint the migration as completed against a corrupt file.
124
+ const tmpPath = `${configPath}.migration-160.tmp`;
125
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
126
+ renameSync(tmpPath, configPath);
127
+ },
128
+ // The exact-match rewrite is idempotent, so a transient failure (full
129
+ // disk, I/O error) is safe to retry on later startups.
130
+ retryFailedCheckpoint: true,
131
+ down(_workspaceDir: string): void {
132
+ // Forward-only: reintroducing a retired model ID would break
133
+ // Fireworks calls.
134
+ },
135
+ };
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Helpers: self-contained per workspace migrations AGENTS.md
139
+ // ---------------------------------------------------------------------------
140
+
141
+ const REPLACEMENT_BY_STALE_MODEL_ID = new Map([
142
+ [
143
+ "accounts/fireworks/models/deepseek-v4-flash-0731",
144
+ "accounts/fireworks/models/deepseek-v4p1-flash",
145
+ ],
146
+ [
147
+ "accounts/fireworks/models/deepseek-v4-pro-0813",
148
+ "accounts/fireworks/models/deepseek-v4p1-flash",
149
+ ],
150
+ ["accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k3"],
151
+ ]);
152
+ const REPAIRABLE_PROVIDERS = new Set(["fireworks", "vellum"]);
153
+
154
+ function repairFragment(
155
+ fragment: Record<string, unknown> | null,
156
+ isRepairableProvider: (provider: unknown) => boolean,
157
+ ): boolean {
158
+ if (fragment === null) {
159
+ return false;
160
+ }
161
+ if (typeof fragment.model !== "string") {
162
+ return false;
163
+ }
164
+ const replacement = REPLACEMENT_BY_STALE_MODEL_ID.get(fragment.model);
165
+ if (replacement === undefined) {
166
+ return false;
167
+ }
168
+ if (!isRepairableProvider(fragment.provider)) {
169
+ return false;
170
+ }
171
+ fragment.model = replacement;
172
+ return true;
173
+ }
174
+
175
+ /**
176
+ * Connection name -> provider kind, or null when the DB or table is not
177
+ * readable. The caller fails the run on null: entry-name providers must be
178
+ * judged against real rows, never guessed. An absent DB file is a real
179
+ * state (no rows, so every entry name is dangling and stays untouched).
180
+ */
181
+ function readConnectionRows(workspaceDir: string): Map<string, string> | null {
182
+ const dbPath = join(workspaceDir, "data", "db", "assistant.db");
183
+ if (!existsSync(dbPath)) {
184
+ return new Map();
185
+ }
186
+ let db: Database;
187
+ try {
188
+ db = new Database(dbPath);
189
+ } catch {
190
+ return null;
191
+ }
192
+ try {
193
+ const rows = db
194
+ .query(`SELECT name, provider FROM provider_connections`)
195
+ .all() as Array<{ name: string; provider: string }>;
196
+ return new Map(rows.map((r) => [r.name, r.provider]));
197
+ } catch {
198
+ return null;
199
+ } finally {
200
+ db.close();
201
+ }
202
+ }
203
+
204
+ function readObject(value: unknown): Record<string, unknown> | null {
205
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
206
+ return null;
207
+ }
208
+ return value as Record<string, unknown>;
209
+ }