windows-development-sandbox 1.18.2 → 1.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/config.example.json +1 -1
  2. package/dist/app/features/defs.d.ts +26 -0
  3. package/dist/app/features/defs.js +2 -1
  4. package/dist/app/features/sandbox/sandbox.def.d.ts +34 -0
  5. package/dist/app/features/sandbox/sandbox.def.js +52 -10
  6. package/dist/app/features/sandbox/sandbox.run.d.ts +24 -0
  7. package/dist/app/features/sandbox/sandbox.run.js +45 -1
  8. package/dist/app/registry.js +2 -1
  9. package/dist/bin/agent-sandbox.js +2 -2
  10. package/dist/bin/{chunk-MQIA7GNB.js → chunk-FRPLQLSY.js} +3188 -2765
  11. package/dist/bin/{chunk-M4OM632I.js → chunk-MY7NLQQY.js} +1 -1
  12. package/dist/bin/{chunk-IIHL7W6B.js → chunk-N4GXYTLV.js} +1 -1
  13. package/dist/bin/claude-host.js +2 -2
  14. package/dist/bin/claude-sbx.js +2 -2
  15. package/dist/bin/codex-sbx.js +2 -2
  16. package/dist/bin/gemini-sbx.js +2 -2
  17. package/dist/bin/sandbox.js +2 -2
  18. package/dist/bin/sbx-hide.js +2 -2
  19. package/dist/bin/sbx-show.js +2 -2
  20. package/dist/cli/commands/launch/launch.js +4 -1
  21. package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
  22. package/dist/cli/commands/launch/leftover-offer.js +26 -0
  23. package/dist/cli/commands/repair/checks/sandbox.d.ts +3 -1
  24. package/dist/cli/commands/repair/checks/sandbox.js +53 -2
  25. package/dist/core/config/defaults.js +1 -1
  26. package/dist/core/schema/sandbox.js +1 -1
  27. package/dist/core/schema/toolchains.js +2 -2
  28. package/dist/guest/daemon/daemon.js +1 -1
  29. package/dist/guest/daemon/heartbeat.d.ts +11 -0
  30. package/dist/guest/daemon/heartbeat.js +59 -0
  31. package/dist/guest/daemon/idle-shutdown.js +2 -0
  32. package/dist/guest/entries/daemon.js +2 -0
  33. package/dist/guest/governor/core.d.ts +19 -1
  34. package/dist/guest/governor/core.js +23 -1
  35. package/dist/guest/governor/decide.d.ts +14 -0
  36. package/dist/guest/governor/decide.js +52 -7
  37. package/dist/guest/governor/governor.d.ts +4 -1
  38. package/dist/guest/governor/governor.js +9 -2
  39. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  40. package/dist/guest/toolchains/native/rust-extras.js +26 -3
  41. package/dist/guest/toolchains/plan.js +6 -4
  42. package/dist/host/channel/channel.d.ts +1 -1
  43. package/dist/host/channel/channel.js +10 -2
  44. package/dist/host/health/index.d.ts +1 -0
  45. package/dist/host/health/index.js +1 -0
  46. package/dist/host/health/leftovers.d.ts +3 -0
  47. package/dist/host/health/leftovers.js +40 -0
  48. package/dist/host/health/stale.d.ts +48 -0
  49. package/dist/host/health/stale.js +40 -0
  50. package/dist/host/launch/wsb-file.d.ts +6 -1
  51. package/dist/host/launch/wsb-file.js +9 -5
  52. package/dist/host/life/crash-report.d.ts +3 -1
  53. package/dist/host/life/crash-report.js +9 -3
  54. package/dist/host/life/index.d.ts +1 -0
  55. package/dist/host/life/index.js +1 -0
  56. package/dist/host/life/last-end.d.ts +28 -0
  57. package/dist/host/life/last-end.js +91 -0
  58. package/dist/host/life/life-record.d.ts +3 -0
  59. package/dist/host/life/life-record.js +2 -2
  60. package/dist/host/life/stop-events.d.ts +3 -1
  61. package/dist/host/life/stop-events.js +59 -0
  62. package/dist/host/life/stop-warning.js +3 -0
  63. package/dist/host/life/watchdog.d.ts +2 -0
  64. package/dist/host/life/watchdog.js +8 -2
  65. package/dist/host/sandbox/cleanup.d.ts +37 -0
  66. package/dist/host/sandbox/cleanup.js +82 -0
  67. package/dist/host/sandbox/index.d.ts +1 -0
  68. package/dist/host/sandbox/index.js +1 -0
  69. package/dist/host/sandbox/platform.js +1 -1
  70. package/dist/runtime/agent.js +28 -3
  71. package/dist/runtime/daemon.js +629 -479
  72. package/dist/runtime/db.js +28 -3
  73. package/dist/runtime/main.js +59 -11
  74. package/dist/runtime/netproxy.js +28 -3
  75. package/dist/runtime/tool.js +28 -3
  76. package/dist/runtime/view.js +28 -3
  77. package/dist/selftest/heavy-load.test.js +212 -14
  78. package/dist/selftest/live-terminal.test.js +4 -0
  79. package/dist/shared/channel/run-dir.d.ts +4 -1
  80. package/dist/shared/channel/run-dir.js +7 -1
  81. package/dist/shared/channel/schema.d.ts +24 -0
  82. package/dist/shared/channel/schema.js +22 -0
  83. package/dist/shared/memory/policy.d.ts +10 -0
  84. package/dist/shared/memory/policy.js +17 -1
  85. package/dist/shared/system.d.ts +1 -0
  86. package/dist/shared/system.js +1 -0
  87. package/dist/sync/watcher.js +10 -5
  88. package/dist/web/index.html +1 -1
  89. package/dist/web/{main-CWIF4VQI.js → main-GVZWWP4I.js} +63 -63
  90. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  91. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  92. package/native/csharp/HostTools/Program.cs +5 -1
  93. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  94. package/native/scripts/stop-events.ps1 +7 -3
  95. package/package.json +1 -1
  96. package/sandbox/csharp/MemGov.cs +58 -0
  97. package/schemas/config.schema.json +3 -3
  98. package/schemas/sandbox-config.schema.json +3 -3
@@ -8,12 +8,17 @@ 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, vanishedCause, stopWarning } 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';
16
+ import { judgeSandbox } from '../host/health/index.js';
17
+ import { cleanUpLeftovers, cleanupQuestion, killArgs } from '../host/sandbox/index.js';
18
+ import { parseTasklist } from '../host/sharing/index.js';
14
19
  import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
15
20
  // 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';
21
+ import { canLower, canPause, decide, INITIAL_STATE, isLinker, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
17
22
  import { GovernorCore } from '../guest/governor/core.js';
18
23
  const GB = 1024;
19
24
  const MB_SANDBOX = DEFAULTS.sandbox;
@@ -63,9 +68,9 @@ export const heavyLoadSuite = {
63
68
  t.check(refused.includes('RAM plus paging file'), 'too little commit refuses with the real reason', refused);
64
69
  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
70
  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 &&
71
+ residentCapMb({ ...MB_SANDBOX, vGpu: true }, 32768) === 20480 &&
72
+ 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');
73
+ t.check(cappedMemoryMb(32768, { ...MB_SANDBOX, vGpu: true }, 32768) === 20480 &&
69
74
  cappedMemoryMb(16384, { ...MB_SANDBOX, vGpu: true }, 32768) === 16384 &&
70
75
  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
76
  t.check(residentNote(32768, 22528)?.includes('gets 22528 MB') === true && residentNote(16384, 16384) === null, 'and the launch says so');
@@ -173,13 +178,22 @@ export const heavyLoadSuite = {
173
178
  t.section('the gentle profile (vGPU on)');
174
179
  t.check(DIAL_STEPS.gentle[0].availablePercent > DIAL_STEPS.normal[0].availablePercent &&
175
180
  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');
181
+ const idle = [proc(3, 'claude.exe', 900), proc(5, 'modern_mohaa.exe', 4400), proc(6, 'cargo.exe', 50)];
182
+ const g = decide(INITIAL_STATE, sample(50, 50, idle), 0, 'gentle');
183
+ t.check(g.state.step === 0, 'with vGPU on and no heavy compiler running the dial stays at 0 (a game alone is fine)');
184
+ const building = decide(INITIAL_STATE, sample(50, 50, [...procs, proc(5, 'modern_mohaa.exe', 4400)]), 0, 'gentle');
185
+ t.check(building.state.step === 2 &&
186
+ of(building, 'priority').length === 3 &&
187
+ 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');
188
+ t.check(decide(INITIAL_STATE, sample(50, 50, procs), 0, 'normal').state.step === 0, 'without vGPU the same build leaves the dial at 0');
189
+ 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');
190
+ const done = decide(building.state, sample(50, 50, idle), 1000, 'gentle').state;
191
+ t.check(done.step === 2, 'when the build ends the dial turns down slowly, like any other');
181
192
  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');
193
+ t.check(maxCompilers(16, 'gentle') === 4 &&
194
+ maxCompilers(64, 'gentle') === 4 &&
195
+ maxCompilers(4, 'gentle') === 2 &&
196
+ maxCompilers(16, 'normal') === null, 'at most 4 (and at least 2: cores / 4) with vGPU, none otherwise');
183
197
  const many = [
184
198
  proc(10, 'rustc.exe', 800),
185
199
  proc(11, 'rustc.exe', 900),
@@ -188,21 +202,139 @@ export const heavyLoadSuite = {
188
202
  proc(3, 'claude.exe', 500),
189
203
  proc(14, 'node.exe', 400),
190
204
  ];
191
- // The cap is a memory measure: with memory to spare no compiler waits (a waiting rustc stalls cargo's whole build).
205
+ // With vGPU on a build runs at step 2 at least, so the cap holds even with memory to spare.
192
206
  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');
207
+ 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');
208
+ 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
209
  const capped = decide(INITIAL_STATE, sample(14, 40, many), 0, 'gentle', 2);
195
210
  const holds = of(capped, 'hold').map((a) => ('pid' in a ? a.pid : 0));
196
211
  t.check(capped.state.step === 2 && holds.length === 2, 'under memory pressure (step 2), compilers beyond the cap wait their turn', holds.join(','));
197
212
  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
213
  t.check(of(decide(INITIAL_STATE, sample(60, 40, many.slice(0, 2)), 0, 'gentle', 2), 'hold').length === 0, 'under the cap nothing waits');
214
+ t.section('I/O priority and linkers');
215
+ const ioOf = (d) => d.actions.flatMap((a) => (a.kind === 'io' ? [a.pid + ' ' + a.to] : []));
216
+ const below = decide(INITIAL_STATE, sample(14, 50, procs), 0, 'normal');
217
+ 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());
218
+ const hard = decide(INITIAL_STATE, sample(5, 60, procs), 0, 'normal');
219
+ 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());
220
+ const calmState = { ...hard.state, step: 0, calm: 0 };
221
+ 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');
222
+ t.check(linkerSlots(32768, 16) === 4 &&
223
+ linkerSlots(8192, 16) === 1 &&
224
+ linkerSlots(65536, 2) === 2 &&
225
+ linkerSlots(2048, 8) === 1, 'link slots: one per 6 GB of RAM, 1 to 4, never more than the processors');
226
+ t.check(['ld.exe', 'ld.lld.exe', 'lld-link.exe', 'link.exe'].every(isLinker) &&
227
+ !isLinker('rustc.exe') &&
228
+ !isLinker('claude.exe'), 'ld, ld.lld, lld-link and link are linkers');
229
+ const links = [
230
+ proc(20, 'ld.exe', 1600),
231
+ proc(21, 'ld.exe', 1700),
232
+ proc(22, 'ld.lld.exe', 1500),
233
+ proc(3, 'claude.exe', 900),
234
+ ];
235
+ t.check(of(decide(INITIAL_STATE, sample(40, 40, links), 0, 'normal', null), 'hold').length === 0, 'at step 0 several links may run');
236
+ const linkHolds = of(decide(INITIAL_STATE, sample(8, 40, links), 0, 'normal', null), 'hold');
237
+ t.check(linkHolds.length === 2, 'at step 2 the linkers run one at a time', String(linkHolds.length));
238
+ t.section('where Rust builds, the heartbeat and how the last sandbox ended');
239
+ t.check(chooseTargetDir('auto', 30 * 1024 ** 3).targetDir === 'local' &&
240
+ chooseTargetDir('auto', 10 * 1024 ** 3).targetDir === 'separate' &&
241
+ Boolean(chooseTargetDir('auto', 10 * 1024 ** 3).warning) &&
242
+ chooseTargetDir('shared', 1).targetDir === 'shared', 'targetDir "auto": local with 25 GB free on C:, else separate with a warning; other values stay');
243
+ const beat = {
244
+ at: '2026-11-02T21:31:00.000Z',
245
+ uptimeS: 2800,
246
+ ramFreePct: 30,
247
+ commitPct: 40,
248
+ step: 2,
249
+ hostStep: 4,
250
+ topProcesses: [{ name: 'rustc.exe', privateMb: 1700 }],
251
+ diskQueueReported: 41,
252
+ phase: 'working',
253
+ };
254
+ t.check(HeartbeatSchema.safeParse(beat).success, 'a heartbeat matches its schema');
255
+ t.check(!HeartbeatSchema.safeParse({ ...beat, topProcesses: Array.from({ length: 6 }, () => beat.topProcesses[0]) })
256
+ .success &&
257
+ !HeartbeatSchema.safeParse({ ...beat, topProcesses: [{ name: 'x'.repeat(500), privateMb: 1 }] }).success, 'an oversize heartbeat is refused');
258
+ t.check(heartbeatLine({ ...beat, topProcesses: [{ name: 'a\u0007b', privateMb: 5 }] }).includes('\u0007') === false, 'guest text in the heartbeat line goes through safeText');
259
+ const life = { started: '2026-11-02T20:45:00.000Z', lastSeen: '2026-11-02T21:31:30.000Z', keepAwake: false };
260
+ t.check((lastEndOf(life, beat, false)?.text ?? '').includes('disks were overloaded') &&
261
+ lastEndOf(life, beat, true) === null &&
262
+ lastEndOf(null, beat, false) === null &&
263
+ 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');
264
+ t.section('Windows Sandbox programs left behind by a dead sandbox');
265
+ const list = [
266
+ '"explorer.exe","1200","Console","1","90,000 K"',
267
+ '"WindowsSandboxClient.exe","4100","Console","1","30,000 K"',
268
+ '"WindowsSandboxRemoteSession.exe","4200","Console","1","40,000 K"',
269
+ '"vmwp.exe","4300","Services","0","50,000 K"',
270
+ '"WindowsSandboxServer.exe","4400","Console","2","30,000 K"',
271
+ ].join('\n');
272
+ const baseFacts = {
273
+ rows: parseTasklist(list),
274
+ mySession: 1,
275
+ heartbeatAgeMs: null,
276
+ lifeEnded: false,
277
+ startedAgeMs: 3_600_000,
278
+ answers: null,
279
+ };
280
+ const judged = (patch) => judgeSandbox({ ...baseFacts, ...patch });
281
+ t.check(judged({ rows: parseTasklist('"explorer.exe","1200","Console","1","1 K"') }).state === 'none', 'no Windows Sandbox program: none');
282
+ t.check(judged({ heartbeatAgeMs: 10_000 }).state === 'running' &&
283
+ judged({ answers: true, lifeEnded: true }).state === 'running', 'a fresh heartbeat, or a ping answered: the sandbox is ours and running');
284
+ t.check(judged({ startedAgeMs: 60_000 }).state === 'starting', 'started a minute ago, no heartbeat yet: starting');
285
+ const leftoverJudged = judged({ lifeEnded: true });
286
+ t.check(leftoverJudged.state === 'leftover' &&
287
+ leftoverJudged.pids.join() === '4100,4200' &&
288
+ leftoverJudged.images.join() === 'WindowsSandboxClient.exe,WindowsSandboxRemoteSession.exe', 'the watcher saw it end but the programs remain: leftover; only this session, only the user programs (not vmwp.exe, not session 2)', leftoverJudged.pids.join());
289
+ t.check(judged({ heartbeatAgeMs: 600_000 }).state === 'leftover', 'a heartbeat that stopped ten minutes ago: leftover');
290
+ t.check(judged({ heartbeatAgeMs: 100_000 }).state === 'running', 'a heartbeat 100 seconds old is not yet "gone"');
291
+ t.check(judged({}).state === 'running', 'no heartbeat at all (an older sandbox) and no end: treated as running, never ended');
292
+ t.check(judged({ mySession: null, lifeEnded: true }).state === 'foreign' &&
293
+ judged({ mySession: 3, lifeEnded: true }).pids.length === 0, "another session's sandbox is never touched");
294
+ t.check(killArgs([4100, 4200, 0, 2, -5, 1.5]).join(' ') === '/F /PID 4100 /PID 4200' &&
295
+ cleanupQuestion(leftoverJudged).lines.join(' ').includes('Nothing else'), 'taskkill gets only real process ids; the question names what is closed');
296
+ const run = async (facts, answers, over = {}) => {
297
+ const did = [];
298
+ let n = 0;
299
+ let asked = 0;
300
+ const outcome = await cleanUpLeftovers({
301
+ facts: () => Promise.resolve(facts[Math.min(n++, facts.length - 1)]),
302
+ end: (pids) => {
303
+ did.push('end ' + pids.join(','));
304
+ return true;
305
+ },
306
+ restartService: () => {
307
+ did.push('service');
308
+ return true;
309
+ },
310
+ confirm: () => Promise.resolve(answers[asked++] ?? false),
311
+ say: (line) => did.push('say ' + line),
312
+ ...over,
313
+ });
314
+ return { outcome, did };
315
+ };
316
+ const noneLeft = { ...baseFacts, rows: [] };
317
+ const asked = await run([{ ...baseFacts, lifeEnded: true }, noneLeft], [true]);
318
+ t.check(asked.outcome === 'cleaned' && asked.did[0] === 'end 4100,4200' && !asked.did.includes('service'), 'leftover, yes: they are closed and nothing else happens');
319
+ const declinedRun = await run([{ ...baseFacts, lifeEnded: true }], [false]);
320
+ t.check(declinedRun.outcome === 'declined' && declinedRun.did.length === 0, 'leftover, no: nothing is closed');
321
+ const live = await run([{ ...baseFacts, heartbeatAgeMs: 5000 }], [true]);
322
+ t.check(live.outcome === 'alive' && !live.did.some((d) => d.startsWith('end')), 'a sandbox that is alive is never closed, whatever the answer');
323
+ t.check((await run([noneLeft], [true])).outcome === 'nothing', 'nothing left: nothing to do');
324
+ const stuck = { ...baseFacts, lifeEnded: true };
325
+ const serviceYes = await run([stuck, stuck], [true, true]);
326
+ t.check(serviceYes.outcome === 'service-restarted' && serviceYes.did.includes('service'), 'programs that will not go: asks, then restarts the service (elevated)');
327
+ const serviceNo = await run([stuck, stuck], [true, false]);
328
+ t.check(serviceNo.outcome === 'declined' && !serviceNo.did.includes('service'), 'the service is only restarted when you say yes');
329
+ const forced = await run([stuck, noneLeft], [true], { alsoService: true });
330
+ t.check(forced.outcome === 'service-restarted', 'asked to restart the service as well: it does, without a second question');
200
331
  t.section('the governor over a port (fake Windows)');
201
332
  const calls = [];
202
333
  const port = {
203
334
  setPriority: (pid, to) => calls.push('prio ' + pid + ' ' + to),
204
335
  hold: (pid, ms) => calls.push('hold ' + pid + ' ' + ms),
205
336
  setMemoryPriority: (pid, low) => calls.push('mem ' + pid + ' ' + (low ? 'low' : 'normal')),
337
+ setIoPriority: (pid, to) => calls.push('io ' + pid + ' ' + to),
206
338
  throttle: (pid) => calls.push('throttle ' + pid),
207
339
  setCpuRate: (percent) => calls.push('cpu ' + percent),
208
340
  trim: (pid) => calls.push('trim ' + pid),
@@ -290,6 +422,25 @@ export const heavyLoadSuite = {
290
422
  }
291
423
  t.check(diskSent && disk.low, 'a disk queue of 32 or more for 40 s: asks the sandbox to slow down');
292
424
  t.check(!watchdogStep(WATCHDOG_START, input(pc({ diskQueue: 40 }), 0)).state.low, 'a short disk peak is nothing');
425
+ t.check(!disk.severe, 'a queue of 40 (1.25 times the limit) for 40 s is low but not yet severe');
426
+ let double = WATCHDOG_START;
427
+ let doubleSevere = false;
428
+ for (let i = 0; i < 4; i++) {
429
+ const st = watchdogStep(double, input(pc({ diskQueue: 70 }), i * 10000));
430
+ double = st.state;
431
+ doubleSevere = doubleSevere || (st.severe && st.send);
432
+ }
433
+ t.check(doubleSevere, 'a queue of twice the limit for 40 s is severe and sent at once');
434
+ let long = WATCHDOG_START;
435
+ let longReason = '';
436
+ for (let i = 0; i < 13; i++) {
437
+ const st = watchdogStep(long, input(pc({ diskQueue: 40 }), i * 10000));
438
+ long = st.state;
439
+ if (st.severe)
440
+ longReason = st.reason;
441
+ }
442
+ t.check(longReason.includes('overloaded'), 'the limit itself for three times as long is severe: "disks are overloaded"', longReason);
443
+ 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
444
  t.check(!watchdogStep(WATCHDOG_START, input(pc(), 0)).state.low, 'no disk counter: memory and CPU only');
294
445
  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
446
  t.check(GuestRequestSchema.safeParse({ kind: 'pressure', id: 'p1', reason: 'low', seconds: 90, createdAt: 'now' })
@@ -322,6 +473,53 @@ export const heavyLoadSuite = {
322
473
  t.check(pageFileRisk(pc({ pageFile: 'fixed', commitLimitMb: 90000, commitAvailableMb: 80000 }), 20000) === null, 'a fixed but large one is fine');
323
474
  t.section('the crash report');
324
475
  const ev = (id, provider, at, process = '') => ({ id, provider, at, process });
476
+ t.section('a sandbox that vanishes');
477
+ const kindOf = (events) => explainStop(events)?.kind ?? 'none';
478
+ t.check(kindOf([
479
+ ev(4101, 'Display', '2026-11-02T21:31:00Z', 'Display driver nvlddmkm stopped responding and has successfully recovered.'),
480
+ ]) === 'gpu' && explainStop([ev(4101, 'Display', '2026-11-02T21:31:00Z')]).text.includes('graphics driver'), 'Display 4101: the graphics driver reset (gpu)');
481
+ t.check(kindOf([
482
+ ev(1001, 'Windows Error Reporting', '2026-11-02T21:31:00Z', 'Fault bucket 1, type 5' + ' Event Name: LiveKernelEvent' + ' P1: 141' + ' P2: ffff'),
483
+ ]) === 'gpu' &&
484
+ kindOf([
485
+ ev(1001, 'Windows Error Reporting', '2026-11-02T21:31:00Z', 'Event Name: LiveKernelEvent P1: 1a8 P2: 0'),
486
+ ]) === 'gpu' &&
487
+ kindOf([
488
+ ev(1001, 'Windows Error Reporting', '2026-11-02T21:31:00Z', 'Event Name: LiveKernelEvent P1: 999 P2: 0'),
489
+ ]) === 'none', 'LiveKernelEvent 117, 141 and 1a8 are GPU resets; other codes are not a cause');
490
+ t.check(explainStop([ev(1000, 'Application Error', '2026-11-02T21:31:00Z', 'Faulting application name: vmwp.exe')])
491
+ ?.kind === 'hyperv' &&
492
+ kindOf([ev(1001, 'Windows Error Reporting', '2026-11-02T21:31:00Z', 'vmmemWindowsSandbox crashed')]) ===
493
+ 'hyperv' &&
494
+ kindOf([ev(1000, 'Application Error', '2026-11-02T21:31:00Z', 'Faulting application name: notepad.exe')]) ===
495
+ 'none', 'a crash report of vmwp.exe or vmmem is a cause; another program is not');
496
+ t.check(kindOf([ev(12010, 'Microsoft-Windows-Hyper-V-VmSwitch', '2026-11-02T21:31:00Z')]) === 'hyperv' &&
497
+ kindOf([ev(40001, 'Microsoft-Windows-Hyper-V-Worker', '2026-11-02T21:31:00Z')]) === 'none' &&
498
+ kindOf([ev(18590, 'Microsoft-Windows-Hyper-V-Worker', '2026-11-02T21:31:00Z', 'ErrorCode0: 0x50')]) ===
499
+ 'sandboxCrash', 'Hyper-V worker, switch and compute errors are a cause; the sandbox crashing inside is still its own kind');
500
+ const started = '2026-11-02T20:45:00.000Z';
501
+ const endMs = Date.parse('2026-11-02T21:32:00.000Z');
502
+ const working = {
503
+ at: '2026-11-02T21:31:50.000Z',
504
+ uptimeS: 2800,
505
+ ramFreePct: 60,
506
+ commitPct: 30,
507
+ step: 0,
508
+ hostStep: 0,
509
+ topProcesses: [],
510
+ phase: 'working',
511
+ };
512
+ t.check(vanishedCause(working, started, false, endMs)?.kind === 'vanished', 'busy, fresh heartbeat, no word of leaving: vanished');
513
+ t.check(vanishedCause({ ...working, phase: 'ending' }, started, false, endMs) === null &&
514
+ vanishedCause(working, started, true, endMs) === null &&
515
+ vanishedCause(null, started, false, endMs) === null, 'an orderly end, a stop on purpose, or no heartbeat is not a vanishing');
516
+ t.check(vanishedCause({ ...working, phase: 'ready' }, started, false, endMs) === null &&
517
+ vanishedCause({ ...working, phase: 'ready', step: 2 }, started, false, endMs)?.kind === 'vanished', 'an idle sandbox (window closed by hand) is on purpose; one that was slowing builds down is not');
518
+ t.check(vanishedCause({ ...working, at: '2026-11-02T21:20:00.000Z' }, started, false, endMs) === null &&
519
+ vanishedCause({ ...working, at: '2026-11-01T21:31:50.000Z' }, started, false, endMs) === null, 'a stale heartbeat, or one from an earlier run, says nothing about this end');
520
+ const gone = vanishedCause(working, started, false, endMs);
521
+ const warning = stopWarning({ started, lastSeen: '2026-11-02T21:31:30.000Z', keepAwake: false, ended: { at: gone.at, cause: gone } }, () => null);
522
+ t.check((warning ?? '').includes('ended suddenly') && (warning ?? '').includes('vGPU'), 'the next launch warns about a vanished sandbox, and says what usually causes it');
325
523
  t.check(bugcheckCode('BugcheckCode 209') === '0xD1' && bugcheckCode('BugcheckCode 0') === null, 'Kernel-Power 41 stop code');
326
524
  const cause = explainStop([ev(41, 'Microsoft-Windows-Kernel-Power', '2026-10-15T20:53:47Z', 'BugcheckCode 0')]);
327
525
  t.check(cause?.kind === 'crash' && cause.text.includes('freeze'), 'no stop code: a freeze, crash or power cut');
@@ -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,29 @@ 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
+ ending: "ending";
832
+ }>;
833
+ }, z.core.$strip>;
834
+ export type Heartbeat = z.output<typeof HeartbeatSchema>;
811
835
  /** C:\AgentData\run\responses\<id>.json: the guest's reply. */
812
836
  export declare const GuestResponseSchema: z.ZodObject<{
813
837
  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,26 @@ 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
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
154
+ phase: z.enum(['ready', 'working', 'ending']),
155
+ });
134
156
  /** C:\AgentData\run\responses\<id>.json: the guest's reply. */
135
157
  export const GuestResponseSchema = z.object({
136
158
  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
  }
@@ -3,6 +3,7 @@ export declare const SYSTEM: {
3
3
  readonly cmd: () => string;
4
4
  readonly powershell: () => string;
5
5
  readonly tasklist: () => string;
6
+ readonly taskkill: () => string;
6
7
  readonly whoami: () => string;
7
8
  readonly icacls: () => string;
8
9
  readonly tar: () => string;
@@ -9,6 +9,7 @@ export const SYSTEM = {
9
9
  cmd: () => system32('cmd.exe'),
10
10
  powershell: () => system32('WindowsPowerShell\\v1.0\\powershell.exe'),
11
11
  tasklist: () => system32('tasklist.exe'),
12
+ taskkill: () => system32('taskkill.exe'),
12
13
  whoami: () => system32('whoami.exe'),
13
14
  icacls: () => system32('icacls.exe'),
14
15
  tar: () => system32('tar.exe'),
@@ -13,7 +13,7 @@ import { readForwards, sandboxAddress, startPortForwards } from '../host/network
13
13
  import { AuditCollector, AuditLog, readAuditSettings } from '../host/audit/index.js';
14
14
  import { readMarkedProjects, saveDataSize, watchAndMark } from '../host/guard/index.js';
15
15
  import { sizeWithin } from '../core/disk.js';
16
- import { autoRestartAllowed, causeSince, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, HostMemoryStream, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
16
+ import { autoRestartAllowed, causeSince, readHeartbeat, vanishedCause, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, HostMemoryStream, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
17
17
  import { SandboxChannel } from '../host/channel/index.js';
18
18
  import { ProjectRegistry } from '../host/projects/registry.js';
19
19
  import { ContainedStorage, setNameOf, storageOf } from '../host/storage/index.js';
@@ -67,7 +67,7 @@ export function watcherRunning(dataPath) {
67
67
  }
68
68
  }
69
69
  /** The ways the sandbox itself ends badly (the PC going down takes the watcher with it, so it never gets here). */
70
- const RESTARTABLE = ['sandboxCrash', 'memory'];
70
+ const RESTARTABLE = ['sandboxCrash', 'memory', 'gpu', 'hyperv', 'vanished'];
71
71
  /**
72
72
  * The background watcher of one data folder: while the sandbox runs it syncs history back every minute, guards the
73
73
  * projects' git folders, keeps filtered workspaces in step, forwards ports, marks downloaded files, records the audit
@@ -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));
@@ -318,8 +318,13 @@ export class Watcher {
318
318
  recordEnd() {
319
319
  try {
320
320
  const last = this.life.read();
321
- const cause = last ? causeSince(last.lastSeen, this.storageRoot) : null;
322
- const report = last && cause ? saveCrashReport(this.storageRoot, this.dataPath, last.lastSeen) : null;
321
+ // Windows' own log first; when it says nothing, a sandbox that was busy and left no word of leaving "vanished".
322
+ const logged = last ? causeSince(last.lastSeen, this.storageRoot) : null;
323
+ const cause = logged ??
324
+ (last
325
+ ? vanishedCause(readHeartbeat(this.dataPath), last.started, this.ledger.read()?.closedOnPurpose === true, Date.now())
326
+ : null);
327
+ const report = last && cause ? saveCrashReport(this.storageRoot, this.dataPath, last.lastSeen, cause) : null;
323
328
  this.life.end(cause, new Date(), report);
324
329
  this.cause = cause;
325
330
  // Ended with nothing wrong (its window closed, or it shut itself down idle): on purpose.
@@ -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-GVZWWP4I.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>