@aviaratech/ai-delivery 0.3.6 → 0.3.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.
@@ -35,6 +35,8 @@ declare const ResourceSummarySchema: z.ZodObject<{
35
35
  sampleCount: z.ZodNumber;
36
36
  }, z.core.$strict>;
37
37
  export type VerificationResourceSummary = z.infer<typeof ResourceSummarySchema>;
38
+ /** Run a real filesystem-negative fixture between strict scans; the callback must clean it up before returning. */
39
+ export declare function withVerificationFilesystemFixture<T>(operation: () => T | Promise<T>): Promise<T>;
38
40
  /** @internal Durable transition evidence must precede changes to the existing writer slot. */
39
41
  export interface WorktreeTransitionWriterLeaseInput {
40
42
  repoRoot: string;
@@ -1,7 +1,7 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { createRequire } from 'node:module';
4
- import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, readFileSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
4
+ import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, mkdirSync, rmdirSync, renameSync, readFileSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
5
5
  import { basename, dirname, join, resolve, sep } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { z } from 'zod';
@@ -125,6 +125,254 @@ function assertResourceBounds(bounds) {
125
125
  if (process.platform === 'win32')
126
126
  throw new DeliveryError('Resource observation requires POSIX process support.');
127
127
  }
128
+ const OUTPUT_GATE_ENV = 'AI_DELIVERY_OUTPUT_OBSERVATION_GATE';
129
+ const GateOwnerSchema = z.strictObject({ pid: z.number().int().positive(), identity: z.string().min(1) });
130
+ const OutputGateSchema = z.strictObject({ directory: z.string().min(1), id: z.uuid(), owner: GateOwnerSchema });
131
+ const GateClaimSchema = z.strictObject({
132
+ gateId: z.uuid(),
133
+ id: z.uuid(),
134
+ kind: z.enum(['scan', 'fixture']),
135
+ owner: GateOwnerSchema,
136
+ });
137
+ function gateOwner() {
138
+ const owner = processSnapshot().get(process.pid);
139
+ if (!owner)
140
+ throw new DeliveryError('Filesystem observation gate owner is unavailable.');
141
+ return { pid: owner.pid, identity: owner.identity };
142
+ }
143
+ function assertGateDirectory(path) {
144
+ const metadata = lstatSync(path);
145
+ if (!metadata.isDirectory() || (metadata.mode & 0o077) !== 0 || realpathSync(path) !== path)
146
+ throw new DeliveryError('Filesystem observation gate directory is invalid.');
147
+ }
148
+ function readOutputGate(value) {
149
+ try {
150
+ const gate = OutputGateSchema.parse(JSON.parse(value));
151
+ assertOutputGate(gate);
152
+ return gate;
153
+ }
154
+ catch (error) {
155
+ throw new DeliveryError('Filesystem observation gate capability is invalid or stale: ' +
156
+ (error instanceof Error ? error.message : String(error)));
157
+ }
158
+ }
159
+ function gateOwnerAlive(owner, snapshot = processSnapshot()) {
160
+ const current = snapshot.get(owner.pid);
161
+ return current?.identity === owner.identity && !current.status.startsWith('Z');
162
+ }
163
+ function assertOutputGate(gate, abandoned = false) {
164
+ assertGateDirectory(gate.directory);
165
+ const manifest = OutputGateSchema.parse(JSON.parse(assertPrivateFile(join(gate.directory, 'gate.json')).toString('utf8')));
166
+ const snapshot = processSnapshot();
167
+ let actor = snapshot.get(process.pid);
168
+ const ancestors = new Set();
169
+ while (actor && actor.pid !== gate.owner.pid && !ancestors.has(actor.pid)) {
170
+ ancestors.add(actor.pid);
171
+ actor = snapshot.get(actor.ppid);
172
+ }
173
+ if (digestValue(manifest) !== digestValue(gate) ||
174
+ (abandoned
175
+ ? gateOwnerAlive(gate.owner, snapshot)
176
+ : !gateOwnerAlive(gate.owner, snapshot) || actor?.identity !== gate.owner.identity))
177
+ throw new DeliveryError('Filesystem observation gate ownership changed, was abandoned or is not an inherited ancestor.');
178
+ }
179
+ function readGateClaim(gate, path) {
180
+ const claim = GateClaimSchema.parse(JSON.parse(assertPrivateFile(path).toString('utf8')));
181
+ if (claim.gateId !== gate.id)
182
+ throw new DeliveryError('Filesystem observation gate claim belongs to another run.');
183
+ return claim;
184
+ }
185
+ function gateScanRequests(gate) {
186
+ const directory = join(gate.directory, 'requests');
187
+ assertGateDirectory(directory);
188
+ const requests = readdirSync(directory);
189
+ if (requests.length > 1024 || requests.some((name) => !/^[a-f0-9-]{36}\.json$/u.test(name)))
190
+ throw new DeliveryError('Filesystem observation gate scan requests are invalid.');
191
+ const live = [];
192
+ const snapshot = processSnapshot();
193
+ for (const name of requests) {
194
+ let claim;
195
+ try {
196
+ claim = readGateClaim(gate, join(directory, name));
197
+ }
198
+ catch (error) {
199
+ if (error.code === 'ENOENT')
200
+ continue;
201
+ throw error;
202
+ }
203
+ if (claim.kind !== 'scan' || name !== claim.id + '.json' || !gateOwnerAlive(claim.owner, snapshot))
204
+ throw new DeliveryError('Filesystem observation gate scan request was abandoned.');
205
+ live.push(name);
206
+ }
207
+ return live;
208
+ }
209
+ function removeGateClaim(gate, path, claim) {
210
+ if (digestValue(readGateClaim(gate, path)) !== digestValue(claim))
211
+ throw new DeliveryError('Filesystem observation gate claim changed before release.');
212
+ unlinkSync(path);
213
+ }
214
+ async function withOutputGateLock(gate, kind, operation, signal) {
215
+ if (!gate)
216
+ return operation();
217
+ assertOutputGate(gate);
218
+ const claim = { gateId: gate.id, id: randomUUID(), kind, owner: gateOwner() };
219
+ const request = kind === 'scan' ? join(gate.directory, 'requests', claim.id + '.json') : undefined;
220
+ const lock = join(gate.directory, 'held');
221
+ const holder = join(lock, 'owner.json');
222
+ if (request) {
223
+ const staging = join(gate.directory, claim.id + '.pending');
224
+ writePrivateJsonFileAtomically(staging, claim);
225
+ renameSync(staging, request);
226
+ }
227
+ let acquired;
228
+ let incompleteSince;
229
+ let fixtureWait;
230
+ const release = () => {
231
+ if (!acquired)
232
+ return;
233
+ const metadata = lstatSync(lock);
234
+ if (!metadata.isDirectory() || metadata.dev !== acquired.dev || metadata.ino !== acquired.ino)
235
+ throw new DeliveryError('Filesystem observation gate lock changed before release.');
236
+ if (readdirSync(lock).join(',') !== 'owner.json')
237
+ throw new DeliveryError('Filesystem observation gate lock has unrelated metadata.');
238
+ removeGateClaim(gate, holder, claim);
239
+ rmdirSync(lock);
240
+ acquired = undefined;
241
+ };
242
+ try {
243
+ for (;;) {
244
+ if (signal?.aborted)
245
+ throw new DeliveryError('Filesystem observation gate wait cancelled.');
246
+ assertOutputGate(gate);
247
+ const requests = gateScanRequests(gate);
248
+ if (kind === 'scan' || requests.length === 0) {
249
+ let created = false;
250
+ try {
251
+ mkdirSync(lock, { mode: 0o700 });
252
+ created = true;
253
+ }
254
+ catch (error) {
255
+ if (error.code !== 'EEXIST')
256
+ throw error;
257
+ }
258
+ if (created) {
259
+ const metadata = lstatSync(lock);
260
+ acquired = { dev: metadata.dev, ino: metadata.ino };
261
+ writePrivateJsonFileAtomically(holder, claim);
262
+ // A scan may queue between the fixture's request check and atomic lock acquisition.
263
+ if (kind === 'scan' || gateScanRequests(gate).length === 0)
264
+ return await operation();
265
+ release();
266
+ }
267
+ }
268
+ if (existsSync(lock)) {
269
+ try {
270
+ assertGateDirectory(lock);
271
+ const current = readGateClaim(gate, holder);
272
+ incompleteSince = undefined;
273
+ if (!gateOwnerAlive(current.owner))
274
+ throw new DeliveryError('Filesystem observation gate fixture or scanner was abandoned.');
275
+ if (kind === 'scan' && current.kind === 'fixture') {
276
+ if (fixtureWait?.claimId !== current.id)
277
+ fixtureWait = { claimId: current.id, since: Date.now() };
278
+ // A negative reader that blocks prevents strict output observation; terminate this command, never steal its live lease.
279
+ if (Date.now() - fixtureWait.since > 5000)
280
+ throw new DeliveryError('Filesystem-negative fixture did not release a waiting output scan within 5 seconds; reader or teardown is stalled.');
281
+ }
282
+ else
283
+ fixtureWait = undefined;
284
+ }
285
+ catch (error) {
286
+ if (error.code !== 'ENOENT')
287
+ throw error;
288
+ incompleteSince ??= Date.now();
289
+ // A concrete publication failure, never an elapsed fixture-work allowance or stale-lock takeover.
290
+ if (Date.now() - incompleteSince > 2000)
291
+ throw new DeliveryError('Filesystem observation gate owner publication is incomplete.');
292
+ }
293
+ }
294
+ await new Promise((resolve) => setTimeout(resolve, 100));
295
+ }
296
+ }
297
+ finally {
298
+ try {
299
+ release();
300
+ }
301
+ finally {
302
+ if (request)
303
+ removeGateClaim(gate, request, claim);
304
+ }
305
+ }
306
+ }
307
+ /** Run a real filesystem-negative fixture between strict scans; the callback must clean it up before returning. */
308
+ export async function withVerificationFilesystemFixture(operation) {
309
+ const capability = process.env[OUTPUT_GATE_ENV];
310
+ return withOutputGateLock(capability === undefined ? undefined : readOutputGate(capability), 'fixture', operation);
311
+ }
312
+ function retireOutputGate(gate, abandoned = false) {
313
+ assertOutputGate(gate, abandoned);
314
+ const requests = join(gate.directory, 'requests');
315
+ const held = join(gate.directory, 'held');
316
+ if (readdirSync(gate.directory).some((name) => !['gate.json', 'requests', 'held'].includes(name)))
317
+ throw new DeliveryError('Filesystem observation gate has unrelated or incomplete metadata.');
318
+ if (existsSync(held)) {
319
+ assertGateDirectory(held);
320
+ if (readdirSync(held).join(',') !== 'owner.json')
321
+ throw new DeliveryError('Filesystem observation gate lock has unrelated metadata.');
322
+ const claim = readGateClaim(gate, join(held, 'owner.json'));
323
+ if (gateOwnerAlive(claim.owner))
324
+ throw new DeliveryError('Filesystem observation gate still has a live holder.');
325
+ removeGateClaim(gate, join(held, 'owner.json'), claim);
326
+ rmdirSync(held);
327
+ }
328
+ for (const name of readdirSync(requests)) {
329
+ if (!/^[a-f0-9-]{36}\.json$/u.test(name))
330
+ throw new DeliveryError('Filesystem observation gate has unrelated metadata.');
331
+ const path = join(requests, name);
332
+ const claim = readGateClaim(gate, path);
333
+ if (claim.kind !== 'scan' || name !== claim.id + '.json')
334
+ throw new DeliveryError('Filesystem observation gate has unrelated scan metadata.');
335
+ if (gateOwnerAlive(claim.owner))
336
+ throw new DeliveryError('Filesystem observation gate still has a live scan request.');
337
+ removeGateClaim(gate, path, claim);
338
+ }
339
+ rmdirSync(requests);
340
+ if (digestValue(JSON.parse(assertPrivateFile(join(gate.directory, 'gate.json')).toString('utf8'))) !==
341
+ digestValue(gate))
342
+ throw new DeliveryError('Filesystem observation gate changed before retirement.');
343
+ unlinkSync(join(gate.directory, 'gate.json'));
344
+ rmdirSync(gate.directory);
345
+ }
346
+ async function withOutputObservationGate(root, enabled, operation) {
347
+ const inherited = process.env[OUTPUT_GATE_ENV];
348
+ if (inherited !== undefined)
349
+ return operation(readOutputGate(inherited));
350
+ if (!enabled)
351
+ return operation(undefined);
352
+ const id = randomUUID();
353
+ const namespace = join(gitCommonDir(root), 'ai-delivery', 'output-observation', worktreeDigest(root).slice(7));
354
+ ensurePrivateDirectoryDurably(namespace);
355
+ // The existing writer has recovered and confirmed old command quiescence before this operation.
356
+ for (const priorId of readdirSync(namespace)) {
357
+ z.uuid().parse(priorId);
358
+ const path = join(namespace, priorId);
359
+ assertGateDirectory(path);
360
+ const prior = OutputGateSchema.parse(JSON.parse(assertPrivateFile(join(path, 'gate.json')).toString('utf8')));
361
+ if (prior.id !== priorId || prior.directory !== path || gateOwnerAlive(prior.owner))
362
+ throw new DeliveryError('Prior filesystem observation gate is live or has conflicting ownership.');
363
+ retireOutputGate(prior, true);
364
+ }
365
+ const directory = join(namespace, id);
366
+ ensurePrivateDirectoryDurably(join(directory, 'requests'));
367
+ const gate = { directory, id, owner: gateOwner() };
368
+ writePrivateJsonFileAtomically(join(directory, 'gate.json'), gate);
369
+ try {
370
+ return await operation(gate);
371
+ }
372
+ finally {
373
+ retireOutputGate(gate);
374
+ }
375
+ }
128
376
  const OutputBaselineCheckpointSchema = z.strictObject({
129
377
  baselineId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
130
378
  files: z.array(z.tuple([z.string().min(1), z.number().int().nonnegative().safe()])),
@@ -187,7 +435,7 @@ function scanOutputRoots(roots) {
187
435
  }
188
436
  return files;
189
437
  }
190
- function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
438
+ async function outputBaseline(repoRoot, common, classificationReceiptId, bounds, gate, signal) {
191
439
  if (bounds.outputRoots === undefined)
192
440
  return undefined;
193
441
  const roots = bounds.outputRoots.map((path) => resolve(repoRoot, path)).sort();
@@ -236,7 +484,7 @@ function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
236
484
  let checkpoint;
237
485
  if (state?.status !== 'complete') {
238
486
  const content = {
239
- files: [...scanOutputRoots(roots)].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
487
+ files: [...(await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots), signal))].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
240
488
  inputId,
241
489
  roots,
242
490
  schemaVersion: 'ai-delivery.output-baseline@1',
@@ -261,11 +509,11 @@ function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
261
509
  checkpoint.files.some(([file], index) => (index > 0 && file <= checkpoint.files[index - 1][0]) ||
262
510
  !roots.some((root) => file === root || file.startsWith(`${root}${sep}`))))
263
511
  throw new DeliveryError('Filesystem output baseline checkpoint is corrupt or belongs to another input.');
264
- return { baselineId, roots, files: new Map(checkpoint.files) };
512
+ return { baselineId, roots, files: new Map(checkpoint.files), ...(gate === undefined ? {} : { gate }) };
265
513
  }
266
- function positiveNewOutputBytes(baseline) {
514
+ async function positiveNewOutputBytes(baseline, signal) {
267
515
  let bytes = 0;
268
- for (const [path, size] of scanOutputRoots(baseline.roots)) {
516
+ for (const [path, size] of await withOutputGateLock(baseline.gate, 'scan', () => scanOutputRoots(baseline.roots), signal)) {
269
517
  bytes += Math.max(0, size - (baseline.files.get(path) ?? 0));
270
518
  if (!Number.isSafeInteger(bytes))
271
519
  throw new DeliveryError('Filesystem output observation overflowed.');
@@ -382,7 +630,7 @@ function observeOwnedProcesses(state, snapshot) {
382
630
  state.sampled = true;
383
631
  return [...selected.values()];
384
632
  }
385
- function sampleOwnedTree(state, repoRoot, bounds, baseline) {
633
+ function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes) {
386
634
  const owned = observeOwnedProcesses(state, processSnapshot());
387
635
  const rssBytes = owned.reduce((total, member) => total + member.rssBytes, 0);
388
636
  if (!Number.isSafeInteger(rssBytes))
@@ -393,7 +641,6 @@ function sampleOwnedTree(state, repoRoot, bounds, baseline) {
393
641
  const available = freeDiskBytes(repoRoot, baseline);
394
642
  if (available < bounds.minFreeDiskBytes)
395
643
  throw new DeliveryError(`Free disk fell below limit ${bounds.minFreeDiskBytes}.`);
396
- const newOutputBytes = baseline === undefined ? undefined : positiveNewOutputBytes(baseline);
397
644
  if (newOutputBytes !== undefined && !Number.isSafeInteger(newOutputBytes)) {
398
645
  throw new DeliveryError('Filesystem output observation overflowed.');
399
646
  }
@@ -886,6 +1133,9 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
886
1133
  cwd: repoRoot,
887
1134
  detached: process.platform !== 'win32',
888
1135
  stdio: ['pipe', 'pipe', 'pipe'],
1136
+ ...(baseline?.gate === undefined
1137
+ ? {}
1138
+ : { env: { ...process.env, [OUTPUT_GATE_ENV]: JSON.stringify(baseline.gate) } }),
889
1139
  });
890
1140
  const stdout = [];
891
1141
  const stderr = [];
@@ -899,6 +1149,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
899
1149
  let closed = false;
900
1150
  let settled = false;
901
1151
  let commandReleased = false;
1152
+ let pendingOutput;
1153
+ const scanCancellation = new AbortController();
902
1154
  let closeTimer;
903
1155
  let exitPipeTimer;
904
1156
  let resourceTimer;
@@ -955,6 +1207,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
955
1207
  if (failure !== undefined)
956
1208
  return;
957
1209
  failure = reason;
1210
+ scanCancellation.abort();
958
1211
  killOwned();
959
1212
  if (!closed) {
960
1213
  closeTimer = setTimeout(() => {
@@ -1011,10 +1264,26 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1011
1264
  onResourceSample?.(result);
1012
1265
  onRunning?.(outputBytes, result);
1013
1266
  }
1014
- if (!commandReleased) {
1015
- commandReleased = true;
1016
- child.stdin.end('run\n');
1017
- }
1267
+ if (closed || pendingOutput !== undefined)
1268
+ return;
1269
+ pendingOutput = (async () => {
1270
+ if (resourceBounds !== undefined && baseline !== undefined) {
1271
+ const bytes = await positiveNewOutputBytes(baseline, scanCancellation.signal);
1272
+ if (failure !== undefined)
1273
+ return;
1274
+ const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, bytes);
1275
+ onResourceSample?.(result);
1276
+ onRunning?.(outputBytes, result);
1277
+ }
1278
+ if (failure === undefined && !commandReleased) {
1279
+ commandReleased = true;
1280
+ child.stdin.end('run\n');
1281
+ }
1282
+ })()
1283
+ .catch((error) => stop(error instanceof Error ? error.message : String(error)))
1284
+ .finally(() => {
1285
+ pendingOutput = undefined;
1286
+ });
1018
1287
  }
1019
1288
  catch (error) {
1020
1289
  stop(error instanceof Error ? error.message : String(error));
@@ -1038,11 +1307,11 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1038
1307
  closed = true;
1039
1308
  if (settled)
1040
1309
  return;
1041
- clearWatchers();
1310
+ await pendingOutput;
1042
1311
  if (failure === undefined && owned !== undefined) {
1043
1312
  try {
1044
1313
  if (resourceBounds !== undefined) {
1045
- const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline);
1314
+ const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, baseline === undefined ? undefined : await positiveNewOutputBytes(baseline, scanCancellation.signal));
1046
1315
  onResourceSample?.(sample);
1047
1316
  onRunning?.(outputBytes, sample);
1048
1317
  }
@@ -1085,18 +1354,20 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1085
1354
  resolve(Buffer.concat([...stdout, ...stderr], outputBytes));
1086
1355
  };
1087
1356
  child.once('close', (code, signal) => {
1088
- void handleClose(code, signal).catch((error) => {
1357
+ void handleClose(code, signal)
1358
+ .catch((error) => {
1089
1359
  settled = true;
1090
1360
  reject(error);
1091
- });
1361
+ })
1362
+ .finally(clearWatchers);
1092
1363
  });
1093
1364
  });
1094
1365
  }
1095
1366
  export async function verifyIssue(input) {
1096
1367
  const root = gitRoot(input.repoRoot);
1097
- return withVerificationWriter(root, (writer) => verifyIssueOwned(input, writer));
1368
+ return withVerificationWriter(root, (writer) => withOutputObservationGate(root, input.resourceBounds?.outputRoots !== undefined, (gate) => verifyIssueOwned(input, writer, gate)));
1098
1369
  }
1099
- async function verifyIssueOwned(input, writer) {
1370
+ async function verifyIssueOwned(input, writer, gate) {
1100
1371
  const assertNotCancelled = () => {
1101
1372
  if (input.signal?.aborted)
1102
1373
  throw new DeliveryError('Verification cancelled.');
@@ -1126,7 +1397,7 @@ async function verifyIssueOwned(input, writer) {
1126
1397
  });
1127
1398
  const baseline = input.resourceBounds === undefined
1128
1399
  ? undefined
1129
- : outputBaseline(root, common, classification.receiptId, input.resourceBounds);
1400
+ : await outputBaseline(root, common, classification.receiptId, input.resourceBounds, gate, input.signal);
1130
1401
  if (input.resourceBounds !== undefined)
1131
1402
  assertDiskHeadroom(root, input.resourceBounds, baseline);
1132
1403
  const receipts = [];
@@ -1279,6 +1550,9 @@ async function verifyIssueOwned(input, writer) {
1279
1550
  sampledFreeDiskBytes: sample.freeDiskBytes,
1280
1551
  ...(sample.newOutputBytes === undefined ? {} : { sampledNewOutputBytes: sample.newOutputBytes }),
1281
1552
  ownedProcessCount: sample.ownedProcessCount,
1553
+ ...(baseline !== undefined && sample.newOutputBytes === undefined
1554
+ ? { reason: 'waiting for strict filesystem output scan' }
1555
+ : {}),
1282
1556
  }),
1283
1557
  });
1284
1558
  progress(0);
@@ -1341,6 +1615,19 @@ async function verifyIssueOwned(input, writer) {
1341
1615
  });
1342
1616
  }
1343
1617
  assertNotCancelled();
1618
+ if (baseline !== undefined && input.resourceBounds !== undefined) {
1619
+ const bytes = await positiveNewOutputBytes(baseline, input.signal);
1620
+ if (bytes > input.resourceBounds.maxNewOutputBytes)
1621
+ throw new DeliveryError('Positive new filesystem output ' + bytes + ' exceeded limit ' + input.resourceBounds.maxNewOutputBytes + '.');
1622
+ assertDiskHeadroom(root, input.resourceBounds, baseline);
1623
+ recordResourceSample({
1624
+ aggregateRssBytes: 0,
1625
+ freeDiskBytes: freeDiskBytes(root, baseline),
1626
+ newOutputBytes: bytes,
1627
+ ownedProcessCount: 0,
1628
+ });
1629
+ }
1630
+ assertNotCancelled();
1344
1631
  assertRepositoryClassificationCurrent({
1345
1632
  classification,
1346
1633
  configDigest: loaded.configDigest,
@@ -1429,41 +1716,48 @@ export async function withRuntimeSetupWriter(root, operation) {
1429
1716
  if (roots[i] === roots[i - 1] || roots[i].startsWith(`${roots[i - 1]}${sep}`))
1430
1717
  throw new DeliveryError('Filesystem output roots must not overlap.');
1431
1718
  }
1432
- const files = roots === undefined ? undefined : scanOutputRoots(roots);
1433
- const baseline = roots === undefined || files === undefined
1434
- ? undefined
1435
- : {
1436
- roots,
1437
- files,
1438
- baselineId: digestValue([...files]),
1439
- };
1440
- assertDiskHeadroom(root, bounds, baseline);
1441
- const started = Date.now();
1442
- const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample) => reportVerificationProgress({
1443
- state: 'running',
1444
- stageId: 'runtime-setup',
1445
- completedStages: 0,
1446
- reusedStages: 0,
1447
- remainingStages: 1,
1448
- elapsedMs: Date.now() - started,
1449
- capturedOutputBytes,
1450
- ...(sample === undefined
1451
- ? {}
1719
+ return withOutputObservationGate(root, roots !== undefined, async (gate) => {
1720
+ const files = roots === undefined
1721
+ ? undefined
1722
+ : await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots), signal);
1723
+ const baseline = roots === undefined || files === undefined
1724
+ ? undefined
1452
1725
  : {
1453
- sampledAggregateRssBytes: sample.aggregateRssBytes,
1454
- sampledFreeDiskBytes: sample.freeDiskBytes,
1455
- sampledNewOutputBytes: sample.newOutputBytes,
1456
- }),
1457
- }), bounds, baseline, undefined, writer);
1458
- reportVerificationProgress({
1459
- state: 'completed',
1460
- stageId: 'runtime-setup',
1461
- completedStages: 1,
1462
- reusedStages: 0,
1463
- remainingStages: 0,
1464
- elapsedMs: Date.now() - started,
1726
+ roots,
1727
+ files,
1728
+ baselineId: digestValue([...files]),
1729
+ ...(gate === undefined ? {} : { gate }),
1730
+ };
1731
+ assertDiskHeadroom(root, bounds, baseline);
1732
+ const started = Date.now();
1733
+ const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample) => reportVerificationProgress({
1734
+ state: 'running',
1735
+ stageId: 'runtime-setup',
1736
+ completedStages: 0,
1737
+ reusedStages: 0,
1738
+ remainingStages: 1,
1739
+ elapsedMs: Date.now() - started,
1740
+ capturedOutputBytes,
1741
+ ...(sample === undefined
1742
+ ? {}
1743
+ : {
1744
+ sampledAggregateRssBytes: sample.aggregateRssBytes,
1745
+ sampledFreeDiskBytes: sample.freeDiskBytes,
1746
+ sampledNewOutputBytes: sample.newOutputBytes,
1747
+ }),
1748
+ }), bounds, baseline, undefined, writer);
1749
+ if (signal?.aborted)
1750
+ throw new DeliveryError('Runtime setup cancelled.');
1751
+ reportVerificationProgress({
1752
+ state: 'completed',
1753
+ stageId: 'runtime-setup',
1754
+ completedStages: 1,
1755
+ reusedStages: 0,
1756
+ remainingStages: 0,
1757
+ elapsedMs: Date.now() - started,
1758
+ });
1759
+ return output;
1465
1760
  });
1466
- return output;
1467
1761
  },
1468
1762
  }));
1469
1763
  }