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
@@ -8,12 +8,14 @@ import { DEFAULTS } from '../core/config/index.js';
8
8
  import { isWin } from '../core/util/index.js';
9
9
  import { SYSTEM } from '../shared/system.js';
10
10
  import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
11
- import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, } from '../shared/memory/index.js';
11
+ import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, } from '../shared/memory/index.js';
12
+ import { HeartbeatSchema } from '../shared/channel/index.js';
13
+ import { heartbeatLine, lastEndOf } from '../host/life/index.js';
12
14
  import { cappedMemoryMb, decideMemory, memoryNote, residentCapMb, residentNote, resolveMemoryMb, } from '../host/launch/index.js';
13
15
  import { vgpuWarning, GENTLE_NOTE } from '../host/launch/risk-notes.js';
14
16
  import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
15
17
  // The pure governor files only: governor.ts loads the guest runtime (session.json), which exists only in a sandbox.
16
- import { canLower, canPause, decide, INITIAL_STATE, ROTATE_MS, } from '../guest/governor/decide.js';
18
+ import { canLower, canPause, decide, INITIAL_STATE, isLinker, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
17
19
  import { GovernorCore } from '../guest/governor/core.js';
18
20
  const GB = 1024;
19
21
  const MB_SANDBOX = DEFAULTS.sandbox;
@@ -63,9 +65,9 @@ export const heavyLoadSuite = {
63
65
  t.check(refused.includes('RAM plus paging file'), 'too little commit refuses with the real reason', refused);
64
66
  t.check(resolveMemoryMb({ ...MB_SANDBOX, memoryMb: 5000 }, 1, 1, 1) === 5000, 'a fixed memoryMb is taken as asked (the resident cap below is the only limit on it)');
65
67
  t.check(residentCapMb({ ...MB_SANDBOX, vGpu: false }, 32768) === 24576 &&
66
- residentCapMb({ ...MB_SANDBOX, vGpu: true }, 32768) === 22528 &&
67
- residentCapMb({ ...MB_SANDBOX, vGpu: false }, 65536) === 49152, 'what this PC can keep in RAM for the sandbox: its RAM minus max(reserve, 25 %), 2 GB more with vGPU');
68
- t.check(cappedMemoryMb(32768, { ...MB_SANDBOX, vGpu: true }, 32768) === 22528 &&
68
+ residentCapMb({ ...MB_SANDBOX, vGpu: true }, 32768) === 20480 &&
69
+ residentCapMb({ ...MB_SANDBOX, vGpu: false }, 65536) === 49152, 'what this PC can keep in RAM for the sandbox: its RAM minus max(reserve, 25 %), 4 GB more with vGPU');
70
+ t.check(cappedMemoryMb(32768, { ...MB_SANDBOX, vGpu: true }, 32768) === 20480 &&
69
71
  cappedMemoryMb(16384, { ...MB_SANDBOX, vGpu: true }, 32768) === 16384 &&
70
72
  cappedMemoryMb(8192, MB_SANDBOX, 6144) === 8192, 'a fixed memoryMb above it is lowered to it (never on a PC too small to keep 4 GB)');
71
73
  t.check(residentNote(32768, 22528)?.includes('gets 22528 MB') === true && residentNote(16384, 16384) === null, 'and the launch says so');
@@ -173,13 +175,22 @@ export const heavyLoadSuite = {
173
175
  t.section('the gentle profile (vGPU on)');
174
176
  t.check(DIAL_STEPS.gentle[0].availablePercent > DIAL_STEPS.normal[0].availablePercent &&
175
177
  DIAL_STEPS.gentle[0].commitPercent < DIAL_STEPS.normal[0].commitPercent, 'every step starts earlier');
176
- const g = decide(INITIAL_STATE, sample(50, 50, procs), 0, 'gentle');
177
- t.check(g.state.step === 0 &&
178
- g.actions.length === 3 &&
179
- g.actions.every((a) => a.kind === 'priority' && a.to === 'below' && a.pid !== 3), 'compilers rest below normal even with plenty of memory (the agent does not)');
180
- t.check(decide(INITIAL_STATE, sample(19, 50, procs), 0, 'gentle').state.step === 1, '19 % free is step 1');
178
+ const idle = [proc(3, 'claude.exe', 900), proc(5, 'modern_mohaa.exe', 4400), proc(6, 'cargo.exe', 50)];
179
+ const g = decide(INITIAL_STATE, sample(50, 50, idle), 0, 'gentle');
180
+ t.check(g.state.step === 0, 'with vGPU on and no heavy compiler running the dial stays at 0 (a game alone is fine)');
181
+ const building = decide(INITIAL_STATE, sample(50, 50, [...procs, proc(5, 'modern_mohaa.exe', 4400)]), 0, 'gentle');
182
+ t.check(building.state.step === 2 &&
183
+ of(building, 'priority').length === 3 &&
184
+ of(building, 'priority').every((a) => a.kind === 'priority' && a.to === 'idle' && a.pid !== 3 && a.pid !== 5), 'vGPU on: while rustc or link runs the builds are at step 2 at least; the agent and the GPU game are never touched');
185
+ t.check(decide(INITIAL_STATE, sample(50, 50, procs), 0, 'normal').state.step === 0, 'without vGPU the same build leaves the dial at 0');
186
+ t.check(decide(INITIAL_STATE, sample(19, 50, procs), 0, 'gentle').state.step === 2, '19 % free is step 1, lifted to 2 by the build');
187
+ const done = decide(building.state, sample(50, 50, idle), 1000, 'gentle').state;
188
+ t.check(done.step === 2, 'when the build ends the dial turns down slowly, like any other');
181
189
  t.section('the compiler cap (vGPU on)');
182
- t.check(maxCompilers(16, 'gentle') === 4 && maxCompilers(4, 'gentle') === 2 && maxCompilers(16, 'normal') === null, 'at most max(2, cores / 4) with vGPU, none otherwise');
190
+ t.check(maxCompilers(16, 'gentle') === 4 &&
191
+ maxCompilers(64, 'gentle') === 4 &&
192
+ maxCompilers(4, 'gentle') === 2 &&
193
+ maxCompilers(16, 'normal') === null, 'at most 4 (and at least 2: cores / 4) with vGPU, none otherwise');
183
194
  const many = [
184
195
  proc(10, 'rustc.exe', 800),
185
196
  proc(11, 'rustc.exe', 900),
@@ -188,21 +199,72 @@ export const heavyLoadSuite = {
188
199
  proc(3, 'claude.exe', 500),
189
200
  proc(14, 'node.exe', 400),
190
201
  ];
191
- // The cap is a memory measure: with memory to spare no compiler waits (a waiting rustc stalls cargo's whole build).
202
+ // With vGPU on a build runs at step 2 at least, so the cap holds even with memory to spare.
192
203
  const free = decide(INITIAL_STATE, sample(60, 40, many), 0, 'gentle', 2);
193
- t.check(of(free, 'hold').length === 0, 'with memory to spare, nothing waits even beyond the cap');
204
+ t.check(free.state.step === 2 && of(free, 'hold').length === 2, 'vGPU on: even with memory to spare compilers beyond the cap wait their turn');
205
+ t.check(of(decide(INITIAL_STATE, sample(60, 40, many), 0, 'normal', null), 'hold').length === 0, 'without vGPU, with memory to spare, nothing waits');
194
206
  const capped = decide(INITIAL_STATE, sample(14, 40, many), 0, 'gentle', 2);
195
207
  const holds = of(capped, 'hold').map((a) => ('pid' in a ? a.pid : 0));
196
208
  t.check(capped.state.step === 2 && holds.length === 2, 'under memory pressure (step 2), compilers beyond the cap wait their turn', holds.join(','));
197
209
  t.check(!holds.includes(3) && !holds.includes(14), 'agents (claude, and codex or gemini as node) are never held');
198
- t.check(of(decide(INITIAL_STATE, sample(60, 40, many), 0, 'gentle', null), 'hold').length === 0, 'no cap, no pressure: nothing waits');
199
210
  t.check(of(decide(INITIAL_STATE, sample(60, 40, many.slice(0, 2)), 0, 'gentle', 2), 'hold').length === 0, 'under the cap nothing waits');
211
+ t.section('I/O priority and linkers');
212
+ const ioOf = (d) => d.actions.flatMap((a) => (a.kind === 'io' ? [a.pid + ' ' + a.to] : []));
213
+ const below = decide(INITIAL_STATE, sample(14, 50, procs), 0, 'normal');
214
+ t.check(below.state.step === 1 && ioOf(below).join() === '1 low,2 low,4 low', 'compilers and linkers at below-normal priority get low I/O (the agent does not)', ioOf(below).join());
215
+ const hard = decide(INITIAL_STATE, sample(5, 60, procs), 0, 'normal');
216
+ t.check(hard.state.step >= 3 && ioOf(hard).join() === '1 verylow,2 verylow,4 verylow', 'from step 3 (a disk-severe PC asks for 4) their I/O is very low', ioOf(hard).join());
217
+ const calmState = { ...hard.state, step: 0, calm: 0 };
218
+ t.check(ioOf(decide(calmState, sample(90, 10, procs), 2000, 'normal')).join() === '1 normal,2 normal,4 normal', 'released compilers get normal I/O again');
219
+ t.check(linkerSlots(32768, 16) === 4 &&
220
+ linkerSlots(8192, 16) === 1 &&
221
+ linkerSlots(65536, 2) === 2 &&
222
+ linkerSlots(2048, 8) === 1, 'link slots: one per 6 GB of RAM, 1 to 4, never more than the processors');
223
+ t.check(['ld.exe', 'ld.lld.exe', 'lld-link.exe', 'link.exe'].every(isLinker) &&
224
+ !isLinker('rustc.exe') &&
225
+ !isLinker('claude.exe'), 'ld, ld.lld, lld-link and link are linkers');
226
+ const links = [
227
+ proc(20, 'ld.exe', 1600),
228
+ proc(21, 'ld.exe', 1700),
229
+ proc(22, 'ld.lld.exe', 1500),
230
+ proc(3, 'claude.exe', 900),
231
+ ];
232
+ t.check(of(decide(INITIAL_STATE, sample(40, 40, links), 0, 'normal', null), 'hold').length === 0, 'at step 0 several links may run');
233
+ const linkHolds = of(decide(INITIAL_STATE, sample(8, 40, links), 0, 'normal', null), 'hold');
234
+ t.check(linkHolds.length === 2, 'at step 2 the linkers run one at a time', String(linkHolds.length));
235
+ t.section('where Rust builds, the heartbeat and how the last sandbox ended');
236
+ t.check(chooseTargetDir('auto', 30 * 1024 ** 3).targetDir === 'local' &&
237
+ chooseTargetDir('auto', 10 * 1024 ** 3).targetDir === 'separate' &&
238
+ Boolean(chooseTargetDir('auto', 10 * 1024 ** 3).warning) &&
239
+ chooseTargetDir('shared', 1).targetDir === 'shared', 'targetDir "auto": local with 25 GB free on C:, else separate with a warning; other values stay');
240
+ const beat = {
241
+ at: '2026-11-02T21:31:00.000Z',
242
+ uptimeS: 2800,
243
+ ramFreePct: 30,
244
+ commitPct: 40,
245
+ step: 2,
246
+ hostStep: 4,
247
+ topProcesses: [{ name: 'rustc.exe', privateMb: 1700 }],
248
+ diskQueueReported: 41,
249
+ phase: 'working',
250
+ };
251
+ t.check(HeartbeatSchema.safeParse(beat).success, 'a heartbeat matches its schema');
252
+ t.check(!HeartbeatSchema.safeParse({ ...beat, topProcesses: Array.from({ length: 6 }, () => beat.topProcesses[0]) })
253
+ .success &&
254
+ !HeartbeatSchema.safeParse({ ...beat, topProcesses: [{ name: 'x'.repeat(500), privateMb: 1 }] }).success, 'an oversize heartbeat is refused');
255
+ t.check(heartbeatLine({ ...beat, topProcesses: [{ name: 'ab', privateMb: 5 }] }).includes('') === false, 'guest text in the heartbeat line goes through safeText');
256
+ const life = { started: '2026-11-02T20:45:00.000Z', lastSeen: '2026-11-02T21:31:30.000Z', keepAwake: false };
257
+ t.check((lastEndOf(life, beat, false)?.text ?? '').includes('disks were overloaded') &&
258
+ lastEndOf(life, beat, true) === null &&
259
+ lastEndOf(null, beat, false) === null &&
260
+ lastEndOf({ ...life, ended: { at: life.lastSeen, cause: null } }, beat, false) === null, 'last end: an unexpected end names the disks; running, no record or a normal close say nothing');
200
261
  t.section('the governor over a port (fake Windows)');
201
262
  const calls = [];
202
263
  const port = {
203
264
  setPriority: (pid, to) => calls.push('prio ' + pid + ' ' + to),
204
265
  hold: (pid, ms) => calls.push('hold ' + pid + ' ' + ms),
205
266
  setMemoryPriority: (pid, low) => calls.push('mem ' + pid + ' ' + (low ? 'low' : 'normal')),
267
+ setIoPriority: (pid, to) => calls.push('io ' + pid + ' ' + to),
206
268
  throttle: (pid) => calls.push('throttle ' + pid),
207
269
  setCpuRate: (percent) => calls.push('cpu ' + percent),
208
270
  trim: (pid) => calls.push('trim ' + pid),
@@ -290,6 +352,25 @@ export const heavyLoadSuite = {
290
352
  }
291
353
  t.check(diskSent && disk.low, 'a disk queue of 32 or more for 40 s: asks the sandbox to slow down');
292
354
  t.check(!watchdogStep(WATCHDOG_START, input(pc({ diskQueue: 40 }), 0)).state.low, 'a short disk peak is nothing');
355
+ t.check(!disk.severe, 'a queue of 40 (1.25 times the limit) for 40 s is low but not yet severe');
356
+ let double = WATCHDOG_START;
357
+ let doubleSevere = false;
358
+ for (let i = 0; i < 4; i++) {
359
+ const st = watchdogStep(double, input(pc({ diskQueue: 70 }), i * 10000));
360
+ double = st.state;
361
+ doubleSevere = doubleSevere || (st.severe && st.send);
362
+ }
363
+ t.check(doubleSevere, 'a queue of twice the limit for 40 s is severe and sent at once');
364
+ let long = WATCHDOG_START;
365
+ let longReason = '';
366
+ for (let i = 0; i < 13; i++) {
367
+ const st = watchdogStep(long, input(pc({ diskQueue: 40 }), i * 10000));
368
+ long = st.state;
369
+ if (st.severe)
370
+ longReason = st.reason;
371
+ }
372
+ t.check(longReason.includes('overloaded'), 'the limit itself for three times as long is severe: "disks are overloaded"', longReason);
373
+ t.check(watchdogStep(long, input(pc({ diskQueue: 0 }), 200000)).state.low, 'calm still needs the queue gone and the calm samples: one good sample is not recovery');
293
374
  t.check(!watchdogStep(WATCHDOG_START, input(pc(), 0)).state.low, 'no disk counter: memory and CPU only');
294
375
  t.check(cpuPercentBetween([{ times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } }], [{ times: { user: 90, nice: 0, sys: 0, idle: 10, irq: 0 } }]) === 90, 'CPU use from two readings');
295
376
  t.check(GuestRequestSchema.safeParse({ kind: 'pressure', id: 'p1', reason: 'low', seconds: 90, createdAt: 'now' })
@@ -235,6 +235,10 @@ export const liveTerminalSuite = {
235
235
  if (target) {
236
236
  for (const command of [
237
237
  'memprio ' + target.pid + ' low',
238
+ 'iopriority ' + target.pid + ' verylow',
239
+ 'iopriority ' + target.pid + ' low',
240
+ 'iopriority ' + target.pid + ' normal',
241
+ 'iopriority ' + target.pid + ' bogus',
238
242
  'throttle ' + target.pid,
239
243
  'cpurate 20',
240
244
  'trim ' + target.pid,
@@ -1,4 +1,4 @@
1
- import type { GuestRequest, GuestResponse, PressureStatus, ProjectStatus, SandboxStatus, SessionStatus, VpnStatus } from './schema.js';
1
+ import type { GuestRequest, GuestResponse, Heartbeat, PressureStatus, ProjectStatus, SandboxStatus, SessionStatus, VpnStatus } from './schema.js';
2
2
  /** Ids become file names, so only a strict alphabet is allowed (no separators, no dots). */
3
3
  export declare function safeId(id: string): string;
4
4
  export declare function now(): string;
@@ -9,6 +9,8 @@ export declare class RunDir {
9
9
  readonly vpnFile: string;
10
10
  /** What the memory governor in the sandbox is doing (pressure.json). */
11
11
  readonly pressureFile: string;
12
+ /** The daemon's heartbeat every 10 seconds (heartbeat.json): what the sandbox did last. */
13
+ readonly heartbeatFile: string;
12
14
  /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
13
15
  readonly budgetFile: string;
14
16
  readonly requestsDir: string;
@@ -39,6 +41,7 @@ export declare class RunDir {
39
41
  readSandboxStatus(): SandboxStatus | null;
40
42
  readVpnStatus(): VpnStatus | null;
41
43
  readPressure(): PressureStatus | null;
44
+ readHeartbeat(): Heartbeat | null;
42
45
  readSession(id: string): SessionStatus | null;
43
46
  readResponse(id: string): GuestResponse | null;
44
47
  listSessions(): SessionStatus[];
@@ -3,7 +3,7 @@
3
3
  // written atomically and parsed with the channel schemas on read. Anything that does not parse is ignored.
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import { GuestRequestSchema, GuestResponseSchema, IdSchema, PressureStatusSchema, ProjectStatusSchema, SandboxStatusSchema, SessionStatusSchema, VpnStatusSchema, } from './schema.js';
6
+ import { GuestRequestSchema, GuestResponseSchema, HeartbeatSchema, IdSchema, PressureStatusSchema, ProjectStatusSchema, SandboxStatusSchema, SessionStatusSchema, VpnStatusSchema, } from './schema.js';
7
7
  import { readValid } from '../io/index.js';
8
8
  /** Ids become file names, so only a strict alphabet is allowed (no separators, no dots). */
9
9
  export function safeId(id) {
@@ -36,6 +36,8 @@ export class RunDir {
36
36
  vpnFile;
37
37
  /** What the memory governor in the sandbox is doing (pressure.json). */
38
38
  pressureFile;
39
+ /** The daemon's heartbeat every 10 seconds (heartbeat.json): what the sandbox did last. */
40
+ heartbeatFile;
39
41
  /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
40
42
  budgetFile;
41
43
  requestsDir;
@@ -59,6 +61,7 @@ export class RunDir {
59
61
  this.sandboxFile = path.join(this.root, 'sandbox.json');
60
62
  this.vpnFile = path.join(this.root, 'vpn.json');
61
63
  this.pressureFile = path.join(this.root, 'pressure.json');
64
+ this.heartbeatFile = path.join(this.root, 'heartbeat.json');
62
65
  this.budgetFile = path.join(this.root, 'budget.json');
63
66
  this.requestsDir = path.join(this.root, 'requests');
64
67
  this.responsesDir = path.join(this.root, 'responses');
@@ -103,6 +106,9 @@ export class RunDir {
103
106
  readPressure() {
104
107
  return readValid(PressureStatusSchema, this.pressureFile);
105
108
  }
109
+ readHeartbeat() {
110
+ return readValid(HeartbeatSchema, this.heartbeatFile);
111
+ }
106
112
  readSession(id) {
107
113
  return readValid(SessionStatusSchema, this.sessionFile(id));
108
114
  }
@@ -780,6 +780,7 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
780
780
  slow: "slow";
781
781
  severe: "severe";
782
782
  }>>;
783
+ diskQueue: z.ZodOptional<z.ZodNumber>;
783
784
  createdAt: z.ZodString;
784
785
  }, z.core.$strict>], "kind">;
785
786
  /** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
@@ -808,6 +809,28 @@ export declare const PressureStatusSchema: z.ZodObject<{
808
809
  updatedAt: z.ZodString;
809
810
  }, z.core.$strip>;
810
811
  export type PressureStatus = z.output<typeof PressureStatusSchema>;
812
+ /**
813
+ * C:\AgentData\run\heartbeat.json: written by the daemon every 10 seconds, so that when the whole sandbox vanishes the
814
+ * host can say what it was doing in its last seconds (guest-written, every list and string capped).
815
+ */
816
+ export declare const HeartbeatSchema: z.ZodObject<{
817
+ at: z.ZodString;
818
+ uptimeS: z.ZodNumber;
819
+ ramFreePct: z.ZodNumber;
820
+ commitPct: z.ZodNumber;
821
+ step: z.ZodNumber;
822
+ hostStep: z.ZodNumber;
823
+ topProcesses: z.ZodArray<z.ZodObject<{
824
+ name: z.ZodString;
825
+ privateMb: z.ZodNumber;
826
+ }, z.core.$strip>>;
827
+ diskQueueReported: z.ZodOptional<z.ZodNumber>;
828
+ phase: z.ZodEnum<{
829
+ ready: "ready";
830
+ working: "working";
831
+ }>;
832
+ }, z.core.$strip>;
833
+ export type Heartbeat = z.output<typeof HeartbeatSchema>;
811
834
  /** C:\AgentData\run\responses\<id>.json: the guest's reply. */
812
835
  export declare const GuestResponseSchema: z.ZodObject<{
813
836
  id: z.ZodString;
@@ -108,6 +108,8 @@ export const GuestRequestSchema = z.discriminatedUnion('kind', [
108
108
  seconds: z.number().int().min(10).max(300),
109
109
  /** "severe": the PC is nearly out of memory; builds go to the governor's last step at once. */
110
110
  level: z.enum(['slow', 'severe']).optional(),
111
+ /** The disk queue the PC reported (shown in the heartbeat). */
112
+ diskQueue: z.number().min(0).max(100000).optional(),
111
113
  createdAt: TimeSchema,
112
114
  }),
113
115
  ]);
@@ -131,6 +133,25 @@ export const PressureStatusSchema = z.object({
131
133
  lastAction: Text(300),
132
134
  updatedAt: TimeSchema,
133
135
  });
136
+ /**
137
+ * C:\AgentData\run\heartbeat.json: written by the daemon every 10 seconds, so that when the whole sandbox vanishes the
138
+ * host can say what it was doing in its last seconds (guest-written, every list and string capped).
139
+ */
140
+ export const HeartbeatSchema = z.object({
141
+ at: TimeSchema,
142
+ /** Seconds since the sandbox booted. */
143
+ uptimeS: z.number().int().min(0).max(1e9),
144
+ ramFreePct: z.number().min(0).max(100),
145
+ commitPct: z.number().min(0).max(100),
146
+ /** The governor's dial now, and the step this PC asked for. */
147
+ step: z.number().int().min(0).max(4),
148
+ hostStep: z.number().int().min(0).max(4),
149
+ /** The biggest programs in the agents' job. */
150
+ topProcesses: z.array(z.object({ name: Text(64), privateMb: z.number().int().min(0).max(1e7) })).max(5),
151
+ /** The disk queue this PC last reported while asking the sandbox to slow down. */
152
+ diskQueueReported: z.number().min(0).max(100000).optional(),
153
+ phase: z.enum(['ready', 'working']),
154
+ });
134
155
  /** C:\AgentData\run\responses\<id>.json: the guest's reply. */
135
156
  export const GuestResponseSchema = z.object({
136
157
  id: IdSchema,
@@ -55,5 +55,15 @@ export declare function linkSlotsFor(memoryGb: number): number;
55
55
  export declare function usableCpus(cpus: number, percent: number | null): number;
56
56
  /** Cargo jobs for one more agent session: what fits the memory and CPUs, shared with the sessions already building. */
57
57
  export declare function cargoJobs(cpus: number, memoryGb: number, sessions: number): number;
58
+ /** C: needs this much free at boot for rust.targetDir "auto" to build on the sandbox's own disk. */
59
+ export declare const LOCAL_TARGET_MIN_BYTES: number;
60
+ /**
61
+ * rust.targetDir: "auto" becomes "local" when C: has 25 GB free, else "separate" with a warning (every object file is
62
+ * then written through the mapped workspace, which can overload this PC's disk). Other values stay as set.
63
+ */
64
+ export declare function chooseTargetDir(targetDir: string, freeBytes: number): {
65
+ readonly targetDir: string;
66
+ readonly warning?: string;
67
+ };
58
68
  /** The most heavy compilers (rustc, clippy-driver, link ...) that run at once in the agents' job: only with vGPU on. */
59
69
  export declare function maxCompilers(cpus: number, profile: LoadProfile): number | null;
@@ -88,7 +88,23 @@ export function cargoJobs(cpus, memoryGb, sessions) {
88
88
  const jobs = Math.min(byCpu, byMemory);
89
89
  return Math.max(1, Math.min(jobs, Math.floor(buildJobs(cpus, memoryGb) / Math.max(1, sessions))));
90
90
  }
91
+ /** C: needs this much free at boot for rust.targetDir "auto" to build on the sandbox's own disk. */
92
+ export const LOCAL_TARGET_MIN_BYTES = 25 * 1024 ** 3;
93
+ /**
94
+ * rust.targetDir: "auto" becomes "local" when C: has 25 GB free, else "separate" with a warning (every object file is
95
+ * then written through the mapped workspace, which can overload this PC's disk). Other values stay as set.
96
+ */
97
+ export function chooseTargetDir(targetDir, freeBytes) {
98
+ if (targetDir !== 'auto')
99
+ return { targetDir };
100
+ if (freeBytes >= LOCAL_TARGET_MIN_BYTES)
101
+ return { targetDir: 'local' };
102
+ return {
103
+ targetDir: 'separate',
104
+ 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)",
105
+ };
106
+ }
91
107
  /** The most heavy compilers (rustc, clippy-driver, link ...) that run at once in the agents' job: only with vGPU on. */
92
108
  export function maxCompilers(cpus, profile) {
93
- return profile === 'gentle' ? Math.max(2, Math.floor(cpus / 4)) : null;
109
+ return profile === 'gentle' ? Math.min(4, Math.max(2, Math.floor(cpus / 4))) : null;
94
110
  }
@@ -256,7 +256,7 @@ export class Watcher {
256
256
  ? 'this PC is low (' + step.reason + '): asking the sandbox to slow its builds down'
257
257
  : 'this PC has recovered: no longer asking the sandbox to slow down');
258
258
  if (step.send)
259
- new SandboxChannel(this.dataPath).pressure(step.reason, REQUEST_SECONDS, step.severe ? 'severe' : 'slow');
259
+ new SandboxChannel(this.dataPath).pressure(step.reason, REQUEST_SECONDS, step.severe ? 'severe' : 'slow', memory?.diskQueue);
260
260
  }
261
261
  catch (e) {
262
262
  this.note('host watchdog: ' + errMsg(e));
@@ -6,7 +6,7 @@
6
6
  <title>Sandbox</title>
7
7
  <link rel="icon" href="data:," />
8
8
  <link rel="stylesheet" href="/main-XT7T6ODM.css" />
9
- <script type="module" src="/main-CWIF4VQI.js"></script>
9
+ <script type="module" src="/main-6XYO5FZC.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>