@fitlab-ai/agent-infra 0.9.6 → 0.9.7

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 (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -20,7 +20,8 @@ import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../manag
20
20
  import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.ts';
21
21
  import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from '../workspace-identity.ts';
22
22
  import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.ts';
23
- import { quiesceSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.ts';
23
+ import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.ts';
24
+ import { inspectSandboxControlContainer } from '../control/container-identity.ts';
24
25
  import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
25
26
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
26
27
  import type { SandboxTool } from '../tools.ts';
@@ -172,9 +173,40 @@ function removeEmptyManagedParent(base: string, directory: string): void {
172
173
  assertRemoved(parent, 'Empty sandbox container directory');
173
174
  }
174
175
 
175
- async function quiesceProjectControlRoots(config: SandboxConfig): Promise<void> {
176
+ async function removeExactSandboxContainer(
177
+ engine: string,
178
+ manifest: ReturnType<typeof readSandboxControlManifest>,
179
+ timeoutMs: number
180
+ ): Promise<void> {
181
+ const deadlineAt = Date.now() + timeoutMs;
182
+ const remaining = (): number => Math.max(1, deadlineAt - Date.now());
183
+ const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
184
+ const before = await inspect();
185
+ if (before.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
186
+ if (before.state === 'absent') return;
187
+ if (before.running && !runOkEngine(engine, 'docker', ['stop', manifest.containerIdentity.id], { timeout: remaining() })) {
188
+ const afterStop = await inspect();
189
+ if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
190
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
191
+ }
192
+ if (afterStop.state === 'found' && afterStop.running) {
193
+ throw new Error(`Failed to stop sandbox container: ${manifest.containerIdentity.id}`);
194
+ }
195
+ }
196
+ const afterStop = await inspect();
197
+ if (afterStop.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
198
+ if (afterStop.state === 'found' && !runOkEngine(engine, 'docker', ['rm', manifest.containerIdentity.id], { timeout: remaining() })) {
199
+ const afterRemove = await inspect();
200
+ if (afterRemove.state !== 'absent') {
201
+ throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
202
+ }
203
+ }
204
+ }
205
+
206
+ async function removeProjectControlRoots(config: SandboxConfig, engine: string): Promise<Set<string>> {
207
+ const containers = new Set<string>();
176
208
  const projectRoot = path.join(config.controlBase, config.project);
177
- if (!fs.existsSync(projectRoot)) return;
209
+ if (!fs.existsSync(projectRoot)) return containers;
178
210
  assertManagedPath(config.controlBase, projectRoot);
179
211
  const projectStat = fs.lstatSync(projectRoot);
180
212
  if (!projectStat.isDirectory() || projectStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
@@ -183,9 +215,18 @@ async function quiesceProjectControlRoots(config: SandboxConfig): Promise<void>
183
215
  const containerRoot = path.join(projectRoot, container.name);
184
216
  for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
185
217
  if (!identity.isDirectory() || identity.isSymbolicLink()) continue;
186
- await quiesceSandboxControlRoot(path.join(containerRoot, identity.name));
218
+ const root = path.join(containerRoot, identity.name);
219
+ const manifestPath = path.join(root, 'manifest.json');
220
+ if (!fs.existsSync(manifestPath)) throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${root}`);
221
+ const manifest = readSandboxControlManifest(manifestPath);
222
+ containers.add(manifest.container);
223
+ await removeSandboxControlRoot(root, {
224
+ inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
225
+ removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
226
+ });
187
227
  }
188
228
  }
229
+ return containers;
189
230
  }
190
231
 
191
232
  function inspectionBlockers(inspections: readonly WorktreeInspection[]): WorktreeInspection[] {
@@ -277,23 +318,30 @@ async function rmOne(
277
318
  return;
278
319
  }
279
320
 
321
+ const coordinatedContainers = new Set<string>();
280
322
  for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
281
323
  assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
282
324
  assertControlRootMatchesTarget(root, effectiveBranch, workspace);
283
- await quiesceSandboxControlRoot(root);
325
+ const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
326
+ coordinatedContainers.add(manifest.container);
327
+ await removeSandboxControlRoot(root, {
328
+ inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
329
+ removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
330
+ });
284
331
  }
285
332
 
286
- if (matchedContainers.length > 0) {
333
+ const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
334
+ if (uncoordinatedContainers.length > 0) {
287
335
  const spinner = p.spinner();
288
- spinner.start(`Stopping container(s): ${matchedContainers.join(', ')}`);
289
- for (const name of matchedContainers) {
336
+ spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
337
+ for (const name of uncoordinatedContainers) {
290
338
  if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
291
339
  if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
292
340
  }
293
341
  const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
294
- const leftovers = matchedContainers.filter((name) => remaining.includes(name));
342
+ const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
295
343
  if (leftovers.length > 0) throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
296
- spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
344
+ spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
297
345
  } else {
298
346
  p.log.warn(`No sandbox container found for '${branch}'`);
299
347
  }
@@ -400,7 +448,7 @@ async function rmPurge(
400
448
  }
401
449
  const permits = cleanPermits(inspections);
402
450
 
403
- await quiesceProjectControlRoots(config);
451
+ const coordinatedContainers = await removeProjectControlRoots(config, engine);
404
452
 
405
453
  const containers = runEngine(engine, 'docker', [
406
454
  'ps',
@@ -410,17 +458,19 @@ async function rmPurge(
410
458
  '--format',
411
459
  '{{.Names}}'
412
460
  ]);
413
- if (containers) {
461
+ const uncoordinatedContainers = containers.split('\n').filter((name) => name && !coordinatedContainers.has(name));
462
+ if (uncoordinatedContainers.length > 0) {
414
463
  const spinner = p.spinner();
415
464
  spinner.start('Stopping project sandbox containers...');
416
- for (const name of containers.split('\n').filter(Boolean)) {
465
+ for (const name of uncoordinatedContainers) {
417
466
  if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
418
467
  if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
419
468
  }
420
469
  const remaining = runEngine(engine, 'docker', [
421
470
  'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
422
471
  ]);
423
- if (remaining) throw new Error(`Project sandbox container(s) still exist after removal: ${remaining.replaceAll('\n', ', ')}`);
472
+ const leftovers = remaining.split('\n').filter((name) => name && !coordinatedContainers.has(name));
473
+ if (leftovers.length > 0) throw new Error(`Project sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
424
474
  spinner.stop(pc.green('Project sandbox containers removed'));
425
475
  } else {
426
476
  p.log.warn('No project sandbox containers found');
@@ -17,9 +17,9 @@ const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
17
17
 
18
18
  Start an existing sandbox container that has stopped (for example after the
19
19
  Docker daemon was restarted or replaced). The container must already exist:
20
- if none is found, run 'ai sandbox create <branch>' first. A container that is
21
- already running is checked without modifying healthy runtime state. --recreate
22
- allows container-only replacement after in-place recovery fails.`;
20
+ if none is found, run 'ai sandbox create <branch>' first. Without --recreate, a
21
+ container that is already running is checked without modifying healthy runtime
22
+ state. --recreate replaces the existing container even when it is healthy.`;
23
23
 
24
24
  export function parseStartArgs(args: string[]): { target: string; recreate: boolean; help: boolean } {
25
25
  let recreate = false;
@@ -83,6 +83,7 @@ export async function start(args: string[]): Promise<void> {
83
83
  workspace: target.workspace,
84
84
  row: found,
85
85
  allowRecreate: parsed.recreate,
86
+ forceRecreate: parsed.recreate,
86
87
  recreate: async () => {
87
88
  const { create } = await import('./create.ts');
88
89
  await create([target.requestedRef, '--no-refresh']);
@@ -0,0 +1,93 @@
1
+ import type { SandboxControlManifest } from './protocol.ts';
2
+ import { commandForEngine, runEngine, runProbe } from '../shell.ts';
3
+
4
+ export type ContainerObservation =
5
+ | Readonly<{ state: 'found'; id: string; running: boolean; labels: Readonly<Record<string, string>> }>
6
+ | Readonly<{ state: 'absent'; id: string }>
7
+ | Readonly<{ state: 'unknown'; reason: string }>;
8
+
9
+ export type ContainerInspection = Readonly<{
10
+ id?: unknown;
11
+ running?: unknown;
12
+ labels?: unknown;
13
+ }>;
14
+
15
+ function isAuthoritativeNotFound(error: unknown): boolean {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ return /(?:no such container|container .* not found|cannot find container)/i.test(message);
18
+ }
19
+
20
+ export function classifySandboxContainerInspection(
21
+ expected: SandboxControlManifest['containerIdentity'],
22
+ inspection: ContainerInspection | { notFound: true } | { error: unknown }
23
+ ): ContainerObservation {
24
+ if ('notFound' in inspection) return { state: 'absent', id: expected.id };
25
+ if ('error' in inspection) {
26
+ return isAuthoritativeNotFound(inspection.error)
27
+ ? { state: 'absent', id: expected.id }
28
+ : { state: 'unknown', reason: `container inspection failed: ${inspection.error instanceof Error ? inspection.error.message : String(inspection.error)}` };
29
+ }
30
+ const response = inspection as ContainerInspection;
31
+ if (typeof response.id !== 'string' || response.id !== expected.id
32
+ || typeof response.running !== 'boolean' || !response.labels
33
+ || typeof response.labels !== 'object' || Array.isArray(response.labels)) {
34
+ return { state: 'unknown', reason: 'container inspection response is incomplete' };
35
+ }
36
+ const labels = response.labels as Record<string, unknown>;
37
+ for (const [key, value] of Object.entries(expected.labels)) {
38
+ if (labels[key] !== value) return { state: 'unknown', reason: 'container labels do not match the manifest' };
39
+ }
40
+ return {
41
+ state: 'found',
42
+ id: response.id,
43
+ running: response.running,
44
+ labels: Object.fromEntries(
45
+ Object.entries(labels).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : [])
46
+ )
47
+ };
48
+ }
49
+
50
+ export async function inspectSandboxControlContainer(
51
+ manifest: SandboxControlManifest,
52
+ options: { run?: typeof runEngine; probe?: typeof runProbe; timeoutMs?: number } = {}
53
+ ): Promise<ContainerObservation> {
54
+ try {
55
+ const args = ['inspect', '--format', '{{json .}}', manifest.containerIdentity.id];
56
+ let raw: string;
57
+ if (options.run) {
58
+ raw = options.run(manifest.engine, 'docker', args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
59
+ } else {
60
+ const command = commandForEngine(manifest.engine, 'docker', args);
61
+ const result = (options.probe ?? runProbe)(
62
+ command.cmd,
63
+ command.args,
64
+ options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs }
65
+ );
66
+ if (result.status !== 0) {
67
+ const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
68
+ return classifySandboxContainerInspection(manifest.containerIdentity, {
69
+ error: new Error(stderr || `container inspection exited with ${result.status ?? 'unknown'}`)
70
+ });
71
+ }
72
+ raw = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
73
+ }
74
+ const parsed = JSON.parse(raw) as {
75
+ Id?: unknown;
76
+ State?: { Running?: unknown };
77
+ Config?: { Labels?: unknown };
78
+ } | Array<{
79
+ Id?: unknown;
80
+ State?: { Running?: unknown };
81
+ Config?: { Labels?: unknown };
82
+ }>;
83
+ const value = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
84
+ if (!value || Array.isArray(value)) return { state: 'unknown', reason: 'container inspection response is incomplete' };
85
+ return classifySandboxContainerInspection(manifest.containerIdentity, {
86
+ id: value.Id,
87
+ running: value.State?.Running,
88
+ labels: value.Config?.Labels
89
+ });
90
+ } catch (error) {
91
+ return classifySandboxContainerInspection(manifest.containerIdentity, { error });
92
+ }
93
+ }
@@ -15,9 +15,9 @@ export type SandboxControlExecutionResult = {
15
15
 
16
16
  export type PreparedSandboxControlExecution = {
17
17
  execution: SandboxControlExecution;
18
- start(): void;
18
+ start(canWrite?: () => boolean): void;
19
19
  completion: Promise<SandboxControlExecutionResult>;
20
- terminate(): boolean;
20
+ terminate(updateState?: boolean): boolean;
21
21
  };
22
22
 
23
23
  function safeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
@@ -30,11 +30,11 @@ export function nodeEntryArgs(entry: string, args: string[]): string[] {
30
30
  : [entry, ...args];
31
31
  }
32
32
 
33
- async function waitForStartTime(pid: number, timeoutMs = 2_000): Promise<string> {
33
+ async function waitForStartTime(pid: number, timeoutMs = 2_000): Promise<number> {
34
34
  const deadline = Date.now() + timeoutMs;
35
35
  while (Date.now() < deadline) {
36
36
  const startTime = getProcessStartTime(pid);
37
- if (startTime) return startTime;
37
+ if (startTime !== null) return startTime;
38
38
  await new Promise<void>((resolve) => setTimeout(resolve, 10));
39
39
  }
40
40
  throw new Error('SANDBOX_CONTROL_EXECUTOR_IDENTITY_UNAVAILABLE');
@@ -73,7 +73,7 @@ export async function prepareSandboxControlExecution(params: {
73
73
  });
74
74
  const startTime = await waitForStartTime(child.pid);
75
75
  const execution: SandboxControlExecution = {
76
- version: 1,
76
+ version: 2,
77
77
  generation: params.manifest.generation,
78
78
  requestId: params.request.id,
79
79
  nonce,
@@ -88,16 +88,20 @@ export async function prepareSandboxControlExecution(params: {
88
88
  atomicWriteJson(executionPath(params.manifest, params.request.id), execution);
89
89
  return {
90
90
  execution,
91
- start() {
91
+ start(canWrite = () => true) {
92
92
  if (!child.connected) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
93
+ if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
93
94
  atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
95
+ if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
94
96
  child.send({ version: 1, nonce });
95
97
  },
96
98
  completion,
97
- terminate() {
98
- atomicWriteJson(executionPath(params.manifest, params.request.id), {
99
- ...execution, phase: 'terminating', updatedAt: Date.now()
100
- });
99
+ terminate(updateState = true) {
100
+ if (updateState) {
101
+ atomicWriteJson(executionPath(params.manifest, params.request.id), {
102
+ ...execution, phase: 'terminating', updatedAt: Date.now()
103
+ });
104
+ }
101
105
  return terminateSandboxControlExecution(execution);
102
106
  }
103
107
  };