@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
@@ -11,7 +11,8 @@ import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../manag
11
11
  import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
12
12
  import { resolveSandboxTarget } from '../workspace-identity.js';
13
13
  import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.js';
14
- import { quiesceSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.js';
14
+ import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.js';
15
+ import { inspectSandboxControlContainer } from '../control/container-identity.js';
15
16
  import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
16
17
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
17
18
  import { fetchSandboxRows } from './list-running.js';
@@ -118,10 +119,39 @@ function removeEmptyManagedParent(base, directory) {
118
119
  }
119
120
  assertRemoved(parent, 'Empty sandbox container directory');
120
121
  }
121
- async function quiesceProjectControlRoots(config) {
122
+ async function removeExactSandboxContainer(engine, manifest, timeoutMs) {
123
+ const deadlineAt = Date.now() + timeoutMs;
124
+ const remaining = () => Math.max(1, deadlineAt - Date.now());
125
+ const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
126
+ const before = await inspect();
127
+ if (before.state === 'unknown')
128
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
129
+ if (before.state === 'absent')
130
+ return;
131
+ if (before.running && !runOkEngine(engine, 'docker', ['stop', manifest.containerIdentity.id], { timeout: remaining() })) {
132
+ const afterStop = await inspect();
133
+ if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
134
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
135
+ }
136
+ if (afterStop.state === 'found' && afterStop.running) {
137
+ throw new Error(`Failed to stop sandbox container: ${manifest.containerIdentity.id}`);
138
+ }
139
+ }
140
+ const afterStop = await inspect();
141
+ if (afterStop.state === 'unknown')
142
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
143
+ if (afterStop.state === 'found' && !runOkEngine(engine, 'docker', ['rm', manifest.containerIdentity.id], { timeout: remaining() })) {
144
+ const afterRemove = await inspect();
145
+ if (afterRemove.state !== 'absent') {
146
+ throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
147
+ }
148
+ }
149
+ }
150
+ async function removeProjectControlRoots(config, engine) {
151
+ const containers = new Set();
122
152
  const projectRoot = path.join(config.controlBase, config.project);
123
153
  if (!fs.existsSync(projectRoot))
124
- return;
154
+ return containers;
125
155
  assertManagedPath(config.controlBase, projectRoot);
126
156
  const projectStat = fs.lstatSync(projectRoot);
127
157
  if (!projectStat.isDirectory() || projectStat.isSymbolicLink())
@@ -133,9 +163,19 @@ async function quiesceProjectControlRoots(config) {
133
163
  for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
134
164
  if (!identity.isDirectory() || identity.isSymbolicLink())
135
165
  continue;
136
- await quiesceSandboxControlRoot(path.join(containerRoot, identity.name));
166
+ const root = path.join(containerRoot, identity.name);
167
+ const manifestPath = path.join(root, 'manifest.json');
168
+ if (!fs.existsSync(manifestPath))
169
+ throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${root}`);
170
+ const manifest = readSandboxControlManifest(manifestPath);
171
+ containers.add(manifest.container);
172
+ await removeSandboxControlRoot(root, {
173
+ inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
174
+ removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
175
+ });
137
176
  }
138
177
  }
178
+ return containers;
139
179
  }
140
180
  function inspectionBlockers(inspections) {
141
181
  return inspections.filter((inspection) => inspection.status !== 'clean');
@@ -211,25 +251,32 @@ async function rmOne(config, tools, branch, options = {}) {
211
251
  p.outro('Cancelled');
212
252
  return;
213
253
  }
254
+ const coordinatedContainers = new Set();
214
255
  for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
215
256
  assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
216
257
  assertControlRootMatchesTarget(root, effectiveBranch, workspace);
217
- await quiesceSandboxControlRoot(root);
258
+ const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
259
+ coordinatedContainers.add(manifest.container);
260
+ await removeSandboxControlRoot(root, {
261
+ inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
262
+ removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
263
+ });
218
264
  }
219
- if (matchedContainers.length > 0) {
265
+ const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
266
+ if (uncoordinatedContainers.length > 0) {
220
267
  const spinner = p.spinner();
221
- spinner.start(`Stopping container(s): ${matchedContainers.join(', ')}`);
222
- for (const name of matchedContainers) {
268
+ spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
269
+ for (const name of uncoordinatedContainers) {
223
270
  if (!runOkEngine(engine, 'docker', ['stop', name]))
224
271
  throw new Error(`Failed to stop sandbox container: ${name}`);
225
272
  if (!runOkEngine(engine, 'docker', ['rm', name]))
226
273
  throw new Error(`Failed to remove sandbox container: ${name}`);
227
274
  }
228
275
  const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
229
- const leftovers = matchedContainers.filter((name) => remaining.includes(name));
276
+ const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
230
277
  if (leftovers.length > 0)
231
278
  throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
232
- spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
279
+ spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
233
280
  }
234
281
  else {
235
282
  p.log.warn(`No sandbox container found for '${branch}'`);
@@ -330,7 +377,7 @@ async function rmPurge(config, tools, prompt = {}) {
330
377
  throw new Error(`Refusing to purge because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
331
378
  }
332
379
  const permits = cleanPermits(inspections);
333
- await quiesceProjectControlRoots(config);
380
+ const coordinatedContainers = await removeProjectControlRoots(config, engine);
334
381
  const containers = runEngine(engine, 'docker', [
335
382
  'ps',
336
383
  '-a',
@@ -339,10 +386,11 @@ async function rmPurge(config, tools, prompt = {}) {
339
386
  '--format',
340
387
  '{{.Names}}'
341
388
  ]);
342
- if (containers) {
389
+ const uncoordinatedContainers = containers.split('\n').filter((name) => name && !coordinatedContainers.has(name));
390
+ if (uncoordinatedContainers.length > 0) {
343
391
  const spinner = p.spinner();
344
392
  spinner.start('Stopping project sandbox containers...');
345
- for (const name of containers.split('\n').filter(Boolean)) {
393
+ for (const name of uncoordinatedContainers) {
346
394
  if (!runOkEngine(engine, 'docker', ['stop', name]))
347
395
  throw new Error(`Failed to stop sandbox container: ${name}`);
348
396
  if (!runOkEngine(engine, 'docker', ['rm', name]))
@@ -351,8 +399,9 @@ async function rmPurge(config, tools, prompt = {}) {
351
399
  const remaining = runEngine(engine, 'docker', [
352
400
  'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
353
401
  ]);
354
- if (remaining)
355
- throw new Error(`Project sandbox container(s) still exist after removal: ${remaining.replaceAll('\n', ', ')}`);
402
+ const leftovers = remaining.split('\n').filter((name) => name && !coordinatedContainers.has(name));
403
+ if (leftovers.length > 0)
404
+ throw new Error(`Project sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
356
405
  spinner.stop(pc.green('Project sandbox containers removed'));
357
406
  }
358
407
  else {
@@ -8,9 +8,9 @@ const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
8
8
 
9
9
  Start an existing sandbox container that has stopped (for example after the
10
10
  Docker daemon was restarted or replaced). The container must already exist:
11
- if none is found, run 'ai sandbox create <branch>' first. A container that is
12
- already running is checked without modifying healthy runtime state. --recreate
13
- allows container-only replacement after in-place recovery fails.`;
11
+ if none is found, run 'ai sandbox create <branch>' first. Without --recreate, a
12
+ container that is already running is checked without modifying healthy runtime
13
+ state. --recreate replaces the existing container even when it is healthy.`;
14
14
  export function parseStartArgs(args) {
15
15
  let recreate = false;
16
16
  let help = false;
@@ -62,6 +62,7 @@ export async function start(args) {
62
62
  workspace: target.workspace,
63
63
  row: found,
64
64
  allowRecreate: parsed.recreate,
65
+ forceRecreate: parsed.recreate,
65
66
  recreate: async () => {
66
67
  const { create } = await import('./create.js');
67
68
  await create([target.requestedRef, '--no-refresh']);
@@ -0,0 +1,64 @@
1
+ import { commandForEngine, runEngine, runProbe } from '../shell.js';
2
+ function isAuthoritativeNotFound(error) {
3
+ const message = error instanceof Error ? error.message : String(error);
4
+ return /(?:no such container|container .* not found|cannot find container)/i.test(message);
5
+ }
6
+ export function classifySandboxContainerInspection(expected, inspection) {
7
+ if ('notFound' in inspection)
8
+ return { state: 'absent', id: expected.id };
9
+ if ('error' in inspection) {
10
+ return isAuthoritativeNotFound(inspection.error)
11
+ ? { state: 'absent', id: expected.id }
12
+ : { state: 'unknown', reason: `container inspection failed: ${inspection.error instanceof Error ? inspection.error.message : String(inspection.error)}` };
13
+ }
14
+ const response = inspection;
15
+ if (typeof response.id !== 'string' || response.id !== expected.id
16
+ || typeof response.running !== 'boolean' || !response.labels
17
+ || typeof response.labels !== 'object' || Array.isArray(response.labels)) {
18
+ return { state: 'unknown', reason: 'container inspection response is incomplete' };
19
+ }
20
+ const labels = response.labels;
21
+ for (const [key, value] of Object.entries(expected.labels)) {
22
+ if (labels[key] !== value)
23
+ return { state: 'unknown', reason: 'container labels do not match the manifest' };
24
+ }
25
+ return {
26
+ state: 'found',
27
+ id: response.id,
28
+ running: response.running,
29
+ labels: Object.fromEntries(Object.entries(labels).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : []))
30
+ };
31
+ }
32
+ export async function inspectSandboxControlContainer(manifest, options = {}) {
33
+ try {
34
+ const args = ['inspect', '--format', '{{json .}}', manifest.containerIdentity.id];
35
+ let raw;
36
+ if (options.run) {
37
+ raw = options.run(manifest.engine, 'docker', args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
38
+ }
39
+ else {
40
+ const command = commandForEngine(manifest.engine, 'docker', args);
41
+ const result = (options.probe ?? runProbe)(command.cmd, command.args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
42
+ if (result.status !== 0) {
43
+ const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
44
+ return classifySandboxContainerInspection(manifest.containerIdentity, {
45
+ error: new Error(stderr || `container inspection exited with ${result.status ?? 'unknown'}`)
46
+ });
47
+ }
48
+ raw = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
49
+ }
50
+ const parsed = JSON.parse(raw);
51
+ const value = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
52
+ if (!value || Array.isArray(value))
53
+ return { state: 'unknown', reason: 'container inspection response is incomplete' };
54
+ return classifySandboxContainerInspection(manifest.containerIdentity, {
55
+ id: value.Id,
56
+ running: value.State?.Running,
57
+ labels: value.Config?.Labels
58
+ });
59
+ }
60
+ catch (error) {
61
+ return classifySandboxContainerInspection(manifest.containerIdentity, { error });
62
+ }
63
+ }
64
+ //# sourceMappingURL=container-identity.js.map
@@ -18,7 +18,7 @@ async function waitForStartTime(pid, timeoutMs = 2_000) {
18
18
  const deadline = Date.now() + timeoutMs;
19
19
  while (Date.now() < deadline) {
20
20
  const startTime = getProcessStartTime(pid);
21
- if (startTime)
21
+ if (startTime !== null)
22
22
  return startTime;
23
23
  await new Promise((resolve) => setTimeout(resolve, 10));
24
24
  }
@@ -48,7 +48,7 @@ export async function prepareSandboxControlExecution(params) {
48
48
  });
49
49
  const startTime = await waitForStartTime(child.pid);
50
50
  const execution = {
51
- version: 1,
51
+ version: 2,
52
52
  generation: params.manifest.generation,
53
53
  requestId: params.request.id,
54
54
  nonce,
@@ -63,17 +63,23 @@ export async function prepareSandboxControlExecution(params) {
63
63
  atomicWriteJson(executionPath(params.manifest, params.request.id), execution);
64
64
  return {
65
65
  execution,
66
- start() {
66
+ start(canWrite = () => true) {
67
67
  if (!child.connected)
68
68
  throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
69
+ if (!canWrite())
70
+ throw new Error('SANDBOX_CONTROL_OWNER_LOST');
69
71
  atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
72
+ if (!canWrite())
73
+ throw new Error('SANDBOX_CONTROL_OWNER_LOST');
70
74
  child.send({ version: 1, nonce });
71
75
  },
72
76
  completion,
73
- terminate() {
74
- atomicWriteJson(executionPath(params.manifest, params.request.id), {
75
- ...execution, phase: 'terminating', updatedAt: Date.now()
76
- });
77
+ terminate(updateState = true) {
78
+ if (updateState) {
79
+ atomicWriteJson(executionPath(params.manifest, params.request.id), {
80
+ ...execution, phase: 'terminating', updatedAt: Date.now()
81
+ });
82
+ }
77
83
  return terminateSandboxControlExecution(execution);
78
84
  }
79
85
  };
@@ -3,10 +3,33 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { buildProcessTreeStopCommand } from '../../server/process-control.js';
5
5
  import { processIdentityMatches } from '../../server/process-state.js';
6
+ import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
7
+ import { inspectSandboxControlContainer } from './container-identity.js';
6
8
  import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
7
- const DEFAULT_QUIESCE_TIMEOUT_MS = 7_000;
9
+ const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
8
10
  const QUIESCING_FILE = 'quiescing.json';
9
11
  const BROKER_STARTING_FILE = 'broker-starting.json';
12
+ async function awaitWithDeadline(operation, deadlineAt, errorCode) {
13
+ const remaining = deadlineAt - Date.now();
14
+ if (remaining <= 0)
15
+ throw new Error(errorCode);
16
+ let timer;
17
+ try {
18
+ return await Promise.race([
19
+ operation(),
20
+ new Promise((_, reject) => {
21
+ timer = setTimeout(() => reject(new Error(errorCode)), remaining);
22
+ })
23
+ ]);
24
+ }
25
+ finally {
26
+ if (timer)
27
+ clearTimeout(timer);
28
+ }
29
+ }
30
+ function forceDeadline(deadlineAt, timeoutMs) {
31
+ return Math.max(Date.now(), deadlineAt - Math.max(1, Math.floor(timeoutMs / 2)));
32
+ }
10
33
  function regularFile(filePath) {
11
34
  try {
12
35
  const stat = fs.lstatSync(filePath);
@@ -44,8 +67,8 @@ function readStartupOwner(filePath) {
44
67
  const raw = fs.readFileSync(filePath, 'utf8');
45
68
  try {
46
69
  const record = JSON.parse(raw);
47
- if (record.version !== 1 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
48
- || typeof record.startTime !== 'string')
70
+ if (record.version !== 2 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
71
+ || typeof record.startTime !== 'number' || !Number.isSafeInteger(record.startTime))
49
72
  return null;
50
73
  return { raw, owner: { pid: record.pid, startTime: record.startTime } };
51
74
  }
@@ -77,7 +100,7 @@ async function waitForStartupTransition(root, timeoutMs) {
77
100
  export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
78
101
  const resolvedRoot = path.resolve(root);
79
102
  const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
80
- const record = `${JSON.stringify({ version: 1, ...owner })}\n`;
103
+ const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
81
104
  const deadline = Date.now() + timeoutMs;
82
105
  while (Date.now() < deadline) {
83
106
  if (isSandboxControlRootQuiescing(resolvedRoot))
@@ -133,11 +156,17 @@ export function readSandboxControlManifest(manifestPath) {
133
156
  catch {
134
157
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
135
158
  }
136
- if (manifest.version !== 3) {
137
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 3; container-only recreation is required');
159
+ if (manifest.version !== 4) {
160
+ throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 4; container-only recreation is required');
138
161
  }
139
- if (typeof manifest.repoRoot !== 'string' || typeof manifest.worktreeRoot !== 'string'
162
+ if (typeof manifest.engine !== 'string' || manifest.engine.length === 0
163
+ || typeof manifest.repoRoot !== 'string' || typeof manifest.worktreeRoot !== 'string'
140
164
  || typeof manifest.project !== 'string' || typeof manifest.container !== 'string'
165
+ || !manifest.containerIdentity || typeof manifest.containerIdentity.id !== 'string'
166
+ || manifest.containerIdentity.id.length === 0 || !manifest.containerIdentity.labels
167
+ || typeof manifest.containerIdentity.labels !== 'object'
168
+ || Array.isArray(manifest.containerIdentity.labels)
169
+ || Object.values(manifest.containerIdentity.labels).some((value) => typeof value !== 'string')
141
170
  || typeof manifest.branch !== 'string' || !['task-bound', 'branch-only'].includes(manifest.mode ?? '')
142
171
  || (manifest.taskId !== null && typeof manifest.taskId !== 'string')
143
172
  || typeof manifest.channelDir !== 'string' || typeof manifest.publicStatusDir !== 'string'
@@ -164,9 +193,10 @@ function readBrokerOwner(filePath) {
164
193
  catch {
165
194
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
166
195
  }
167
- if (!owner || owner.version !== 2 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
168
- || typeof owner.startTime !== 'string' || typeof owner.token !== 'string'
169
- || typeof owner.generation !== 'string')
196
+ if (!owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
197
+ || typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
198
+ || typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
199
+ || typeof owner.token !== 'string' || typeof owner.generation !== 'string')
170
200
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
171
201
  return owner;
172
202
  }
@@ -183,10 +213,11 @@ function readStatusOwner(filePath) {
183
213
  }
184
214
  }
185
215
  function sameOwner(left, right) {
186
- return left.pid === right.pid && left.startTime === right.startTime;
216
+ return left.pid === right.pid && left.startTime === right.startTime
217
+ && (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
187
218
  }
188
219
  function ownerLive(owner) {
189
- return processIdentityMatches({ version: 1, pid: owner.pid, startTime: owner.startTime });
220
+ return processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
190
221
  }
191
222
  function readExecutions(manifest) {
192
223
  if (!fs.existsSync(manifest.processingDir))
@@ -241,10 +272,10 @@ function executionAlive(execution, platform) {
241
272
  if (platform !== 'win32' && execution.child.processGroupId) {
242
273
  return processGroupAlive(execution.child.processGroupId);
243
274
  }
244
- return processIdentityMatches({ version: 1, pid: execution.child.pid, startTime: execution.child.startTime });
275
+ return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
245
276
  }
246
- async function waitForExit(owner, timeoutMs) {
247
- const deadline = Date.now() + timeoutMs;
277
+ async function waitForExit(owner, timeoutMs, deadlineAt) {
278
+ const deadline = deadlineAt ?? (Date.now() + timeoutMs);
248
279
  while (Date.now() < deadline) {
249
280
  if (!ownerLive(owner))
250
281
  return true;
@@ -286,13 +317,16 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
286
317
  if (!stat.isDirectory() || stat.isSymbolicLink())
287
318
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
288
319
  }
320
+ const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
321
+ const deadlineAt = Date.now() + timeoutMs;
322
+ const forceAt = forceDeadline(deadlineAt, timeoutMs);
289
323
  markSandboxControlRootQuiescing(resolvedRoot);
290
- await waitForStartupTransition(resolvedRoot, options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS);
324
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
291
325
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
292
326
  const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
293
327
  const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
294
328
  const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
295
- const statusOwner = status ? { version: 1, ...status.broker } : null;
329
+ const statusOwner = status ? status.broker : null;
296
330
  const brokerLive = broker ? ownerLive(broker) : false;
297
331
  const statusLive = statusOwner ? ownerLive(statusOwner) : false;
298
332
  if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
@@ -307,14 +341,28 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
307
341
  if (!manifest && brokerLive)
308
342
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
309
343
  const platform = options.platform ?? process.platform;
310
- const timeoutMs = options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
311
344
  let executions = manifest ? readExecutions(manifest) : [];
312
345
  const owner = brokerLive ? broker : statusLive ? statusOwner : null;
313
346
  if (!owner) {
314
347
  if (manifest && !broker && !status)
315
348
  throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
316
349
  for (const execution of executions) {
317
- if (!terminateSandboxControlExecution(execution, { platform, timeoutMs })) {
350
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
351
+ }
352
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
353
+ while (Date.now() < softDeadlineAt) {
354
+ if (!executions.some((execution) => executionAlive(execution, platform)))
355
+ break;
356
+ await new Promise((resolve) => setTimeout(resolve, 25));
357
+ }
358
+ if (manifest)
359
+ executions = mergeExecutions(executions, readExecutions(manifest));
360
+ if (Date.now() >= deadlineAt)
361
+ throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
362
+ for (const execution of executions) {
363
+ if (!terminateSandboxControlExecution(execution, {
364
+ platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now()
365
+ })) {
318
366
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
319
367
  }
320
368
  }
@@ -323,26 +371,187 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
323
371
  }
324
372
  return broker || status ? 'stale' : 'missing';
325
373
  }
374
+ const remaining = () => Math.max(0, deadlineAt - Date.now());
326
375
  signalOwner(owner, platform, false);
327
- if (await waitForExit(owner, timeoutMs)) {
328
- if (executions.some((execution) => executionAlive(execution, platform))) {
329
- throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
330
- }
376
+ for (const execution of executions) {
377
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
378
+ }
379
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
380
+ while (Date.now() < softDeadlineAt) {
381
+ if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
382
+ break;
383
+ await new Promise((resolve) => setTimeout(resolve, 25));
384
+ }
385
+ if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) {
331
386
  return 'stopped';
332
387
  }
333
388
  if (manifest)
334
389
  executions = mergeExecutions(executions, readExecutions(manifest));
390
+ if (Date.now() >= deadlineAt)
391
+ throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
335
392
  for (const execution of executions) {
336
- if (!terminateSandboxControlExecution(execution, { platform, timeoutMs })) {
337
- throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
338
- }
393
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now() });
339
394
  }
340
395
  signalOwner(owner, platform, true);
341
- if (!await waitForExit(owner, timeoutMs))
396
+ while (Date.now() < deadlineAt) {
397
+ if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
398
+ return 'stopped';
399
+ await new Promise((resolve) => setTimeout(resolve, 25));
400
+ }
401
+ if (ownerLive(owner))
342
402
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
343
- if (executions.some((execution) => executionAlive(execution, platform))) {
403
+ if (executions.some((execution) => executionAlive(execution, platform)))
344
404
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
345
- }
346
405
  return 'stopped';
347
406
  }
407
+ export async function removeSandboxControlRoot(root, options) {
408
+ const resolvedRoot = path.resolve(root);
409
+ if (!fs.existsSync(resolvedRoot))
410
+ return;
411
+ const stat = fs.lstatSync(resolvedRoot);
412
+ if (!stat.isDirectory() || stat.isSymbolicLink())
413
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
414
+ const manifestPath = path.join(resolvedRoot, 'manifest.json');
415
+ const manifest = readSandboxControlManifest(manifestPath);
416
+ const inspectContainer = options.inspectContainer
417
+ ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
418
+ const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
419
+ const deadlineAt = Date.now() + timeoutMs;
420
+ const forceAt = forceDeadline(deadlineAt, timeoutMs);
421
+ const remaining = () => Math.max(0, deadlineAt - Date.now());
422
+ const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
423
+ if (observation.state === 'unknown')
424
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
425
+ if (options.requireAbsent && observation.state !== 'absent') {
426
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
427
+ }
428
+ markSandboxControlRootQuiescing(resolvedRoot);
429
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
430
+ const brokerPath = path.join(resolvedRoot, 'broker.json');
431
+ const statusPath = path.join(resolvedRoot, 'public', 'status.json');
432
+ const broker = readBrokerOwner(brokerPath);
433
+ const status = readStatusOwner(statusPath);
434
+ if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
435
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
436
+ }
437
+ if (status && status.generation !== manifest.generation) {
438
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
439
+ }
440
+ if (broker && status && !sameOwner(broker, status.broker)
441
+ && (ownerLive(broker) || ownerLive(status.broker))) {
442
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
443
+ }
444
+ const platform = options.platform ?? process.platform;
445
+ const owner = broker && ownerLive(broker) ? broker
446
+ : status && ownerLive(status.broker) ? status.broker : null;
447
+ const selfOwner = options.selfOwner;
448
+ const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
449
+ if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
450
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
451
+ }
452
+ let executions = readExecutions(manifest);
453
+ if (owner && !selfOwned)
454
+ signalOwner(owner, platform, false);
455
+ for (const execution of executions) {
456
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
457
+ }
458
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
459
+ while (Date.now() < softDeadlineAt) {
460
+ if ((!owner || selfOwned || !ownerLive(owner)) && !executions.some((execution) => executionAlive(execution, platform)))
461
+ break;
462
+ await new Promise((resolve) => setTimeout(resolve, 25));
463
+ }
464
+ if (observation.state === 'found') {
465
+ if (Date.now() >= deadlineAt)
466
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
467
+ await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
468
+ const afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
469
+ if (afterRemoval.state !== 'absent') {
470
+ throw new Error(afterRemoval.state === 'unknown'
471
+ ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
472
+ : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
473
+ }
474
+ }
475
+ const currentManifest = readSandboxControlManifest(manifestPath);
476
+ if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
477
+ || !isSandboxControlRootQuiescing(resolvedRoot)) {
478
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
479
+ }
480
+ const currentBroker = readBrokerOwner(brokerPath);
481
+ if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
482
+ || currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
483
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
484
+ }
485
+ if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
486
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
487
+ }
488
+ if (!owner && currentBroker && ownerLive(currentBroker)) {
489
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
490
+ }
491
+ if (owner && ownerLive(owner) && !currentBroker) {
492
+ throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
493
+ }
494
+ executions = mergeExecutions(executions, readExecutions(manifest));
495
+ if (Date.now() >= deadlineAt)
496
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
497
+ for (const execution of executions) {
498
+ if (!terminateSandboxControlExecution(execution, {
499
+ platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now()
500
+ })) {
501
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
502
+ }
503
+ }
504
+ if (owner && currentBroker && ownerLive(currentBroker) && !selfOwned) {
505
+ if (remaining() <= 0)
506
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
507
+ signalOwner(currentBroker, platform, true);
508
+ if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt)) {
509
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
510
+ }
511
+ }
512
+ const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
513
+ if (finalExecutions.some((execution) => executionAlive(execution, platform))) {
514
+ throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
515
+ }
516
+ const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
517
+ if (finalBrokerRaw !== null) {
518
+ const finalBroker = readBrokerOwner(brokerPath);
519
+ if (!finalBroker)
520
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
521
+ const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
522
+ && finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
523
+ if (ownerLive(finalBroker) && !finalSelfOwner)
524
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
525
+ if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
526
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
527
+ }
528
+ fs.unlinkSync(brokerPath);
529
+ }
530
+ const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
531
+ if (finalStatusRaw !== null) {
532
+ const finalStatus = readStatusOwner(statusPath);
533
+ if (!finalStatus)
534
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
535
+ if (ownerLive(finalStatus.broker) && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
536
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
537
+ }
538
+ if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
539
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
540
+ }
541
+ fs.unlinkSync(statusPath);
542
+ }
543
+ if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
544
+ throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
545
+ }
546
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
547
+ }
548
+ export async function garbageCollectSandboxControlRoot(root, options = {}) {
549
+ await removeSandboxControlRoot(path.resolve(root), {
550
+ ...options,
551
+ requireAbsent: true,
552
+ removeContainer: async () => {
553
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
554
+ }
555
+ });
556
+ }
348
557
  //# sourceMappingURL=lifecycle.js.map
@@ -5,6 +5,13 @@ export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
5
5
  export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
6
6
  export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
7
7
  export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
8
+ export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
9
+ controlTickMs: 250,
10
+ parkedBindingInitialMs: 1_000,
11
+ slowCheckMs: 5_000,
12
+ containerHeartbeatMs: 5_000,
13
+ quiesceDeadlineMs: 7_000
14
+ });
8
15
  export class SandboxControlProtocolError extends Error {
9
16
  code;
10
17
  retryable;