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.
- package/config.example.json +1 -1
- package/dist/app/features/defs.d.ts +26 -0
- package/dist/app/features/defs.js +2 -1
- package/dist/app/features/sandbox/sandbox.def.d.ts +34 -0
- package/dist/app/features/sandbox/sandbox.def.js +52 -10
- package/dist/app/features/sandbox/sandbox.run.d.ts +24 -0
- package/dist/app/features/sandbox/sandbox.run.js +45 -1
- package/dist/app/registry.js +2 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-MQIA7GNB.js → chunk-FRPLQLSY.js} +3188 -2765
- package/dist/bin/{chunk-M4OM632I.js → chunk-MY7NLQQY.js} +1 -1
- package/dist/bin/{chunk-IIHL7W6B.js → chunk-N4GXYTLV.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +4 -1
- package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
- package/dist/cli/commands/launch/leftover-offer.js +26 -0
- package/dist/cli/commands/repair/checks/sandbox.d.ts +3 -1
- package/dist/cli/commands/repair/checks/sandbox.js +53 -2
- package/dist/core/config/defaults.js +1 -1
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/core/schema/toolchains.js +2 -2
- package/dist/guest/daemon/daemon.js +1 -1
- package/dist/guest/daemon/heartbeat.d.ts +11 -0
- package/dist/guest/daemon/heartbeat.js +59 -0
- package/dist/guest/daemon/idle-shutdown.js +2 -0
- package/dist/guest/entries/daemon.js +2 -0
- package/dist/guest/governor/core.d.ts +19 -1
- package/dist/guest/governor/core.js +23 -1
- package/dist/guest/governor/decide.d.ts +14 -0
- package/dist/guest/governor/decide.js +52 -7
- package/dist/guest/governor/governor.d.ts +4 -1
- package/dist/guest/governor/governor.js +9 -2
- package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
- package/dist/guest/toolchains/native/rust-extras.js +26 -3
- package/dist/guest/toolchains/plan.js +6 -4
- package/dist/host/channel/channel.d.ts +1 -1
- package/dist/host/channel/channel.js +10 -2
- package/dist/host/health/index.d.ts +1 -0
- package/dist/host/health/index.js +1 -0
- package/dist/host/health/leftovers.d.ts +3 -0
- package/dist/host/health/leftovers.js +40 -0
- package/dist/host/health/stale.d.ts +48 -0
- package/dist/host/health/stale.js +40 -0
- package/dist/host/launch/wsb-file.d.ts +6 -1
- package/dist/host/launch/wsb-file.js +9 -5
- package/dist/host/life/crash-report.d.ts +3 -1
- package/dist/host/life/crash-report.js +9 -3
- package/dist/host/life/index.d.ts +1 -0
- package/dist/host/life/index.js +1 -0
- package/dist/host/life/last-end.d.ts +28 -0
- package/dist/host/life/last-end.js +91 -0
- package/dist/host/life/life-record.d.ts +3 -0
- package/dist/host/life/life-record.js +2 -2
- package/dist/host/life/stop-events.d.ts +3 -1
- package/dist/host/life/stop-events.js +59 -0
- package/dist/host/life/stop-warning.js +3 -0
- package/dist/host/life/watchdog.d.ts +2 -0
- package/dist/host/life/watchdog.js +8 -2
- package/dist/host/sandbox/cleanup.d.ts +37 -0
- package/dist/host/sandbox/cleanup.js +82 -0
- package/dist/host/sandbox/index.d.ts +1 -0
- package/dist/host/sandbox/index.js +1 -0
- package/dist/host/sandbox/platform.js +1 -1
- package/dist/runtime/agent.js +28 -3
- package/dist/runtime/daemon.js +629 -479
- package/dist/runtime/db.js +28 -3
- package/dist/runtime/main.js +59 -11
- package/dist/runtime/netproxy.js +28 -3
- package/dist/runtime/tool.js +28 -3
- package/dist/runtime/view.js +28 -3
- package/dist/selftest/heavy-load.test.js +212 -14
- package/dist/selftest/live-terminal.test.js +4 -0
- package/dist/shared/channel/run-dir.d.ts +4 -1
- package/dist/shared/channel/run-dir.js +7 -1
- package/dist/shared/channel/schema.d.ts +24 -0
- package/dist/shared/channel/schema.js +22 -0
- package/dist/shared/memory/policy.d.ts +10 -0
- package/dist/shared/memory/policy.js +17 -1
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/watcher.js +10 -5
- package/dist/web/index.html +1 -1
- package/dist/web/{main-CWIF4VQI.js → main-GVZWWP4I.js} +63 -63
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Program.cs +5 -1
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/stop-events.ps1 +7 -3
- package/package.json +1 -1
- package/sandbox/csharp/MemGov.cs +58 -0
- package/schemas/config.schema.json +3 -3
- 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) ===
|
|
67
|
-
residentCapMb({ ...MB_SANDBOX, vGpu: false }, 65536) === 49152, 'what this PC can keep in RAM for the sandbox: its RAM minus max(reserve, 25 %),
|
|
68
|
-
t.check(cappedMemoryMb(32768, { ...MB_SANDBOX, vGpu: true }, 32768) ===
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
t.check(
|
|
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 &&
|
|
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
|
-
//
|
|
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 ===
|
|
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
|
}
|
package/dist/shared/system.d.ts
CHANGED
package/dist/shared/system.js
CHANGED
|
@@ -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'),
|
package/dist/sync/watcher.js
CHANGED
|
@@ -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
|
-
|
|
322
|
-
const
|
|
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.
|
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
9
|
+
<script type="module" src="/main-GVZWWP4I.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|