windows-development-sandbox 1.27.3 → 1.28.0

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 (68) hide show
  1. package/dist/bin/agent-sandbox.js +2 -2
  2. package/dist/bin/{chunk-RU7XRN62.js → chunk-CB44HTTN.js} +196 -75
  3. package/dist/bin/{chunk-FA5VWLP4.js → chunk-UZ5S3EMB.js} +1 -1
  4. package/dist/bin/{chunk-4M5SZZ5P.js → chunk-ZMZI3625.js} +1 -1
  5. package/dist/bin/claude-host.js +2 -2
  6. package/dist/bin/claude-sbx.js +2 -2
  7. package/dist/bin/codex-sbx.js +2 -2
  8. package/dist/bin/gemini-sbx.js +2 -2
  9. package/dist/bin/sandbox.js +2 -2
  10. package/dist/bin/sbx-hide.js +2 -2
  11. package/dist/bin/sbx-show.js +2 -2
  12. package/dist/core/schema/sandbox.js +1 -1
  13. package/dist/guest/daemon/heartbeat.js +1 -0
  14. package/dist/guest/governor/core.d.ts +53 -1
  15. package/dist/guest/governor/core.js +95 -1
  16. package/dist/guest/governor/decide.d.ts +4 -0
  17. package/dist/guest/governor/decide.js +2 -0
  18. package/dist/guest/governor/governor.js +23 -9
  19. package/dist/guest/toolchains/native/unreal.js +22 -3
  20. package/dist/guest/toolchains/runtimes.js +7 -6
  21. package/dist/host/life/crash-report.d.ts +4 -0
  22. package/dist/host/life/crash-report.js +30 -1
  23. package/dist/host/life/last-end.js +1 -0
  24. package/dist/host/tools/windows-terminal.d.ts +4 -1
  25. package/dist/host/tools/windows-terminal.js +25 -9
  26. package/dist/runtime/agent.js +5 -1
  27. package/dist/runtime/daemon.js +183 -70
  28. package/dist/runtime/db.js +5 -1
  29. package/dist/runtime/main.js +242 -56
  30. package/dist/runtime/netproxy.js +5 -1
  31. package/dist/runtime/tool.js +5 -1
  32. package/dist/runtime/view.js +125 -27
  33. package/dist/selftest/agent-paths.test.js +3 -1
  34. package/dist/selftest/channel.test.js +33 -0
  35. package/dist/selftest/heavy-load.test.js +116 -4
  36. package/dist/selftest/live-terminal.test.js +103 -0
  37. package/dist/selftest/personalization.test.js +2 -1
  38. package/dist/selftest/support/engine-golden.js +2 -0
  39. package/dist/shared/channel/schema.d.ts +2 -0
  40. package/dist/shared/channel/schema.js +4 -0
  41. package/dist/shared/memory/index.d.ts +1 -0
  42. package/dist/shared/memory/index.js +1 -0
  43. package/dist/shared/memory/policy.d.ts +12 -0
  44. package/dist/shared/memory/policy.js +4 -0
  45. package/dist/shared/memory/unreal-build.d.ts +2 -0
  46. package/dist/shared/memory/unreal-build.js +21 -0
  47. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  48. package/dist/shared/pty/clipboard-watch.js +40 -0
  49. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  50. package/dist/shared/pty/ctrl-c.js +35 -19
  51. package/dist/shared/pty/index.d.ts +1 -0
  52. package/dist/shared/pty/index.js +1 -0
  53. package/dist/shared/pty/terminal.d.ts +1 -1
  54. package/dist/shared/pty/terminal.js +16 -13
  55. package/dist/shared/web/ctrl-c.d.ts +26 -0
  56. package/dist/shared/web/ctrl-c.js +64 -0
  57. package/dist/shared/web/index.d.ts +1 -0
  58. package/dist/shared/web/index.js +1 -0
  59. package/dist/web/index.html +1 -1
  60. package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
  61. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  62. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  63. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  64. package/package.json +1 -1
  65. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  66. package/sandbox/csharp/MemGov.cs +752 -7
  67. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  68. package/schemas/config.schema.json +1 -1
@@ -1,12 +1,62 @@
1
1
  import type { PressureStatus } from '../../shared/channel/schema.js';
2
- import type { LoadProfile } from '../../shared/memory/index.js';
2
+ import { z } from 'zod';
3
+ import { type LoadProfile } from '../../shared/memory/index.js';
3
4
  import { type GovernorState, type MemSample } from './decide.js';
5
+ /** memgov.exe's arguments: samples every 2 s and the memory gate for this profile. */
6
+ export declare function memgovArgs(profile: LoadProfile): string[];
7
+ /** What the memory gate in memgov reports on its stdout (one JSON line each). */
8
+ export declare const GateEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
9
+ kind: z.ZodLiteral<"ready">;
10
+ mode: z.ZodEnum<{
11
+ notify: "notify";
12
+ poll: "poll";
13
+ }>;
14
+ growthMb: z.ZodNumber;
15
+ windowMs: z.ZodNumber;
16
+ event: z.ZodLiteral<"gate">;
17
+ at: z.ZodNumber;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ kind: z.ZodLiteral<"off">;
20
+ reason: z.ZodString;
21
+ event: z.ZodLiteral<"gate">;
22
+ at: z.ZodNumber;
23
+ }, z.core.$strict>, z.ZodObject<{
24
+ kind: z.ZodLiteral<"trip">;
25
+ grewMb: z.ZodNumber;
26
+ ms: z.ZodNumber;
27
+ jobMb: z.ZodNumber;
28
+ held: z.ZodArray<z.ZodObject<{
29
+ pid: z.ZodNumber;
30
+ name: z.ZodString;
31
+ }, z.core.$strict>>;
32
+ event: z.ZodLiteral<"gate">;
33
+ at: z.ZodNumber;
34
+ }, z.core.$strict>, z.ZodObject<{
35
+ kind: z.ZodLiteral<"hold">;
36
+ pid: z.ZodNumber;
37
+ name: z.ZodString;
38
+ event: z.ZodLiteral<"gate">;
39
+ at: z.ZodNumber;
40
+ }, z.core.$strict>, z.ZodObject<{
41
+ kind: z.ZodLiteral<"release">;
42
+ pid: z.ZodNumber;
43
+ name: z.ZodString;
44
+ heldMs: z.ZodNumber;
45
+ forced: z.ZodBoolean;
46
+ event: z.ZodLiteral<"gate">;
47
+ at: z.ZodNumber;
48
+ }, z.core.$strict>], "kind">;
49
+ export type GateEvent = z.output<typeof GateEventSchema>;
50
+ /** One memory-log line for a gate event (names cut to 64 characters). */
51
+ export declare function gateLine(e: GateEvent): string;
4
52
  /** The newest numbers the governor saw (for the heartbeat file). */
5
53
  export interface GovernorSnapshot {
6
54
  readonly ramFreePercent: number;
7
55
  readonly commitPercent: number;
8
56
  readonly step: number;
9
57
  readonly hostStep: number;
58
+ /** Build programs the memory gate holds now. */
59
+ readonly held: number;
10
60
  readonly diskQueue: number | null;
11
61
  readonly top: readonly {
12
62
  readonly name: string;
@@ -57,6 +107,8 @@ export declare class GovernorCore {
57
107
  get snapshot(): GovernorSnapshot | null;
58
108
  get current(): GovernorState;
59
109
  onSample(sample: MemSample): void;
110
+ /** An event from the memory gate in memgov: goes to the memory log. */
111
+ onGate(event: GateEvent): void;
60
112
  private apply;
61
113
  private note;
62
114
  }
@@ -1,6 +1,94 @@
1
1
  import { now } from '../../shared/channel/run-dir.js';
2
+ import { z } from 'zod';
3
+ import { BURST_GATE } from '../../shared/memory/index.js';
2
4
  import { errMsg } from '../../core/util/errors.js';
3
- import { decide, HOLD_MS, INITIAL_STATE } from './decide.js';
5
+ import { decide, GATE_NAMES, HOLD_MS, INITIAL_STATE, } from './decide.js';
6
+ /** memgov.exe's arguments: samples every 2 s and the memory gate for this profile. */
7
+ export function memgovArgs(profile) {
8
+ const g = BURST_GATE[profile];
9
+ return [
10
+ 'serve',
11
+ '--interval',
12
+ '2',
13
+ '--gate',
14
+ [g.growthMb, g.windowMs, g.releaseMs, g.maxHoldMs].join(':'),
15
+ '--gate-names',
16
+ GATE_NAMES.join(','),
17
+ ];
18
+ }
19
+ const GateBase = { event: z.literal('gate'), at: z.number() };
20
+ const GateName = z.string().max(128);
21
+ const GateCount = z.number().int().min(0);
22
+ /** What the memory gate in memgov reports on its stdout (one JSON line each). */
23
+ export const GateEventSchema = z.discriminatedUnion('kind', [
24
+ z.strictObject({
25
+ ...GateBase,
26
+ kind: z.literal('ready'),
27
+ mode: z.enum(['notify', 'poll']),
28
+ growthMb: GateCount,
29
+ windowMs: GateCount,
30
+ }),
31
+ z.strictObject({ ...GateBase, kind: z.literal('off'), reason: z.string().max(200) }),
32
+ z.strictObject({
33
+ ...GateBase,
34
+ kind: z.literal('trip'),
35
+ grewMb: GateCount,
36
+ ms: GateCount,
37
+ jobMb: GateCount,
38
+ held: z.array(z.strictObject({ pid: z.number().int(), name: GateName })).max(64),
39
+ }),
40
+ z.strictObject({ ...GateBase, kind: z.literal('hold'), pid: z.number().int(), name: GateName }),
41
+ z.strictObject({
42
+ ...GateBase,
43
+ kind: z.literal('release'),
44
+ pid: z.number().int(),
45
+ name: GateName,
46
+ heldMs: GateCount,
47
+ forced: z.boolean(),
48
+ }),
49
+ ]);
50
+ /** One memory-log line for a gate event (names cut to 64 characters). */
51
+ export function gateLine(e) {
52
+ const cut = (s) => s.slice(0, 64);
53
+ switch (e.kind) {
54
+ case 'ready':
55
+ return ('gate: on (' +
56
+ e.mode +
57
+ "): new compilers wait when the agents' programs grow by more than " +
58
+ e.growthMb +
59
+ ' MB within ' +
60
+ e.windowMs / 1000 +
61
+ ' s');
62
+ case 'off':
63
+ return 'gate: off (' + e.reason + '): builds are only slowed down by the dial';
64
+ case 'trip':
65
+ return ("gate: the agents' programs grew " +
66
+ e.grewMb +
67
+ ' MB in ' +
68
+ (e.ms / 1000).toFixed(1) +
69
+ ' s (now ' +
70
+ e.jobMb +
71
+ ' MB); holding ' +
72
+ e.held.length +
73
+ ': ' +
74
+ e.held
75
+ .slice(0, 8)
76
+ .map((p) => cut(p.name) + ' (' + p.pid + ')')
77
+ .join(', ') +
78
+ (e.held.length > 8 ? ' …' : ''));
79
+ case 'hold':
80
+ return 'gate: ' + cut(e.name) + ' (' + e.pid + ') waits at start while a build burst goes through in turn';
81
+ case 'release':
82
+ return ('gate: ' +
83
+ cut(e.name) +
84
+ ' (' +
85
+ e.pid +
86
+ ') goes on after ' +
87
+ (e.heldMs / 1000).toFixed(1) +
88
+ ' s' +
89
+ (e.forced ? ' (the longest it may wait)' : ''));
90
+ }
91
+ }
4
92
  const STEP_TEXT = [
5
93
  'builds back to normal',
6
94
  'build programs at lower priority',
@@ -55,6 +143,7 @@ export class GovernorCore {
55
143
  commitPercent: d.commitPercent,
56
144
  step: d.state.step,
57
145
  hostStep: nowMs < d.state.hostUntil ? d.state.hostStep : 0,
146
+ held: sample.gateHeld ?? 0,
58
147
  diskQueue: null,
59
148
  top: [...sample.procs]
60
149
  .sort((a, b) => b.mb - a.mb)
@@ -89,6 +178,7 @@ export class GovernorCore {
89
178
  paused: [...this.held]
90
179
  .slice(0, 8)
91
180
  .map(([pid, name]) => ({ name: name.slice(0, 64), pid, seconds: HOLD_MS / 1000 })),
181
+ held: sample.gateHeld ?? 0,
92
182
  cpuPercent: d.state.cpuRate,
93
183
  pcLow: nowMs < d.state.hostUntil,
94
184
  lastAction: this.lastAction.slice(0, 300),
@@ -102,6 +192,10 @@ export class GovernorCore {
102
192
  this.deps.saveStatus(status);
103
193
  }
104
194
  }
195
+ /** An event from the memory gate in memgov: goes to the memory log. */
196
+ onGate(event) {
197
+ this.note(gateLine(event));
198
+ }
105
199
  apply(action) {
106
200
  const port = this.deps.port;
107
201
  try {
@@ -16,6 +16,8 @@ export interface MemSample {
16
16
  readonly commitAvailMb: number;
17
17
  /** Only the processes in the agents' job. */
18
18
  readonly procs: readonly ProcSample[];
19
+ /** Build programs the memory gate holds now (memgov). */
20
+ readonly gateHeld?: number;
19
21
  }
20
22
  export declare const isNeverTouched: (name: string) => boolean;
21
23
  export declare const canLower: (name: string) => boolean;
@@ -24,6 +26,8 @@ export declare const isLinker: (name: string) => boolean;
24
26
  export declare function linkerSlots(totalMb: number, cpus: number): number;
25
27
  export declare const isHeavyCompiler: (name: string) => boolean;
26
28
  export declare const canPause: (name: string) => boolean;
29
+ /** What the memory gate in memgov may freeze (compilers, linkers, Unreal's shader workers); passed as --gate-names. */
30
+ export declare const GATE_NAMES: readonly string[];
27
31
  export type Level = 'ok' | 'throttled' | 'suspending';
28
32
  export interface GovernorState {
29
33
  /** The dial: 0 nothing wrong, 1 lower priority, 2 less CPU and fewer compilers, 3 and 4 harder. */
@@ -109,6 +109,8 @@ export function linkerSlots(totalMb, cpus) {
109
109
  }
110
110
  export const isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
111
111
  export const canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
112
+ /** What the memory gate in memgov may freeze (compilers, linkers, Unreal's shader workers); passed as --gate-names. */
113
+ export const GATE_NAMES = [...COMPILERS, 'shadercompileworker'].filter((n) => !NEVER.has(n)).sort();
112
114
  export const INITIAL_STATE = {
113
115
  step: 0,
114
116
  calm: 0,
@@ -1,6 +1,7 @@
1
1
  // The memory governor: samples the sandbox's memory every 2 seconds (memgov.exe, sandbox/csharp/MemGov.cs) and turns
2
2
  // a dial as it fills: the build programs the agents started get lower priority, lower memory priority, less CPU, and
3
- // take turns (decide.ts). Builds slow down; nothing fails. What it does goes to the memory log and run\pressure.json.
3
+ // take turns (decide.ts). A memory gate in memgov also holds new compilers when memory grows faster than a few GB in
4
+ // seconds. Builds slow down; nothing fails. What it does goes to the memory log and run\pressure.json.
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
@@ -8,14 +9,14 @@ import { spawn } from 'node:child_process';
8
9
  import { createInterface } from 'node:readline';
9
10
  import { z } from 'zod';
10
11
  import { writeJsonAtomic } from '../../shared/io/json.js';
11
- import { DIAL_STEPS, maxCompilers } from '../../shared/memory/index.js';
12
+ import { BURST_GATE, DIAL_STEPS, maxCompilers } from '../../shared/memory/index.js';
12
13
  import { ensureCompiled } from '../runtime/csc.js';
13
14
  import { dataDir } from '../runtime/paths.js';
14
15
  import { warn } from '../runtime/log.js';
15
16
  import { run } from '../runtime/paths.js';
16
17
  import { session } from '../runtime/session.js';
17
18
  import { errMsg } from '../../core/util/errors.js';
18
- import { GovernorCore } from './core.js';
19
+ import { GateEventSchema, GovernorCore, memgovArgs, } from './core.js';
19
20
  const SampleSchema = z.object({
20
21
  ramTotalMb: z.number(),
21
22
  ramAvailMb: z.number(),
@@ -29,27 +30,36 @@ const SampleSchema = z.object({
29
30
  prio: z.enum(['idle', 'below', 'normal']).catch('normal'),
30
31
  }))
31
32
  .max(1024),
33
+ gateHeld: z.number().int().min(0).max(1024).optional().catch(undefined),
32
34
  });
33
35
  /** memgov.exe as a child process: samples come out of its stdout, commands go into its stdin. */
34
36
  class MemGovProcess {
37
+ args;
35
38
  onSample;
39
+ onGate;
36
40
  child = null;
37
- constructor(onSample) {
41
+ constructor(args, onSample, onGate) {
42
+ this.args = args;
38
43
  this.onSample = onSample;
44
+ this.onGate = onGate;
39
45
  }
40
46
  start() {
41
47
  try {
42
- const child = spawn(ensureCompiled('memgov'), ['serve', '--interval', '2'], {
48
+ const child = spawn(ensureCompiled('memgov'), [...this.args], {
43
49
  stdio: ['pipe', 'pipe', 'ignore'],
44
50
  windowsHide: true,
45
51
  });
46
52
  this.child = child;
47
53
  createInterface({ input: child.stdout }).on('line', (line) => {
48
54
  try {
49
- this.onSample(SampleSchema.parse(JSON.parse(line)));
55
+ const raw = JSON.parse(line);
56
+ if (typeof raw === 'object' && raw !== null && raw.event === 'gate')
57
+ this.onGate(GateEventSchema.parse(raw));
58
+ else
59
+ this.onSample(SampleSchema.parse(raw));
50
60
  }
51
61
  catch (e) {
52
- warn('governor: bad sample: ' + errMsg(e));
62
+ warn('governor: bad line from memgov: ' + errMsg(e));
53
63
  }
54
64
  });
55
65
  child.stdin.on('error', () => undefined);
@@ -120,13 +130,17 @@ export class Governor {
120
130
  first.availablePercent +
121
131
  '% free RAM or ' +
122
132
  first.commitPercent +
123
- '% commit, in steps';
133
+ '% commit, in steps; holds new compilers when memory grows faster than ' +
134
+ BURST_GATE[profile].growthMb +
135
+ ' MB in ' +
136
+ BURST_GATE[profile].windowMs / 1000 +
137
+ ' s';
124
138
  }
125
139
  start() {
126
140
  if (session.pressureResponse === false)
127
141
  return;
128
142
  const profile = session.loadProfile === 'gentle' ? 'gentle' : 'normal';
129
- const proc = new MemGovProcess((s) => this.core?.onSample(s));
143
+ const proc = new MemGovProcess(memgovArgs(profile), (s) => this.core?.onSample(s), (e) => this.core?.onGate(e));
130
144
  this.core = new GovernorCore({
131
145
  port: proc,
132
146
  profile,
@@ -1,20 +1,23 @@
1
1
  // Unreal Engine development, machine-wide (toolchains.unreal, once per boot): the DirectX End-User Runtime the engine
2
2
  // lists and the Epic Games Launcher (real installers: they run every boot), the launcher pointed at the data disk for
3
- // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, and a check that
4
- // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API). Visual Studio itself is its own
5
- // step (visual-studio.ts), which the plan runs first; each project's engine is set up by engines/unreal-engines.ts.
3
+ // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, a check that
4
+ // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API), and its parallel compiles capped.
5
+ // Visual Studio itself is its own step (visual-studio.ts), which the plan runs first; each project's engine is set up
6
+ // by engines/unreal-engines.ts.
6
7
  import fs from 'node:fs';
7
8
  import os from 'node:os';
8
9
  import path from 'node:path';
9
10
  import { errMsg } from '../../../core/util/errors.js';
10
11
  import { readValid } from '../../../shared/io/json.js';
11
12
  import { safeRemove } from '../../../shared/io/safe-remove.js';
13
+ import { buildJobs, ubtBuildConfiguration, usableCpus } from '../../../shared/memory/index.js';
12
14
  import { VsSetupReportSchema, vsVerdict } from '../../../shared/unreal/index.js';
13
15
  import { runInstaller } from '../../runtime/pools.js';
14
16
  import { dryRun } from '../../runtime/paths.js';
15
17
  import { exists, mkdirp } from '../../runtime/fs.js';
16
18
  import { log, warn } from '../../runtime/log.js';
17
19
  import { runScriptAsync } from '../../runtime/process.js';
20
+ import { session } from '../../runtime/session.js';
18
21
  import { ensureLauncher, fetchLauncher, placeEngines, restoreLauncherItems } from '../engines/epic-launcher.js';
19
22
  import { resetUnrealStatus, updateUnrealStatus } from '../engines/unreal-status.js';
20
23
  import { SIGNERS, signedInstaller } from '../installers.js';
@@ -68,6 +71,18 @@ async function checkVisualStudio() {
68
71
  fs.rmSync(dir, { recursive: true, force: true });
69
72
  }
70
73
  }
74
+ /**
75
+ * UnrealBuildTool's per-user BuildConfiguration.xml (%APPDATA%, wiped with the sandbox): compiles at once from the
76
+ * sandbox's build budget, so a build start cannot outrun the memory Windows backs the sandbox with. Returns the cap.
77
+ */
78
+ function capUnrealBuilds() {
79
+ const cpus = usableCpus(os.cpus().length, session.cpuLimitPercent ?? null);
80
+ const jobs = buildJobs(cpus, os.totalmem() / 1024 ** 3);
81
+ const appData = process.env.APPDATA ?? 'C:\\Users\\WDAGUtilityAccount\\AppData\\Roaming';
82
+ const dir = mkdirp(path.join(appData, 'Unreal Engine', 'UnrealBuildTool'));
83
+ fs.writeFileSync(path.join(dir, 'BuildConfiguration.xml'), ubtBuildConfiguration(jobs), 'utf8');
84
+ return jobs;
85
+ }
71
86
  /** Runs one part, warning (not failing the step) when it does not work. */
72
87
  async function part(what, work, fallback) {
73
88
  try {
@@ -86,6 +101,9 @@ export async function setupUnreal(env, config, _ctx) {
86
101
  return;
87
102
  }
88
103
  resetUnrealStatus();
104
+ const jobs = await part('capping parallel compiles', async () => capUnrealBuilds(), 0);
105
+ if (jobs)
106
+ log('Unreal: UnrealBuildTool runs at most ' + jobs + ' compiles at once (the build budget)');
89
107
  await part('the DirectX End-User Runtime', installDirectX, undefined);
90
108
  const launcher = await part('the Epic Games Launcher', ensureLauncher, null);
91
109
  // Before anyone opens the launcher: engines go to the data disk, and it knows the ones it installed there before.
@@ -131,6 +149,7 @@ export async function setupUnreal(env, config, _ctx) {
131
149
  vs.version +
132
150
  (check.ok ? ' (UnrealBuildTool finds it)' : ' (UnrealBuildTool would NOT find it)')
133
151
  : 'no Visual Studio') +
152
+ (jobs ? ', at most ' + jobs + ' compiles at once' : '') +
134
153
  ', DirectX End-User Runtime' +
135
154
  (launcher ? ', Epic Games Launcher' : '') +
136
155
  (debuggers ? ', cdb/windbg' : ''));
@@ -28,12 +28,13 @@ export async function setupSingleExe(env, name, repo, pattern, exeName, dir, opt
28
28
  }
29
29
  export async function setupBun(env) {
30
30
  await setupSingleExe(env, 'bun', 'oven-sh/bun', arch === 'arm64' ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/, 'bun.exe', tool('bun', 'bin'), { size: '35 MB' });
31
- // Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk.
32
- Object.assign(env.vars, {
33
- BUN_INSTALL: tool('bun'),
34
- BUN_INSTALL_CACHE_DIR: path.join(localDir, 'bun-cache'),
35
- BUN_TMPDIR: path.join(localDir, 'bun-tmp'),
36
- });
31
+ // Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk. The temp
32
+ // folder must exist: programs compiled with Bun (opencode) unpack their DLLs there and fail when it is missing.
33
+ const cache = path.join(localDir, 'bun-cache');
34
+ const temp = path.join(localDir, 'bun-tmp');
35
+ if (!dryRun)
36
+ [cache, temp].forEach((dir) => mkdirp(dir));
37
+ Object.assign(env.vars, { BUN_INSTALL: tool('bun'), BUN_INSTALL_CACHE_DIR: cache, BUN_TMPDIR: temp });
37
38
  }
38
39
  export async function setupDeno(env) {
39
40
  await setupSingleExe(env, 'deno', 'denoland/deno', arch === 'arm64' ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/, 'deno.exe', tool('deno', 'bin'), { size: '40 MB' });
@@ -22,6 +22,10 @@ export declare function findingLines(events: readonly StopEvent[]): string[];
22
22
  export declare function findDumps(dirs: readonly string[], since: Date): DumpFile[];
23
23
  /** Whether the newest launch started at or before `lastSeen` had vGPU on (host-only .wsb in <storage>\launches). */
24
24
  export declare function launchHadVgpu(storageRoot: string, lastSeen: string): boolean;
25
+ /**
26
+ * "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
27
+ */
28
+ export declare function memoryWasFineLine(hostMemoryLog: string): string | null;
25
29
  /**
26
30
  * The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
27
31
  * last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
@@ -54,7 +54,10 @@ export function findingLines(events) {
54
54
  lines.push('WHEA-Logger: Windows reported a hardware error (CPU, memory, a bus or a disk): heat or failing hardware.');
55
55
  if (has((e) => lower(e).startsWith('hyper-v')))
56
56
  lines.push("Hyper-V events: the sandbox's virtual machine (or the service that runs it) reported a problem.");
57
- if (has((e) => /^(application error|windows error reporting)$/i.test(e.provider)))
57
+ const isAppCrash = (e) => /^(application error|windows error reporting)$/i.test(e.provider);
58
+ if (has((e) => isAppCrash(e) && /vmwp\.exe/i.test(e.process)))
59
+ lines.push("vmwp.exe, the Windows program that runs the sandbox's VM, crashed. That is a fault in Windows' Hyper-V code or a driver it uses (with vGPU on, usually the graphics driver), not a program inside the sandbox running out of memory.");
60
+ else if (has(isAppCrash))
58
61
  lines.push("A crash report names vmwp.exe or vmmem, the programs that hold the sandbox's VM.");
59
62
  return lines;
60
63
  }
@@ -122,6 +125,30 @@ function hostMemoryTail(storageRoot, dataPath) {
122
125
  return '';
123
126
  }
124
127
  }
128
+ /**
129
+ * "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
130
+ */
131
+ export function memoryWasFineLine(hostMemoryLog) {
132
+ const last = hostMemoryLog
133
+ .split(/\r?\n/)
134
+ .filter((l) => l.trim() !== '')
135
+ .pop();
136
+ if (!last)
137
+ return null;
138
+ const ram = /RAM free\/total MB (\d{1,9})\/(\d{1,9})/.exec(last);
139
+ const commit = /commit free\/limit MB (\d{1,9})\/(\d{1,9})/.exec(last);
140
+ if (!ram || !commit)
141
+ return null;
142
+ const freeRam = Number(ram[1]);
143
+ const freeCommit = Number(commit[1]);
144
+ if (freeRam < 4096 || freeCommit < 4096)
145
+ return null;
146
+ return ('This PC still had ' +
147
+ Math.floor(freeRam / 1024) +
148
+ ' GB of RAM and ' +
149
+ Math.floor(freeCommit / 1024) +
150
+ ' GB of memory (RAM plus paging file) free just before, so it did not run out of memory.');
151
+ }
125
152
  /**
126
153
  * The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
127
154
  * last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
@@ -152,6 +179,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
152
179
  .map((l) => ' ' + safeText(l));
153
180
  const freeze = freezeLine(cause, lastSeen);
154
181
  const meaning = findingLines(unique);
182
+ const fine = memoryWasFineLine(extra.hostMemoryLog ?? '');
155
183
  const dumps = extra.dumps ?? [];
156
184
  const vgpu = extra.vgpu ? ['vGPU was on for this sandbox. ' + vgpuWarning('its launch settings'), ''] : [];
157
185
  return [
@@ -165,6 +193,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
165
193
  ...vgpu,
166
194
  'What that usually means:',
167
195
  ...(meaning.length ? meaning.map((l) => ' ' + l) : [' (no memory, blue screen or hardware event was logged)']),
196
+ ...(fine ? [' ' + fine] : []),
168
197
  '',
169
198
  'Crash dumps Windows left (C:\\Windows\\Minidump, MEMORY.DMP):',
170
199
  ...(dumps.length ? dumps.map((d) => ' ' + d.name + ' ' + d.at) : [' (none from that time)']),
@@ -39,6 +39,7 @@ export function heartbeatLine(hb) {
39
39
  hb.step +
40
40
  (hb.hostStep > 0 ? ' (this PC asked for step ' + hb.hostStep + ')' : '') +
41
41
  (hb.diskQueueReported !== undefined ? ', this PC disk queue ' + hb.diskQueueReported : '') +
42
+ (hb.held ? ', ' + hb.held + ' builds held by the memory gate' : '') +
42
43
  (top ? '; biggest: ' + top : ''));
43
44
  }
44
45
  /** The likeliest reason, from the last heartbeat (pure). */
@@ -19,10 +19,13 @@ declare const TerminalSettingsSchema: z.ZodObject<{
19
19
  export declare function withPwshProfile(settings: z.output<typeof TerminalSettingsSchema>, visible: boolean): z.output<typeof TerminalSettingsSchema> | null;
20
20
  /** settings.json that makes new tabs read the current environment; null when it already does. Pure. */
21
21
  export declare function withReloadEnvironment(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
22
+ /** settings.json that copies on select; null when the user already chose (true or false). Pure. */
23
+ export declare function withCopyOnSelect(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
22
24
  /**
23
25
  * Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
24
26
  * not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
25
- * shape-checked, only rewritten when the profile changes; a file that does not parse is left alone.
27
+ * shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
28
+ * copy on select; a value the user set stays); a file that does not parse is left alone.
26
29
  */
27
30
  export declare function syncPwshProfile(dataPath: string, allowed: boolean): void;
28
31
  export declare function ensureTerminal(dataPath: string, refresh: boolean, options?: {
@@ -24,6 +24,9 @@ const SETTINGS = {
24
24
  theme: 'dark',
25
25
  firstWindowPreference: 'defaultProfile',
26
26
  confirmCloseAllTabs: false,
27
+ // Copied the moment the mouse is released, so an agent's redraw cannot clear the selection first.
28
+ copyOnSelect: true,
29
+ copyFormatting: 'none',
27
30
  // New tabs pick up the PATH and variables the runner writes machine-wide after the window opened.
28
31
  compatibility: { reloadEnvironmentVariables: true },
29
32
  warning: { confirmCloseAllTabs: false, largePaste: false, multiLinePaste: false },
@@ -75,10 +78,17 @@ export function withReloadEnvironment(settings) {
75
78
  return null;
76
79
  return { ...settings, compatibility: { ...compatibility, reloadEnvironmentVariables: true } };
77
80
  }
81
+ /** settings.json that copies on select; null when the user already chose (true or false). Pure. */
82
+ export function withCopyOnSelect(settings) {
83
+ if ('copyOnSelect' in settings)
84
+ return null;
85
+ return { ...settings, copyOnSelect: true };
86
+ }
78
87
  /**
79
88
  * Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
80
89
  * not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
81
- * shape-checked, only rewritten when the profile changes; a file that does not parse is left alone.
90
+ * shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
91
+ * copy on select; a value the user set stays); a file that does not parse is left alone.
82
92
  */
83
93
  export function syncPwshProfile(dataPath, allowed) {
84
94
  const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
@@ -100,16 +110,22 @@ export function syncPwshProfile(dataPath, allowed) {
100
110
  }
101
111
  const withProfile = withPwshProfile(parsed, visible);
102
112
  if (withProfile) {
103
- writeJson(file, withReloadEnvironment(withProfile) ?? withProfile);
113
+ const reloaded = withReloadEnvironment(withProfile) ?? withProfile;
114
+ writeJson(file, withCopyOnSelect(reloaded) ?? reloaded);
104
115
  return;
105
116
  }
106
- // Only the reload switch is missing: set that one value in the text, so comments and the layout stay.
107
- if (withReloadEnvironment(parsed)) {
108
- const edits = modify(text, ['compatibility', 'reloadEnvironmentVariables'], true, {
109
- formattingOptions: { insertSpaces: true, tabSize: 4 },
110
- });
111
- writeFileAtomic(file, applyEdits(text, edits));
112
- }
117
+ // Only default switches are missing: set those values in the text, so comments and the layout stay.
118
+ const missing = [];
119
+ if (withReloadEnvironment(parsed))
120
+ missing.push([['compatibility', 'reloadEnvironmentVariables'], true]);
121
+ if (withCopyOnSelect(parsed))
122
+ missing.push([['copyOnSelect'], true]);
123
+ if (missing.length === 0)
124
+ return;
125
+ let edited = text;
126
+ for (const [at, value] of missing)
127
+ edited = applyEdits(edited, modify(edited, at, value, { formattingOptions: { insertSpaces: true, tabSize: 4 } }));
128
+ writeFileAtomic(file, edited);
113
129
  }
114
130
  export async function ensureTerminal(dataPath, refresh, options = {}) {
115
131
  const result = await installTerminal(dataPath, refresh);
@@ -25502,7 +25502,7 @@ var SandboxSchema = external_exports.strictObject({
25502
25502
  'The last resort that keeps the sandbox up, in MB below its commit limit (RAM plus paging file): builds are slowed down long before (sandbox.pressureResponse); only if everything the agents start together would take the sandbox to the very edge of its memory, where Windows inside it would start failing, is a build refused memory instead of Windows. "auto" = the larger of 1536 MB and 3 % of the commit limit; "none" = no backstop. Yours only: a project file cannot change it.'
25503
25503
  ),
25504
25504
  pressureResponse: external_exports.boolean().describe(
25505
- "When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. Yours only: a project file cannot change it."
25505
+ "When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. A memory gate also holds new compilers and linkers for a moment when the memory of the build programs grows faster than 4 GB in 3 seconds (3 GB with vGPU), and lets them go one at a time. Yours only: a project file cannot change it."
25506
25506
  ),
25507
25507
  vGpu: external_exports.boolean().describe(
25508
25508
  "Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it). Browsers in the sandbox never use it (they draw in software)."
@@ -27700,6 +27700,8 @@ var PressureStatusSchema = external_exports.object({
27700
27700
  lowered: external_exports.number().int().min(0).max(1024),
27701
27701
  /** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
27702
27702
  paused: external_exports.array(external_exports.object({ name: Text(64), pid: external_exports.number().int(), seconds: external_exports.number().int().min(0).max(60) })).max(8),
27703
+ /** Build programs the memory gate holds right now (let go one at a time). */
27704
+ held: external_exports.number().int().min(0).max(1024).optional(),
27703
27705
  /** The CPU share the compilers get together (percent; 100 = no cap). */
27704
27706
  cpuPercent: external_exports.number().int().min(0).max(100).optional(),
27705
27707
  /** The PC itself asked for slowing down (its memory or CPU is low). */
@@ -27720,6 +27722,8 @@ var HeartbeatSchema = external_exports.object({
27720
27722
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
27721
27723
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
27722
27724
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
27725
+ /** Build programs the memory gate holds right now (let go one at a time). */
27726
+ held: external_exports.number().int().min(0).max(1024).optional(),
27723
27727
  /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
27724
27728
  phase: external_exports.enum(["ready", "working", "ending"])
27725
27729
  });