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
@@ -0,0 +1,91 @@
1
+ // What the sandbox did in its last seconds and how it ended, for the status page and the crash report. The sandbox's
2
+ // daemon writes run\heartbeat.json every 10 seconds; when the sandbox vanishes, the last one is the best evidence.
3
+ // The guest wrote it, so it is read through the schema, never through a link, and printed through safeText.
4
+ import { assertNoLinks, safeText } from '../../core/util/index.js';
5
+ import { RunDir, HeartbeatSchema } from '../../shared/channel/index.js';
6
+ import { readValid } from '../../shared/io/index.js';
7
+ import { hostDataRoot } from '../storage/index.js';
8
+ /** The sandbox's last heartbeat, or null (none written, damaged, or reached through a link). */
9
+ export function readHeartbeat(dataPath) {
10
+ try {
11
+ const root = hostDataRoot(dataPath);
12
+ const file = new RunDir(root).heartbeatFile;
13
+ assertNoLinks(root, file);
14
+ return readValid(HeartbeatSchema, file);
15
+ }
16
+ catch {
17
+ // A link or an unreadable folder: no evidence, never an error.
18
+ return null;
19
+ }
20
+ }
21
+ const clock = (iso) => {
22
+ const at = new Date(iso);
23
+ return Number.isNaN(at.getTime())
24
+ ? iso
25
+ : at.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
26
+ };
27
+ /** One line of what the sandbox was doing at its last heartbeat (pure; guest text goes through safeText). */
28
+ export function heartbeatLine(hb) {
29
+ const top = hb.topProcesses.map((p) => safeText(p.name) + ' ' + p.privateMb + ' MB').join(', ');
30
+ return ('last seen inside at ' +
31
+ clock(hb.at) +
32
+ ': up ' +
33
+ Math.round(hb.uptimeS / 60) +
34
+ ' min, ' +
35
+ hb.ramFreePct +
36
+ '% RAM free, ' +
37
+ hb.commitPct +
38
+ '% commit used, builds at step ' +
39
+ hb.step +
40
+ (hb.hostStep > 0 ? ' (this PC asked for step ' + hb.hostStep + ')' : '') +
41
+ (hb.diskQueueReported !== undefined ? ', this PC disk queue ' + hb.diskQueueReported : '') +
42
+ (top ? '; biggest: ' + top : ''));
43
+ }
44
+ /** The likeliest reason, from the last heartbeat (pure). */
45
+ export function guessReason(hb) {
46
+ if (hb && hb.diskQueueReported !== undefined && hb.hostStep > 0)
47
+ return "this PC's disks were overloaded (queue " + hb.diskQueueReported + ')';
48
+ if (hb && (hb.ramFreePct < 8 || hb.commitPct > 92))
49
+ return 'the sandbox was running out of memory';
50
+ return 'Windows stopped it (no reason was logged)';
51
+ }
52
+ /** A heartbeat older than this before the end is not "it was working when it went". */
53
+ const FRESH_MS = 2 * 60 * 1000;
54
+ /**
55
+ * The sandbox went and nothing was logged (pure): its last heartbeat is fresh, from this run, says it was working
56
+ * (a session running, or builds slowed down) and does not say it was shutting down in order, and nobody stopped it on
57
+ * purpose (`sandbox stop`, a restart). The guest cannot tell a closed window from a crash, so the wording says both.
58
+ */
59
+ export function vanishedCause(hb, startedIso, closedOnPurpose, endedAtMs) {
60
+ if (!hb || closedOnPurpose || hb.phase === 'ending')
61
+ return null;
62
+ const seen = Date.parse(hb.at);
63
+ if (!Number.isFinite(seen) || seen < Date.parse(startedIso) - 60000 || endedAtMs - seen > FRESH_MS)
64
+ return null;
65
+ if (hb.phase !== 'working' && hb.step === 0 && hb.hostStep === 0)
66
+ return null;
67
+ return {
68
+ kind: 'vanished',
69
+ at: new Date(endedAtMs).toISOString(),
70
+ text: 'the sandbox ended suddenly while it was working (its window was closed, or it crashed: Windows logged nothing)',
71
+ };
72
+ }
73
+ /**
74
+ * How the last sandbox ended (pure): null when it was closed normally, or is still running. `heartbeat` only counts
75
+ * when it was written during that run.
76
+ */
77
+ export function lastEndOf(life, heartbeat, running) {
78
+ if (!life || running)
79
+ return null;
80
+ if (life.ended && !life.ended.cause)
81
+ return null;
82
+ const hb = heartbeat && Date.parse(heartbeat.at) >= Date.parse(life.started) - 60000 ? heartbeat : null;
83
+ const at = life.ended?.at ?? life.lastSeen;
84
+ const why = life.ended?.cause ? safeText(life.ended.cause.text) : guessReason(hb);
85
+ return { at, text: 'ended unexpectedly ' + clock(at) + ': ' + why };
86
+ }
87
+ /** The status page's "Last sandbox" fact for this data folder. */
88
+ export function readLastEnd(lifeRead, dataPath, running) {
89
+ const life = lifeRead();
90
+ return lastEndOf(life, life && !running ? readHeartbeat(dataPath) : null, running);
91
+ }
@@ -14,6 +14,9 @@ declare const LifeSchema: z.ZodObject<{
14
14
  sleep: "sleep";
15
15
  memory: "memory";
16
16
  sandboxCrash: "sandboxCrash";
17
+ gpu: "gpu";
18
+ hyperv: "hyperv";
19
+ vanished: "vanished";
17
20
  }>;
18
21
  at: z.ZodString;
19
22
  text: z.ZodString;
@@ -5,7 +5,7 @@
5
5
  import { z } from 'zod';
6
6
  import { JsonFile } from '../../shared/io/index.js';
7
7
  import { hostStateFile } from '../storage/index.js';
8
- import { causeSince, IsoSchema } from './stop-events.js';
8
+ import { causeSince, IsoSchema, STOP_KINDS } from './stop-events.js';
9
9
  import { stopWarning } from './stop-warning.js';
10
10
  import { saveCrashReport } from './crash-report.js';
11
11
  const LifeSchema = z.object({
@@ -17,7 +17,7 @@ const LifeSchema = z.object({
17
17
  at: IsoSchema,
18
18
  cause: z
19
19
  .object({
20
- kind: z.enum(['updates', 'restart', 'crash', 'sleep', 'memory', 'sandboxCrash']),
20
+ kind: z.enum(STOP_KINDS),
21
21
  at: IsoSchema,
22
22
  text: z.string().max(400),
23
23
  })
@@ -6,7 +6,9 @@ export interface StopEvent {
6
6
  provider: string;
7
7
  process: string;
8
8
  }
9
- export type StopKind = 'updates' | 'restart' | 'crash' | 'sleep' | 'memory' | 'sandboxCrash';
9
+ /** The ways a sandbox can end that nobody asked for (the life record stores one of these). */
10
+ export declare const STOP_KINDS: readonly ["updates", "restart", "crash", "sleep", "memory", "sandboxCrash", "gpu", "hyperv", "vanished"];
11
+ export type StopKind = (typeof STOP_KINDS)[number];
10
12
  export interface StopCause {
11
13
  kind: StopKind;
12
14
  at: string;
@@ -17,6 +17,24 @@ const StopEventsSchema = z
17
17
  .transform((p) => p ?? ''),
18
18
  }))
19
19
  .max(100);
20
+ /** The ways a sandbox can end that nobody asked for (the life record stores one of these). */
21
+ export const STOP_KINDS = [
22
+ 'updates',
23
+ 'restart',
24
+ 'crash',
25
+ 'sleep',
26
+ 'memory',
27
+ 'sandboxCrash',
28
+ /** The graphics driver reset (display 4101, a GPU LiveKernelEvent): vGPU and the GPU's shared memory. */
29
+ 'gpu',
30
+ /** Hyper-V reported an error, or a program that holds the VM (vmwp.exe, vmmem) crashed. */
31
+ 'hyperv',
32
+ /** Nothing was logged, but the sandbox was working and left no word of leaving. */
33
+ 'vanished',
34
+ ];
35
+ /** Windows live kernel events that are a graphics driver (GPU) reset or timeout. */
36
+ const GPU_LIVE_KERNEL = ['117', '141', '1a8'];
37
+ const VM_PROGRAMS = /vmwp\.exe|vmmem\w*|vmcompute|windowssandbox\w*/i;
20
38
  const UPDATERS = /^(trustedinstaller|mousocoreworker|musnotification(ux)?|usoclient|wuauclt|tiworker|updateorchestrator)\.exe$/i;
21
39
  /** The stop code in Kernel-Power 41's detail ("BugcheckCode 209" -> "0xD1"); null for 0 (no blue screen). */
22
40
  export function bugcheckCode(detail) {
@@ -85,6 +103,47 @@ export function explainStop(events) {
85
103
  },
86
104
  ];
87
105
  }
106
+ // The graphics driver stopped responding and was reset: with vGPU the sandbox shares the GPU, so the VM goes too.
107
+ if (e.id === 4101 && p === 'display')
108
+ return [
109
+ {
110
+ kind: 'gpu',
111
+ at: e.at,
112
+ text: 'the graphics driver stopped responding and was reset while the sandbox used the GPU (vGPU)',
113
+ },
114
+ ];
115
+ const programs = VM_PROGRAMS.exec(e.process)?.[0];
116
+ if (e.id === 1001 && p.includes('windows error reporting') && /LiveKernelEvent/i.test(e.process)) {
117
+ const code = /\bP1:\s*([0-9a-f]{2,4})\b/i.exec(e.process)?.[1]?.toLowerCase();
118
+ if (code && GPU_LIVE_KERNEL.includes(code))
119
+ return [
120
+ {
121
+ kind: 'gpu',
122
+ at: e.at,
123
+ text: 'the graphics driver reset (Windows live kernel event ' +
124
+ code +
125
+ ') while the sandbox used the GPU (vGPU)',
126
+ },
127
+ ];
128
+ }
129
+ if ((e.id === 1000 || e.id === 1001) &&
130
+ programs &&
131
+ /^(application error|windows error reporting)$/i.test(e.provider))
132
+ return [
133
+ {
134
+ kind: 'hyperv',
135
+ at: e.at,
136
+ text: 'a program that runs the sandbox VM (' + programs.toLowerCase() + ') crashed',
137
+ },
138
+ ];
139
+ if (/^microsoft-windows-hyper-v-(worker|vmswitch|compute)/.test(p) && e.id !== 18590 && e.id !== 40001)
140
+ return [
141
+ {
142
+ kind: 'hyperv',
143
+ at: e.at,
144
+ text: 'Hyper-V, which runs the sandbox VM, reported an error (event ' + e.id + ')',
145
+ },
146
+ ];
88
147
  return [];
89
148
  });
90
149
  return causes.sort((a, b) => Date.parse(a.at) - Date.parse(b.at))[0] ?? null;
@@ -4,6 +4,9 @@ const ADVICE = {
4
4
  crash: 'sandbox logs --crash prints what Windows logged and how much memory this PC had; Event Viewer > Windows Logs > System has the rest. A laptop also needs to stay plugged in.',
5
5
  sleep: 'sandbox.keepAwake keeps this PC awake while a sandbox runs (closing a laptop lid still sleeps it). Check it is on, and that sleep is not forced by a policy.',
6
6
  sandboxCrash: 'C:\\AgentData\\logs\\memory-*.log shows what used memory just before. Fewer builds at once inside the sandbox (e.g. cargo build.jobs) makes it less likely.',
7
+ gpu: 'The graphics driver reset while the sandbox used the GPU (vGPU). Turn vGPU off (sandbox config set sandbox.vGpu false) or lower the GPU load, and keep heavy builds and a GPU game apart.',
8
+ hyperv: 'Hyper-V, which runs the sandbox, reported an error. Closing other virtual machines, updating the graphics driver, or turning vGPU off usually helps; sandbox logs --crash has the events.',
9
+ vanished: 'If you did not close its window, this is the known pattern of heavy builds plus a GPU game with vGPU on: turn vGPU off or run fewer builds at once. sandbox logs --crash says what it was doing in its last seconds.',
7
10
  memory: 'Give the sandbox less RAM (sandbox.maxMemoryMb, sandbox.reserveHostMb) or run fewer builds at once inside it.',
8
11
  };
9
12
  const when = (iso) => new Date(iso).toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit' });
@@ -10,6 +10,8 @@ export interface WatchdogState {
10
10
  readonly cpuHighSeconds: number;
11
11
  /** Seconds the disk queue has been above the limit. */
12
12
  readonly diskHighSeconds: number;
13
+ /** Seconds the disk queue has been at or above twice the limit (for the severe step). */
14
+ readonly diskSevereSeconds: number;
13
15
  /** Time (ms) the last request went out. */
14
16
  readonly lastSentAt: number;
15
17
  }
@@ -9,6 +9,7 @@ export const WATCHDOG_START = {
9
9
  calm: 0,
10
10
  cpuHighSeconds: 0,
11
11
  diskHighSeconds: 0,
12
+ diskSevereSeconds: 0,
12
13
  lastSentAt: 0,
13
14
  };
14
15
  /** The request is repeated this often while the PC stays low, and each one lasts a bit longer than that. */
@@ -50,8 +51,12 @@ export function watchdogStep(prev, input) {
50
51
  reason = "this PC's CPU has been above " + t.cpuPercent + '% for ' + cpuHigh + ' seconds';
51
52
  const diskQueue = input.memory?.diskQueue;
52
53
  const diskHigh = diskQueue !== undefined && diskQueue >= t.diskQueue ? prev.diskHighSeconds + input.intervalSeconds : 0;
54
+ const diskDouble = diskQueue !== undefined && diskQueue >= 2 * t.diskQueue ? prev.diskSevereSeconds + input.intervalSeconds : 0;
55
+ const diskSevere = diskDouble >= t.diskSeconds || diskHigh >= 3 * t.diskSeconds;
53
56
  if (!reason && diskHigh >= t.diskSeconds)
54
- reason = "this PC's disks have had a queue of " + diskQueue + ' for ' + diskHigh + ' seconds';
57
+ reason = diskSevere
58
+ ? "this PC's disks are overloaded (queue " + diskQueue + ' for ' + diskHigh + ' seconds)'
59
+ : "this PC's disks have had a queue of " + diskQueue + ' for ' + diskHigh + ' seconds';
55
60
  const calmNow = input.memory ? lowReason(input.memory, input.reserveHostMb, t, 1.25) === '' : true;
56
61
  let low = prev.low;
57
62
  let calm = prev.calm;
@@ -66,7 +71,7 @@ export function watchdogStep(prev, input) {
66
71
  calm = 0;
67
72
  }
68
73
  }
69
- const severe = low && input.memory !== null && severeHost(input.memory);
74
+ const severe = low && ((input.memory !== null && severeHost(input.memory)) || diskSevere);
70
75
  // Getting severe is sent at once; otherwise the request is repeated every RESEND_MS while the PC stays low.
71
76
  const send = low && (input.nowMs - prev.lastSentAt >= RESEND_MS || (severe && !prev.severe));
72
77
  return {
@@ -76,6 +81,7 @@ export function watchdogStep(prev, input) {
76
81
  calm,
77
82
  cpuHighSeconds: cpuHigh,
78
83
  diskHighSeconds: diskHigh,
84
+ diskSevereSeconds: diskDouble,
79
85
  lastSentAt: send ? input.nowMs : prev.lastSentAt,
80
86
  },
81
87
  send,
@@ -0,0 +1,37 @@
1
+ import { LEFTOVER_IMAGES, type SandboxFacts, type SandboxJudgement } from '../health/stale.js';
2
+ /** taskkill arguments that end exactly these programs (pure). */
3
+ export declare function killArgs(pids: readonly number[]): string[];
4
+ /** Ends these Windows Sandbox programs (taskkill by absolute path). Returns false when taskkill failed. */
5
+ export declare function endSandboxPrograms(pids: readonly number[]): boolean;
6
+ /** Restarts the Windows Sandbox service (CmService) in an elevated PowerShell: Windows asks for permission. */
7
+ export declare function restartSandboxService(): boolean;
8
+ /** What the person is asked, and what is done about it: everything outside the decision is passed in. */
9
+ export interface CleanupDeps {
10
+ /** The facts now (a fresh process list each time). */
11
+ readonly facts: () => Promise<SandboxFacts>;
12
+ readonly end: (pids: readonly number[]) => boolean;
13
+ readonly restartService: () => boolean;
14
+ /** An interactive question: true for yes. */
15
+ readonly confirm: (q: {
16
+ title: string;
17
+ lines: string[];
18
+ yes: string;
19
+ }) => Promise<boolean>;
20
+ readonly say: (line: string) => void;
21
+ /** Also restart the service even when the programs ended (the caller said Windows still refuses). */
22
+ readonly alsoService?: boolean;
23
+ }
24
+ export type CleanupOutcome =
25
+ /** Nothing of the sandbox is on this PC. */
26
+ 'nothing'
27
+ /** A live sandbox (or another user's): nothing was ended. */
28
+ | 'alive' | 'declined' | 'cleaned' | 'service-restarted' | 'failed';
29
+ /** The question for a judgement of "leftover" (pure). */
30
+ export declare function cleanupQuestion(j: SandboxJudgement): {
31
+ title: string;
32
+ lines: string[];
33
+ yes: string;
34
+ };
35
+ /** The whole clean-up: judge, ask, end, check, and (when needed and allowed) restart the service. */
36
+ export declare function cleanUpLeftovers(deps: CleanupDeps): Promise<CleanupOutcome>;
37
+ export { LEFTOVER_IMAGES };
@@ -0,0 +1,82 @@
1
+ // Windows Sandbox leaves its programs behind when the VM dies (a crash, a kill from the host side): the next start then
2
+ // fails ("the sandbox was signed out", or "already running"). This ends those leftovers, after asking, and when Windows
3
+ // still refuses, restarts the Windows Sandbox service (CmService), which needs administrator permission. The decisions
4
+ // are pure and the actions are injected, so tests never end a process.
5
+ import { spawnSync } from 'node:child_process';
6
+ import { SYSTEM } from '../../shared/system.js';
7
+ import { refuseInSelfTest } from '../test-guard.js';
8
+ import { nativeScript } from '../native.js';
9
+ import { judgeSandbox, LEFTOVER_IMAGES } from '../health/stale.js';
10
+ const HELPER_TIMEOUT_MS = 15000;
11
+ const SERVICE_TIMEOUT_MS = 120000;
12
+ /** taskkill arguments that end exactly these programs (pure). */
13
+ export function killArgs(pids) {
14
+ const safe = pids.filter((pid) => Number.isInteger(pid) && pid > 4);
15
+ return ['/F', ...safe.flatMap((pid) => ['/PID', String(pid)])];
16
+ }
17
+ /** Ends these Windows Sandbox programs (taskkill by absolute path). Returns false when taskkill failed. */
18
+ export function endSandboxPrograms(pids) {
19
+ refuseInSelfTest('end Windows Sandbox programs');
20
+ if (killArgs(pids).length <= 1)
21
+ return true;
22
+ const result = spawnSync(SYSTEM.taskkill(), killArgs(pids), { windowsHide: true, timeout: HELPER_TIMEOUT_MS });
23
+ return result.status === 0;
24
+ }
25
+ /** Restarts the Windows Sandbox service (CmService) in an elevated PowerShell: Windows asks for permission. */
26
+ export function restartSandboxService() {
27
+ refuseInSelfTest('restart the Windows Sandbox service');
28
+ const result = spawnSync(SYSTEM.powershell(), nativeScript('run-elevated.ps1', ['-Command', 'Restart-Service -Name CmService -Force']), { windowsHide: true, timeout: SERVICE_TIMEOUT_MS });
29
+ return result.status === 0;
30
+ }
31
+ const NAMES = (j) => j.images.join(', ');
32
+ /** The question for a judgement of "leftover" (pure). */
33
+ export function cleanupQuestion(j) {
34
+ return {
35
+ title: 'Close the leftover Windows Sandbox programs?',
36
+ lines: [
37
+ j.why,
38
+ 'Windows Sandbox leaves its programs behind when the sandbox dies, and then refuses to start again.',
39
+ 'Only these programs of your Windows session are closed: ' + NAMES(j) + '. Nothing else.',
40
+ ],
41
+ yes: 'Close them',
42
+ };
43
+ }
44
+ /** The whole clean-up: judge, ask, end, check, and (when needed and allowed) restart the service. */
45
+ export async function cleanUpLeftovers(deps) {
46
+ const first = judgeSandbox(await deps.facts());
47
+ if (first.state === 'none')
48
+ return 'nothing';
49
+ if (first.state !== 'leftover') {
50
+ deps.say(first.why + ' Nothing was closed.');
51
+ return 'alive';
52
+ }
53
+ if (!(await deps.confirm(cleanupQuestion(first))))
54
+ return 'declined';
55
+ const ended = deps.end(first.pids);
56
+ const after = judgeSandbox(await deps.facts());
57
+ const gone = after.state === 'none' || after.state === 'foreign';
58
+ if (gone && !deps.alsoService) {
59
+ deps.say('Closed ' + first.pids.length + ' leftover program(s). The sandbox can start again now.');
60
+ return 'cleaned';
61
+ }
62
+ if (!gone)
63
+ deps.say(ended ? 'Some programs are still there.' : 'Windows would not let them be closed.');
64
+ const restart = deps.alsoService === true ||
65
+ (await deps.confirm({
66
+ title: 'Restart the Windows Sandbox service?',
67
+ lines: [
68
+ 'Windows still holds the old sandbox. Restarting its service (CmService) clears that.',
69
+ 'Windows asks for administrator permission once.',
70
+ ],
71
+ yes: 'Restart it',
72
+ }));
73
+ if (!restart)
74
+ return gone ? 'cleaned' : 'declined';
75
+ if (!deps.restartService()) {
76
+ deps.say('The service was not restarted (the permission was declined, or Windows refused).');
77
+ return 'failed';
78
+ }
79
+ deps.say('The Windows Sandbox service was restarted.');
80
+ return 'service-restarted';
81
+ }
82
+ export { LEFTOVER_IMAGES };
@@ -1,4 +1,5 @@
1
1
  export * from './platform.js';
2
+ export * from './cleanup.js';
2
3
  export * from './probe.js';
3
4
  export * from './tasklist.js';
4
5
  export * from './host-claude.js';
@@ -1,4 +1,5 @@
1
1
  export * from './platform.js';
2
+ export * from './cleanup.js';
2
3
  export * from './probe.js';
3
4
  export * from './tasklist.js';
4
5
  export * from './host-claude.js';
@@ -89,7 +89,7 @@ export async function confirmSandboxStarted(wsbPath, waitMs = START_WAIT_MS) {
89
89
  if (Date.now() >= deadline)
90
90
  fail('Windows Sandbox did not start within ' + Math.round(waitMs / 1000) + ' seconds.', 'Open ' +
91
91
  wsbPath +
92
- ' yourself to see the message Windows gives, or run sandbox doctor to check the Windows Sandbox feature.');
92
+ ' yourself to see the message Windows gives, or run sandbox doctor to check the Windows Sandbox feature. If Windows says the sandbox was signed out, a sandbox that died left its programs behind: run sandbox cleanup.');
93
93
  await sleep(1000);
94
94
  }
95
95
  }
@@ -5541,6 +5541,7 @@ var SYSTEM = {
5541
5541
  cmd: () => system32("cmd.exe"),
5542
5542
  powershell: () => system32("WindowsPowerShell\\v1.0\\powershell.exe"),
5543
5543
  tasklist: () => system32("tasklist.exe"),
5544
+ taskkill: () => system32("taskkill.exe"),
5544
5545
  whoami: () => system32("whoami.exe"),
5545
5546
  icacls: () => system32("icacls.exe"),
5546
5547
  tar: () => system32("tar.exe"),
@@ -25466,7 +25467,7 @@ var SandboxSchema = external_exports.strictObject({
25466
25467
  '(alpha) What runs the sandbox. "windows-sandbox" = Windows Sandbox (the default: desktop, Windows Terminal, GPU, clipboard, audio, printers). "hcs" = a headless Hyper-V isolated Server Core container for agents (no desktop, GPU or clipboard); it needs Windows features and an image on this PC (sandbox engine check hcs lists what is missing), and starting it is not enabled yet in this alpha. sandbox engine shows what each engine can do. Yours only: a project file (.sandbox-config.json) cannot set it.'
25467
25468
  ),
25468
25469
  memoryMb: external_exports.union([external_exports.literal("auto"), external_exports.number().int().min(2048)]).describe(
25469
- 'Sandbox RAM in MB (>= 2048), or "auto": free RAM minus reserveHostMb, capped at maxMemoryMb. Never more than this PC can keep in RAM for it (its RAM minus the larger of reserveHostMb and 25 %, 2 GB more with vGPU): a bigger value is lowered.'
25470
+ 'Sandbox RAM in MB (>= 2048), or "auto": free RAM minus reserveHostMb, capped at maxMemoryMb. Never more than this PC can keep in RAM for it (its RAM minus the larger of reserveHostMb and 25 %, 4 GB more with vGPU): a bigger value is lowered.'
25470
25471
  ),
25471
25472
  maxMemoryMb: external_exports.number().int().positive().describe('Upper limit for memoryMb "auto".'),
25472
25473
  maxMemoryPercent: external_exports.number().int().min(10).max(100).describe(`memoryMb "auto" never takes more than this share of this PC's total RAM (percent).`),
@@ -25628,13 +25629,13 @@ var ToolchainsSchema = external_exports.strictObject({
25628
25629
  '["sccache", "cargo-nextest", "cargo-deny"]'
25629
25630
  ),
25630
25631
  targetDir: external_exports.string().min(1).describe(
25631
- `"shared": cargo's default target\\ (shared with builds on this PC, so builds on either side invalidate the other), "separate": <workspace>\\target\\sandbox (kept, no cross-talk), "local": on the sandbox's own disk (fastest, lost every boot), or a guest path.`
25632
+ `"auto" (default): on the sandbox's own disk when C: has 25 GB free at boot, else "separate" (builds then write through the mapped workspace, which can overload this PC's disk). "shared": cargo's default target\\ (shared with builds on this PC, so builds on either side invalidate the other), "separate": <workspace>\\target\\sandbox (kept, no cross-talk), "local": on the sandbox's own disk (fastest, lost every boot), or a guest path.`
25632
25633
  ),
25633
25634
  linker: external_exports.enum(["lld", "gnu"]).describe(
25634
25635
  `GNU host: "lld" links with Rust's own lld through MinGW's gcc (much less memory than GNU ld), "gnu" with GNU ld.`
25635
25636
  ),
25636
25637
  linkSlots: external_exports.union([external_exports.literal("auto"), external_exports.number().int().min(1).max(32)]).describe(
25637
- 'GNU host: how many programs may link at the same time in the whole sandbox (other links wait). "auto" = one per 3 GB of sandbox memory after 6 GB, 2 to 8 (the memory governor slows links down too when memory runs short).'
25638
+ 'GNU host: how many programs may link at the same time in the whole sandbox (other links wait). "auto" = one per 6 GB of sandbox memory, 1 to 4 (the memory governor slows links down too when memory runs short).'
25638
25639
  )
25639
25640
  }),
25640
25641
  msvc: external_exports.strictObject({
@@ -27282,6 +27283,8 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
27282
27283
  seconds: external_exports.number().int().min(10).max(300),
27283
27284
  /** "severe": the PC is nearly out of memory; builds go to the governor's last step at once. */
27284
27285
  level: external_exports.enum(["slow", "severe"]).optional(),
27286
+ /** The disk queue the PC reported (shown in the heartbeat). */
27287
+ diskQueue: external_exports.number().min(0).max(1e5).optional(),
27285
27288
  createdAt: TimeSchema
27286
27289
  })
27287
27290
  ]);
@@ -27304,6 +27307,22 @@ var PressureStatusSchema = external_exports.object({
27304
27307
  lastAction: Text(300),
27305
27308
  updatedAt: TimeSchema
27306
27309
  });
27310
+ var HeartbeatSchema = external_exports.object({
27311
+ at: TimeSchema,
27312
+ /** Seconds since the sandbox booted. */
27313
+ uptimeS: external_exports.number().int().min(0).max(1e9),
27314
+ ramFreePct: external_exports.number().min(0).max(100),
27315
+ commitPct: external_exports.number().min(0).max(100),
27316
+ /** The governor's dial now, and the step this PC asked for. */
27317
+ step: external_exports.number().int().min(0).max(4),
27318
+ hostStep: external_exports.number().int().min(0).max(4),
27319
+ /** The biggest programs in the agents' job. */
27320
+ topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27321
+ /** The disk queue this PC last reported while asking the sandbox to slow down. */
27322
+ diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27323
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27324
+ phase: external_exports.enum(["ready", "working", "ending"])
27325
+ });
27307
27326
  var GuestResponseSchema = external_exports.object({
27308
27327
  id: IdSchema,
27309
27328
  ok: external_exports.boolean(),
@@ -27489,6 +27508,8 @@ var RunDir = class {
27489
27508
  vpnFile;
27490
27509
  /** What the memory governor in the sandbox is doing (pressure.json). */
27491
27510
  pressureFile;
27511
+ /** The daemon's heartbeat every 10 seconds (heartbeat.json): what the sandbox did last. */
27512
+ heartbeatFile;
27492
27513
  /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
27493
27514
  budgetFile;
27494
27515
  requestsDir;
@@ -27512,6 +27533,7 @@ var RunDir = class {
27512
27533
  this.sandboxFile = path5.join(this.root, "sandbox.json");
27513
27534
  this.vpnFile = path5.join(this.root, "vpn.json");
27514
27535
  this.pressureFile = path5.join(this.root, "pressure.json");
27536
+ this.heartbeatFile = path5.join(this.root, "heartbeat.json");
27515
27537
  this.budgetFile = path5.join(this.root, "budget.json");
27516
27538
  this.requestsDir = path5.join(this.root, "requests");
27517
27539
  this.responsesDir = path5.join(this.root, "responses");
@@ -27556,6 +27578,9 @@ var RunDir = class {
27556
27578
  readPressure() {
27557
27579
  return readValid(PressureStatusSchema, this.pressureFile);
27558
27580
  }
27581
+ readHeartbeat() {
27582
+ return readValid(HeartbeatSchema, this.heartbeatFile);
27583
+ }
27559
27584
  readSession(id) {
27560
27585
  return readValid(SessionStatusSchema, this.sessionFile(id));
27561
27586
  }