windows-development-sandbox 1.18.3 → 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 (67) hide show
  1. package/dist/app/features/defs.d.ts +18 -0
  2. package/dist/app/features/defs.js +2 -1
  3. package/dist/app/features/sandbox/sandbox.def.d.ts +22 -0
  4. package/dist/app/features/sandbox/sandbox.def.js +41 -14
  5. package/dist/app/features/sandbox/sandbox.run.d.ts +20 -0
  6. package/dist/app/features/sandbox/sandbox.run.js +41 -1
  7. package/dist/app/registry.js +2 -1
  8. package/dist/bin/agent-sandbox.js +2 -2
  9. package/dist/bin/{chunk-FQWYCF6L.js → chunk-FRPLQLSY.js} +3453 -3145
  10. package/dist/bin/{chunk-M6XUQK53.js → chunk-MY7NLQQY.js} +1 -1
  11. package/dist/bin/{chunk-3G53NNZP.js → chunk-N4GXYTLV.js} +1 -1
  12. package/dist/bin/claude-host.js +2 -2
  13. package/dist/bin/claude-sbx.js +2 -2
  14. package/dist/bin/codex-sbx.js +2 -2
  15. package/dist/bin/gemini-sbx.js +2 -2
  16. package/dist/bin/sandbox.js +2 -2
  17. package/dist/bin/sbx-hide.js +2 -2
  18. package/dist/bin/sbx-show.js +2 -2
  19. package/dist/cli/commands/launch/launch.js +4 -1
  20. package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
  21. package/dist/cli/commands/launch/leftover-offer.js +26 -0
  22. package/dist/cli/commands/repair/checks/sandbox.js +29 -2
  23. package/dist/guest/daemon/heartbeat.d.ts +7 -2
  24. package/dist/guest/daemon/heartbeat.js +34 -15
  25. package/dist/guest/daemon/idle-shutdown.js +2 -0
  26. package/dist/guest/entries/daemon.js +1 -1
  27. package/dist/host/health/index.d.ts +1 -0
  28. package/dist/host/health/index.js +1 -0
  29. package/dist/host/health/leftovers.d.ts +3 -0
  30. package/dist/host/health/leftovers.js +40 -0
  31. package/dist/host/health/stale.d.ts +48 -0
  32. package/dist/host/health/stale.js +40 -0
  33. package/dist/host/life/crash-report.d.ts +1 -1
  34. package/dist/host/life/crash-report.js +5 -3
  35. package/dist/host/life/last-end.d.ts +7 -0
  36. package/dist/host/life/last-end.js +21 -0
  37. package/dist/host/life/life-record.d.ts +3 -0
  38. package/dist/host/life/life-record.js +2 -2
  39. package/dist/host/life/stop-events.d.ts +3 -1
  40. package/dist/host/life/stop-events.js +59 -0
  41. package/dist/host/life/stop-warning.js +3 -0
  42. package/dist/host/sandbox/cleanup.d.ts +37 -0
  43. package/dist/host/sandbox/cleanup.js +82 -0
  44. package/dist/host/sandbox/index.d.ts +1 -0
  45. package/dist/host/sandbox/index.js +1 -0
  46. package/dist/host/sandbox/platform.js +1 -1
  47. package/dist/runtime/agent.js +3 -1
  48. package/dist/runtime/daemon.js +368 -351
  49. package/dist/runtime/db.js +3 -1
  50. package/dist/runtime/main.js +3 -1
  51. package/dist/runtime/netproxy.js +3 -1
  52. package/dist/runtime/tool.js +3 -1
  53. package/dist/runtime/view.js +3 -1
  54. package/dist/selftest/heavy-load.test.js +119 -2
  55. package/dist/shared/channel/schema.d.ts +1 -0
  56. package/dist/shared/channel/schema.js +2 -1
  57. package/dist/shared/system.d.ts +1 -0
  58. package/dist/shared/system.js +1 -0
  59. package/dist/sync/watcher.js +9 -4
  60. package/dist/web/index.html +1 -1
  61. package/dist/web/{main-6XYO5FZC.js → main-GVZWWP4I.js} +62 -62
  62. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  63. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  64. package/native/csharp/HostTools/Program.cs +5 -1
  65. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  66. package/native/scripts/stop-events.ps1 +7 -3
  67. package/package.json +1 -1
@@ -27404,7 +27404,8 @@ var HeartbeatSchema = external_exports.object({
27404
27404
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27405
27405
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27406
27406
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27407
- phase: external_exports.enum(["ready", "working"])
27407
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27408
+ phase: external_exports.enum(["ready", "working", "ending"])
27408
27409
  });
27409
27410
  var GuestResponseSchema = external_exports.object({
27410
27411
  id: IdSchema,
@@ -27889,6 +27890,7 @@ var SYSTEM = {
27889
27890
  cmd: () => system32("cmd.exe"),
27890
27891
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
27891
27892
  tasklist: () => system32("tasklist.exe"),
27893
+ taskkill: () => system32("taskkill.exe"),
27892
27894
  whoami: () => system32("whoami.exe"),
27893
27895
  icacls: () => system32("icacls.exe"),
27894
27896
  tar: () => system32("tar.exe"),
@@ -33956,11 +33958,11 @@ function execCommand(req) {
33956
33958
  }
33957
33959
 
33958
33960
  // src/guest/daemon/sessions.ts
33959
- import fs63 from "node:fs";
33960
- import path80 from "node:path";
33961
+ import fs64 from "node:fs";
33962
+ import path81 from "node:path";
33961
33963
  import crypto5 from "node:crypto";
33962
- import os13 from "node:os";
33963
- import { spawn as spawn8 } from "node:child_process";
33964
+ import os15 from "node:os";
33965
+ import { spawn as spawn9 } from "node:child_process";
33964
33966
 
33965
33967
  // src/shared/pty/protocol.ts
33966
33968
  var FrameType = {
@@ -34542,71 +34544,368 @@ async function flushAccounts(final = false) {
34542
34544
  }
34543
34545
  }
34544
34546
 
34545
- // src/guest/daemon/idle-shutdown.ts
34546
- var GRACE_MS = 15e3;
34547
- var IdleShutdown = class {
34548
- /** `busy`: a session runs or is starting. */
34549
- constructor(busy) {
34550
- 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;
34551
34561
  }
34552
- busy;
34553
- /** After a session ended or failed: shuts a headless sandbox down if it stays idle for a moment. */
34554
- check() {
34555
- if (!session.headless || !isWin) return;
34556
- if (!this.idle()) return;
34557
- setTimeout(() => {
34558
- if (!this.idle()) return;
34559
- log("Headless sandbox has no sessions left; shutting down.");
34560
- safely("claude-mem save", saveClaudeMem);
34561
- safely("agent databases save", saveLocalState);
34562
- void flushAccounts(true).finally(() => spawnSync6(SYSTEM.shutdown(), ["/s", "/t", "5"], { windowsHide: true }));
34563
- }, GRACE_MS);
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 };
34564
34582
  }
34565
- idle() {
34566
- return !this.busy() && run.scanRequests().valid.length === 0;
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);
34567
34672
  }
34568
34673
  };
34569
34674
 
34570
- // src/guest/daemon/sessions.ts
34571
- var CLOSE_WAIT_MS = 8e3;
34572
- var ROOTS_KEPT_MS = 10 * 60 * 1e3;
34573
- function terminalTab(id, title, cwd) {
34574
- const view = [process.execPath, path80.join(path80.dirname(process.argv[1]), "view.js"), id];
34575
- const wt = path80.join(toolsDir, "terminal", "wt.exe");
34576
- if (exists(wt))
34577
- spawn8(wt, ["-w", "0", "nt", "--title", title, "-d", cwd, ...view], { detached: true, stdio: "ignore" }).unref();
34578
- else spawn8(SYSTEM.cmd(), ["/d", "/c", "start", title, ...view], { detached: true, stdio: "ignore", cwd }).unref();
34579
- }
34580
- var harnessFor = (req) => {
34581
- if (req.harness === session.harnessName && session.harness) return session.harness;
34582
- const sent = req.harnessSpec ?? req.project?.harnesses[req.harness];
34583
- if (sent) return sent;
34584
- throw new Error(
34585
- 'Agent "' + req.harness + '" is not configured for this sandbox (it runs ' + session.harnessName + ")."
34586
- );
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
+ }
34587
34748
  };
34588
- function memoryEnv(env, sessions2) {
34589
- const out = {};
34590
- out.PTYHOST_BACKSTOP_MB = String(session.agentBackstop ?? "auto");
34591
- const cpus = usableCpus(os13.cpus().length, session.cpuLimitPercent ?? null);
34592
- const memoryGb = os13.totalmem() / 1024 ** 3;
34593
- const jobs = cargoJobs(cpus, memoryGb, sessions2);
34594
- out.NUMBER_OF_PROCESSORS = String(cpus);
34595
- if (!env.CARGO_BUILD_JOBS) out.CARGO_BUILD_JOBS = String(jobs);
34596
- if (!env.CMAKE_BUILD_PARALLEL_LEVEL) out.CMAKE_BUILD_PARALLEL_LEVEL = String(jobs);
34597
- out.SANDBOX_BUDGET_FILE = run.budgetFile;
34598
- writeBudget(cpus, memoryGb, jobs, env);
34599
- return out;
34600
- }
34601
- function writeBudget(cpus, memoryGb, jobs, env) {
34749
+ function appendToMemoryLog(line) {
34602
34750
  try {
34603
- writeJsonAtomic(run.budgetFile, {
34604
- processors: cpus,
34605
- memoryMb: Math.round(memoryGb * 1024),
34606
- buildJobs: buildJobs(cpus, memoryGb),
34607
- cargoJobsPerSession: jobs,
34608
- linkSlots: Number(env.AGENT_LINK_SLOTS) || linkSlotsFor(memoryGb),
34609
- cpuLimitPercent: session.cpuLimitPercent ?? null,
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() {
34853
+ if (!session.headless || !isWin) return;
34854
+ if (!this.idle()) return;
34855
+ setTimeout(() => {
34856
+ if (!this.idle()) return;
34857
+ log("Headless sandbox has no sessions left; shutting down.");
34858
+ safely("heartbeat", markEnding);
34859
+ safely("claude-mem save", saveClaudeMem);
34860
+ safely("agent databases save", saveLocalState);
34861
+ void flushAccounts(true).finally(() => spawnSync6(SYSTEM.shutdown(), ["/s", "/t", "5"], { windowsHide: true }));
34862
+ }, GRACE_MS);
34863
+ }
34864
+ idle() {
34865
+ return !this.busy() && run.scanRequests().valid.length === 0;
34866
+ }
34867
+ };
34868
+
34869
+ // src/guest/daemon/sessions.ts
34870
+ var CLOSE_WAIT_MS = 8e3;
34871
+ var ROOTS_KEPT_MS = 10 * 60 * 1e3;
34872
+ function terminalTab(id, title, cwd) {
34873
+ const view = [process.execPath, path81.join(path81.dirname(process.argv[1]), "view.js"), id];
34874
+ const wt = path81.join(toolsDir, "terminal", "wt.exe");
34875
+ if (exists(wt))
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();
34878
+ }
34879
+ var harnessFor = (req) => {
34880
+ if (req.harness === session.harnessName && session.harness) return session.harness;
34881
+ const sent = req.harnessSpec ?? req.project?.harnesses[req.harness];
34882
+ if (sent) return sent;
34883
+ throw new Error(
34884
+ 'Agent "' + req.harness + '" is not configured for this sandbox (it runs ' + session.harnessName + ")."
34885
+ );
34886
+ };
34887
+ function memoryEnv(env, sessions2) {
34888
+ const out = {};
34889
+ out.PTYHOST_BACKSTOP_MB = String(session.agentBackstop ?? "auto");
34890
+ const cpus = usableCpus(os15.cpus().length, session.cpuLimitPercent ?? null);
34891
+ const memoryGb = os15.totalmem() / 1024 ** 3;
34892
+ const jobs = cargoJobs(cpus, memoryGb, sessions2);
34893
+ out.NUMBER_OF_PROCESSORS = String(cpus);
34894
+ if (!env.CARGO_BUILD_JOBS) out.CARGO_BUILD_JOBS = String(jobs);
34895
+ if (!env.CMAKE_BUILD_PARALLEL_LEVEL) out.CMAKE_BUILD_PARALLEL_LEVEL = String(jobs);
34896
+ out.SANDBOX_BUDGET_FILE = run.budgetFile;
34897
+ writeBudget(cpus, memoryGb, jobs, env);
34898
+ return out;
34899
+ }
34900
+ function writeBudget(cpus, memoryGb, jobs, env) {
34901
+ try {
34902
+ writeJsonAtomic(run.budgetFile, {
34903
+ processors: cpus,
34904
+ memoryMb: Math.round(memoryGb * 1024),
34905
+ buildJobs: buildJobs(cpus, memoryGb),
34906
+ cargoJobsPerSession: jobs,
34907
+ linkSlots: Number(env.AGENT_LINK_SLOTS) || linkSlotsFor(memoryGb),
34908
+ cpuLimitPercent: session.cpuLimitPercent ?? null,
34610
34909
  governor: run.pressureFile,
34611
34910
  note: "Plan builds with these numbers. When memory runs short the sandbox slows builds down in steps (governor); nothing fails."
34612
34911
  });
@@ -34628,10 +34927,10 @@ var agentProgram = async (req, env, say) => {
34628
34927
  };
34629
34928
  function shellPaths() {
34630
34929
  const candidates = [
34631
- path80.join(pwshDir(), "pwsh.exe"),
34632
- 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")
34633
34932
  ];
34634
- 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() };
34635
34934
  }
34636
34935
  var shellProgram = (shell) => {
34637
34936
  const line = commandLine(shell.file, shell.args);
@@ -34665,7 +34964,7 @@ var launchPtyHost = async (req, program, remote, env, sessions2, onSpawn) => {
34665
34964
  } catch (e) {
34666
34965
  const tail = ptyHostLogTail(req.id);
34667
34966
  throw new Error(
34668
- 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")
34669
34968
  );
34670
34969
  }
34671
34970
  return { child, port, token };
@@ -34779,7 +35078,7 @@ var SessionManager = class {
34779
35078
  };
34780
35079
  this.starting++;
34781
35080
  if (req.id !== session.launchId && process.env.AGENT_NO_TABS !== "1")
34782
- 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));
34783
35082
  try {
34784
35083
  if (problem) throw new Error(problem);
34785
35084
  const place = await placeFor(req, say);
@@ -34947,255 +35246,6 @@ var SessionManager = class {
34947
35246
  };
34948
35247
  var sessions = new SessionManager();
34949
35248
 
34950
- // src/guest/governor/core.ts
34951
- var STEP_TEXT = [
34952
- "builds back to normal",
34953
- "build programs at lower priority",
34954
- "builds get less CPU and take turns",
34955
- "builds take turns, waiting ones go to the paging file",
34956
- "one compiler at a time"
34957
- ];
34958
- var GovernorCore = class {
34959
- constructor(deps) {
34960
- this.deps = deps;
34961
- }
34962
- deps;
34963
- state = INITIAL_STATE;
34964
- lastAction = "";
34965
- lastWrite = "";
34966
- lastWriteAt = 0;
34967
- /** Compilers waiting their turn now, by pid (name), to log a hold once. */
34968
- held = /* @__PURE__ */ new Map();
34969
- latest = null;
34970
- diskQueue = null;
34971
- hostPressure(seconds, reason, level = "slow", diskQueue) {
34972
- if (diskQueue !== void 0) this.diskQueue = diskQueue;
34973
- const nowMs = this.deps.now();
34974
- const until = nowMs + seconds * 1e3;
34975
- const active = this.state.hostUntil > nowMs;
34976
- const step = Math.max(level === "severe" ? 4 : 2, active ? this.state.hostStep : 0);
34977
- if (!active || step > this.state.hostStep)
34978
- this.deps.logLine(
34979
- "governor: this PC is " + (level === "severe" ? "nearly out of memory" : "low") + " (" + reason + "); builds to step " + step
34980
- );
34981
- this.state = { ...this.state, hostUntil: Math.max(until, this.state.hostUntil), hostStep: step };
34982
- }
34983
- /** The newest numbers, null before the first sample. */
34984
- get snapshot() {
34985
- return this.latest ? { ...this.latest, diskQueue: this.diskQueue } : null;
34986
- }
34987
- get current() {
34988
- return this.state;
34989
- }
34990
- onSample(sample) {
34991
- const nowMs = this.deps.now();
34992
- const before = this.state.step;
34993
- const d = decide(this.state, sample, nowMs, this.deps.profile, this.deps.maxCompilers ?? null);
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
- };
35003
- const holding = /* @__PURE__ */ new Set();
35004
- for (const action of d.actions) {
35005
- if (action.kind === "hold") holding.add(action.pid);
35006
- this.apply(action);
35007
- }
35008
- for (const pid of [...this.held.keys()]) if (!holding.has(pid)) this.held.delete(pid);
35009
- if (d.state.step !== before)
35010
- this.deps.logLine(
35011
- "governor: step " + d.state.step + " (" + d.ramFreePercent + "% RAM free, commit " + d.commitPercent + "% used): " + STEP_TEXT[d.state.step]
35012
- );
35013
- const status = {
35014
- level: d.level,
35015
- step: d.state.step,
35016
- profile: this.deps.profile,
35017
- ramFreePercent: Math.max(0, Math.min(100, d.ramFreePercent)),
35018
- commitPercent: Math.max(0, Math.min(100, d.commitPercent)),
35019
- lowered: Object.keys(d.state.lowered).length,
35020
- paused: [...this.held].slice(0, 8).map(([pid, name]) => ({ name: name.slice(0, 64), pid, seconds: HOLD_MS / 1e3 })),
35021
- cpuPercent: d.state.cpuRate,
35022
- pcLow: nowMs < d.state.hostUntil,
35023
- lastAction: this.lastAction.slice(0, 300),
35024
- updatedAt: now()
35025
- };
35026
- const shape = JSON.stringify({ ...status, updatedAt: "", paused: status.paused.map((p) => p.pid) });
35027
- if (shape !== this.lastWrite || nowMs - this.lastWriteAt > 6e4) {
35028
- this.lastWrite = shape;
35029
- this.lastWriteAt = nowMs;
35030
- this.deps.saveStatus(status);
35031
- }
35032
- }
35033
- apply(action) {
35034
- const port = this.deps.port;
35035
- try {
35036
- switch (action.kind) {
35037
- case "hold":
35038
- port.hold(action.pid, action.ms);
35039
- if (!this.held.has(action.pid)) {
35040
- this.held.set(action.pid, action.name);
35041
- this.note(action.name + " (" + action.pid + ") waits its turn");
35042
- }
35043
- return;
35044
- case "priority":
35045
- port.setPriority(action.pid, action.to);
35046
- this.note(action.name + " (" + action.pid + ") priority " + action.to);
35047
- return;
35048
- case "io":
35049
- port.setIoPriority(action.pid, action.to);
35050
- return;
35051
- case "memory":
35052
- port.setMemoryPriority(action.pid, action.low);
35053
- return;
35054
- case "throttle":
35055
- port.throttle(action.pid);
35056
- return;
35057
- case "cpu":
35058
- port.setCpuRate(action.percent);
35059
- this.note(action.percent >= 100 ? "compilers: no CPU cap" : "compilers: at most " + action.percent + "% CPU");
35060
- return;
35061
- case "trim":
35062
- port.trim(action.pid);
35063
- return;
35064
- }
35065
- } catch (e) {
35066
- this.deps.warn("governor: " + errMsg(e));
35067
- }
35068
- }
35069
- note(text) {
35070
- this.lastAction = text;
35071
- this.deps.logLine("governor: " + text);
35072
- }
35073
- };
35074
-
35075
- // src/guest/governor/governor.ts
35076
- import fs64 from "node:fs";
35077
- import os14 from "node:os";
35078
- import path81 from "node:path";
35079
- import { spawn as spawn9 } from "node:child_process";
35080
- import { createInterface } from "node:readline";
35081
- var SampleSchema = external_exports.object({
35082
- ramTotalMb: external_exports.number(),
35083
- ramAvailMb: external_exports.number(),
35084
- commitLimitMb: external_exports.number(),
35085
- commitAvailMb: external_exports.number(),
35086
- procs: external_exports.array(
35087
- external_exports.object({
35088
- pid: external_exports.number().int(),
35089
- name: external_exports.string().max(128),
35090
- mb: external_exports.number(),
35091
- prio: external_exports.enum(["idle", "below", "normal"]).catch("normal")
35092
- })
35093
- ).max(1024)
35094
- });
35095
- var MemGovProcess = class {
35096
- constructor(onSample) {
35097
- this.onSample = onSample;
35098
- }
35099
- onSample;
35100
- child = null;
35101
- start() {
35102
- try {
35103
- const child = spawn9(ensureCompiled("memgov"), ["serve", "--interval", "2"], {
35104
- stdio: ["pipe", "pipe", "ignore"],
35105
- windowsHide: true
35106
- });
35107
- this.child = child;
35108
- createInterface({ input: child.stdout }).on("line", (line) => {
35109
- try {
35110
- this.onSample(SampleSchema.parse(JSON.parse(line)));
35111
- } catch (e) {
35112
- warn("governor: bad sample: " + errMsg(e));
35113
- }
35114
- });
35115
- child.stdin.on("error", () => void 0);
35116
- child.once("exit", () => {
35117
- this.child = null;
35118
- setTimeout(() => this.start(), 3e4).unref();
35119
- });
35120
- } catch (e) {
35121
- warn("governor: could not start memgov.exe (no memory governor): " + errMsg(e));
35122
- }
35123
- }
35124
- setPriority(pid, to) {
35125
- this.send("priority " + pid + " " + to);
35126
- }
35127
- hold(pid, ms) {
35128
- this.send("suspend " + pid + " " + ms);
35129
- }
35130
- setMemoryPriority(pid, low) {
35131
- this.send("memprio " + pid + " " + (low ? "low" : "normal"));
35132
- }
35133
- setIoPriority(pid, to) {
35134
- this.send("iopriority " + pid + " " + to);
35135
- }
35136
- throttle(pid) {
35137
- this.send("throttle " + pid);
35138
- }
35139
- setCpuRate(percent) {
35140
- this.send("cpurate " + Math.round(percent));
35141
- }
35142
- trim(pid) {
35143
- this.send("trim " + pid);
35144
- }
35145
- send(line) {
35146
- this.child?.stdin?.write(line + "\n");
35147
- }
35148
- };
35149
- function appendToMemoryLog(line) {
35150
- try {
35151
- const logs = path81.join(dataDir, "logs");
35152
- const newest = fs64.readdirSync(logs).filter((f) => /^memory-\d{8}-\d{6}\.log$/.test(f)).sort().pop();
35153
- if (!newest) return;
35154
- const t = /* @__PURE__ */ new Date();
35155
- const stamp = [t.getHours(), t.getMinutes(), t.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
35156
- fs64.appendFileSync(path81.join(logs, newest), stamp + " | " + line + "\r\n");
35157
- } catch {
35158
- }
35159
- }
35160
- var Governor = class {
35161
- core = null;
35162
- describe() {
35163
- const profile = session.loadProfile === "gentle" ? "gentle" : "normal";
35164
- const first = DIAL_STEPS[profile][0];
35165
- return session.pressureResponse === false ? "off (sandbox.pressureResponse)" : profile + " profile: slows builds down from " + first.availablePercent + "% free RAM or " + first.commitPercent + "% commit, in steps";
35166
- }
35167
- start() {
35168
- if (session.pressureResponse === false) return;
35169
- const profile = session.loadProfile === "gentle" ? "gentle" : "normal";
35170
- const proc = new MemGovProcess((s) => this.core?.onSample(s));
35171
- this.core = new GovernorCore({
35172
- port: proc,
35173
- profile,
35174
- maxCompilers: maxCompilers(os14.cpus().length, profile),
35175
- now: () => Date.now(),
35176
- logLine: appendToMemoryLog,
35177
- warn,
35178
- saveStatus: (status) => {
35179
- try {
35180
- writeJsonAtomic(run.pressureFile, status);
35181
- } catch (e) {
35182
- warn("governor: could not write its status: " + errMsg(e));
35183
- }
35184
- }
35185
- });
35186
- proc.start();
35187
- }
35188
- /** The host's watchdog: this PC is low (or nearly out of memory) for a while. */
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;
35195
- }
35196
- };
35197
- var governor = new Governor();
35198
-
35199
35249
  // src/guest/daemon/live.ts
35200
35250
  import fs66 from "node:fs";
35201
35251
  import path83 from "node:path";
@@ -35559,39 +35609,6 @@ var Daemon = class {
35559
35609
  }
35560
35610
  };
35561
35611
 
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
-
35595
35612
  // src/guest/entries/daemon.ts
35596
35613
  process.on("uncaughtException", (e) => warn("daemon: unexpected error (continuing): " + (e.stack ?? errMsg(e))));
35597
35614
  process.on("unhandledRejection", (e) => warn("daemon: unexpected error (continuing): " + errMsg(e)));
@@ -35604,5 +35621,5 @@ safely("leftover processes", () => sessions.sweepLeftovers());
35604
35621
  safely("memory governor", () => governor.start());
35605
35622
  log("Memory governor: " + governor.describe());
35606
35623
  safely("service sign-ins", startAccountWatch);
35607
- safely("heartbeat", startHeartbeat);
35624
+ safely("heartbeat", () => startHeartbeat(() => sessions.count));
35608
35625
  new Daemon().start();