windows-development-sandbox 1.27.3 → 1.29.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 (125) hide show
  1. package/README.md +5 -4
  2. package/dist/app/features/sandbox/sandbox.run.js +1 -1
  3. package/dist/bin/{chunk-FA5VWLP4.js → chunk-O4EL32TM.js} +1 -1
  4. package/dist/bin/{chunk-RU7XRN62.js → chunk-QYRRWGFY.js} +1637 -1354
  5. package/dist/bin/sandbox.js +10772 -3
  6. package/dist/cli/commands/launch/launch.js +1 -1
  7. package/dist/cli/commands/launch/running.js +2 -2
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/menu/pages/agents-page.js +1 -1
  10. package/dist/cli/services/sandbox-service.js +1 -1
  11. package/dist/core/schema/sandbox.js +2 -2
  12. package/dist/guest/apps/app-installer.js +1 -1
  13. package/dist/guest/apps/store-check.d.ts +2 -0
  14. package/dist/guest/apps/store-check.js +3 -1
  15. package/dist/guest/boot/guide-lines.d.ts +2 -0
  16. package/dist/guest/boot/guide-lines.js +7 -0
  17. package/dist/guest/boot/guide.js +2 -1
  18. package/dist/guest/daemon/heartbeat.js +1 -0
  19. package/dist/guest/governor/core.d.ts +53 -1
  20. package/dist/guest/governor/core.js +95 -1
  21. package/dist/guest/governor/decide.d.ts +4 -0
  22. package/dist/guest/governor/decide.js +2 -0
  23. package/dist/guest/governor/governor.js +23 -9
  24. package/dist/guest/toolchains/runtimes.js +7 -6
  25. package/dist/host/accounts/deliver.js +16 -2
  26. package/dist/host/apps/store-package.d.ts +2 -0
  27. package/dist/host/apps/store-package.js +16 -15
  28. package/dist/host/channel/attach.js +1 -1
  29. package/dist/host/channel/channel.js +3 -3
  30. package/dist/host/guard/commands.js +1 -0
  31. package/dist/host/health/path-copies.d.ts +1 -1
  32. package/dist/host/health/path-copies.js +2 -2
  33. package/dist/host/install/index.d.ts +1 -1
  34. package/dist/host/install/index.js +1 -1
  35. package/dist/host/install/shims.d.ts +8 -1
  36. package/dist/host/install/shims.js +39 -1
  37. package/dist/host/launch/generate.js +4 -0
  38. package/dist/host/launch/index.d.ts +1 -0
  39. package/dist/host/launch/index.js +1 -0
  40. package/dist/host/launch/pc-logs.d.ts +6 -0
  41. package/dist/host/launch/pc-logs.js +129 -0
  42. package/dist/host/life/crash-report.d.ts +4 -0
  43. package/dist/host/life/crash-report.js +30 -1
  44. package/dist/host/life/last-end.js +1 -0
  45. package/dist/host/sandbox/platform.js +1 -1
  46. package/dist/host/sandbox/window.js +1 -1
  47. package/dist/host/tools/windows-terminal.d.ts +4 -1
  48. package/dist/host/tools/windows-terminal.js +25 -9
  49. package/dist/runtime/agent.js +6 -2
  50. package/dist/runtime/daemon.js +111 -16
  51. package/dist/runtime/db.js +6 -2
  52. package/dist/runtime/main.js +199 -35
  53. package/dist/runtime/netproxy.js +6 -2
  54. package/dist/runtime/tool.js +6 -2
  55. package/dist/runtime/view.js +126 -28
  56. package/dist/selftest/agent-paths.test.js +3 -1
  57. package/dist/selftest/channel.test.js +33 -0
  58. package/dist/selftest/cli.test.js +5 -5
  59. package/dist/selftest/config.test.js +1 -1
  60. package/dist/selftest/credentials.test.js +1 -1
  61. package/dist/selftest/engine.test.js +1 -1
  62. package/dist/selftest/generate.test.js +66 -1
  63. package/dist/selftest/hcs.test.js +1 -1
  64. package/dist/selftest/health.test.js +21 -10
  65. package/dist/selftest/heavy-load.test.js +112 -4
  66. package/dist/selftest/host-sim.test.js +13 -14
  67. package/dist/selftest/install.test.js +6 -2
  68. package/dist/selftest/live-terminal.test.js +103 -0
  69. package/dist/selftest/personalization.test.js +2 -1
  70. package/dist/selftest/store-app.test.js +62 -4
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/web-preview.js +2 -2
  73. package/dist/shared/accounts/kinds.d.ts +5 -0
  74. package/dist/shared/accounts/kinds.js +8 -0
  75. package/dist/shared/apps/index.d.ts +1 -1
  76. package/dist/shared/apps/index.js +1 -1
  77. package/dist/shared/apps/selection.d.ts +1 -0
  78. package/dist/shared/apps/selection.js +4 -3
  79. package/dist/shared/apps/store-package.d.ts +13 -0
  80. package/dist/shared/apps/store-package.js +24 -0
  81. package/dist/shared/channel/schema.d.ts +2 -0
  82. package/dist/shared/channel/schema.js +4 -0
  83. package/dist/shared/memory/policy.d.ts +12 -0
  84. package/dist/shared/memory/policy.js +4 -0
  85. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  86. package/dist/shared/pty/clipboard-watch.js +40 -0
  87. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  88. package/dist/shared/pty/ctrl-c.js +35 -19
  89. package/dist/shared/pty/index.d.ts +1 -0
  90. package/dist/shared/pty/index.js +1 -0
  91. package/dist/shared/pty/terminal.d.ts +1 -1
  92. package/dist/shared/pty/terminal.js +16 -13
  93. package/dist/shared/web/ctrl-c.d.ts +26 -0
  94. package/dist/shared/web/ctrl-c.js +64 -0
  95. package/dist/shared/web/index.d.ts +1 -0
  96. package/dist/shared/web/index.js +1 -0
  97. package/dist/web/index.html +1 -1
  98. package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
  99. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  100. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  101. package/native/csharp/HostTools/Window.cs +1 -1
  102. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  103. package/native/scripts/get-store-package.ps1 +24 -13
  104. package/package.json +4 -14
  105. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  106. package/sandbox/csharp/MemGov.cs +752 -7
  107. package/sandbox/scripts/install-store-package.ps1 +11 -4
  108. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  109. package/schemas/config.schema.json +2 -2
  110. package/schemas/sandbox-config.schema.json +1 -1
  111. package/dist/bin/agent-sandbox.d.ts +0 -2
  112. package/dist/bin/agent-sandbox.js +0 -9
  113. package/dist/bin/chunk-4M5SZZ5P.js +0 -10778
  114. package/dist/bin/claude-host.d.ts +0 -2
  115. package/dist/bin/claude-host.js +0 -11
  116. package/dist/bin/claude-sbx.d.ts +0 -2
  117. package/dist/bin/claude-sbx.js +0 -9
  118. package/dist/bin/codex-sbx.d.ts +0 -2
  119. package/dist/bin/codex-sbx.js +0 -9
  120. package/dist/bin/gemini-sbx.d.ts +0 -2
  121. package/dist/bin/gemini-sbx.js +0 -9
  122. package/dist/bin/sbx-hide.d.ts +0 -2
  123. package/dist/bin/sbx-hide.js +0 -9
  124. package/dist/bin/sbx-show.d.ts +0 -2
  125. package/dist/bin/sbx-show.js +0 -9
@@ -417,7 +417,7 @@ class Launcher {
417
417
  console.log(headsUp + '\nFollow along: sandbox logs -f');
418
418
  if (hidden)
419
419
  console.log((await hidden)
420
- ? 'Sandbox running headless. Open the window: sbx-show Attach here: sandbox claude -t'
420
+ ? 'Sandbox running headless. Open the window: sandbox show Attach here: sandbox claude -t'
421
421
  : 'Could not find the sandbox window to hide (it may still be starting); run sandbox hide in a moment.');
422
422
  else
423
423
  console.log('Sandbox starting. ' +
@@ -114,7 +114,7 @@ export async function openSession(ctx, ready, joined, o) {
114
114
  session.id +
115
115
  ' opened in a new tab inside the sandbox. Attach here with: sandbox attach ' +
116
116
  session.id +
117
- (ready.headless ? ' (headless: sbx-show opens the window)' : ''));
117
+ (ready.headless ? ' (headless: sandbox show opens the window)' : ''));
118
118
  return 0;
119
119
  }
120
120
  /** The sessions a launch asks for: each --session, else one (the agent, in this folder). */
@@ -169,7 +169,7 @@ async function openSessions(ctx, ready, list) {
169
169
  if (!first)
170
170
  return 1;
171
171
  console.log('Each has its own tab inside the sandbox' +
172
- (ready.headless ? ' (headless: sbx-show opens the window)' : '') +
172
+ (ready.headless ? ' (headless: sandbox show opens the window)' : '') +
173
173
  '. Attach to any of them, from any terminal: sandbox attach');
174
174
  return results.every((r) => r.status === 'fulfilled') ? 0 : 1;
175
175
  }
package/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ import { versionText } from './version.js';
17
17
  import { HANDLERS } from './commands/index.js';
18
18
  import { definitionHandler, operationHandler } from './operations/index.js';
19
19
  import { runUpdate } from '../app/features/updates/index.js';
20
- const entry = path.join(PACKAGE_ROOT, 'dist', 'bin', 'agent-sandbox.js');
20
+ const entry = path.join(PACKAGE_ROOT, 'dist', 'bin', 'sandbox.js');
21
21
  /** A first word that is no command and no agent, but close to one: "Did you mean: sandbox status?". */
22
22
  function failOnTypo(word, agents) {
23
23
  const names = [...COMMANDS.filter((c) => !c.hidden).map((c) => c.name), ...agents];
@@ -152,7 +152,7 @@ export function agentsOptions(m, cwd = process.cwd()) {
152
152
  category: 'options',
153
153
  section: 'This start',
154
154
  hint: WINDOW_TEXT[m.window],
155
- description: 'Hide or show the Windows Sandbox window (sbx-show brings a hidden one back).',
155
+ description: 'Hide or show the Windows Sandbox window (sandbox show brings a hidden one back).',
156
156
  },
157
157
  {
158
158
  value: 'opt:folder',
@@ -71,7 +71,7 @@ export class SandboxService {
71
71
  }
72
72
  async hideWindow() {
73
73
  if (!(await hideSandboxWindow(this.ctx.storageRoot, 5)))
74
- fail('Could not find a visible Windows Sandbox window.', 'It may already be hidden (sbx-show brings it back).');
74
+ fail('Could not find a visible Windows Sandbox window.', 'It may already be hidden (sandbox show brings it back).');
75
75
  }
76
76
  /** What stopping ends. Fails when this sandbox was not started by this tool. */
77
77
  stopPlan() {
@@ -48,7 +48,7 @@ export const SandboxSchema = z.strictObject({
48
48
  .describe('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.'),
49
49
  pressureResponse: z
50
50
  .boolean()
51
- .describe('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.'),
51
+ .describe('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.'),
52
52
  vGpu: z
53
53
  .boolean()
54
54
  .describe("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)."),
@@ -62,7 +62,7 @@ export const SandboxSchema = z.strictObject({
62
62
  protectedClient: z.boolean().describe('Run the sandbox window in Protected Client mode (recommended).'),
63
63
  headless: z
64
64
  .boolean()
65
- .describe('Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends.'),
65
+ .describe('Hide the sandbox window ("Show the window" brings it back). A headless sandbox shuts down when its last session ends.'),
66
66
  keepAwake: z
67
67
  .boolean()
68
68
  .describe('Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it).'),
@@ -140,7 +140,7 @@ export class AppInstaller {
140
140
  }
141
141
  catch (e) {
142
142
  warn(entry.name + ' could not be installed and is skipped; the other apps are not affected. Reason: ' + errMsg(e));
143
- this.env.summary.push(entry.name + ': FAILED');
143
+ this.env.summary.push('store' in entry.source ? entry.name + ': not installed - ' + errMsg(e).slice(0, 300) : entry.name + ': FAILED');
144
144
  }
145
145
  }
146
146
  /** The command line an app's shortcut starts it with (Comet: its own profile, DevTools on 127.0.0.1). */
@@ -4,5 +4,7 @@ export interface CheckedPackage {
4
4
  readonly main: string;
5
5
  readonly dependencies: string[];
6
6
  }
7
+ /** Why a Store app has no package here, and what to do (the PC's launch notes say what went wrong there). */
8
+ export declare const MISSING_PACKAGE = "this PC did not hand over its package (it could not fetch it: see the launch notes). Fix: on this PC run \"winget download --id <product id> --source msstore\", then launch again";
7
9
  /** The package in `dir` when its manifest is valid and every file is there as hashed; throws otherwise. */
8
10
  export declare function checkedPackage(dir: string): Promise<CheckedPackage>;
@@ -16,11 +16,13 @@ export function fileSha256(file) {
16
16
  stream.on('end', () => resolve(hash.digest('hex')));
17
17
  });
18
18
  }
19
+ /** Why a Store app has no package here, and what to do (the PC's launch notes say what went wrong there). */
20
+ export const MISSING_PACKAGE = 'this PC did not hand over its package (it could not fetch it: see the launch notes). Fix: on this PC run "winget download --id <product id> --source msstore", then launch again';
19
21
  /** The package in `dir` when its manifest is valid and every file is there as hashed; throws otherwise. */
20
22
  export async function checkedPackage(dir) {
21
23
  const manifest = readValid(StorePackageManifestSchema, path.join(dir, STORE_PACKAGE_MANIFEST));
22
24
  if (!manifest)
23
- throw new Error('the package was not handed over (this PC could not fetch it: see the launch notes)');
25
+ throw new Error(MISSING_PACKAGE);
24
26
  for (const f of manifest.files) {
25
27
  if ((await fileSha256(path.join(dir, f.name)).catch(() => '')) !== f.sha256)
26
28
  throw new Error(f.name + ' is missing or changed');
@@ -23,3 +23,5 @@ export declare function projectFolderLines(workspace: string, workspaceHost: str
23
23
  * (a project that has not been set up yet is not listed: its tools are decided from its own files).
24
24
  */
25
25
  export declare function toolLines(primaryName: string, toolSummary: readonly string[], projects: readonly GuideProject[]): string[];
26
+ /** The PC logs line, only when the launch folder has them (a launch of the sandbox's own source repo). */
27
+ export declare function pcLogsGuideLine(launchDir: string, exists: (dir: string) => boolean): string[];
@@ -39,3 +39,10 @@ export function toolLines(primaryName, toolSummary, projects) {
39
39
  "- Tools by project: each project gets the tools its own files ask for; python and rust already installed here are on every project's PATH.",
40
40
  ];
41
41
  }
42
+ /** The PC logs line, only when the launch folder has them (a launch of the sandbox's own source repo). */
43
+ export function pcLogsGuideLine(launchDir, exists) {
44
+ const dir = path.win32.join(launchDir, 'pc-logs');
45
+ return exists(dir)
46
+ ? ['- PC crash reports and logs (this project only, read-only, never commit them): `' + dir + '`']
47
+ : [];
48
+ }
@@ -6,7 +6,7 @@ import { claudeDir, session, stateDir, workspace } from '../runtime/session.js';
6
6
  import { dataDir, launchDir, logsDir, toolsDir } from '../runtime/paths.js';
7
7
  import { addonGuideLine, APPS, headroomGuideLine, RTK_GUIDE_LINE } from '../../shared/apps/index.js';
8
8
  import { installedAddon, installedRtk } from '../setup/agent-addons.js';
9
- import { projectFolderLines, toolLines } from './guide-lines.js';
9
+ import { pcLogsGuideLine, projectFolderLines, toolLines } from './guide-lines.js';
10
10
  import { sandboxScriptsDir } from '../../shared/scripts.js';
11
11
  import { findUproject, unrealGuideLines } from '../../shared/unreal/index.js';
12
12
  import { DISK } from '../toolchains/engines/unreal-engines.js';
@@ -84,6 +84,7 @@ export function sandboxGuide(toolSummary, lan = 'blocked', projects = [], addons
84
84
  '`',
85
85
  '- RO: `' + launchDir + "` (this boot's scripts) · lost on close: everything else, incl. `C:\\nvm` (Node symlink)",
86
86
  ...mirrorLine(),
87
+ ...pcLogsGuideLine(launchDir, fs.existsSync),
87
88
  ...toolLines(session.project, toolSummary, projects),
88
89
  ...unrealLines(projects),
89
90
  ...unityLines(projects),
@@ -23,6 +23,7 @@ export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes,
23
23
  ...(snapshot?.diskQueue !== null && snapshot?.diskQueue !== undefined
24
24
  ? { diskQueueReported: snapshot.diskQueue }
25
25
  : {}),
26
+ ...(snapshot && snapshot.held > 0 ? { held: Math.min(1024, snapshot.held) } : {}),
26
27
  phase: ending ? 'ending' : sessionCount > 0 ? 'working' : 'ready',
27
28
  };
28
29
  }
@@ -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,
@@ -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' });
@@ -1,4 +1,4 @@
1
- import { fingerprint, SERVICE_KINDS, SERVICE_SPECS, serviceWanted, } from '../../shared/accounts/index.js';
1
+ import { appsNeededFor, fingerprint, SERVICE_KINDS, SERVICE_SPECS, serviceWanted, usedByText, } from '../../shared/accounts/index.js';
2
2
  import { setNameOf } from '../storage/index.js';
3
3
  import { takeHandoffs } from './handoff.js';
4
4
  import { AccountLeases } from './leases.js';
@@ -62,8 +62,22 @@ export function prepareServiceAccounts(i) {
62
62
  }
63
63
  for (const kind of SERVICE_KINDS) {
64
64
  const spec = SERVICE_SPECS[kind];
65
- if (!i.config.accounts[kind].share || !serviceWanted(kind, i.on))
65
+ if (!i.config.accounts[kind].share)
66
66
  continue;
67
+ if (!serviceWanted(kind, i.on)) {
68
+ const apps = appsNeededFor(kind);
69
+ if (apps.length)
70
+ notes.push('Your ' +
71
+ spec.label +
72
+ ' sign-in is on (accounts.' +
73
+ kind +
74
+ '.share) but ' +
75
+ usedByText(spec.usedBy) +
76
+ ' is not installed, so there is nothing to hand it to. Fix: turn the app on in Apps (apps.install: ' +
77
+ apps.join(', ') +
78
+ ').');
79
+ continue;
80
+ }
67
81
  const off = i.others.find((o) => !o.config.accounts[kind].share);
68
82
  if (off) {
69
83
  notes.push('Your ' + spec.label + ' sign-in was not handed over: ' + off.name + ' (also in this sandbox) turns it off.');
@@ -12,6 +12,8 @@ export declare const spawnStoreRunner: StoreRunner;
12
12
  /** What an app needs fetched (catalogue source.store). */
13
13
  export interface StoreApp {
14
14
  readonly id: string;
15
+ /** The name the notes use (the catalogue's); the id when not given. */
16
+ readonly name?: string;
15
17
  readonly productId: string;
16
18
  readonly packageName: string;
17
19
  }
@@ -10,7 +10,7 @@ import { spawn } from 'node:child_process';
10
10
  import { z } from 'zod';
11
11
  import { assertNoLinks, errMsg } from '../../core/util/index.js';
12
12
  import { readValid } from '../../shared/io/index.js';
13
- import { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageManifestSchema, storePackageRole, } from '../../shared/apps/index.js';
13
+ import { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageManifestSchema, assignStoreRoles, storePackageFitsX64, } from '../../shared/apps/index.js';
14
14
  import { SYSTEM } from '../../shared/system.js';
15
15
  import { nativeScript } from '../native.js';
16
16
  import { hostDataFolder } from '../storage/contained/index.js';
@@ -24,6 +24,8 @@ const ScriptResultSchema = z.object({
24
24
  ok: z.boolean(),
25
25
  message: z.string().max(2000),
26
26
  files: z.array(z.string().max(300)).max(64),
27
+ /** The files that came from winget's Dependencies folder (the frameworks); the rest is the app. */
28
+ dependencies: z.array(z.string().max(300)).max(64).default([]),
27
29
  });
28
30
  export const spawnStoreRunner = (exe, args, timeoutMs) => new Promise((resolve) => {
29
31
  const child = spawn(exe, [...args], { windowsHide: true });
@@ -117,16 +119,15 @@ export class StorePackages {
117
119
  }
118
120
  if (!parsed.ok)
119
121
  throw new Error(parsed.message);
120
- const files = parsed.files
121
- .filter((n) => isStorePackageFile(n) && n === path.basename(n) && fs.existsSync(path.join(work, n)))
122
- .map((name) => {
122
+ const frameworks = new Set(parsed.dependencies.map((n) => n.toLowerCase()));
123
+ // Only the builds that run on this sandbox (64-bit): the 32-bit and ARM frameworks winget may add are left out.
124
+ const names = parsed.files.filter((n) => isStorePackageFile(n) &&
125
+ n === path.basename(n) &&
126
+ fs.existsSync(path.join(work, n)) &&
127
+ !(frameworks.has(n.toLowerCase()) && !storePackageFitsX64(n)));
128
+ const files = assignStoreRoles(names, app.packageName, parsed.dependencies).map(({ name, role }) => {
123
129
  const full = path.join(work, name);
124
- return {
125
- name,
126
- role: storePackageRole(name, app.packageName),
127
- sha256: sha256Of(full),
128
- bytes: fs.statSync(full).size,
129
- };
130
+ return { name, role, sha256: sha256Of(full), bytes: fs.statSync(full).size };
130
131
  });
131
132
  const manifest = StorePackageManifestSchema.parse({
132
133
  productId: app.productId,
@@ -197,12 +198,12 @@ export async function prepareStorePackages(apps, o) {
197
198
  notes.push(app.id + ': package ' + note + ' (' + manifest.files.length + ' files)');
198
199
  }
199
200
  catch (e) {
200
- notes.push(app.id +
201
- ' is not prepared: ' +
202
- errMsg(e) +
203
- ' Fix: sign in to the Microsoft Store on this PC and check that winget works (winget download --id ' +
201
+ notes.push((app.name ?? app.id) +
202
+ ': NOT installed in this sandbox. ' +
203
+ errMsg(e).replace(/\.$/, '') +
204
+ '. Fix: sign in to the Microsoft Store on this PC and check that winget works (winget download --id ' +
204
205
  app.productId +
205
- ' --source msstore), then launch again.');
206
+ ' --source msstore --architecture x64 --download-directory <an empty folder>), then launch again.');
206
207
  }
207
208
  }
208
209
  return { notes };
@@ -61,7 +61,7 @@ export async function attachTerminal(session, o = {}) {
61
61
  const endpoint = hostEndpoint(session) ??
62
62
  fail("Refusing to connect: the sandbox reported no address on this PC's Hyper-V (vEthernet) network (" +
63
63
  session.attach.addresses.join(', ') +
64
- ').', 'Use the sandbox window instead (sbx-show); if this keeps happening, run sandbox doctor.');
64
+ ').', 'Use the sandbox window instead (sandbox show); if this keeps happening, run sandbox doctor.');
65
65
  const socket = await connect(endpoint);
66
66
  console.log('Attaching to ' +
67
67
  safeText(session.harness) +
@@ -50,7 +50,7 @@ export class SandboxChannel {
50
50
  fail('Windows Sandbox closed (or is not running).', 'Start it again with sandbox claude; sandbox logs shows what happened.');
51
51
  await sleep(500);
52
52
  }
53
- fail(timeoutMessage, 'Check what the sandbox is doing with sandbox logs -f (or sbx-show to see its window).');
53
+ fail(timeoutMessage, 'Check what the sandbox is doing with sandbox logs -f (or sandbox show to see its window).');
54
54
  }
55
55
  /** Deletes an answer once read; a planted link means the answer is read and nothing is deleted. */
56
56
  dropResponse(requestId) {
@@ -73,7 +73,7 @@ export class SandboxChannel {
73
73
  const status = this.run.readSandboxStatus();
74
74
  if (!status || (launchId !== null && status.launchId !== launchId)) {
75
75
  if (bootSilent(Date.now() - started, seen))
76
- fail('The sandbox has not reported anything for ' + Math.round(FIRST_STATUS_MS / 60000) + ' minutes.', 'It may still be starting, or it cannot reach its folders. Look at its window (sbx-show), the run folder ' +
76
+ fail('The sandbox has not reported anything for ' + Math.round(FIRST_STATUS_MS / 60000) + ' minutes.', 'It may still be starting, or it cannot reach its folders. Look at its window (sandbox show), the run folder ' +
77
77
  this.run.root +
78
78
  ' and the data folder ' +
79
79
  this.dataPath +
@@ -87,7 +87,7 @@ export class SandboxChannel {
87
87
  last = status.message;
88
88
  }
89
89
  if (status.phase === 'failed')
90
- fail('The sandbox failed to start: ' + status.message, 'See sandbox logs for details; the sandbox window shows the error too (sbx-show if headless).');
90
+ fail('The sandbox failed to start: ' + status.message, 'See sandbox logs for details; the sandbox window shows the error too (sandbox show if headless).');
91
91
  return status.phase === 'ready' ? status : null;
92
92
  }, 'Timed out waiting for the sandbox to finish provisioning.');
93
93
  }