windows-development-sandbox 1.18.3 → 1.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/app/features/defs.d.ts +18 -0
  2. package/dist/app/features/defs.js +2 -1
  3. package/dist/app/features/sandbox/sandbox.def.d.ts +22 -0
  4. package/dist/app/features/sandbox/sandbox.def.js +41 -14
  5. package/dist/app/features/sandbox/sandbox.run.d.ts +20 -0
  6. package/dist/app/features/sandbox/sandbox.run.js +41 -1
  7. package/dist/app/registry.js +2 -1
  8. package/dist/bin/agent-sandbox.js +2 -2
  9. package/dist/bin/{chunk-FQWYCF6L.js → chunk-FRPLQLSY.js} +3453 -3145
  10. package/dist/bin/{chunk-M6XUQK53.js → chunk-MY7NLQQY.js} +1 -1
  11. package/dist/bin/{chunk-3G53NNZP.js → chunk-N4GXYTLV.js} +1 -1
  12. package/dist/bin/claude-host.js +2 -2
  13. package/dist/bin/claude-sbx.js +2 -2
  14. package/dist/bin/codex-sbx.js +2 -2
  15. package/dist/bin/gemini-sbx.js +2 -2
  16. package/dist/bin/sandbox.js +2 -2
  17. package/dist/bin/sbx-hide.js +2 -2
  18. package/dist/bin/sbx-show.js +2 -2
  19. package/dist/cli/commands/launch/launch.js +4 -1
  20. package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
  21. package/dist/cli/commands/launch/leftover-offer.js +26 -0
  22. package/dist/cli/commands/repair/checks/sandbox.js +29 -2
  23. package/dist/guest/daemon/heartbeat.d.ts +7 -2
  24. package/dist/guest/daemon/heartbeat.js +34 -15
  25. package/dist/guest/daemon/idle-shutdown.js +2 -0
  26. package/dist/guest/entries/daemon.js +1 -1
  27. package/dist/host/health/index.d.ts +1 -0
  28. package/dist/host/health/index.js +1 -0
  29. package/dist/host/health/leftovers.d.ts +3 -0
  30. package/dist/host/health/leftovers.js +40 -0
  31. package/dist/host/health/stale.d.ts +48 -0
  32. package/dist/host/health/stale.js +40 -0
  33. package/dist/host/life/crash-report.d.ts +1 -1
  34. package/dist/host/life/crash-report.js +5 -3
  35. package/dist/host/life/last-end.d.ts +7 -0
  36. package/dist/host/life/last-end.js +21 -0
  37. package/dist/host/life/life-record.d.ts +3 -0
  38. package/dist/host/life/life-record.js +2 -2
  39. package/dist/host/life/stop-events.d.ts +3 -1
  40. package/dist/host/life/stop-events.js +59 -0
  41. package/dist/host/life/stop-warning.js +3 -0
  42. package/dist/host/sandbox/cleanup.d.ts +37 -0
  43. package/dist/host/sandbox/cleanup.js +82 -0
  44. package/dist/host/sandbox/index.d.ts +1 -0
  45. package/dist/host/sandbox/index.js +1 -0
  46. package/dist/host/sandbox/platform.js +1 -1
  47. package/dist/runtime/agent.js +3 -1
  48. package/dist/runtime/daemon.js +368 -351
  49. package/dist/runtime/db.js +3 -1
  50. package/dist/runtime/main.js +3 -1
  51. package/dist/runtime/netproxy.js +3 -1
  52. package/dist/runtime/tool.js +3 -1
  53. package/dist/runtime/view.js +3 -1
  54. package/dist/selftest/heavy-load.test.js +119 -2
  55. package/dist/shared/channel/schema.d.ts +1 -0
  56. package/dist/shared/channel/schema.js +2 -1
  57. package/dist/shared/system.d.ts +1 -0
  58. package/dist/shared/system.js +1 -0
  59. package/dist/sync/watcher.js +9 -4
  60. package/dist/web/index.html +1 -1
  61. package/dist/web/{main-6XYO5FZC.js → main-GVZWWP4I.js} +62 -62
  62. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  63. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  64. package/native/csharp/HostTools/Program.cs +5 -1
  65. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  66. package/native/scripts/stop-events.ps1 +7 -3
  67. package/package.json +1 -1
@@ -5546,6 +5546,7 @@ var SYSTEM = {
5546
5546
  cmd: () => system32("cmd.exe"),
5547
5547
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
5548
5548
  tasklist: () => system32("tasklist.exe"),
5549
+ taskkill: () => system32("taskkill.exe"),
5549
5550
  whoami: () => system32("whoami.exe"),
5550
5551
  icacls: () => system32("icacls.exe"),
5551
5552
  tar: () => system32("tar.exe"),
@@ -27295,7 +27296,8 @@ var HeartbeatSchema = external_exports.object({
27295
27296
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27296
27297
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27297
27298
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27298
- phase: external_exports.enum(["ready", "working"])
27299
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27300
+ phase: external_exports.enum(["ready", "working", "ending"])
27299
27301
  });
27300
27302
  var GuestResponseSchema = external_exports.object({
27301
27303
  id: IdSchema,
@@ -27425,7 +27425,8 @@ var HeartbeatSchema = external_exports.object({
27425
27425
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27426
27426
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27427
27427
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27428
- phase: external_exports.enum(["ready", "working"])
27428
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27429
+ phase: external_exports.enum(["ready", "working", "ending"])
27429
27430
  });
27430
27431
  var GuestResponseSchema = external_exports.object({
27431
27432
  id: IdSchema,
@@ -27883,6 +27884,7 @@ var SYSTEM = {
27883
27884
  cmd: () => system32("cmd.exe"),
27884
27885
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
27885
27886
  tasklist: () => system32("tasklist.exe"),
27887
+ taskkill: () => system32("taskkill.exe"),
27886
27888
  whoami: () => system32("whoami.exe"),
27887
27889
  icacls: () => system32("icacls.exe"),
27888
27890
  tar: () => system32("tar.exe"),
@@ -27348,7 +27348,8 @@ var HeartbeatSchema = external_exports.object({
27348
27348
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27349
27349
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27350
27350
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27351
- phase: external_exports.enum(["ready", "working"])
27351
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27352
+ phase: external_exports.enum(["ready", "working", "ending"])
27352
27353
  });
27353
27354
  var GuestResponseSchema = external_exports.object({
27354
27355
  id: IdSchema,
@@ -27681,6 +27682,7 @@ var SYSTEM = {
27681
27682
  cmd: () => system32("cmd.exe"),
27682
27683
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
27683
27684
  tasklist: () => system32("tasklist.exe"),
27685
+ taskkill: () => system32("taskkill.exe"),
27684
27686
  whoami: () => system32("whoami.exe"),
27685
27687
  icacls: () => system32("icacls.exe"),
27686
27688
  tar: () => system32("tar.exe"),
@@ -25775,6 +25775,7 @@ var SYSTEM = {
25775
25775
  cmd: () => system32("cmd.exe"),
25776
25776
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
25777
25777
  tasklist: () => system32("tasklist.exe"),
25778
+ taskkill: () => system32("taskkill.exe"),
25778
25779
  whoami: () => system32("whoami.exe"),
25779
25780
  icacls: () => system32("icacls.exe"),
25780
25781
  tar: () => system32("tar.exe"),
@@ -27902,7 +27903,8 @@ var HeartbeatSchema = external_exports.object({
27902
27903
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27903
27904
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27904
27905
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27905
- phase: external_exports.enum(["ready", "working"])
27906
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27907
+ phase: external_exports.enum(["ready", "working", "ending"])
27906
27908
  });
27907
27909
  var GuestResponseSchema = external_exports.object({
27908
27910
  id: IdSchema,
@@ -5756,6 +5756,7 @@ var SYSTEM = {
5756
5756
  cmd: () => system32("cmd.exe"),
5757
5757
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
5758
5758
  tasklist: () => system32("tasklist.exe"),
5759
+ taskkill: () => system32("taskkill.exe"),
5759
5760
  whoami: () => system32("whoami.exe"),
5760
5761
  icacls: () => system32("icacls.exe"),
5761
5762
  tar: () => system32("tar.exe"),
@@ -27923,7 +27924,8 @@ var HeartbeatSchema = external_exports.object({
27923
27924
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27924
27925
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27925
27926
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27926
- phase: external_exports.enum(["ready", "working"])
27927
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27928
+ phase: external_exports.enum(["ready", "working", "ending"])
27927
27929
  });
27928
27930
  var GuestResponseSchema = external_exports.object({
27929
27931
  id: IdSchema,
@@ -10,9 +10,12 @@ import { SYSTEM } from '../shared/system.js';
10
10
  import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
11
11
  import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, } from '../shared/memory/index.js';
12
12
  import { HeartbeatSchema } from '../shared/channel/index.js';
13
- import { heartbeatLine, lastEndOf } from '../host/life/index.js';
13
+ import { heartbeatLine, lastEndOf, vanishedCause, stopWarning } from '../host/life/index.js';
14
14
  import { cappedMemoryMb, decideMemory, memoryNote, residentCapMb, residentNote, resolveMemoryMb, } from '../host/launch/index.js';
15
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';
16
19
  import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
17
20
  // The pure governor files only: governor.ts loads the guest runtime (session.json), which exists only in a sandbox.
18
21
  import { canLower, canPause, decide, INITIAL_STATE, isLinker, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
@@ -252,12 +255,79 @@ export const heavyLoadSuite = {
252
255
  t.check(!HeartbeatSchema.safeParse({ ...beat, topProcesses: Array.from({ length: 6 }, () => beat.topProcesses[0]) })
253
256
  .success &&
254
257
  !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');
258
+ t.check(heartbeatLine({ ...beat, topProcesses: [{ name: 'a\u0007b', privateMb: 5 }] }).includes('\u0007') === false, 'guest text in the heartbeat line goes through safeText');
256
259
  const life = { started: '2026-11-02T20:45:00.000Z', lastSeen: '2026-11-02T21:31:30.000Z', keepAwake: false };
257
260
  t.check((lastEndOf(life, beat, false)?.text ?? '').includes('disks were overloaded') &&
258
261
  lastEndOf(life, beat, true) === null &&
259
262
  lastEndOf(null, beat, false) === null &&
260
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');
261
331
  t.section('the governor over a port (fake Windows)');
262
332
  const calls = [];
263
333
  const port = {
@@ -403,6 +473,53 @@ export const heavyLoadSuite = {
403
473
  t.check(pageFileRisk(pc({ pageFile: 'fixed', commitLimitMb: 90000, commitAvailableMb: 80000 }), 20000) === null, 'a fixed but large one is fine');
404
474
  t.section('the crash report');
405
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');
406
523
  t.check(bugcheckCode('BugcheckCode 209') === '0xD1' && bugcheckCode('BugcheckCode 0') === null, 'Kernel-Power 41 stop code');
407
524
  const cause = explainStop([ev(41, 'Microsoft-Windows-Kernel-Power', '2026-10-15T20:53:47Z', 'BugcheckCode 0')]);
408
525
  t.check(cause?.kind === 'crash' && cause.text.includes('freeze'), 'no stop code: a freeze, crash or power cut');
@@ -828,6 +828,7 @@ export declare const HeartbeatSchema: z.ZodObject<{
828
828
  phase: z.ZodEnum<{
829
829
  ready: "ready";
830
830
  working: "working";
831
+ ending: "ending";
831
832
  }>;
832
833
  }, z.core.$strip>;
833
834
  export type Heartbeat = z.output<typeof HeartbeatSchema>;
@@ -150,7 +150,8 @@ export const HeartbeatSchema = z.object({
150
150
  topProcesses: z.array(z.object({ name: Text(64), privateMb: z.number().int().min(0).max(1e7) })).max(5),
151
151
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
152
152
  diskQueueReported: z.number().min(0).max(100000).optional(),
153
- phase: z.enum(['ready', 'working']),
153
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
154
+ phase: z.enum(['ready', 'working', 'ending']),
154
155
  });
155
156
  /** C:\AgentData\run\responses\<id>.json: the guest's reply. */
156
157
  export const GuestResponseSchema = z.object({
@@ -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
@@ -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-6XYO5FZC.js"></script>
9
+ <script type="module" src="/main-GVZWWP4I.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>