windows-development-sandbox 1.18.0 → 1.18.3

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 (73) hide show
  1. package/config.example.json +1 -1
  2. package/dist/app/features/defs.d.ts +8 -0
  3. package/dist/app/features/sandbox/sandbox.def.d.ts +12 -0
  4. package/dist/app/features/sandbox/sandbox.def.js +17 -2
  5. package/dist/app/features/sandbox/sandbox.run.d.ts +4 -0
  6. package/dist/app/features/sandbox/sandbox.run.js +4 -0
  7. package/dist/bin/agent-sandbox.js +2 -2
  8. package/dist/bin/{chunk-BKF43GJ3.js → chunk-3G53NNZP.js} +1 -1
  9. package/dist/bin/{chunk-YD3A67RS.js → chunk-FQWYCF6L.js} +984 -868
  10. package/dist/bin/{chunk-Z5D32FN3.js → chunk-M6XUQK53.js} +1 -1
  11. package/dist/bin/claude-host.js +2 -2
  12. package/dist/bin/claude-sbx.js +2 -2
  13. package/dist/bin/codex-sbx.js +2 -2
  14. package/dist/bin/gemini-sbx.js +2 -2
  15. package/dist/bin/sandbox.js +2 -2
  16. package/dist/bin/sbx-hide.js +2 -2
  17. package/dist/bin/sbx-show.js +2 -2
  18. package/dist/cli/commands/repair/checks/sandbox.d.ts +3 -1
  19. package/dist/cli/commands/repair/checks/sandbox.js +24 -0
  20. package/dist/core/config/defaults.js +1 -1
  21. package/dist/core/schema/sandbox.js +1 -1
  22. package/dist/core/schema/toolchains.js +2 -2
  23. package/dist/guest/daemon/daemon.js +1 -1
  24. package/dist/guest/daemon/heartbeat.d.ts +6 -0
  25. package/dist/guest/daemon/heartbeat.js +40 -0
  26. package/dist/guest/entries/daemon.js +2 -0
  27. package/dist/guest/governor/core.d.ts +19 -1
  28. package/dist/guest/governor/core.js +23 -1
  29. package/dist/guest/governor/decide.d.ts +14 -0
  30. package/dist/guest/governor/decide.js +52 -7
  31. package/dist/guest/governor/governor.d.ts +4 -1
  32. package/dist/guest/governor/governor.js +9 -2
  33. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  34. package/dist/guest/toolchains/native/rust-extras.js +26 -3
  35. package/dist/guest/toolchains/plan.js +6 -4
  36. package/dist/host/channel/channel.d.ts +1 -1
  37. package/dist/host/channel/channel.js +10 -2
  38. package/dist/host/launch/wsb-file.d.ts +6 -1
  39. package/dist/host/launch/wsb-file.js +9 -5
  40. package/dist/host/life/crash-report.d.ts +2 -0
  41. package/dist/host/life/crash-report.js +4 -0
  42. package/dist/host/life/index.d.ts +1 -0
  43. package/dist/host/life/index.js +1 -0
  44. package/dist/host/life/last-end.d.ts +21 -0
  45. package/dist/host/life/last-end.js +70 -0
  46. package/dist/host/life/watchdog.d.ts +2 -0
  47. package/dist/host/life/watchdog.js +8 -2
  48. package/dist/host/profile/claude-login.js +3 -2
  49. package/dist/runtime/agent.js +26 -3
  50. package/dist/runtime/daemon.js +367 -234
  51. package/dist/runtime/db.js +26 -3
  52. package/dist/runtime/main.js +57 -11
  53. package/dist/runtime/netproxy.js +26 -3
  54. package/dist/runtime/tool.js +26 -3
  55. package/dist/runtime/view.js +26 -3
  56. package/dist/selftest/heavy-load.test.js +95 -14
  57. package/dist/selftest/live-terminal.test.js +4 -0
  58. package/dist/shared/channel/run-dir.d.ts +4 -1
  59. package/dist/shared/channel/run-dir.js +7 -1
  60. package/dist/shared/channel/schema.d.ts +23 -0
  61. package/dist/shared/channel/schema.js +21 -0
  62. package/dist/shared/memory/policy.d.ts +10 -0
  63. package/dist/shared/memory/policy.js +17 -1
  64. package/dist/sync/watcher.js +1 -1
  65. package/dist/web/index.html +1 -1
  66. package/dist/web/{main-CWIF4VQI.js → main-6XYO5FZC.js} +43 -43
  67. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  68. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  69. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  70. package/package.json +1 -1
  71. package/sandbox/csharp/MemGov.cs +58 -0
  72. package/schemas/config.schema.json +3 -3
  73. 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,21 @@ 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
+ phase: external_exports.enum(["ready", "working"])
27408
+ });
27392
27409
  var GuestResponseSchema = external_exports.object({
27393
27410
  id: IdSchema,
27394
27411
  ok: external_exports.boolean(),
@@ -27699,6 +27716,8 @@ var RunDir = class {
27699
27716
  vpnFile;
27700
27717
  /** What the memory governor in the sandbox is doing (pressure.json). */
27701
27718
  pressureFile;
27719
+ /** The daemon's heartbeat every 10 seconds (heartbeat.json): what the sandbox did last. */
27720
+ heartbeatFile;
27702
27721
  /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
27703
27722
  budgetFile;
27704
27723
  requestsDir;
@@ -27722,6 +27741,7 @@ var RunDir = class {
27722
27741
  this.sandboxFile = path7.join(this.root, "sandbox.json");
27723
27742
  this.vpnFile = path7.join(this.root, "vpn.json");
27724
27743
  this.pressureFile = path7.join(this.root, "pressure.json");
27744
+ this.heartbeatFile = path7.join(this.root, "heartbeat.json");
27725
27745
  this.budgetFile = path7.join(this.root, "budget.json");
27726
27746
  this.requestsDir = path7.join(this.root, "requests");
27727
27747
  this.responsesDir = path7.join(this.root, "responses");
@@ -27766,6 +27786,9 @@ var RunDir = class {
27766
27786
  readPressure() {
27767
27787
  return readValid(PressureStatusSchema, this.pressureFile);
27768
27788
  }
27789
+ readHeartbeat() {
27790
+ return readValid(HeartbeatSchema, this.heartbeatFile);
27791
+ }
27769
27792
  readSession(id) {
27770
27793
  return readValid(SessionStatusSchema, this.sessionFile(id));
27771
27794
  }
@@ -33038,11 +33061,280 @@ function cargoJobs(cpus, memoryGb, sessions2) {
33038
33061
  const jobs = Math.min(byCpu, byMemory);
33039
33062
  return Math.max(1, Math.min(jobs, Math.floor(buildJobs(cpus, memoryGb) / Math.max(1, sessions2))));
33040
33063
  }
33064
+ var LOCAL_TARGET_MIN_BYTES = 25 * 1024 ** 3;
33065
+ function chooseTargetDir(targetDir, freeBytes2) {
33066
+ if (targetDir !== "auto") return { targetDir };
33067
+ if (freeBytes2 >= LOCAL_TARGET_MIN_BYTES) return { targetDir: "local" };
33068
+ return {
33069
+ targetDir: "separate",
33070
+ 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)"
33071
+ };
33072
+ }
33041
33073
  function maxCompilers(cpus, profile) {
33042
- return profile === "gentle" ? Math.max(2, Math.floor(cpus / 4)) : null;
33074
+ return profile === "gentle" ? Math.min(4, Math.max(2, Math.floor(cpus / 4))) : null;
33075
+ }
33076
+
33077
+ // src/guest/governor/decide.ts
33078
+ var COMPILERS = /* @__PURE__ */ new Set([
33079
+ "rustc",
33080
+ "clippy-driver",
33081
+ "rustdoc",
33082
+ "cc1",
33083
+ "cc1plus",
33084
+ "cc1obj",
33085
+ "collect2",
33086
+ "gcc",
33087
+ "g++",
33088
+ "c++",
33089
+ "cc",
33090
+ "clang",
33091
+ "clang++",
33092
+ "clang-cl",
33093
+ "cl",
33094
+ "link",
33095
+ "lld",
33096
+ "lld-link",
33097
+ "ld",
33098
+ "ld.lld",
33099
+ "as",
33100
+ "tsc",
33101
+ "javac"
33102
+ ]);
33103
+ var BUILD_TOOLS = /* @__PURE__ */ new Set([
33104
+ "dotnet",
33105
+ "msbuild",
33106
+ "vbcscompiler",
33107
+ "ninja",
33108
+ "make",
33109
+ "mingw32-make",
33110
+ "cmake",
33111
+ "cargo",
33112
+ "go",
33113
+ "java",
33114
+ "gradle",
33115
+ "python"
33116
+ ]);
33117
+ var NEVER2 = /* @__PURE__ */ new Set([
33118
+ "claude",
33119
+ "codex",
33120
+ "gemini",
33121
+ "opencode",
33122
+ "aider",
33123
+ "cursor-agent",
33124
+ "cmd",
33125
+ "powershell",
33126
+ "pwsh",
33127
+ "bash",
33128
+ "sh",
33129
+ "zsh",
33130
+ "git",
33131
+ "conhost",
33132
+ "openconsole",
33133
+ "windowsterminal",
33134
+ "wt",
33135
+ "ptyhost",
33136
+ "memgov",
33137
+ "sandboxtools",
33138
+ "sbx-db",
33139
+ "explorer",
33140
+ "csrss",
33141
+ "wininit",
33142
+ "winlogon",
33143
+ "services",
33144
+ "svchost",
33145
+ "lsass",
33146
+ "system",
33147
+ "smss",
33148
+ "dwm"
33149
+ ]);
33150
+ var key = (name) => name.toLowerCase().replace(/\.exe$/, "");
33151
+ var isNeverTouched = (name) => NEVER2.has(key(name));
33152
+ var canLower = (name) => !isNeverTouched(name) && (COMPILERS.has(key(name)) || BUILD_TOOLS.has(key(name)));
33153
+ var HEAVY = /* @__PURE__ */ new Set([
33154
+ "rustc",
33155
+ "clippy-driver",
33156
+ "rustdoc",
33157
+ "cc1",
33158
+ "cc1plus",
33159
+ "clang",
33160
+ "clang++",
33161
+ "clang-cl",
33162
+ "cl",
33163
+ "link",
33164
+ "lld",
33165
+ "lld-link",
33166
+ "ld",
33167
+ "ld.lld"
33168
+ ]);
33169
+ var LINKERS = /* @__PURE__ */ new Set(["link", "lld", "lld-link", "ld", "ld.lld"]);
33170
+ var isLinker = (name) => !isNeverTouched(name) && LINKERS.has(key(name));
33171
+ function linkerSlots(totalMb, cpus) {
33172
+ return Math.max(1, Math.min(4, Math.max(1, Math.floor(cpus)), Math.floor(totalMb / 6144)));
33173
+ }
33174
+ var isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
33175
+ var canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
33176
+ var INITIAL_STATE = {
33177
+ step: 0,
33178
+ calm: 0,
33179
+ lowered: {},
33180
+ memLow: [],
33181
+ throttled: [],
33182
+ trimmed: [],
33183
+ ioLowered: {},
33184
+ cpuRate: 100,
33185
+ lastCommitMb: 0,
33186
+ lastAt: 0,
33187
+ growth: 0,
33188
+ hostUntil: 0,
33189
+ hostStep: 0
33190
+ };
33191
+ var GROWTH_WEIGHT = 0.3;
33192
+ var HOLD_MS = 3e3;
33193
+ var ROTATE_MS = 2e4;
33194
+ var SOON_WITHIN_PERCENT = 12;
33195
+ function stepFor(ramFree, commit, secondsToLimit, profile) {
33196
+ let step = 0;
33197
+ DIAL_STEPS[profile].forEach((t, i) => {
33198
+ const near = commit > t.commitPercent - SOON_WITHIN_PERCENT;
33199
+ const soon = near && t.secondsToLimit !== void 0 && secondsToLimit < t.secondsToLimit;
33200
+ if (ramFree < t.availablePercent || commit > t.commitPercent || soon) step = i + 1;
33201
+ });
33202
+ return step;
33203
+ }
33204
+ function clearOf(step, ramFree, commit, profile) {
33205
+ if (step === 0) return true;
33206
+ const t = DIAL_STEPS[profile][step - 1];
33207
+ return ramFree > t.availablePercent + DIAL_EXIT_MARGIN.availablePercent && commit < t.commitPercent - DIAL_EXIT_MARGIN.commitPercent;
33208
+ }
33209
+ function decide(prev, sample, nowMs, profile, maxCompilers2 = null) {
33210
+ const ramFree = sample.ramAvailMb / Math.max(1, sample.ramTotalMb) * 100;
33211
+ const usedMb = sample.commitLimitMb - sample.commitAvailMb;
33212
+ const commit = usedMb / Math.max(1, sample.commitLimitMb) * 100;
33213
+ const seconds = (nowMs - prev.lastAt) / 1e3;
33214
+ const instant = prev.lastAt > 0 && seconds > 0 ? (usedMb - prev.lastCommitMb) / seconds : 0;
33215
+ const growth = prev.lastAt > 0 ? prev.growth * (1 - GROWTH_WEIGHT) + instant * GROWTH_WEIGHT : 0;
33216
+ const secondsToLimit = growth > 0 ? sample.commitAvailMb / growth : Number.POSITIVE_INFINITY;
33217
+ const pcLow = nowMs < prev.hostUntil;
33218
+ const wanted2 = Math.max(stepFor(ramFree, commit, secondsToLimit, profile), pcLow ? prev.hostStep : 0);
33219
+ let step = prev.step;
33220
+ let calm = 0;
33221
+ if (wanted2 > prev.step) step = wanted2;
33222
+ else if (prev.step > 0 && wanted2 < prev.step && clearOf(prev.step, ramFree, commit, profile)) {
33223
+ calm = prev.calm + 1;
33224
+ if (calm >= DIAL_CALM_SAMPLES) {
33225
+ step = prev.step - 1;
33226
+ calm = 0;
33227
+ }
33228
+ }
33229
+ if (profile === "gentle" && step < 2 && sample.procs.some((p) => isHeavyCompiler(p.name))) step = 2;
33230
+ const actions = [];
33231
+ const present2 = new Set(sample.procs.map((p) => p.pid));
33232
+ const keep = (pids) => pids.filter((pid) => present2.has(pid));
33233
+ const lowered = {};
33234
+ for (const [pid, to] of Object.entries(prev.lowered)) if (present2.has(Number(pid))) lowered[Number(pid)] = to;
33235
+ const memLow = new Set(keep(prev.memLow));
33236
+ const throttled = new Set(keep(prev.throttled));
33237
+ const trimmed = new Set(keep(prev.trimmed));
33238
+ for (const p of sample.procs) {
33239
+ if (!canLower(p.name)) continue;
33240
+ const rest = profile === "gentle" && canPause(p.name) ? "below" : "normal";
33241
+ const target = step >= 2 ? "idle" : step === 1 ? "below" : rest;
33242
+ const was = lowered[p.pid];
33243
+ const touched = was !== void 0 || target !== "normal";
33244
+ if (touched && was !== target && p.prio !== target)
33245
+ actions.push({ kind: "priority", pid: p.pid, name: p.name, to: target });
33246
+ if (target === "normal") delete lowered[p.pid];
33247
+ else lowered[p.pid] = target;
33248
+ }
33249
+ const ioLowered = {};
33250
+ for (const [pid, to] of Object.entries(prev.ioLowered)) if (present2.has(Number(pid))) ioLowered[Number(pid)] = to;
33251
+ for (const p of sample.procs) {
33252
+ if (!canPause(p.name)) continue;
33253
+ const cpu = lowered[p.pid] ?? "normal";
33254
+ const target = step >= 3 ? "verylow" : cpu !== "normal" ? "low" : "normal";
33255
+ const was = ioLowered[p.pid] ?? "normal";
33256
+ if (was !== target) actions.push({ kind: "io", pid: p.pid, name: p.name, to: target });
33257
+ if (target === "normal") delete ioLowered[p.pid];
33258
+ else ioLowered[p.pid] = target;
33259
+ }
33260
+ const lowMemory = step >= 1;
33261
+ for (const p of sample.procs) {
33262
+ if (!canLower(p.name) || lowMemory === memLow.has(p.pid)) continue;
33263
+ actions.push({ kind: "memory", pid: p.pid, name: p.name, low: lowMemory });
33264
+ if (lowMemory) memLow.add(p.pid);
33265
+ else memLow.delete(p.pid);
33266
+ }
33267
+ if (step >= 2) {
33268
+ for (const p of sample.procs)
33269
+ if (canPause(p.name) && !throttled.has(p.pid)) {
33270
+ actions.push({ kind: "throttle", pid: p.pid, name: p.name });
33271
+ throttled.add(p.pid);
33272
+ }
33273
+ }
33274
+ const cpuRate = DIAL_CPU_RATE[step] ?? 100;
33275
+ if (cpuRate !== prev.cpuRate) actions.push({ kind: "cpu", percent: cpuRate });
33276
+ const heavy = sample.procs.filter((p) => isHeavyCompiler(p.name)).sort((a, b) => a.pid - b.pid);
33277
+ let allowed = compilersAllowed(step, heavy.length);
33278
+ if (maxCompilers2 !== null && step >= 1) allowed = Math.min(allowed, Math.max(1, maxCompilers2));
33279
+ const turn = heavy.length > 0 ? Math.floor(nowMs / ROTATE_MS) % heavy.length : 0;
33280
+ const order = [...heavy.slice(turn), ...heavy.slice(0, turn)];
33281
+ let waiting = order.slice(Math.min(allowed, order.length));
33282
+ if (step >= 2) {
33283
+ const waitingPids = new Set(waiting.map((p) => p.pid));
33284
+ let linking = 0;
33285
+ for (const p of order)
33286
+ if (isLinker(p.name) && !waitingPids.has(p.pid) && ++linking > 1) {
33287
+ waiting = [...waiting, p];
33288
+ waitingPids.add(p.pid);
33289
+ }
33290
+ }
33291
+ if (waiting.length > 0) {
33292
+ const waitingPids = new Set(waiting.map((p) => p.pid));
33293
+ for (const p of waiting) {
33294
+ actions.push({ kind: "hold", pid: p.pid, name: p.name, ms: HOLD_MS });
33295
+ if (step >= 3 && !trimmed.has(p.pid)) {
33296
+ actions.push({ kind: "trim", pid: p.pid, name: p.name });
33297
+ trimmed.add(p.pid);
33298
+ }
33299
+ }
33300
+ for (const p of order) if (!waitingPids.has(p.pid)) trimmed.delete(p.pid);
33301
+ } else trimmed.clear();
33302
+ const level = step >= 3 ? "suspending" : step >= 1 ? "throttled" : "ok";
33303
+ return {
33304
+ state: {
33305
+ step,
33306
+ calm,
33307
+ lowered,
33308
+ memLow: [...memLow],
33309
+ throttled: [...throttled],
33310
+ trimmed: [...trimmed],
33311
+ ioLowered,
33312
+ cpuRate,
33313
+ lastCommitMb: usedMb,
33314
+ lastAt: nowMs,
33315
+ growth,
33316
+ hostUntil: prev.hostUntil,
33317
+ hostStep: prev.hostStep
33318
+ },
33319
+ actions,
33320
+ level,
33321
+ ramFreePercent: Math.round(ramFree),
33322
+ commitPercent: Math.round(commit)
33323
+ };
33043
33324
  }
33044
33325
 
33045
33326
  // src/guest/toolchains/native/rust-extras.ts
33327
+ function resolveTargetDir(rust, free = freeBytes((process.env.SystemDrive ?? "C:") + "\\"), note = warn) {
33328
+ if (rust.targetDir !== "auto") return rust;
33329
+ const chosen = chooseTargetDir(rust.targetDir, free);
33330
+ if (chosen.warning) note(chosen.warning);
33331
+ return { ...rust, targetDir: chosen.targetDir };
33332
+ }
33333
+ function targetSummary(rust) {
33334
+ const target = cargoTarget(rust.targetDir);
33335
+ if (target) return "rust builds: " + target + " (CARGO_TARGET_DIR)";
33336
+ 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)";
33337
+ }
33046
33338
  var cargoTarget = (targetDir) => {
33047
33339
  if (targetDir === "shared" || targetDir === "separate") return null;
33048
33340
  if (targetDir === "local") return path70.join(localDir, "cargo-target");
@@ -33061,8 +33353,7 @@ function linkGate(env, rust, host) {
33061
33353
  if (!host.endsWith("-pc-windows-gnu") || !exists(gcc)) return;
33062
33354
  const gate = path70.join(mkdirp(tool("shims")), "linkgate.exe");
33063
33355
  fs56.copyFileSync(ensureCompiled("linkgate"), gate);
33064
- const memoryGb = os10.totalmem() / 1024 ** 3;
33065
- const slots = rust.linkSlots === "auto" ? linkSlotsFor(memoryGb) : rust.linkSlots;
33356
+ const slots = rust.linkSlots === "auto" ? linkerSlots(os10.totalmem() / 1024 ** 2, os10.cpus().length) : rust.linkSlots;
33066
33357
  env.vars.AGENT_LINK_SLOTS = String(slots);
33067
33358
  env.vars.AGENT_REAL_LINKER = gcc;
33068
33359
  env.vars.CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = gate;
@@ -33174,11 +33465,13 @@ function plan(config2, versions, o) {
33174
33465
  after: ["C/C++", "MSVC", "Visual Studio", "Unreal"],
33175
33466
  install: async (env) => {
33176
33467
  await setupRust(env, { ...config2.rust, host: rustHost(config2) }, o.refresh);
33177
- Object.assign(env.vars, rustCaches(config2.rust));
33468
+ const rust = resolveTargetDir(config2.rust);
33469
+ Object.assign(env.vars, rustCaches(rust));
33470
+ env.summary.push(targetSummary(rust));
33178
33471
  if (!dryRun) {
33179
33472
  cargoDefaults(rustHost(config2));
33180
- cargoShim(env, config2.rust);
33181
- linkGate(env, config2.rust, rustHost(config2));
33473
+ cargoShim(env, rust);
33474
+ linkGate(env, rust, rustHost(config2));
33182
33475
  }
33183
33476
  }
33184
33477
  },
@@ -34654,225 +34947,6 @@ var SessionManager = class {
34654
34947
  };
34655
34948
  var sessions = new SessionManager();
34656
34949
 
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
34950
  // src/guest/governor/core.ts
34877
34951
  var STEP_TEXT = [
34878
34952
  "builds back to normal",
@@ -34892,7 +34966,10 @@ var GovernorCore = class {
34892
34966
  lastWriteAt = 0;
34893
34967
  /** Compilers waiting their turn now, by pid (name), to log a hold once. */
34894
34968
  held = /* @__PURE__ */ new Map();
34895
- hostPressure(seconds, reason, level = "slow") {
34969
+ latest = null;
34970
+ diskQueue = null;
34971
+ hostPressure(seconds, reason, level = "slow", diskQueue) {
34972
+ if (diskQueue !== void 0) this.diskQueue = diskQueue;
34896
34973
  const nowMs = this.deps.now();
34897
34974
  const until = nowMs + seconds * 1e3;
34898
34975
  const active = this.state.hostUntil > nowMs;
@@ -34903,6 +34980,10 @@ var GovernorCore = class {
34903
34980
  );
34904
34981
  this.state = { ...this.state, hostUntil: Math.max(until, this.state.hostUntil), hostStep: step };
34905
34982
  }
34983
+ /** The newest numbers, null before the first sample. */
34984
+ get snapshot() {
34985
+ return this.latest ? { ...this.latest, diskQueue: this.diskQueue } : null;
34986
+ }
34906
34987
  get current() {
34907
34988
  return this.state;
34908
34989
  }
@@ -34911,6 +34992,14 @@ var GovernorCore = class {
34911
34992
  const before = this.state.step;
34912
34993
  const d = decide(this.state, sample, nowMs, this.deps.profile, this.deps.maxCompilers ?? null);
34913
34994
  this.state = d.state;
34995
+ this.latest = {
34996
+ ramFreePercent: d.ramFreePercent,
34997
+ commitPercent: d.commitPercent,
34998
+ step: d.state.step,
34999
+ hostStep: nowMs < d.state.hostUntil ? d.state.hostStep : 0,
35000
+ diskQueue: null,
35001
+ 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) }))
35002
+ };
34914
35003
  const holding = /* @__PURE__ */ new Set();
34915
35004
  for (const action of d.actions) {
34916
35005
  if (action.kind === "hold") holding.add(action.pid);
@@ -34956,6 +35045,9 @@ var GovernorCore = class {
34956
35045
  port.setPriority(action.pid, action.to);
34957
35046
  this.note(action.name + " (" + action.pid + ") priority " + action.to);
34958
35047
  return;
35048
+ case "io":
35049
+ port.setIoPriority(action.pid, action.to);
35050
+ return;
34959
35051
  case "memory":
34960
35052
  port.setMemoryPriority(action.pid, action.low);
34961
35053
  return;
@@ -35038,6 +35130,9 @@ var MemGovProcess = class {
35038
35130
  setMemoryPriority(pid, low) {
35039
35131
  this.send("memprio " + pid + " " + (low ? "low" : "normal"));
35040
35132
  }
35133
+ setIoPriority(pid, to) {
35134
+ this.send("iopriority " + pid + " " + to);
35135
+ }
35041
35136
  throttle(pid) {
35042
35137
  this.send("throttle " + pid);
35043
35138
  }
@@ -35091,8 +35186,12 @@ var Governor = class {
35091
35186
  proc.start();
35092
35187
  }
35093
35188
  /** 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);
35189
+ hostPressure(seconds, reason, level = "slow", diskQueue) {
35190
+ this.core?.hostPressure(seconds, reason, level, diskQueue);
35191
+ }
35192
+ /** The newest numbers, for the heartbeat (null when the governor is off or has not sampled yet). */
35193
+ snapshot() {
35194
+ return this.core?.snapshot ?? null;
35096
35195
  }
35097
35196
  };
35098
35197
  var governor = new Governor();
@@ -35453,13 +35552,46 @@ var Daemon = class {
35453
35552
  case "close":
35454
35553
  return sessions.close(req);
35455
35554
  case "pressure":
35456
- return governor.hostPressure(req.seconds, req.reason, req.level ?? "slow");
35555
+ return governor.hostPressure(req.seconds, req.reason, req.level ?? "slow", req.diskQueue);
35457
35556
  case "ping":
35458
35557
  return respond(req.id, { ok: true, message: "pong (" + sessions.count + " session(s) running)" });
35459
35558
  }
35460
35559
  }
35461
35560
  };
35462
35561
 
35562
+ // src/guest/daemon/heartbeat.ts
35563
+ import os15 from "node:os";
35564
+ var BEAT_MS = 1e4;
35565
+ var clamp = (n, low, high) => Math.max(low, Math.min(high, n));
35566
+ function buildHeartbeat(snapshot2, sessionCount, uptimeSeconds, freeBytes2, totalBytes, at) {
35567
+ const ramFree = snapshot2?.ramFreePercent ?? Math.round(freeBytes2 / Math.max(1, totalBytes) * 100);
35568
+ return {
35569
+ at,
35570
+ uptimeS: Math.max(0, Math.round(uptimeSeconds)),
35571
+ ramFreePct: clamp(ramFree, 0, 100),
35572
+ commitPct: clamp(snapshot2?.commitPercent ?? 0, 0, 100),
35573
+ step: clamp(snapshot2?.step ?? 0, 0, 4),
35574
+ hostStep: clamp(snapshot2?.hostStep ?? 0, 0, 4),
35575
+ topProcesses: (snapshot2?.top ?? []).slice(0, 5).map((p) => ({ name: p.name.slice(0, 64), privateMb: p.mb })),
35576
+ ...snapshot2?.diskQueue != null ? { diskQueueReported: snapshot2.diskQueue } : {},
35577
+ phase: sessionCount > 0 ? "working" : "ready"
35578
+ };
35579
+ }
35580
+ function startHeartbeat() {
35581
+ const beat = () => {
35582
+ try {
35583
+ writeJsonAtomic(
35584
+ run.heartbeatFile,
35585
+ buildHeartbeat(governor.snapshot(), sessions.count, os15.uptime(), os15.freemem(), os15.totalmem(), now())
35586
+ );
35587
+ } catch (e) {
35588
+ warn("heartbeat: " + errMsg(e));
35589
+ }
35590
+ };
35591
+ beat();
35592
+ setInterval(beat, BEAT_MS).unref();
35593
+ }
35594
+
35463
35595
  // src/guest/entries/daemon.ts
35464
35596
  process.on("uncaughtException", (e) => warn("daemon: unexpected error (continuing): " + (e.stack ?? errMsg(e))));
35465
35597
  process.on("unhandledRejection", (e) => warn("daemon: unexpected error (continuing): " + errMsg(e)));
@@ -35472,4 +35604,5 @@ safely("leftover processes", () => sessions.sweepLeftovers());
35472
35604
  safely("memory governor", () => governor.start());
35473
35605
  log("Memory governor: " + governor.describe());
35474
35606
  safely("service sign-ins", startAccountWatch);
35607
+ safely("heartbeat", startHeartbeat);
35475
35608
  new Daemon().start();