windows-development-sandbox 1.18.2 → 1.18.4

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 (98) hide show
  1. package/config.example.json +1 -1
  2. package/dist/app/features/defs.d.ts +26 -0
  3. package/dist/app/features/defs.js +2 -1
  4. package/dist/app/features/sandbox/sandbox.def.d.ts +34 -0
  5. package/dist/app/features/sandbox/sandbox.def.js +52 -10
  6. package/dist/app/features/sandbox/sandbox.run.d.ts +24 -0
  7. package/dist/app/features/sandbox/sandbox.run.js +45 -1
  8. package/dist/app/registry.js +2 -1
  9. package/dist/bin/agent-sandbox.js +2 -2
  10. package/dist/bin/{chunk-MQIA7GNB.js → chunk-FRPLQLSY.js} +3188 -2765
  11. package/dist/bin/{chunk-M4OM632I.js → chunk-MY7NLQQY.js} +1 -1
  12. package/dist/bin/{chunk-IIHL7W6B.js → chunk-N4GXYTLV.js} +1 -1
  13. package/dist/bin/claude-host.js +2 -2
  14. package/dist/bin/claude-sbx.js +2 -2
  15. package/dist/bin/codex-sbx.js +2 -2
  16. package/dist/bin/gemini-sbx.js +2 -2
  17. package/dist/bin/sandbox.js +2 -2
  18. package/dist/bin/sbx-hide.js +2 -2
  19. package/dist/bin/sbx-show.js +2 -2
  20. package/dist/cli/commands/launch/launch.js +4 -1
  21. package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
  22. package/dist/cli/commands/launch/leftover-offer.js +26 -0
  23. package/dist/cli/commands/repair/checks/sandbox.d.ts +3 -1
  24. package/dist/cli/commands/repair/checks/sandbox.js +53 -2
  25. package/dist/core/config/defaults.js +1 -1
  26. package/dist/core/schema/sandbox.js +1 -1
  27. package/dist/core/schema/toolchains.js +2 -2
  28. package/dist/guest/daemon/daemon.js +1 -1
  29. package/dist/guest/daemon/heartbeat.d.ts +11 -0
  30. package/dist/guest/daemon/heartbeat.js +59 -0
  31. package/dist/guest/daemon/idle-shutdown.js +2 -0
  32. package/dist/guest/entries/daemon.js +2 -0
  33. package/dist/guest/governor/core.d.ts +19 -1
  34. package/dist/guest/governor/core.js +23 -1
  35. package/dist/guest/governor/decide.d.ts +14 -0
  36. package/dist/guest/governor/decide.js +52 -7
  37. package/dist/guest/governor/governor.d.ts +4 -1
  38. package/dist/guest/governor/governor.js +9 -2
  39. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  40. package/dist/guest/toolchains/native/rust-extras.js +26 -3
  41. package/dist/guest/toolchains/plan.js +6 -4
  42. package/dist/host/channel/channel.d.ts +1 -1
  43. package/dist/host/channel/channel.js +10 -2
  44. package/dist/host/health/index.d.ts +1 -0
  45. package/dist/host/health/index.js +1 -0
  46. package/dist/host/health/leftovers.d.ts +3 -0
  47. package/dist/host/health/leftovers.js +40 -0
  48. package/dist/host/health/stale.d.ts +48 -0
  49. package/dist/host/health/stale.js +40 -0
  50. package/dist/host/launch/wsb-file.d.ts +6 -1
  51. package/dist/host/launch/wsb-file.js +9 -5
  52. package/dist/host/life/crash-report.d.ts +3 -1
  53. package/dist/host/life/crash-report.js +9 -3
  54. package/dist/host/life/index.d.ts +1 -0
  55. package/dist/host/life/index.js +1 -0
  56. package/dist/host/life/last-end.d.ts +28 -0
  57. package/dist/host/life/last-end.js +91 -0
  58. package/dist/host/life/life-record.d.ts +3 -0
  59. package/dist/host/life/life-record.js +2 -2
  60. package/dist/host/life/stop-events.d.ts +3 -1
  61. package/dist/host/life/stop-events.js +59 -0
  62. package/dist/host/life/stop-warning.js +3 -0
  63. package/dist/host/life/watchdog.d.ts +2 -0
  64. package/dist/host/life/watchdog.js +8 -2
  65. package/dist/host/sandbox/cleanup.d.ts +37 -0
  66. package/dist/host/sandbox/cleanup.js +82 -0
  67. package/dist/host/sandbox/index.d.ts +1 -0
  68. package/dist/host/sandbox/index.js +1 -0
  69. package/dist/host/sandbox/platform.js +1 -1
  70. package/dist/runtime/agent.js +28 -3
  71. package/dist/runtime/daemon.js +629 -479
  72. package/dist/runtime/db.js +28 -3
  73. package/dist/runtime/main.js +59 -11
  74. package/dist/runtime/netproxy.js +28 -3
  75. package/dist/runtime/tool.js +28 -3
  76. package/dist/runtime/view.js +28 -3
  77. package/dist/selftest/heavy-load.test.js +212 -14
  78. package/dist/selftest/live-terminal.test.js +4 -0
  79. package/dist/shared/channel/run-dir.d.ts +4 -1
  80. package/dist/shared/channel/run-dir.js +7 -1
  81. package/dist/shared/channel/schema.d.ts +24 -0
  82. package/dist/shared/channel/schema.js +22 -0
  83. package/dist/shared/memory/policy.d.ts +10 -0
  84. package/dist/shared/memory/policy.js +17 -1
  85. package/dist/shared/system.d.ts +1 -0
  86. package/dist/shared/system.js +1 -0
  87. package/dist/sync/watcher.js +10 -5
  88. package/dist/web/index.html +1 -1
  89. package/dist/web/{main-CWIF4VQI.js → main-GVZWWP4I.js} +63 -63
  90. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  91. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  92. package/native/csharp/HostTools/Program.cs +5 -1
  93. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  94. package/native/scripts/stop-events.ps1 +7 -3
  95. package/package.json +1 -1
  96. package/sandbox/csharp/MemGov.cs +58 -0
  97. package/schemas/config.schema.json +3 -3
  98. package/schemas/sandbox-config.schema.json +3 -3
@@ -4517,7 +4517,7 @@ var require_lodash = __commonJS({
4517
4517
  function valuesIn(object2) {
4518
4518
  return object2 == null ? [] : baseValues(object2, keysIn(object2));
4519
4519
  }
4520
- function clamp(number4, lower, upper) {
4520
+ function clamp2(number4, lower, upper) {
4521
4521
  if (upper === undefined2) {
4522
4522
  upper = lower;
4523
4523
  lower = undefined2;
@@ -5199,7 +5199,7 @@ var require_lodash = __commonJS({
5199
5199
  lodash.camelCase = camelCase;
5200
5200
  lodash.capitalize = capitalize;
5201
5201
  lodash.ceil = ceil;
5202
- lodash.clamp = clamp;
5202
+ lodash.clamp = clamp2;
5203
5203
  lodash.clone = clone2;
5204
5204
  lodash.cloneDeep = cloneDeep;
5205
5205
  lodash.cloneDeepWith = cloneDeepWith;
@@ -25485,7 +25485,7 @@ var SandboxSchema = external_exports.strictObject({
25485
25485
  '(alpha) What runs the sandbox. "windows-sandbox" = Windows Sandbox (the default: desktop, Windows Terminal, GPU, clipboard, audio, printers). "hcs" = a headless Hyper-V isolated Server Core container for agents (no desktop, GPU or clipboard); it needs Windows features and an image on this PC (sandbox engine check hcs lists what is missing), and starting it is not enabled yet in this alpha. sandbox engine shows what each engine can do. Yours only: a project file (.sandbox-config.json) cannot set it.'
25486
25486
  ),
25487
25487
  memoryMb: external_exports.union([external_exports.literal("auto"), external_exports.number().int().min(2048)]).describe(
25488
- 'Sandbox RAM in MB (>= 2048), or "auto": free RAM minus reserveHostMb, capped at maxMemoryMb. Never more than this PC can keep in RAM for it (its RAM minus the larger of reserveHostMb and 25 %, 2 GB more with vGPU): a bigger value is lowered.'
25488
+ 'Sandbox RAM in MB (>= 2048), or "auto": free RAM minus reserveHostMb, capped at maxMemoryMb. Never more than this PC can keep in RAM for it (its RAM minus the larger of reserveHostMb and 25 %, 4 GB more with vGPU): a bigger value is lowered.'
25489
25489
  ),
25490
25490
  maxMemoryMb: external_exports.number().int().positive().describe('Upper limit for memoryMb "auto".'),
25491
25491
  maxMemoryPercent: external_exports.number().int().min(10).max(100).describe(`memoryMb "auto" never takes more than this share of this PC's total RAM (percent).`),
@@ -25647,13 +25647,13 @@ var ToolchainsSchema = external_exports.strictObject({
25647
25647
  '["sccache", "cargo-nextest", "cargo-deny"]'
25648
25648
  ),
25649
25649
  targetDir: external_exports.string().min(1).describe(
25650
- `"shared": cargo's default target\\ (shared with builds on this PC, so builds on either side invalidate the other), "separate": <workspace>\\target\\sandbox (kept, no cross-talk), "local": on the sandbox's own disk (fastest, lost every boot), or a guest path.`
25650
+ `"auto" (default): on the sandbox's own disk when C: has 25 GB free at boot, else "separate" (builds then write through the mapped workspace, which can overload this PC's disk). "shared": cargo's default target\\ (shared with builds on this PC, so builds on either side invalidate the other), "separate": <workspace>\\target\\sandbox (kept, no cross-talk), "local": on the sandbox's own disk (fastest, lost every boot), or a guest path.`
25651
25651
  ),
25652
25652
  linker: external_exports.enum(["lld", "gnu"]).describe(
25653
25653
  `GNU host: "lld" links with Rust's own lld through MinGW's gcc (much less memory than GNU ld), "gnu" with GNU ld.`
25654
25654
  ),
25655
25655
  linkSlots: external_exports.union([external_exports.literal("auto"), external_exports.number().int().min(1).max(32)]).describe(
25656
- 'GNU host: how many programs may link at the same time in the whole sandbox (other links wait). "auto" = one per 3 GB of sandbox memory after 6 GB, 2 to 8 (the memory governor slows links down too when memory runs short).'
25656
+ 'GNU host: how many programs may link at the same time in the whole sandbox (other links wait). "auto" = one per 6 GB of sandbox memory, 1 to 4 (the memory governor slows links down too when memory runs short).'
25657
25657
  )
25658
25658
  }),
25659
25659
  msvc: external_exports.strictObject({
@@ -27367,6 +27367,8 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
27367
27367
  seconds: external_exports.number().int().min(10).max(300),
27368
27368
  /** "severe": the PC is nearly out of memory; builds go to the governor's last step at once. */
27369
27369
  level: external_exports.enum(["slow", "severe"]).optional(),
27370
+ /** The disk queue the PC reported (shown in the heartbeat). */
27371
+ diskQueue: external_exports.number().min(0).max(1e5).optional(),
27370
27372
  createdAt: TimeSchema
27371
27373
  })
27372
27374
  ]);
@@ -27389,6 +27391,22 @@ var PressureStatusSchema = external_exports.object({
27389
27391
  lastAction: Text(300),
27390
27392
  updatedAt: TimeSchema
27391
27393
  });
27394
+ var HeartbeatSchema = external_exports.object({
27395
+ at: TimeSchema,
27396
+ /** Seconds since the sandbox booted. */
27397
+ uptimeS: external_exports.number().int().min(0).max(1e9),
27398
+ ramFreePct: external_exports.number().min(0).max(100),
27399
+ commitPct: external_exports.number().min(0).max(100),
27400
+ /** The governor's dial now, and the step this PC asked for. */
27401
+ step: external_exports.number().int().min(0).max(4),
27402
+ hostStep: external_exports.number().int().min(0).max(4),
27403
+ /** The biggest programs in the agents' job. */
27404
+ topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27405
+ /** The disk queue this PC last reported while asking the sandbox to slow down. */
27406
+ diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27407
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27408
+ phase: external_exports.enum(["ready", "working", "ending"])
27409
+ });
27392
27410
  var GuestResponseSchema = external_exports.object({
27393
27411
  id: IdSchema,
27394
27412
  ok: external_exports.boolean(),
@@ -27699,6 +27717,8 @@ var RunDir = class {
27699
27717
  vpnFile;
27700
27718
  /** What the memory governor in the sandbox is doing (pressure.json). */
27701
27719
  pressureFile;
27720
+ /** The daemon's heartbeat every 10 seconds (heartbeat.json): what the sandbox did last. */
27721
+ heartbeatFile;
27702
27722
  /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
27703
27723
  budgetFile;
27704
27724
  requestsDir;
@@ -27722,6 +27742,7 @@ var RunDir = class {
27722
27742
  this.sandboxFile = path7.join(this.root, "sandbox.json");
27723
27743
  this.vpnFile = path7.join(this.root, "vpn.json");
27724
27744
  this.pressureFile = path7.join(this.root, "pressure.json");
27745
+ this.heartbeatFile = path7.join(this.root, "heartbeat.json");
27725
27746
  this.budgetFile = path7.join(this.root, "budget.json");
27726
27747
  this.requestsDir = path7.join(this.root, "requests");
27727
27748
  this.responsesDir = path7.join(this.root, "responses");
@@ -27766,6 +27787,9 @@ var RunDir = class {
27766
27787
  readPressure() {
27767
27788
  return readValid(PressureStatusSchema, this.pressureFile);
27768
27789
  }
27790
+ readHeartbeat() {
27791
+ return readValid(HeartbeatSchema, this.heartbeatFile);
27792
+ }
27769
27793
  readSession(id) {
27770
27794
  return readValid(SessionStatusSchema, this.sessionFile(id));
27771
27795
  }
@@ -27866,6 +27890,7 @@ var SYSTEM = {
27866
27890
  cmd: () => system32("cmd.exe"),
27867
27891
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
27868
27892
  tasklist: () => system32("tasklist.exe"),
27893
+ taskkill: () => system32("taskkill.exe"),
27869
27894
  whoami: () => system32("whoami.exe"),
27870
27895
  icacls: () => system32("icacls.exe"),
27871
27896
  tar: () => system32("tar.exe"),
@@ -33038,11 +33063,280 @@ function cargoJobs(cpus, memoryGb, sessions2) {
33038
33063
  const jobs = Math.min(byCpu, byMemory);
33039
33064
  return Math.max(1, Math.min(jobs, Math.floor(buildJobs(cpus, memoryGb) / Math.max(1, sessions2))));
33040
33065
  }
33066
+ var LOCAL_TARGET_MIN_BYTES = 25 * 1024 ** 3;
33067
+ function chooseTargetDir(targetDir, freeBytes2) {
33068
+ if (targetDir !== "auto") return { targetDir };
33069
+ if (freeBytes2 >= LOCAL_TARGET_MIN_BYTES) return { targetDir: "local" };
33070
+ return {
33071
+ targetDir: "separate",
33072
+ warning: "Rust builds write through the mapped workspace; this can overload your PC's disk (C: has less than 25 GB free for a local build folder)"
33073
+ };
33074
+ }
33041
33075
  function maxCompilers(cpus, profile) {
33042
- return profile === "gentle" ? Math.max(2, Math.floor(cpus / 4)) : null;
33076
+ return profile === "gentle" ? Math.min(4, Math.max(2, Math.floor(cpus / 4))) : null;
33077
+ }
33078
+
33079
+ // src/guest/governor/decide.ts
33080
+ var COMPILERS = /* @__PURE__ */ new Set([
33081
+ "rustc",
33082
+ "clippy-driver",
33083
+ "rustdoc",
33084
+ "cc1",
33085
+ "cc1plus",
33086
+ "cc1obj",
33087
+ "collect2",
33088
+ "gcc",
33089
+ "g++",
33090
+ "c++",
33091
+ "cc",
33092
+ "clang",
33093
+ "clang++",
33094
+ "clang-cl",
33095
+ "cl",
33096
+ "link",
33097
+ "lld",
33098
+ "lld-link",
33099
+ "ld",
33100
+ "ld.lld",
33101
+ "as",
33102
+ "tsc",
33103
+ "javac"
33104
+ ]);
33105
+ var BUILD_TOOLS = /* @__PURE__ */ new Set([
33106
+ "dotnet",
33107
+ "msbuild",
33108
+ "vbcscompiler",
33109
+ "ninja",
33110
+ "make",
33111
+ "mingw32-make",
33112
+ "cmake",
33113
+ "cargo",
33114
+ "go",
33115
+ "java",
33116
+ "gradle",
33117
+ "python"
33118
+ ]);
33119
+ var NEVER2 = /* @__PURE__ */ new Set([
33120
+ "claude",
33121
+ "codex",
33122
+ "gemini",
33123
+ "opencode",
33124
+ "aider",
33125
+ "cursor-agent",
33126
+ "cmd",
33127
+ "powershell",
33128
+ "pwsh",
33129
+ "bash",
33130
+ "sh",
33131
+ "zsh",
33132
+ "git",
33133
+ "conhost",
33134
+ "openconsole",
33135
+ "windowsterminal",
33136
+ "wt",
33137
+ "ptyhost",
33138
+ "memgov",
33139
+ "sandboxtools",
33140
+ "sbx-db",
33141
+ "explorer",
33142
+ "csrss",
33143
+ "wininit",
33144
+ "winlogon",
33145
+ "services",
33146
+ "svchost",
33147
+ "lsass",
33148
+ "system",
33149
+ "smss",
33150
+ "dwm"
33151
+ ]);
33152
+ var key = (name) => name.toLowerCase().replace(/\.exe$/, "");
33153
+ var isNeverTouched = (name) => NEVER2.has(key(name));
33154
+ var canLower = (name) => !isNeverTouched(name) && (COMPILERS.has(key(name)) || BUILD_TOOLS.has(key(name)));
33155
+ var HEAVY = /* @__PURE__ */ new Set([
33156
+ "rustc",
33157
+ "clippy-driver",
33158
+ "rustdoc",
33159
+ "cc1",
33160
+ "cc1plus",
33161
+ "clang",
33162
+ "clang++",
33163
+ "clang-cl",
33164
+ "cl",
33165
+ "link",
33166
+ "lld",
33167
+ "lld-link",
33168
+ "ld",
33169
+ "ld.lld"
33170
+ ]);
33171
+ var LINKERS = /* @__PURE__ */ new Set(["link", "lld", "lld-link", "ld", "ld.lld"]);
33172
+ var isLinker = (name) => !isNeverTouched(name) && LINKERS.has(key(name));
33173
+ function linkerSlots(totalMb, cpus) {
33174
+ return Math.max(1, Math.min(4, Math.max(1, Math.floor(cpus)), Math.floor(totalMb / 6144)));
33175
+ }
33176
+ var isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
33177
+ var canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
33178
+ var INITIAL_STATE = {
33179
+ step: 0,
33180
+ calm: 0,
33181
+ lowered: {},
33182
+ memLow: [],
33183
+ throttled: [],
33184
+ trimmed: [],
33185
+ ioLowered: {},
33186
+ cpuRate: 100,
33187
+ lastCommitMb: 0,
33188
+ lastAt: 0,
33189
+ growth: 0,
33190
+ hostUntil: 0,
33191
+ hostStep: 0
33192
+ };
33193
+ var GROWTH_WEIGHT = 0.3;
33194
+ var HOLD_MS = 3e3;
33195
+ var ROTATE_MS = 2e4;
33196
+ var SOON_WITHIN_PERCENT = 12;
33197
+ function stepFor(ramFree, commit, secondsToLimit, profile) {
33198
+ let step = 0;
33199
+ DIAL_STEPS[profile].forEach((t, i) => {
33200
+ const near = commit > t.commitPercent - SOON_WITHIN_PERCENT;
33201
+ const soon = near && t.secondsToLimit !== void 0 && secondsToLimit < t.secondsToLimit;
33202
+ if (ramFree < t.availablePercent || commit > t.commitPercent || soon) step = i + 1;
33203
+ });
33204
+ return step;
33205
+ }
33206
+ function clearOf(step, ramFree, commit, profile) {
33207
+ if (step === 0) return true;
33208
+ const t = DIAL_STEPS[profile][step - 1];
33209
+ return ramFree > t.availablePercent + DIAL_EXIT_MARGIN.availablePercent && commit < t.commitPercent - DIAL_EXIT_MARGIN.commitPercent;
33210
+ }
33211
+ function decide(prev, sample, nowMs, profile, maxCompilers2 = null) {
33212
+ const ramFree = sample.ramAvailMb / Math.max(1, sample.ramTotalMb) * 100;
33213
+ const usedMb = sample.commitLimitMb - sample.commitAvailMb;
33214
+ const commit = usedMb / Math.max(1, sample.commitLimitMb) * 100;
33215
+ const seconds = (nowMs - prev.lastAt) / 1e3;
33216
+ const instant = prev.lastAt > 0 && seconds > 0 ? (usedMb - prev.lastCommitMb) / seconds : 0;
33217
+ const growth = prev.lastAt > 0 ? prev.growth * (1 - GROWTH_WEIGHT) + instant * GROWTH_WEIGHT : 0;
33218
+ const secondsToLimit = growth > 0 ? sample.commitAvailMb / growth : Number.POSITIVE_INFINITY;
33219
+ const pcLow = nowMs < prev.hostUntil;
33220
+ const wanted2 = Math.max(stepFor(ramFree, commit, secondsToLimit, profile), pcLow ? prev.hostStep : 0);
33221
+ let step = prev.step;
33222
+ let calm = 0;
33223
+ if (wanted2 > prev.step) step = wanted2;
33224
+ else if (prev.step > 0 && wanted2 < prev.step && clearOf(prev.step, ramFree, commit, profile)) {
33225
+ calm = prev.calm + 1;
33226
+ if (calm >= DIAL_CALM_SAMPLES) {
33227
+ step = prev.step - 1;
33228
+ calm = 0;
33229
+ }
33230
+ }
33231
+ if (profile === "gentle" && step < 2 && sample.procs.some((p) => isHeavyCompiler(p.name))) step = 2;
33232
+ const actions = [];
33233
+ const present2 = new Set(sample.procs.map((p) => p.pid));
33234
+ const keep = (pids) => pids.filter((pid) => present2.has(pid));
33235
+ const lowered = {};
33236
+ for (const [pid, to] of Object.entries(prev.lowered)) if (present2.has(Number(pid))) lowered[Number(pid)] = to;
33237
+ const memLow = new Set(keep(prev.memLow));
33238
+ const throttled = new Set(keep(prev.throttled));
33239
+ const trimmed = new Set(keep(prev.trimmed));
33240
+ for (const p of sample.procs) {
33241
+ if (!canLower(p.name)) continue;
33242
+ const rest = profile === "gentle" && canPause(p.name) ? "below" : "normal";
33243
+ const target = step >= 2 ? "idle" : step === 1 ? "below" : rest;
33244
+ const was = lowered[p.pid];
33245
+ const touched = was !== void 0 || target !== "normal";
33246
+ if (touched && was !== target && p.prio !== target)
33247
+ actions.push({ kind: "priority", pid: p.pid, name: p.name, to: target });
33248
+ if (target === "normal") delete lowered[p.pid];
33249
+ else lowered[p.pid] = target;
33250
+ }
33251
+ const ioLowered = {};
33252
+ for (const [pid, to] of Object.entries(prev.ioLowered)) if (present2.has(Number(pid))) ioLowered[Number(pid)] = to;
33253
+ for (const p of sample.procs) {
33254
+ if (!canPause(p.name)) continue;
33255
+ const cpu = lowered[p.pid] ?? "normal";
33256
+ const target = step >= 3 ? "verylow" : cpu !== "normal" ? "low" : "normal";
33257
+ const was = ioLowered[p.pid] ?? "normal";
33258
+ if (was !== target) actions.push({ kind: "io", pid: p.pid, name: p.name, to: target });
33259
+ if (target === "normal") delete ioLowered[p.pid];
33260
+ else ioLowered[p.pid] = target;
33261
+ }
33262
+ const lowMemory = step >= 1;
33263
+ for (const p of sample.procs) {
33264
+ if (!canLower(p.name) || lowMemory === memLow.has(p.pid)) continue;
33265
+ actions.push({ kind: "memory", pid: p.pid, name: p.name, low: lowMemory });
33266
+ if (lowMemory) memLow.add(p.pid);
33267
+ else memLow.delete(p.pid);
33268
+ }
33269
+ if (step >= 2) {
33270
+ for (const p of sample.procs)
33271
+ if (canPause(p.name) && !throttled.has(p.pid)) {
33272
+ actions.push({ kind: "throttle", pid: p.pid, name: p.name });
33273
+ throttled.add(p.pid);
33274
+ }
33275
+ }
33276
+ const cpuRate = DIAL_CPU_RATE[step] ?? 100;
33277
+ if (cpuRate !== prev.cpuRate) actions.push({ kind: "cpu", percent: cpuRate });
33278
+ const heavy = sample.procs.filter((p) => isHeavyCompiler(p.name)).sort((a, b) => a.pid - b.pid);
33279
+ let allowed = compilersAllowed(step, heavy.length);
33280
+ if (maxCompilers2 !== null && step >= 1) allowed = Math.min(allowed, Math.max(1, maxCompilers2));
33281
+ const turn = heavy.length > 0 ? Math.floor(nowMs / ROTATE_MS) % heavy.length : 0;
33282
+ const order = [...heavy.slice(turn), ...heavy.slice(0, turn)];
33283
+ let waiting = order.slice(Math.min(allowed, order.length));
33284
+ if (step >= 2) {
33285
+ const waitingPids = new Set(waiting.map((p) => p.pid));
33286
+ let linking = 0;
33287
+ for (const p of order)
33288
+ if (isLinker(p.name) && !waitingPids.has(p.pid) && ++linking > 1) {
33289
+ waiting = [...waiting, p];
33290
+ waitingPids.add(p.pid);
33291
+ }
33292
+ }
33293
+ if (waiting.length > 0) {
33294
+ const waitingPids = new Set(waiting.map((p) => p.pid));
33295
+ for (const p of waiting) {
33296
+ actions.push({ kind: "hold", pid: p.pid, name: p.name, ms: HOLD_MS });
33297
+ if (step >= 3 && !trimmed.has(p.pid)) {
33298
+ actions.push({ kind: "trim", pid: p.pid, name: p.name });
33299
+ trimmed.add(p.pid);
33300
+ }
33301
+ }
33302
+ for (const p of order) if (!waitingPids.has(p.pid)) trimmed.delete(p.pid);
33303
+ } else trimmed.clear();
33304
+ const level = step >= 3 ? "suspending" : step >= 1 ? "throttled" : "ok";
33305
+ return {
33306
+ state: {
33307
+ step,
33308
+ calm,
33309
+ lowered,
33310
+ memLow: [...memLow],
33311
+ throttled: [...throttled],
33312
+ trimmed: [...trimmed],
33313
+ ioLowered,
33314
+ cpuRate,
33315
+ lastCommitMb: usedMb,
33316
+ lastAt: nowMs,
33317
+ growth,
33318
+ hostUntil: prev.hostUntil,
33319
+ hostStep: prev.hostStep
33320
+ },
33321
+ actions,
33322
+ level,
33323
+ ramFreePercent: Math.round(ramFree),
33324
+ commitPercent: Math.round(commit)
33325
+ };
33043
33326
  }
33044
33327
 
33045
33328
  // src/guest/toolchains/native/rust-extras.ts
33329
+ function resolveTargetDir(rust, free = freeBytes((process.env.SystemDrive ?? "C:") + "\\"), note = warn) {
33330
+ if (rust.targetDir !== "auto") return rust;
33331
+ const chosen = chooseTargetDir(rust.targetDir, free);
33332
+ if (chosen.warning) note(chosen.warning);
33333
+ return { ...rust, targetDir: chosen.targetDir };
33334
+ }
33335
+ function targetSummary(rust) {
33336
+ const target = cargoTarget(rust.targetDir);
33337
+ if (target) return "rust builds: " + target + " (CARGO_TARGET_DIR)";
33338
+ return rust.targetDir === "shared" ? "rust builds: target\\ in each project (shared with this PC)" : "rust builds: target\\sandbox in each project (on the mapped workspace)";
33339
+ }
33046
33340
  var cargoTarget = (targetDir) => {
33047
33341
  if (targetDir === "shared" || targetDir === "separate") return null;
33048
33342
  if (targetDir === "local") return path70.join(localDir, "cargo-target");
@@ -33061,8 +33355,7 @@ function linkGate(env, rust, host) {
33061
33355
  if (!host.endsWith("-pc-windows-gnu") || !exists(gcc)) return;
33062
33356
  const gate = path70.join(mkdirp(tool("shims")), "linkgate.exe");
33063
33357
  fs56.copyFileSync(ensureCompiled("linkgate"), gate);
33064
- const memoryGb = os10.totalmem() / 1024 ** 3;
33065
- const slots = rust.linkSlots === "auto" ? linkSlotsFor(memoryGb) : rust.linkSlots;
33358
+ const slots = rust.linkSlots === "auto" ? linkerSlots(os10.totalmem() / 1024 ** 2, os10.cpus().length) : rust.linkSlots;
33066
33359
  env.vars.AGENT_LINK_SLOTS = String(slots);
33067
33360
  env.vars.AGENT_REAL_LINKER = gcc;
33068
33361
  env.vars.CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = gate;
@@ -33174,11 +33467,13 @@ function plan(config2, versions, o) {
33174
33467
  after: ["C/C++", "MSVC", "Visual Studio", "Unreal"],
33175
33468
  install: async (env) => {
33176
33469
  await setupRust(env, { ...config2.rust, host: rustHost(config2) }, o.refresh);
33177
- Object.assign(env.vars, rustCaches(config2.rust));
33470
+ const rust = resolveTargetDir(config2.rust);
33471
+ Object.assign(env.vars, rustCaches(rust));
33472
+ env.summary.push(targetSummary(rust));
33178
33473
  if (!dryRun) {
33179
33474
  cargoDefaults(rustHost(config2));
33180
- cargoShim(env, config2.rust);
33181
- linkGate(env, config2.rust, rustHost(config2));
33475
+ cargoShim(env, rust);
33476
+ linkGate(env, rust, rustHost(config2));
33182
33477
  }
33183
33478
  }
33184
33479
  },
@@ -33663,11 +33958,11 @@ function execCommand(req) {
33663
33958
  }
33664
33959
 
33665
33960
  // src/guest/daemon/sessions.ts
33666
- import fs63 from "node:fs";
33667
- import path80 from "node:path";
33961
+ import fs64 from "node:fs";
33962
+ import path81 from "node:path";
33668
33963
  import crypto5 from "node:crypto";
33669
- import os13 from "node:os";
33670
- import { spawn as spawn8 } from "node:child_process";
33964
+ import os15 from "node:os";
33965
+ import { spawn as spawn9 } from "node:child_process";
33671
33966
 
33672
33967
  // src/shared/pty/protocol.ts
33673
33968
  var FrameType = {
@@ -34249,21 +34544,318 @@ async function flushAccounts(final = false) {
34249
34544
  }
34250
34545
  }
34251
34546
 
34252
- // src/guest/daemon/idle-shutdown.ts
34253
- var GRACE_MS = 15e3;
34254
- var IdleShutdown = class {
34255
- /** `busy`: a session runs or is starting. */
34256
- constructor(busy) {
34257
- this.busy = busy;
34547
+ // src/guest/daemon/heartbeat.ts
34548
+ import os14 from "node:os";
34549
+
34550
+ // src/guest/governor/core.ts
34551
+ var STEP_TEXT = [
34552
+ "builds back to normal",
34553
+ "build programs at lower priority",
34554
+ "builds get less CPU and take turns",
34555
+ "builds take turns, waiting ones go to the paging file",
34556
+ "one compiler at a time"
34557
+ ];
34558
+ var GovernorCore = class {
34559
+ constructor(deps) {
34560
+ this.deps = deps;
34258
34561
  }
34259
- busy;
34260
- /** After a session ended or failed: shuts a headless sandbox down if it stays idle for a moment. */
34261
- check() {
34562
+ deps;
34563
+ state = INITIAL_STATE;
34564
+ lastAction = "";
34565
+ lastWrite = "";
34566
+ lastWriteAt = 0;
34567
+ /** Compilers waiting their turn now, by pid (name), to log a hold once. */
34568
+ held = /* @__PURE__ */ new Map();
34569
+ latest = null;
34570
+ diskQueue = null;
34571
+ hostPressure(seconds, reason, level = "slow", diskQueue) {
34572
+ if (diskQueue !== void 0) this.diskQueue = diskQueue;
34573
+ const nowMs = this.deps.now();
34574
+ const until = nowMs + seconds * 1e3;
34575
+ const active = this.state.hostUntil > nowMs;
34576
+ const step = Math.max(level === "severe" ? 4 : 2, active ? this.state.hostStep : 0);
34577
+ if (!active || step > this.state.hostStep)
34578
+ this.deps.logLine(
34579
+ "governor: this PC is " + (level === "severe" ? "nearly out of memory" : "low") + " (" + reason + "); builds to step " + step
34580
+ );
34581
+ this.state = { ...this.state, hostUntil: Math.max(until, this.state.hostUntil), hostStep: step };
34582
+ }
34583
+ /** The newest numbers, null before the first sample. */
34584
+ get snapshot() {
34585
+ return this.latest ? { ...this.latest, diskQueue: this.diskQueue } : null;
34586
+ }
34587
+ get current() {
34588
+ return this.state;
34589
+ }
34590
+ onSample(sample) {
34591
+ const nowMs = this.deps.now();
34592
+ const before = this.state.step;
34593
+ const d = decide(this.state, sample, nowMs, this.deps.profile, this.deps.maxCompilers ?? null);
34594
+ this.state = d.state;
34595
+ this.latest = {
34596
+ ramFreePercent: d.ramFreePercent,
34597
+ commitPercent: d.commitPercent,
34598
+ step: d.state.step,
34599
+ hostStep: nowMs < d.state.hostUntil ? d.state.hostStep : 0,
34600
+ diskQueue: null,
34601
+ top: [...sample.procs].sort((a, b) => b.mb - a.mb).slice(0, 5).map((p) => ({ name: p.name.slice(0, 64), mb: Math.round(p.mb) }))
34602
+ };
34603
+ const holding = /* @__PURE__ */ new Set();
34604
+ for (const action of d.actions) {
34605
+ if (action.kind === "hold") holding.add(action.pid);
34606
+ this.apply(action);
34607
+ }
34608
+ for (const pid of [...this.held.keys()]) if (!holding.has(pid)) this.held.delete(pid);
34609
+ if (d.state.step !== before)
34610
+ this.deps.logLine(
34611
+ "governor: step " + d.state.step + " (" + d.ramFreePercent + "% RAM free, commit " + d.commitPercent + "% used): " + STEP_TEXT[d.state.step]
34612
+ );
34613
+ const status = {
34614
+ level: d.level,
34615
+ step: d.state.step,
34616
+ profile: this.deps.profile,
34617
+ ramFreePercent: Math.max(0, Math.min(100, d.ramFreePercent)),
34618
+ commitPercent: Math.max(0, Math.min(100, d.commitPercent)),
34619
+ lowered: Object.keys(d.state.lowered).length,
34620
+ paused: [...this.held].slice(0, 8).map(([pid, name]) => ({ name: name.slice(0, 64), pid, seconds: HOLD_MS / 1e3 })),
34621
+ cpuPercent: d.state.cpuRate,
34622
+ pcLow: nowMs < d.state.hostUntil,
34623
+ lastAction: this.lastAction.slice(0, 300),
34624
+ updatedAt: now()
34625
+ };
34626
+ const shape = JSON.stringify({ ...status, updatedAt: "", paused: status.paused.map((p) => p.pid) });
34627
+ if (shape !== this.lastWrite || nowMs - this.lastWriteAt > 6e4) {
34628
+ this.lastWrite = shape;
34629
+ this.lastWriteAt = nowMs;
34630
+ this.deps.saveStatus(status);
34631
+ }
34632
+ }
34633
+ apply(action) {
34634
+ const port = this.deps.port;
34635
+ try {
34636
+ switch (action.kind) {
34637
+ case "hold":
34638
+ port.hold(action.pid, action.ms);
34639
+ if (!this.held.has(action.pid)) {
34640
+ this.held.set(action.pid, action.name);
34641
+ this.note(action.name + " (" + action.pid + ") waits its turn");
34642
+ }
34643
+ return;
34644
+ case "priority":
34645
+ port.setPriority(action.pid, action.to);
34646
+ this.note(action.name + " (" + action.pid + ") priority " + action.to);
34647
+ return;
34648
+ case "io":
34649
+ port.setIoPriority(action.pid, action.to);
34650
+ return;
34651
+ case "memory":
34652
+ port.setMemoryPriority(action.pid, action.low);
34653
+ return;
34654
+ case "throttle":
34655
+ port.throttle(action.pid);
34656
+ return;
34657
+ case "cpu":
34658
+ port.setCpuRate(action.percent);
34659
+ this.note(action.percent >= 100 ? "compilers: no CPU cap" : "compilers: at most " + action.percent + "% CPU");
34660
+ return;
34661
+ case "trim":
34662
+ port.trim(action.pid);
34663
+ return;
34664
+ }
34665
+ } catch (e) {
34666
+ this.deps.warn("governor: " + errMsg(e));
34667
+ }
34668
+ }
34669
+ note(text) {
34670
+ this.lastAction = text;
34671
+ this.deps.logLine("governor: " + text);
34672
+ }
34673
+ };
34674
+
34675
+ // src/guest/governor/governor.ts
34676
+ import fs63 from "node:fs";
34677
+ import os13 from "node:os";
34678
+ import path80 from "node:path";
34679
+ import { spawn as spawn8 } from "node:child_process";
34680
+ import { createInterface } from "node:readline";
34681
+ var SampleSchema = external_exports.object({
34682
+ ramTotalMb: external_exports.number(),
34683
+ ramAvailMb: external_exports.number(),
34684
+ commitLimitMb: external_exports.number(),
34685
+ commitAvailMb: external_exports.number(),
34686
+ procs: external_exports.array(
34687
+ external_exports.object({
34688
+ pid: external_exports.number().int(),
34689
+ name: external_exports.string().max(128),
34690
+ mb: external_exports.number(),
34691
+ prio: external_exports.enum(["idle", "below", "normal"]).catch("normal")
34692
+ })
34693
+ ).max(1024)
34694
+ });
34695
+ var MemGovProcess = class {
34696
+ constructor(onSample) {
34697
+ this.onSample = onSample;
34698
+ }
34699
+ onSample;
34700
+ child = null;
34701
+ start() {
34702
+ try {
34703
+ const child = spawn8(ensureCompiled("memgov"), ["serve", "--interval", "2"], {
34704
+ stdio: ["pipe", "pipe", "ignore"],
34705
+ windowsHide: true
34706
+ });
34707
+ this.child = child;
34708
+ createInterface({ input: child.stdout }).on("line", (line) => {
34709
+ try {
34710
+ this.onSample(SampleSchema.parse(JSON.parse(line)));
34711
+ } catch (e) {
34712
+ warn("governor: bad sample: " + errMsg(e));
34713
+ }
34714
+ });
34715
+ child.stdin.on("error", () => void 0);
34716
+ child.once("exit", () => {
34717
+ this.child = null;
34718
+ setTimeout(() => this.start(), 3e4).unref();
34719
+ });
34720
+ } catch (e) {
34721
+ warn("governor: could not start memgov.exe (no memory governor): " + errMsg(e));
34722
+ }
34723
+ }
34724
+ setPriority(pid, to) {
34725
+ this.send("priority " + pid + " " + to);
34726
+ }
34727
+ hold(pid, ms) {
34728
+ this.send("suspend " + pid + " " + ms);
34729
+ }
34730
+ setMemoryPriority(pid, low) {
34731
+ this.send("memprio " + pid + " " + (low ? "low" : "normal"));
34732
+ }
34733
+ setIoPriority(pid, to) {
34734
+ this.send("iopriority " + pid + " " + to);
34735
+ }
34736
+ throttle(pid) {
34737
+ this.send("throttle " + pid);
34738
+ }
34739
+ setCpuRate(percent) {
34740
+ this.send("cpurate " + Math.round(percent));
34741
+ }
34742
+ trim(pid) {
34743
+ this.send("trim " + pid);
34744
+ }
34745
+ send(line) {
34746
+ this.child?.stdin?.write(line + "\n");
34747
+ }
34748
+ };
34749
+ function appendToMemoryLog(line) {
34750
+ try {
34751
+ const logs = path80.join(dataDir, "logs");
34752
+ const newest = fs63.readdirSync(logs).filter((f) => /^memory-\d{8}-\d{6}\.log$/.test(f)).sort().pop();
34753
+ if (!newest) return;
34754
+ const t = /* @__PURE__ */ new Date();
34755
+ const stamp = [t.getHours(), t.getMinutes(), t.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
34756
+ fs63.appendFileSync(path80.join(logs, newest), stamp + " | " + line + "\r\n");
34757
+ } catch {
34758
+ }
34759
+ }
34760
+ var Governor = class {
34761
+ core = null;
34762
+ describe() {
34763
+ const profile = session.loadProfile === "gentle" ? "gentle" : "normal";
34764
+ const first = DIAL_STEPS[profile][0];
34765
+ return session.pressureResponse === false ? "off (sandbox.pressureResponse)" : profile + " profile: slows builds down from " + first.availablePercent + "% free RAM or " + first.commitPercent + "% commit, in steps";
34766
+ }
34767
+ start() {
34768
+ if (session.pressureResponse === false) return;
34769
+ const profile = session.loadProfile === "gentle" ? "gentle" : "normal";
34770
+ const proc = new MemGovProcess((s) => this.core?.onSample(s));
34771
+ this.core = new GovernorCore({
34772
+ port: proc,
34773
+ profile,
34774
+ maxCompilers: maxCompilers(os13.cpus().length, profile),
34775
+ now: () => Date.now(),
34776
+ logLine: appendToMemoryLog,
34777
+ warn,
34778
+ saveStatus: (status) => {
34779
+ try {
34780
+ writeJsonAtomic(run.pressureFile, status);
34781
+ } catch (e) {
34782
+ warn("governor: could not write its status: " + errMsg(e));
34783
+ }
34784
+ }
34785
+ });
34786
+ proc.start();
34787
+ }
34788
+ /** The host's watchdog: this PC is low (or nearly out of memory) for a while. */
34789
+ hostPressure(seconds, reason, level = "slow", diskQueue) {
34790
+ this.core?.hostPressure(seconds, reason, level, diskQueue);
34791
+ }
34792
+ /** The newest numbers, for the heartbeat (null when the governor is off or has not sampled yet). */
34793
+ snapshot() {
34794
+ return this.core?.snapshot ?? null;
34795
+ }
34796
+ };
34797
+ var governor = new Governor();
34798
+
34799
+ // src/guest/daemon/heartbeat.ts
34800
+ var BEAT_MS = 1e4;
34801
+ var clamp = (n, low, high) => Math.max(low, Math.min(high, n));
34802
+ function buildHeartbeat(snapshot2, sessionCount, uptimeSeconds, freeBytes2, totalBytes, at, ending = false) {
34803
+ const ramFree = snapshot2?.ramFreePercent ?? Math.round(freeBytes2 / Math.max(1, totalBytes) * 100);
34804
+ return {
34805
+ at,
34806
+ uptimeS: Math.max(0, Math.round(uptimeSeconds)),
34807
+ ramFreePct: clamp(ramFree, 0, 100),
34808
+ commitPct: clamp(snapshot2?.commitPercent ?? 0, 0, 100),
34809
+ step: clamp(snapshot2?.step ?? 0, 0, 4),
34810
+ hostStep: clamp(snapshot2?.hostStep ?? 0, 0, 4),
34811
+ topProcesses: (snapshot2?.top ?? []).slice(0, 5).map((p) => ({ name: p.name.slice(0, 64), privateMb: p.mb })),
34812
+ ...snapshot2?.diskQueue !== null && snapshot2?.diskQueue !== void 0 ? { diskQueueReported: snapshot2.diskQueue } : {},
34813
+ phase: ending ? "ending" : sessionCount > 0 ? "working" : "ready"
34814
+ };
34815
+ }
34816
+ var countSessions = () => 0;
34817
+ function writeBeat(ending) {
34818
+ try {
34819
+ writeJsonAtomic(
34820
+ run.heartbeatFile,
34821
+ buildHeartbeat(governor.snapshot(), countSessions(), os14.uptime(), os14.freemem(), os14.totalmem(), now(), ending)
34822
+ );
34823
+ } catch (e) {
34824
+ warn("heartbeat: " + errMsg(e));
34825
+ }
34826
+ }
34827
+ function markEnding() {
34828
+ writeBeat(true);
34829
+ }
34830
+ function startHeartbeat(sessionCount) {
34831
+ countSessions = sessionCount;
34832
+ writeBeat(false);
34833
+ const timer = setInterval(() => writeBeat(false), BEAT_MS);
34834
+ timer.unref();
34835
+ for (const signal of ["SIGHUP", "SIGBREAK", "SIGTERM"])
34836
+ process.on(signal, () => {
34837
+ clearInterval(timer);
34838
+ markEnding();
34839
+ process.exit(0);
34840
+ });
34841
+ }
34842
+
34843
+ // src/guest/daemon/idle-shutdown.ts
34844
+ var GRACE_MS = 15e3;
34845
+ var IdleShutdown = class {
34846
+ /** `busy`: a session runs or is starting. */
34847
+ constructor(busy) {
34848
+ this.busy = busy;
34849
+ }
34850
+ busy;
34851
+ /** After a session ended or failed: shuts a headless sandbox down if it stays idle for a moment. */
34852
+ check() {
34262
34853
  if (!session.headless || !isWin) return;
34263
34854
  if (!this.idle()) return;
34264
34855
  setTimeout(() => {
34265
34856
  if (!this.idle()) return;
34266
34857
  log("Headless sandbox has no sessions left; shutting down.");
34858
+ safely("heartbeat", markEnding);
34267
34859
  safely("claude-mem save", saveClaudeMem);
34268
34860
  safely("agent databases save", saveLocalState);
34269
34861
  void flushAccounts(true).finally(() => spawnSync6(SYSTEM.shutdown(), ["/s", "/t", "5"], { windowsHide: true }));
@@ -34278,11 +34870,11 @@ var IdleShutdown = class {
34278
34870
  var CLOSE_WAIT_MS = 8e3;
34279
34871
  var ROOTS_KEPT_MS = 10 * 60 * 1e3;
34280
34872
  function terminalTab(id, title, cwd) {
34281
- const view = [process.execPath, path80.join(path80.dirname(process.argv[1]), "view.js"), id];
34282
- const wt = path80.join(toolsDir, "terminal", "wt.exe");
34873
+ const view = [process.execPath, path81.join(path81.dirname(process.argv[1]), "view.js"), id];
34874
+ const wt = path81.join(toolsDir, "terminal", "wt.exe");
34283
34875
  if (exists(wt))
34284
- spawn8(wt, ["-w", "0", "nt", "--title", title, "-d", cwd, ...view], { detached: true, stdio: "ignore" }).unref();
34285
- else spawn8(SYSTEM.cmd(), ["/d", "/c", "start", title, ...view], { detached: true, stdio: "ignore", cwd }).unref();
34876
+ spawn9(wt, ["-w", "0", "nt", "--title", title, "-d", cwd, ...view], { detached: true, stdio: "ignore" }).unref();
34877
+ else spawn9(SYSTEM.cmd(), ["/d", "/c", "start", title, ...view], { detached: true, stdio: "ignore", cwd }).unref();
34286
34878
  }
34287
34879
  var harnessFor = (req) => {
34288
34880
  if (req.harness === session.harnessName && session.harness) return session.harness;
@@ -34295,8 +34887,8 @@ var harnessFor = (req) => {
34295
34887
  function memoryEnv(env, sessions2) {
34296
34888
  const out = {};
34297
34889
  out.PTYHOST_BACKSTOP_MB = String(session.agentBackstop ?? "auto");
34298
- const cpus = usableCpus(os13.cpus().length, session.cpuLimitPercent ?? null);
34299
- const memoryGb = os13.totalmem() / 1024 ** 3;
34890
+ const cpus = usableCpus(os15.cpus().length, session.cpuLimitPercent ?? null);
34891
+ const memoryGb = os15.totalmem() / 1024 ** 3;
34300
34892
  const jobs = cargoJobs(cpus, memoryGb, sessions2);
34301
34893
  out.NUMBER_OF_PROCESSORS = String(cpus);
34302
34894
  if (!env.CARGO_BUILD_JOBS) out.CARGO_BUILD_JOBS = String(jobs);
@@ -34335,10 +34927,10 @@ var agentProgram = async (req, env, say) => {
34335
34927
  };
34336
34928
  function shellPaths() {
34337
34929
  const candidates = [
34338
- path80.join(pwshDir(), "pwsh.exe"),
34339
- path80.join(process.env.ProgramFiles ?? "C:\\Program Files", "PowerShell", "7", "pwsh.exe")
34930
+ path81.join(pwshDir(), "pwsh.exe"),
34931
+ path81.join(process.env.ProgramFiles ?? "C:\\Program Files", "PowerShell", "7", "pwsh.exe")
34340
34932
  ];
34341
- return { pwsh: candidates.find((f) => fs63.existsSync(f)) ?? null, powershell: SYSTEM.powershell(), cmd: SYSTEM.cmd() };
34933
+ return { pwsh: candidates.find((f) => fs64.existsSync(f)) ?? null, powershell: SYSTEM.powershell(), cmd: SYSTEM.cmd() };
34342
34934
  }
34343
34935
  var shellProgram = (shell) => {
34344
34936
  const line = commandLine(shell.file, shell.args);
@@ -34372,7 +34964,7 @@ var launchPtyHost = async (req, program, remote, env, sessions2, onSpawn) => {
34372
34964
  } catch (e) {
34373
34965
  const tail = ptyHostLogTail(req.id);
34374
34966
  throw new Error(
34375
- errMsg(e) + (tail ? ". From its log:\n" + tail : "") + "\nLog: " + path80.join(dataDir, "logs", "ptyhost-" + req.id + ".log")
34967
+ errMsg(e) + (tail ? ". From its log:\n" + tail : "") + "\nLog: " + path81.join(dataDir, "logs", "ptyhost-" + req.id + ".log")
34376
34968
  );
34377
34969
  }
34378
34970
  return { child, port, token };
@@ -34486,7 +35078,7 @@ var SessionManager = class {
34486
35078
  };
34487
35079
  this.starting++;
34488
35080
  if (req.id !== session.launchId && process.env.AGENT_NO_TABS !== "1")
34489
- safely("terminal tab", () => terminalTab(req.id, name + " " + path80.basename(req.cwd), req.cwd));
35081
+ safely("terminal tab", () => terminalTab(req.id, name + " " + path81.basename(req.cwd), req.cwd));
34490
35082
  try {
34491
35083
  if (problem) throw new Error(problem);
34492
35084
  const place = await placeFor(req, say);
@@ -34654,449 +35246,6 @@ var SessionManager = class {
34654
35246
  };
34655
35247
  var sessions = new SessionManager();
34656
35248
 
34657
- // src/guest/governor/decide.ts
34658
- var COMPILERS = /* @__PURE__ */ new Set([
34659
- "rustc",
34660
- "clippy-driver",
34661
- "rustdoc",
34662
- "cc1",
34663
- "cc1plus",
34664
- "cc1obj",
34665
- "collect2",
34666
- "gcc",
34667
- "g++",
34668
- "c++",
34669
- "cc",
34670
- "clang",
34671
- "clang++",
34672
- "clang-cl",
34673
- "cl",
34674
- "link",
34675
- "lld",
34676
- "lld-link",
34677
- "ld",
34678
- "ld.lld",
34679
- "as",
34680
- "tsc",
34681
- "javac"
34682
- ]);
34683
- var BUILD_TOOLS = /* @__PURE__ */ new Set([
34684
- "dotnet",
34685
- "msbuild",
34686
- "vbcscompiler",
34687
- "ninja",
34688
- "make",
34689
- "mingw32-make",
34690
- "cmake",
34691
- "cargo",
34692
- "go",
34693
- "java",
34694
- "gradle",
34695
- "python"
34696
- ]);
34697
- var NEVER2 = /* @__PURE__ */ new Set([
34698
- "claude",
34699
- "codex",
34700
- "gemini",
34701
- "opencode",
34702
- "aider",
34703
- "cursor-agent",
34704
- "cmd",
34705
- "powershell",
34706
- "pwsh",
34707
- "bash",
34708
- "sh",
34709
- "zsh",
34710
- "git",
34711
- "conhost",
34712
- "openconsole",
34713
- "windowsterminal",
34714
- "wt",
34715
- "ptyhost",
34716
- "memgov",
34717
- "sandboxtools",
34718
- "sbx-db",
34719
- "explorer",
34720
- "csrss",
34721
- "wininit",
34722
- "winlogon",
34723
- "services",
34724
- "svchost",
34725
- "lsass",
34726
- "system",
34727
- "smss",
34728
- "dwm"
34729
- ]);
34730
- var key = (name) => name.toLowerCase().replace(/\.exe$/, "");
34731
- var isNeverTouched = (name) => NEVER2.has(key(name));
34732
- var canLower = (name) => !isNeverTouched(name) && (COMPILERS.has(key(name)) || BUILD_TOOLS.has(key(name)));
34733
- var HEAVY = /* @__PURE__ */ new Set([
34734
- "rustc",
34735
- "clippy-driver",
34736
- "rustdoc",
34737
- "cc1",
34738
- "cc1plus",
34739
- "clang",
34740
- "clang++",
34741
- "clang-cl",
34742
- "cl",
34743
- "link",
34744
- "lld",
34745
- "lld-link",
34746
- "ld",
34747
- "ld.lld"
34748
- ]);
34749
- var isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
34750
- var canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
34751
- var INITIAL_STATE = {
34752
- step: 0,
34753
- calm: 0,
34754
- lowered: {},
34755
- memLow: [],
34756
- throttled: [],
34757
- trimmed: [],
34758
- cpuRate: 100,
34759
- lastCommitMb: 0,
34760
- lastAt: 0,
34761
- growth: 0,
34762
- hostUntil: 0,
34763
- hostStep: 0
34764
- };
34765
- var GROWTH_WEIGHT = 0.3;
34766
- var HOLD_MS = 3e3;
34767
- var ROTATE_MS = 2e4;
34768
- var SOON_WITHIN_PERCENT = 12;
34769
- function stepFor(ramFree, commit, secondsToLimit, profile) {
34770
- let step = 0;
34771
- DIAL_STEPS[profile].forEach((t, i) => {
34772
- const near = commit > t.commitPercent - SOON_WITHIN_PERCENT;
34773
- const soon = near && t.secondsToLimit !== void 0 && secondsToLimit < t.secondsToLimit;
34774
- if (ramFree < t.availablePercent || commit > t.commitPercent || soon) step = i + 1;
34775
- });
34776
- return step;
34777
- }
34778
- function clearOf(step, ramFree, commit, profile) {
34779
- if (step === 0) return true;
34780
- const t = DIAL_STEPS[profile][step - 1];
34781
- return ramFree > t.availablePercent + DIAL_EXIT_MARGIN.availablePercent && commit < t.commitPercent - DIAL_EXIT_MARGIN.commitPercent;
34782
- }
34783
- function decide(prev, sample, nowMs, profile, maxCompilers2 = null) {
34784
- const ramFree = sample.ramAvailMb / Math.max(1, sample.ramTotalMb) * 100;
34785
- const usedMb = sample.commitLimitMb - sample.commitAvailMb;
34786
- const commit = usedMb / Math.max(1, sample.commitLimitMb) * 100;
34787
- const seconds = (nowMs - prev.lastAt) / 1e3;
34788
- const instant = prev.lastAt > 0 && seconds > 0 ? (usedMb - prev.lastCommitMb) / seconds : 0;
34789
- const growth = prev.lastAt > 0 ? prev.growth * (1 - GROWTH_WEIGHT) + instant * GROWTH_WEIGHT : 0;
34790
- const secondsToLimit = growth > 0 ? sample.commitAvailMb / growth : Number.POSITIVE_INFINITY;
34791
- const pcLow = nowMs < prev.hostUntil;
34792
- const wanted2 = Math.max(stepFor(ramFree, commit, secondsToLimit, profile), pcLow ? prev.hostStep : 0);
34793
- let step = prev.step;
34794
- let calm = 0;
34795
- if (wanted2 > prev.step) step = wanted2;
34796
- else if (prev.step > 0 && wanted2 < prev.step && clearOf(prev.step, ramFree, commit, profile)) {
34797
- calm = prev.calm + 1;
34798
- if (calm >= DIAL_CALM_SAMPLES) {
34799
- step = prev.step - 1;
34800
- calm = 0;
34801
- }
34802
- }
34803
- const actions = [];
34804
- const present2 = new Set(sample.procs.map((p) => p.pid));
34805
- const keep = (pids) => pids.filter((pid) => present2.has(pid));
34806
- const lowered = {};
34807
- for (const [pid, to] of Object.entries(prev.lowered)) if (present2.has(Number(pid))) lowered[Number(pid)] = to;
34808
- const memLow = new Set(keep(prev.memLow));
34809
- const throttled = new Set(keep(prev.throttled));
34810
- const trimmed = new Set(keep(prev.trimmed));
34811
- for (const p of sample.procs) {
34812
- if (!canLower(p.name)) continue;
34813
- const rest = profile === "gentle" && canPause(p.name) ? "below" : "normal";
34814
- const target = step >= 2 ? "idle" : step === 1 ? "below" : rest;
34815
- const was = lowered[p.pid];
34816
- const touched = was !== void 0 || target !== "normal";
34817
- if (touched && was !== target && p.prio !== target)
34818
- actions.push({ kind: "priority", pid: p.pid, name: p.name, to: target });
34819
- if (target === "normal") delete lowered[p.pid];
34820
- else lowered[p.pid] = target;
34821
- }
34822
- const lowMemory = step >= 1;
34823
- for (const p of sample.procs) {
34824
- if (!canLower(p.name) || lowMemory === memLow.has(p.pid)) continue;
34825
- actions.push({ kind: "memory", pid: p.pid, name: p.name, low: lowMemory });
34826
- if (lowMemory) memLow.add(p.pid);
34827
- else memLow.delete(p.pid);
34828
- }
34829
- if (step >= 2) {
34830
- for (const p of sample.procs)
34831
- if (canPause(p.name) && !throttled.has(p.pid)) {
34832
- actions.push({ kind: "throttle", pid: p.pid, name: p.name });
34833
- throttled.add(p.pid);
34834
- }
34835
- }
34836
- const cpuRate = DIAL_CPU_RATE[step] ?? 100;
34837
- if (cpuRate !== prev.cpuRate) actions.push({ kind: "cpu", percent: cpuRate });
34838
- const heavy = sample.procs.filter((p) => isHeavyCompiler(p.name)).sort((a, b) => a.pid - b.pid);
34839
- let allowed = compilersAllowed(step, heavy.length);
34840
- if (maxCompilers2 !== null && step >= 2) allowed = Math.min(allowed, Math.max(1, maxCompilers2));
34841
- if (allowed < heavy.length) {
34842
- const turn = Math.floor(nowMs / ROTATE_MS) % heavy.length;
34843
- const order = [...heavy.slice(turn), ...heavy.slice(0, turn)];
34844
- for (const p of order.slice(allowed)) {
34845
- actions.push({ kind: "hold", pid: p.pid, name: p.name, ms: HOLD_MS });
34846
- if (step >= 3 && !trimmed.has(p.pid)) {
34847
- actions.push({ kind: "trim", pid: p.pid, name: p.name });
34848
- trimmed.add(p.pid);
34849
- }
34850
- }
34851
- for (const p of order.slice(0, allowed)) trimmed.delete(p.pid);
34852
- } else trimmed.clear();
34853
- const level = step >= 3 ? "suspending" : step >= 1 ? "throttled" : "ok";
34854
- return {
34855
- state: {
34856
- step,
34857
- calm,
34858
- lowered,
34859
- memLow: [...memLow],
34860
- throttled: [...throttled],
34861
- trimmed: [...trimmed],
34862
- cpuRate,
34863
- lastCommitMb: usedMb,
34864
- lastAt: nowMs,
34865
- growth,
34866
- hostUntil: prev.hostUntil,
34867
- hostStep: prev.hostStep
34868
- },
34869
- actions,
34870
- level,
34871
- ramFreePercent: Math.round(ramFree),
34872
- commitPercent: Math.round(commit)
34873
- };
34874
- }
34875
-
34876
- // src/guest/governor/core.ts
34877
- var STEP_TEXT = [
34878
- "builds back to normal",
34879
- "build programs at lower priority",
34880
- "builds get less CPU and take turns",
34881
- "builds take turns, waiting ones go to the paging file",
34882
- "one compiler at a time"
34883
- ];
34884
- var GovernorCore = class {
34885
- constructor(deps) {
34886
- this.deps = deps;
34887
- }
34888
- deps;
34889
- state = INITIAL_STATE;
34890
- lastAction = "";
34891
- lastWrite = "";
34892
- lastWriteAt = 0;
34893
- /** Compilers waiting their turn now, by pid (name), to log a hold once. */
34894
- held = /* @__PURE__ */ new Map();
34895
- hostPressure(seconds, reason, level = "slow") {
34896
- const nowMs = this.deps.now();
34897
- const until = nowMs + seconds * 1e3;
34898
- const active = this.state.hostUntil > nowMs;
34899
- const step = Math.max(level === "severe" ? 4 : 2, active ? this.state.hostStep : 0);
34900
- if (!active || step > this.state.hostStep)
34901
- this.deps.logLine(
34902
- "governor: this PC is " + (level === "severe" ? "nearly out of memory" : "low") + " (" + reason + "); builds to step " + step
34903
- );
34904
- this.state = { ...this.state, hostUntil: Math.max(until, this.state.hostUntil), hostStep: step };
34905
- }
34906
- get current() {
34907
- return this.state;
34908
- }
34909
- onSample(sample) {
34910
- const nowMs = this.deps.now();
34911
- const before = this.state.step;
34912
- const d = decide(this.state, sample, nowMs, this.deps.profile, this.deps.maxCompilers ?? null);
34913
- this.state = d.state;
34914
- const holding = /* @__PURE__ */ new Set();
34915
- for (const action of d.actions) {
34916
- if (action.kind === "hold") holding.add(action.pid);
34917
- this.apply(action);
34918
- }
34919
- for (const pid of [...this.held.keys()]) if (!holding.has(pid)) this.held.delete(pid);
34920
- if (d.state.step !== before)
34921
- this.deps.logLine(
34922
- "governor: step " + d.state.step + " (" + d.ramFreePercent + "% RAM free, commit " + d.commitPercent + "% used): " + STEP_TEXT[d.state.step]
34923
- );
34924
- const status = {
34925
- level: d.level,
34926
- step: d.state.step,
34927
- profile: this.deps.profile,
34928
- ramFreePercent: Math.max(0, Math.min(100, d.ramFreePercent)),
34929
- commitPercent: Math.max(0, Math.min(100, d.commitPercent)),
34930
- lowered: Object.keys(d.state.lowered).length,
34931
- paused: [...this.held].slice(0, 8).map(([pid, name]) => ({ name: name.slice(0, 64), pid, seconds: HOLD_MS / 1e3 })),
34932
- cpuPercent: d.state.cpuRate,
34933
- pcLow: nowMs < d.state.hostUntil,
34934
- lastAction: this.lastAction.slice(0, 300),
34935
- updatedAt: now()
34936
- };
34937
- const shape = JSON.stringify({ ...status, updatedAt: "", paused: status.paused.map((p) => p.pid) });
34938
- if (shape !== this.lastWrite || nowMs - this.lastWriteAt > 6e4) {
34939
- this.lastWrite = shape;
34940
- this.lastWriteAt = nowMs;
34941
- this.deps.saveStatus(status);
34942
- }
34943
- }
34944
- apply(action) {
34945
- const port = this.deps.port;
34946
- try {
34947
- switch (action.kind) {
34948
- case "hold":
34949
- port.hold(action.pid, action.ms);
34950
- if (!this.held.has(action.pid)) {
34951
- this.held.set(action.pid, action.name);
34952
- this.note(action.name + " (" + action.pid + ") waits its turn");
34953
- }
34954
- return;
34955
- case "priority":
34956
- port.setPriority(action.pid, action.to);
34957
- this.note(action.name + " (" + action.pid + ") priority " + action.to);
34958
- return;
34959
- case "memory":
34960
- port.setMemoryPriority(action.pid, action.low);
34961
- return;
34962
- case "throttle":
34963
- port.throttle(action.pid);
34964
- return;
34965
- case "cpu":
34966
- port.setCpuRate(action.percent);
34967
- this.note(action.percent >= 100 ? "compilers: no CPU cap" : "compilers: at most " + action.percent + "% CPU");
34968
- return;
34969
- case "trim":
34970
- port.trim(action.pid);
34971
- return;
34972
- }
34973
- } catch (e) {
34974
- this.deps.warn("governor: " + errMsg(e));
34975
- }
34976
- }
34977
- note(text) {
34978
- this.lastAction = text;
34979
- this.deps.logLine("governor: " + text);
34980
- }
34981
- };
34982
-
34983
- // src/guest/governor/governor.ts
34984
- import fs64 from "node:fs";
34985
- import os14 from "node:os";
34986
- import path81 from "node:path";
34987
- import { spawn as spawn9 } from "node:child_process";
34988
- import { createInterface } from "node:readline";
34989
- var SampleSchema = external_exports.object({
34990
- ramTotalMb: external_exports.number(),
34991
- ramAvailMb: external_exports.number(),
34992
- commitLimitMb: external_exports.number(),
34993
- commitAvailMb: external_exports.number(),
34994
- procs: external_exports.array(
34995
- external_exports.object({
34996
- pid: external_exports.number().int(),
34997
- name: external_exports.string().max(128),
34998
- mb: external_exports.number(),
34999
- prio: external_exports.enum(["idle", "below", "normal"]).catch("normal")
35000
- })
35001
- ).max(1024)
35002
- });
35003
- var MemGovProcess = class {
35004
- constructor(onSample) {
35005
- this.onSample = onSample;
35006
- }
35007
- onSample;
35008
- child = null;
35009
- start() {
35010
- try {
35011
- const child = spawn9(ensureCompiled("memgov"), ["serve", "--interval", "2"], {
35012
- stdio: ["pipe", "pipe", "ignore"],
35013
- windowsHide: true
35014
- });
35015
- this.child = child;
35016
- createInterface({ input: child.stdout }).on("line", (line) => {
35017
- try {
35018
- this.onSample(SampleSchema.parse(JSON.parse(line)));
35019
- } catch (e) {
35020
- warn("governor: bad sample: " + errMsg(e));
35021
- }
35022
- });
35023
- child.stdin.on("error", () => void 0);
35024
- child.once("exit", () => {
35025
- this.child = null;
35026
- setTimeout(() => this.start(), 3e4).unref();
35027
- });
35028
- } catch (e) {
35029
- warn("governor: could not start memgov.exe (no memory governor): " + errMsg(e));
35030
- }
35031
- }
35032
- setPriority(pid, to) {
35033
- this.send("priority " + pid + " " + to);
35034
- }
35035
- hold(pid, ms) {
35036
- this.send("suspend " + pid + " " + ms);
35037
- }
35038
- setMemoryPriority(pid, low) {
35039
- this.send("memprio " + pid + " " + (low ? "low" : "normal"));
35040
- }
35041
- throttle(pid) {
35042
- this.send("throttle " + pid);
35043
- }
35044
- setCpuRate(percent) {
35045
- this.send("cpurate " + Math.round(percent));
35046
- }
35047
- trim(pid) {
35048
- this.send("trim " + pid);
35049
- }
35050
- send(line) {
35051
- this.child?.stdin?.write(line + "\n");
35052
- }
35053
- };
35054
- function appendToMemoryLog(line) {
35055
- try {
35056
- const logs = path81.join(dataDir, "logs");
35057
- const newest = fs64.readdirSync(logs).filter((f) => /^memory-\d{8}-\d{6}\.log$/.test(f)).sort().pop();
35058
- if (!newest) return;
35059
- const t = /* @__PURE__ */ new Date();
35060
- const stamp = [t.getHours(), t.getMinutes(), t.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
35061
- fs64.appendFileSync(path81.join(logs, newest), stamp + " | " + line + "\r\n");
35062
- } catch {
35063
- }
35064
- }
35065
- var Governor = class {
35066
- core = null;
35067
- describe() {
35068
- const profile = session.loadProfile === "gentle" ? "gentle" : "normal";
35069
- const first = DIAL_STEPS[profile][0];
35070
- return session.pressureResponse === false ? "off (sandbox.pressureResponse)" : profile + " profile: slows builds down from " + first.availablePercent + "% free RAM or " + first.commitPercent + "% commit, in steps";
35071
- }
35072
- start() {
35073
- if (session.pressureResponse === false) return;
35074
- const profile = session.loadProfile === "gentle" ? "gentle" : "normal";
35075
- const proc = new MemGovProcess((s) => this.core?.onSample(s));
35076
- this.core = new GovernorCore({
35077
- port: proc,
35078
- profile,
35079
- maxCompilers: maxCompilers(os14.cpus().length, profile),
35080
- now: () => Date.now(),
35081
- logLine: appendToMemoryLog,
35082
- warn,
35083
- saveStatus: (status) => {
35084
- try {
35085
- writeJsonAtomic(run.pressureFile, status);
35086
- } catch (e) {
35087
- warn("governor: could not write its status: " + errMsg(e));
35088
- }
35089
- }
35090
- });
35091
- proc.start();
35092
- }
35093
- /** The host's watchdog: this PC is low (or nearly out of memory) for a while. */
35094
- hostPressure(seconds, reason, level = "slow") {
35095
- this.core?.hostPressure(seconds, reason, level);
35096
- }
35097
- };
35098
- var governor = new Governor();
35099
-
35100
35249
  // src/guest/daemon/live.ts
35101
35250
  import fs66 from "node:fs";
35102
35251
  import path83 from "node:path";
@@ -35453,7 +35602,7 @@ var Daemon = class {
35453
35602
  case "close":
35454
35603
  return sessions.close(req);
35455
35604
  case "pressure":
35456
- return governor.hostPressure(req.seconds, req.reason, req.level ?? "slow");
35605
+ return governor.hostPressure(req.seconds, req.reason, req.level ?? "slow", req.diskQueue);
35457
35606
  case "ping":
35458
35607
  return respond(req.id, { ok: true, message: "pong (" + sessions.count + " session(s) running)" });
35459
35608
  }
@@ -35472,4 +35621,5 @@ safely("leftover processes", () => sessions.sweepLeftovers());
35472
35621
  safely("memory governor", () => governor.start());
35473
35622
  log("Memory governor: " + governor.describe());
35474
35623
  safely("service sign-ins", startAccountWatch);
35624
+ safely("heartbeat", () => startHeartbeat(() => sessions.count));
35475
35625
  new Daemon().start();