@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
@@ -1,12 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
3
4
  import { getProcessStartTime } from '../../server/process-state.js';
4
5
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
5
- import { controlError, SANDBOX_CONTROL_STATUS_INTERVAL_MS } from './protocol.js';
6
+ import { controlError, DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
6
7
  import { prepareSandboxControlExecution } from './executor.js';
7
8
  import { appendSandboxControlAudit, atomicWriteJson, cleanupStaleSandboxControlLease, executionPath, readActiveLease, readExecution, terminateSandboxControlExecution, writeSandboxControlStatus } from './state.js';
8
9
  import { validateSandboxControlRequest } from './protocol.js';
9
- import { acquireSandboxControlBrokerStartup, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
10
+ import { inspectSandboxControlContainer } from './container-identity.js';
11
+ import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
12
+ import { nextSandboxControlBackoff } from './timing.js';
10
13
  function assertRealDirectory(directory, parent) {
11
14
  const stat = fs.lstatSync(directory);
12
15
  if (!stat.isDirectory() || stat.isSymbolicLink())
@@ -73,10 +76,12 @@ function bindingReason(manifest) {
73
76
  return 'SANDBOX_WORKTREE_BINDING_LOST';
74
77
  }
75
78
  }
76
- function recoverProcessing(manifest) {
79
+ function recoverProcessing(manifest, brokerOwns) {
77
80
  for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
78
81
  if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name))
79
82
  continue;
83
+ if (!brokerOwns())
84
+ return false;
80
85
  const descriptor = executionPath(manifest, entry.name);
81
86
  const existingResponse = responsePath(manifest, entry.name);
82
87
  if (fs.existsSync(descriptor)) {
@@ -84,6 +89,8 @@ function recoverProcessing(manifest) {
84
89
  if (!terminateSandboxControlExecution(execution)) {
85
90
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
86
91
  }
92
+ if (!brokerOwns())
93
+ return false;
87
94
  appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
88
95
  let terminal = false;
89
96
  if (fs.existsSync(existingResponse)) {
@@ -96,14 +103,22 @@ function recoverProcessing(manifest) {
96
103
  terminal = false;
97
104
  }
98
105
  }
99
- if (!terminal)
106
+ if (!terminal) {
107
+ if (!brokerOwns())
108
+ return false;
100
109
  writeResponse(manifest, unknown(entry.name));
110
+ }
101
111
  }
102
112
  else {
113
+ if (!brokerOwns())
114
+ return false;
103
115
  writeResponse(manifest, notExecuted(entry.name));
104
116
  }
117
+ if (!brokerOwns())
118
+ return false;
105
119
  fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
106
120
  }
121
+ return true;
107
122
  }
108
123
  function delay(ms) {
109
124
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -111,19 +126,38 @@ function delay(ms) {
111
126
  export function sandboxControlSafeEnv(env = process.env) {
112
127
  return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
113
128
  }
114
- export async function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
129
+ export async function serveSandboxControl(manifestPath, signal = new AbortController().signal, options = {}) {
130
+ const timing = options.timing ?? DEFAULT_SANDBOX_CONTROL_TIMING;
131
+ const inspectContainer = options.inspectContainer ?? ((value) => inspectSandboxControlContainer(value));
132
+ const bindingCheck = options.bindingCheck ?? bindingReason;
115
133
  const manifest = readSandboxControlManifest(manifestPath);
116
134
  const root = path.dirname(manifestPath);
117
135
  const startTime = getProcessStartTime(process.pid);
118
136
  if (!startTime)
119
137
  throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
120
- const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime });
138
+ const brokerId = randomUUID();
139
+ const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime, brokerId });
121
140
  const requestsDir = path.join(manifest.channelDir, 'requests');
122
141
  const responsesDir = path.join(manifest.channelDir, 'responses');
123
142
  const consumedDir = path.join(root, 'consumed');
124
- const broker = { pid: process.pid, startTime };
143
+ const broker = {
144
+ version: 3,
145
+ pid: process.pid,
146
+ startTime,
147
+ brokerId,
148
+ token: manifest.token,
149
+ generation: manifest.generation
150
+ };
125
151
  const brokerPath = path.join(root, 'broker.json');
126
- const brokerRecord = `${JSON.stringify({ version: 2, ...broker, token: manifest.token, generation: manifest.generation })}\n`;
152
+ const brokerRecord = `${JSON.stringify(broker)}\n`;
153
+ const brokerOwns = () => {
154
+ try {
155
+ return fs.readFileSync(brokerPath, 'utf8') === brokerRecord;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ };
127
161
  try {
128
162
  for (const directory of [manifest.channelDir, requestsDir, responsesDir, consumedDir, manifest.publicStatusDir, manifest.processingDir]) {
129
163
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -147,11 +181,24 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
147
181
  let active = null;
148
182
  let lastState = '';
149
183
  let lastStatusAt = 0;
184
+ let nextBindingCheckAt = 0;
185
+ let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
186
+ let containerReasonCode = null;
187
+ let containerBackoffMs = timing.parkedBindingInitialMs;
188
+ let bindingReasonCode = null;
189
+ let bindingBackoffMs = timing.parkedBindingInitialMs;
150
190
  try {
191
+ if (!brokerOwns())
192
+ return;
151
193
  writeSandboxControlStatus(manifest, broker, 'starting', null, null);
194
+ if (!brokerOwns())
195
+ return;
152
196
  appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
153
- recoverProcessing(manifest);
197
+ if (!recoverProcessing(manifest, brokerOwns))
198
+ return;
154
199
  while (!signal.aborted) {
200
+ if (!brokerOwns())
201
+ break;
155
202
  let current;
156
203
  try {
157
204
  current = readSandboxControlManifest(manifestPath);
@@ -161,22 +208,78 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
161
208
  }
162
209
  if (current.token !== manifest.token || current.generation !== manifest.generation)
163
210
  break;
211
+ const heartbeatNow = Date.now();
212
+ if (heartbeatNow >= nextContainerHeartbeatAt) {
213
+ let observation;
214
+ try {
215
+ observation = await inspectContainer(manifest);
216
+ }
217
+ catch (error) {
218
+ observation = { state: 'unknown', reason: controlError(error).code };
219
+ }
220
+ if (!brokerOwns())
221
+ break;
222
+ if (observation.state === 'found') {
223
+ containerReasonCode = null;
224
+ containerBackoffMs = timing.parkedBindingInitialMs;
225
+ nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
226
+ }
227
+ else if (observation.state === 'unknown') {
228
+ containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_UNKNOWN';
229
+ appendSandboxControlAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
230
+ const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
231
+ nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
232
+ containerBackoffMs = backoff.nextDelayMs;
233
+ }
234
+ else {
235
+ try {
236
+ await garbageCollectSandboxControlRoot(root, {
237
+ timeoutMs: timing.quiesceDeadlineMs,
238
+ inspectContainer: (timeoutMs) => inspectContainer(manifest),
239
+ selfOwner: broker
240
+ });
241
+ active = null;
242
+ return;
243
+ }
244
+ catch (error) {
245
+ if (!brokerOwns())
246
+ break;
247
+ containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_ABSENT_GC_FAILED';
248
+ appendSandboxControlAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
249
+ const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
250
+ nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
251
+ containerBackoffMs = backoff.nextDelayMs;
252
+ }
253
+ }
254
+ }
164
255
  if (active?.settled) {
256
+ if (!brokerOwns())
257
+ break;
165
258
  if (active.result) {
259
+ if (!brokerOwns())
260
+ break;
166
261
  writeResponse(manifest, {
167
262
  version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
168
263
  stdout: active.result.stdout, stderr: active.result.stderr, error: null
169
264
  });
170
265
  }
171
266
  else {
267
+ if (!brokerOwns())
268
+ break;
172
269
  writeResponse(manifest, unknown(active.request.id));
173
270
  }
271
+ if (!brokerOwns())
272
+ break;
174
273
  fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
175
274
  active = null;
176
275
  }
177
276
  let reasonCode = null;
178
277
  try {
278
+ if (!brokerOwns())
279
+ break;
179
280
  if (cleanupStaleSandboxControlLease(manifest)) {
281
+ if (!brokerOwns())
282
+ break;
180
283
  appendSandboxControlAudit(manifest, 'lease-stale-cleanup');
181
284
  }
182
285
  if (readActiveLease(manifest))
@@ -185,18 +288,32 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
185
288
  catch {
186
289
  reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
187
290
  }
188
- reasonCode ??= bindingReason(manifest);
291
+ const now = Date.now();
292
+ if (now >= nextBindingCheckAt) {
293
+ bindingReasonCode = bindingCheck(manifest);
294
+ if (bindingReasonCode) {
295
+ const backoff = nextSandboxControlBackoff(bindingBackoffMs, timing.slowCheckMs);
296
+ nextBindingCheckAt = now + backoff.delayMs;
297
+ bindingBackoffMs = backoff.nextDelayMs;
298
+ }
299
+ else {
300
+ nextBindingCheckAt = now + timing.slowCheckMs;
301
+ bindingBackoffMs = timing.parkedBindingInitialMs;
302
+ }
303
+ }
304
+ reasonCode ??= containerReasonCode;
305
+ reasonCode ??= bindingReasonCode;
189
306
  const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
190
307
  const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
191
- const now = Date.now();
192
- if (stateKey !== lastState || now - lastStatusAt >= SANDBOX_CONTROL_STATUS_INTERVAL_MS) {
308
+ if (brokerOwns() && (stateKey !== lastState || now - lastStatusAt >= timing.controlTickMs)) {
193
309
  writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now);
194
310
  lastStatusAt = now;
195
311
  }
196
- if (stateKey !== lastState) {
312
+ if (brokerOwns() && stateKey !== lastState) {
197
313
  appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
198
314
  lastState = stateKey;
199
315
  }
316
+ let retiring = false;
200
317
  for (const name of fs.readdirSync(requestsDir).sort()) {
201
318
  if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
202
319
  continue;
@@ -204,14 +321,26 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
204
321
  const source = path.join(requestsDir, name);
205
322
  let claimed = null;
206
323
  try {
324
+ if (!brokerOwns()) {
325
+ retiring = true;
326
+ break;
327
+ }
207
328
  claimed = claimRequest(manifest, source, id);
329
+ if (!brokerOwns()) {
330
+ retiring = true;
331
+ break;
332
+ }
208
333
  consumeRequest(consumedDir, id);
334
+ if (!brokerOwns()) {
335
+ retiring = true;
336
+ break;
337
+ }
209
338
  if (reasonCode)
210
339
  throw new Error(reasonCode);
211
340
  if (active)
212
341
  throw new Error('SANDBOX_CONTROL_BUSY');
213
342
  const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
214
- if (bindingReason(manifest))
343
+ if (bindingCheck(manifest))
215
344
  throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
216
345
  if (readActiveLease(manifest))
217
346
  throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
@@ -221,43 +350,90 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
221
350
  const execution = { request, prepared, result: null, failure: null, settled: false };
222
351
  active = execution;
223
352
  prepared.completion.then((result) => { execution.result = result; execution.settled = true; }, (error) => { execution.failure = error; execution.settled = true; });
353
+ if (!brokerOwns()) {
354
+ prepared.terminate(false);
355
+ active = null;
356
+ retiring = true;
357
+ continue;
358
+ }
359
+ if (!brokerOwns()) {
360
+ prepared.terminate(false);
361
+ active = null;
362
+ retiring = true;
363
+ continue;
364
+ }
224
365
  writeResponse(manifest, {
225
366
  version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
226
367
  });
368
+ if (!brokerOwns()) {
369
+ prepared.terminate(false);
370
+ active = null;
371
+ retiring = true;
372
+ break;
373
+ }
227
374
  try {
228
- prepared.start();
375
+ prepared.start(brokerOwns);
229
376
  }
230
377
  catch (error) {
231
- prepared.terminate();
378
+ const owned = brokerOwns();
379
+ prepared.terminate(owned);
380
+ if (!owned || !brokerOwns()) {
381
+ active = null;
382
+ retiring = true;
383
+ break;
384
+ }
232
385
  writeResponse(manifest, unknown(id));
386
+ if (!brokerOwns()) {
387
+ active = null;
388
+ retiring = true;
389
+ break;
390
+ }
233
391
  fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
234
392
  active = null;
235
393
  claimed = null;
394
+ if (!brokerOwns()) {
395
+ retiring = true;
396
+ break;
397
+ }
236
398
  appendSandboxControlAudit(manifest, 'executor-gate-failed', { requestId: id });
237
399
  continue;
238
400
  }
239
401
  }
240
402
  catch (error) {
403
+ if (!brokerOwns()) {
404
+ retiring = true;
405
+ break;
406
+ }
241
407
  writeResponse(manifest, rejected(id, error));
408
+ if (!brokerOwns()) {
409
+ retiring = true;
410
+ break;
411
+ }
242
412
  if (claimed)
243
413
  fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
244
414
  }
245
415
  }
246
- await delay(25);
416
+ if (retiring || !brokerOwns())
417
+ break;
418
+ await delay(timing.controlTickMs);
247
419
  }
248
420
  }
249
421
  finally {
250
422
  if (active) {
251
- active.prepared.terminate();
252
- writeResponse(manifest, unknown(active.request.id));
423
+ const owned = brokerOwns();
424
+ active.prepared.terminate(owned);
425
+ if (owned && brokerOwns())
426
+ writeResponse(manifest, unknown(active.request.id));
253
427
  }
254
- appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid });
255
- try {
256
- if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
257
- fs.unlinkSync(brokerPath);
258
- }
259
- catch {
260
- // A newer owner or recovery path owns the record.
428
+ if (brokerOwns()) {
429
+ appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
430
+ try {
431
+ if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
432
+ fs.unlinkSync(brokerPath);
433
+ }
434
+ catch {
435
+ // A newer owner or recovery path owns the record.
436
+ }
261
437
  }
262
438
  }
263
439
  }
@@ -32,15 +32,17 @@ export function statusPath(manifest) {
32
32
  }
33
33
  export function writeSandboxControlStatus(manifest, broker, state, reasonCode, activeRequestId, now = Date.now()) {
34
34
  const status = {
35
- version: 1, generation: manifest.generation, broker, state, reasonCode, activeRequestId, updatedAt: now
35
+ version: 2, generation: manifest.generation, broker, state, reasonCode, activeRequestId, updatedAt: now
36
36
  };
37
37
  atomicWriteJson(statusPath(manifest), status, 0o600, false);
38
38
  return status;
39
39
  }
40
40
  export function parseSandboxControlStatus(value) {
41
41
  const status = value;
42
- if (!status || status.version !== 1 || typeof status.generation !== 'string'
43
- || !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== 'string'
42
+ if (!status || status.version !== 2 || typeof status.generation !== 'string'
43
+ || !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== 'number'
44
+ || !Number.isSafeInteger(status.broker.startTime) || typeof status.broker.brokerId !== 'string'
45
+ || status.broker.brokerId.length === 0
44
46
  || !['starting', 'healthy', 'busy', 'parked'].includes(status.state ?? '')
45
47
  || (status.reasonCode !== null && typeof status.reasonCode !== 'string')
46
48
  || (status.activeRequestId !== null && typeof status.activeRequestId !== 'string')
@@ -56,9 +58,10 @@ export function readActiveLease(manifest, now = Date.now()) {
56
58
  if (!fs.existsSync(filePath))
57
59
  return null;
58
60
  const lease = readJsonFile(filePath);
59
- if (!lease || lease.version !== 1
61
+ if (!lease || lease.version !== 2
60
62
  || typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
61
- || typeof lease.owner.startTime !== 'string' || !Number.isSafeInteger(lease.issuedAt)
63
+ || typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
64
+ || !Number.isSafeInteger(lease.issuedAt)
62
65
  || !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
63
66
  || (lease.taskId !== null && typeof lease.taskId !== 'string')
64
67
  || typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
@@ -75,9 +78,10 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
75
78
  return false;
76
79
  const raw = fs.readFileSync(filePath, 'utf8');
77
80
  const lease = JSON.parse(raw);
78
- if (!lease || lease.version !== 1
81
+ if (!lease || lease.version !== 2
79
82
  || typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
80
- || typeof lease.owner.startTime !== 'string' || !Number.isSafeInteger(lease.issuedAt)
83
+ || typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
84
+ || !Number.isSafeInteger(lease.issuedAt)
81
85
  || !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
82
86
  || (lease.taskId !== null && typeof lease.taskId !== 'string')
83
87
  || typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
@@ -85,7 +89,7 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
85
89
  }
86
90
  const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
87
91
  || lease.branch !== manifest.branch || lease.expiresAt <= now || !processIdentityMatches({
88
- version: 1, pid: lease.owner.pid, startTime: lease.owner.startTime
92
+ pid: lease.owner.pid, startTime: lease.owner.startTime
89
93
  });
90
94
  if (!stale)
91
95
  return false;
@@ -109,9 +113,10 @@ export function executionPath(manifest, requestId) {
109
113
  }
110
114
  export function readExecution(filePath) {
111
115
  const execution = readJsonFile(filePath);
112
- if (!execution || execution.version !== 1 || typeof execution.generation !== 'string'
116
+ if (!execution || execution.version !== 2 || typeof execution.generation !== 'string'
113
117
  || typeof execution.requestId !== 'string' || typeof execution.nonce !== 'string'
114
- || !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== 'string'
118
+ || !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== 'number'
119
+ || !Number.isSafeInteger(execution.child.startTime)
115
120
  || !['prepared', 'running', 'terminating'].includes(execution.phase ?? '')
116
121
  || !Number.isSafeInteger(execution.updatedAt))
117
122
  throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
@@ -140,7 +145,7 @@ export function terminateSandboxControlExecution(execution, options = {}) {
140
145
  };
141
146
  const treeAlive = () => {
142
147
  if (platform === 'win32' || !processGroupId) {
143
- return processIdentityMatches({ version: 1, pid: execution.child.pid, startTime: execution.child.startTime });
148
+ return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
144
149
  }
145
150
  return processGroupAlive(processGroupId);
146
151
  };
@@ -156,20 +161,25 @@ export function terminateSandboxControlExecution(execution, options = {}) {
156
161
  catch {
157
162
  // The identity check below distinguishes an already-exited tree from a failed termination.
158
163
  }
159
- const deadline = Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS);
160
- while (Date.now() < deadline) {
164
+ const deadline = options.deadlineAt ?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
165
+ const forceAt = options.forceAt ?? (options.deadlineAt === undefined
166
+ ? deadline
167
+ : Math.min(deadline, Date.now() + Math.floor(Math.max(0, deadline - Date.now()) / 2)));
168
+ while (Date.now() < forceAt) {
161
169
  if (!treeAlive())
162
170
  return true;
163
171
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
164
172
  }
165
- if (platform !== 'win32') {
173
+ if (platform !== 'win32' && options.allowForce !== false
174
+ && (options.deadlineAt === undefined || Date.now() < deadline)) {
166
175
  try {
167
176
  process.kill(processGroupId ? -processGroupId : execution.child.pid, 'SIGKILL');
168
177
  }
169
178
  catch {
170
179
  // The final identity check handles an already-exited tree.
171
180
  }
172
- const killDeadline = Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS);
181
+ const killDeadline = options.deadlineAt
182
+ ?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
173
183
  while (Date.now() < killDeadline) {
174
184
  if (!treeAlive())
175
185
  return true;
@@ -0,0 +1,8 @@
1
+ export function nextSandboxControlBackoff(currentDelayMs, capMs) {
2
+ const delayMs = Math.min(currentDelayMs, capMs);
3
+ return {
4
+ delayMs,
5
+ nextDelayMs: Math.min(capMs, delayMs * 2)
6
+ };
7
+ }
8
+ //# sourceMappingURL=timing.js.map
@@ -1,19 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
3
- import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
4
3
  export function sandboxCoreBindMounts(config, branch, overrides) {
5
4
  const taskBound = Boolean(overrides.taskSource && overrides.taskId);
6
- const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
7
- ? {
8
- hostPaths: [path.join(hostPath, '.short-ids.json')],
9
- containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
10
- readOnly: true
11
- }
12
- : {
13
- hostPaths: [hostPath],
14
- containerPath: path.posix.join('/workspace/.agents/workspace', state),
15
- readOnly: true
16
- });
17
5
  const mounts = [
18
6
  {
19
7
  hostPaths: overrides.worktree
@@ -22,7 +10,11 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
22
10
  containerPath: '/workspace',
23
11
  readOnly: false
24
12
  },
25
- ...workspaceMounts,
13
+ {
14
+ hostPaths: [overrides.workspaceViewRoot],
15
+ containerPath: '/workspace/.agents/workspace',
16
+ readOnly: true
17
+ },
26
18
  ];
27
19
  if (taskBound) {
28
20
  mounts.push({