@agent-delivery-harness/cli 0.8.1 → 0.8.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-delivery-harness/cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "Delivery harness CLI: admit, prepare, review-context, submit-evidence, gate, record, verify, check",
6
6
  "license": "FSL-1.1-ALv2",
@@ -13,7 +13,7 @@
13
13
  "node": ">=22.6.0"
14
14
  },
15
15
  "dependencies": {
16
- "@agent-delivery-harness/kernel": "0.8.1"
16
+ "@agent-delivery-harness/kernel": "0.8.2"
17
17
  },
18
18
  "exports": {
19
19
  ".": "./src/index.ts"
@@ -1,6 +1,6 @@
1
1
  /** Invocation-scoped execution coordinator; durable truth remains per-check evidence. */
2
2
  import { randomUUID } from "node:crypto";
3
- import { access, mkdir, readFile, realpath, rm, stat } from "node:fs/promises";
3
+ import { access, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { deliveryRecordPathFor, recordOutputProvider, computeDeliverableIdentity, capturePhysicalSource, captureCheckBindings, captureCheckOutputSnapshots, candidateTreeEvidenceReader, candidateTreeSourceReader, createExecPort, digestCanonical, resolveRecordStorage, runGitCommand, scopedCheckIdentity, selectScopedCheckAttempt, sha256Hex, submitManifest,
6
6
  createResourceApplicabilityProjection, matchResourceApplicabilityProjection, createResourceApplicabilityBinding, reconstructResourceApplicability, discoverRecords, portableArtifactContents, isRecordFreshForCandidate, isRecordNeutralPath, type ResourceApplicabilityBinding, type InstalledDataAttemptOrigin, type InstalledDataResourceRequest, normalizeInstalledDataResourceRequests, type InstalledDataResourceContext, type InstalledDataResourceCapture,
@@ -60,11 +60,25 @@ function oneAtATime() {
60
60
  let tail: Promise<unknown> = Promise.resolve();
61
61
  return <T>(task: () => Promise<T>): Promise<T> => { const result = tail.then(task); tail = result.catch(() => undefined); return result; };
62
62
  }
63
+ /** Runs the tasks it is given one at a time; of those waiting, the highest `priority` goes next, ties in call order. */
64
+ function highestFirst() {
65
+ const waiting: { priority: number; start: () => void }[] = [];
66
+ let busy = false;
67
+ const next = () => {
68
+ if (busy || waiting.length === 0) return;
69
+ const index = waiting.reduce((best, entry, i) => entry.priority > waiting[best]!.priority ? i : best, 0);
70
+ busy = true; waiting.splice(index, 1)[0]!.start();
71
+ };
72
+ return <T>(priority: number, task: () => Promise<T>): Promise<T> => new Promise<T>((resolve, reject) => {
73
+ waiting.push({ priority, start: () => { task().then(resolve, reject).finally(() => { busy = false; next(); }); } });
74
+ next();
75
+ });
76
+ }
63
77
  type AcquiredSetup = Omit<ScopedSetupDiagnostic, "verification" | "phases">;
64
78
  /** A fail-fast gate's hold on one check: the signal that cancels it, and the check whose failure did. */
65
79
  interface Cancellation { readonly signal: AbortSignal; readonly after: () => string }
66
80
  /** One worker's view while it runs a check: the snapshots only it uses, by setup key, and where its lines go. */
67
- interface Lane { readonly snapshots: Map<string, CheckSnapshot>; readonly write: (text: string) => void }
81
+ interface Lane { readonly snapshots: Map<string, CheckSnapshot>; readonly write: (text: string) => void; readonly priority?: number }
68
82
  export class ScopedChecks {
69
83
  /** `scopedExecution.concurrency` workers, each owning its snapshots; the first is the serial lane. */
70
84
  private readonly workers: Map<string, CheckSnapshot>[];
@@ -82,6 +96,16 @@ export class ScopedChecks {
82
96
  /** With more than one worker: one read-only warm base per setup key, which no command runs in. Each
83
97
  * worker takes a copy-on-write clone of it; without cloning each worker builds its own, as before. */
84
98
  private readonly bases = new Map<string, Promise<CheckSnapshot>>();
99
+ /** Each base's settlement, after which its entry is current: a failed setup has left `bases`. */
100
+ private readonly settling = new WeakMap<Promise<CheckSnapshot>, Promise<void>>();
101
+ private readonly settled = new WeakSet<Promise<CheckSnapshot>>();
102
+ /** Stops the bases `prewarm` started, which no attempt owns, once no check can need one still setting up. */
103
+ private readonly prewarming = new AbortController();
104
+ /** Prewarmed bases not yet taken as a worker's own tree, which a path-bound one becomes. */
105
+ private readonly prewarmedBases = new WeakSet<Promise<CheckSnapshot>>();
106
+ /** Concurrent copy-on-write clones of one large tree each take several times as long as one alone, so
107
+ * they run one at a time, the check expected to run longest first. */
108
+ private readonly cloningOne = highestFirst();
85
109
  private cloning?: Promise<CloneMethod | undefined>;
86
110
  /** Profiles whose checks a tree has run and verified since it was made: their outputs may be in it. */
87
111
  private readonly ranOutputs = new WeakMap<CheckSnapshot, Set<string>>();
@@ -234,14 +258,22 @@ export class ScopedChecks {
234
258
  * duration less that attempt's own dependency setup; never-timed checks follow timed ones and ties keep
235
259
  * declaration order. Checks then run one dependency setup at a time, in the order each setup first
236
260
  * appears, so each setup installs once. */
261
+ /** A check's most recent passed or failed attempt's duration less that attempt's own dependency setup, any
262
+ * wait for another's warm base, and its clone with that clone's queue; undefined for a never-timed check. */
263
+ private async lastCost(id: string): Promise<number | undefined> {
264
+ const last = (await this.stores.get(id)!.read()).filter(r => r.attempt.status === "passed" || r.attempt.status === "failed").at(-1)?.payload;
265
+ const waited = last?.diagnostic?.setup?.phases.filter(p => p.phase === "warm-base-wait" || p.phase === "clone").reduce((sum, p) => sum + p.durationMs, 0) ?? 0;
266
+ return last?.durationMs === undefined ? undefined : last.durationMs - (last.diagnostic?.dependency?.durationMs ?? 0) - waited;
267
+ }
268
+ /** How long each check is expected to run: its last cost, or, never timed, its declared `timeoutMs`. */
269
+ private async expectedCosts(ids: readonly string[]): Promise<Map<string, number>> {
270
+ const costs = new Map<string, number>();
271
+ for (const id of ids) costs.set(id, await this.lastCost(id) ?? this.context.config.providers.find(p => p.id === id)?.check?.timeoutMs ?? 0);
272
+ return costs;
273
+ }
237
274
  private async cheapestFirst(ids: readonly string[]): Promise<string[]> {
238
275
  const cost = new Map<string, number>();
239
- for (const id of ids) {
240
- const last = (await this.stores.get(id)!.read()).filter(r => r.attempt.status === "passed" || r.attempt.status === "failed").at(-1)?.payload;
241
- // A check that waited on another's warm base did not pay that setup either.
242
- const waited = last?.diagnostic?.setup?.phases.filter(p => p.phase === "warm-base-wait").reduce((sum, p) => sum + p.durationMs, 0) ?? 0;
243
- cost.set(id, last?.durationMs === undefined ? Infinity : last.durationMs - (last.diagnostic?.dependency?.durationMs ?? 0) - waited);
244
- }
276
+ for (const id of ids) cost.set(id, await this.lastCost(id) ?? Infinity);
245
277
  const [first, ...rest] = ids;
246
278
  if (first === undefined) return [];
247
279
  const byCost = [first, ...rest.sort((a, b) => cost.get(a)! === cost.get(b)! ? 0 : cost.get(a)! < cost.get(b)! ? -1 : 1)];
@@ -253,21 +285,68 @@ export class ScopedChecks {
253
285
  * snapshots, so two checks never share one at once. A worker prints a check's lines together when it
254
286
  * finishes; a single worker prints them as they come. After a check throws no new id starts, the running
255
287
  * ones finish, and the first failure in `ids` order is thrown.
288
+ *
289
+ * A worker takes the first remaining id whose warm base is not still setting up, so no check's setup waits
290
+ * on a base being built; while every remaining id's base is, the worker waits for one to settle before
291
+ * taking a check. Once `halted`, the remaining ids are taken in order without waiting. With `expected`, a
292
+ * worker takes the startable id expected to run longest instead, and its clone queues by that cost, so
293
+ * the critical path gets its base, clone and worker first.
256
294
  */
257
- private async pool(ids: readonly string[], run: (id: string, lane: Lane) => Promise<void>): Promise<void> {
258
- const failures = new Map<string, unknown>();
259
- let next = 0;
260
- await Promise.all(this.workers.map(async snapshots => {
261
- while (next < ids.length && failures.size === 0) {
262
- const id = ids[next++]!, lines: string[] = [];
263
- try { await run(id, this.concurrency === 1 ? this.serial : { snapshots, write: text => { lines.push(text); } }); }
264
- catch (error) { failures.set(id, error); }
265
- finally { for (const line of lines) this.context.write(line); }
295
+ private async pool(ids: readonly string[], run: (id: string, lane: Lane) => Promise<void>, halted: () => boolean = () => false, expected?: ReadonlyMap<string, number>): Promise<void> {
296
+ const failures = new Map<string, unknown>(), remaining = [...ids];
297
+ const sleepers = new Set<() => void>();
298
+ const woken = () => new Promise<void>(resolve => { sleepers.add(resolve); });
299
+ const waker = () => { for (const sleeper of [...sleepers]) sleeper(); sleepers.clear(); };
300
+ this.poolWakers.add(waker);
301
+ const take = async (): Promise<string | undefined> => {
302
+ for (;;) {
303
+ if (failures.size > 0 || remaining.length === 0) return undefined;
304
+ const index = halted() ? 0 : remaining.reduce((best, id, i) => !this.startable(id) ? best
305
+ : best < 0 || (expected?.get(id) ?? 0) > (expected?.get(remaining[best]!) ?? 0) ? i : best, -1);
306
+ if (index >= 0) { const id = remaining.splice(index, 1)[0]!; this.claim(id); return id; }
307
+ await Promise.race([woken(), ...remaining.flatMap(id => { const key = this.keyOf(id), base = this.bases.get(key); return base ? [this.settling.get(base)!] : this.claims.has(key) ? [this.claims.get(key)!.released] : []; })]);
266
308
  }
267
- }));
309
+ };
310
+ try {
311
+ await Promise.all(this.workers.map(async snapshots => {
312
+ for (let id = await take(); id !== undefined; id = await take()) {
313
+ const lines: string[] = [];
314
+ try { await run(id, this.concurrency === 1 ? this.serial : { snapshots, write: text => { lines.push(text); }, priority: expected?.get(id) ?? 0 }); }
315
+ catch (error) { failures.set(id, error); this.wakePools(); }
316
+ finally { this.release(this.keyOf(id), id); for (const line of lines) this.context.write(line); }
317
+ }
318
+ }));
319
+ } finally { this.poolWakers.delete(waker); }
268
320
  const failed = ids.find(id => failures.has(id));
269
321
  if (failed !== undefined) throw failures.get(failed);
270
322
  }
323
+ private readonly poolWakers = new Set<() => void>();
324
+ private wakePools(): void { for (const wake of [...this.poolWakers]) wake(); }
325
+ private keyOf(id: string): string {
326
+ return setupKey(this.profileOf(id), this.context.config.providers.find(p => p.id === id)?.check?.physicalSource);
327
+ }
328
+ /** A base for this id's setup key whose setup has settled, or no base and no check about to set one up. */
329
+ private startable(id: string): boolean {
330
+ const key = this.keyOf(id), base = this.bases.get(key);
331
+ return base === undefined ? !this.claims.has(key) : this.settled.has(base);
332
+ }
333
+ /** While bases are shared, a check taken for a key with no base holds the key until its setup has started
334
+ * one (or it finished), so a second check of that key waits outside its attempt rather than on that build. */
335
+ private readonly claims = new Map<string, { readonly owner: string; readonly released: Promise<void>; readonly release: () => void }>();
336
+ private sharing = false;
337
+ private claim(id: string): void {
338
+ const key = this.keyOf(id);
339
+ if (!this.sharing || this.bases.has(key) || this.claims.has(key)) return;
340
+ let release = () => {};
341
+ const released = new Promise<void>(resolve => { release = resolve; });
342
+ this.claims.set(key, { owner: id, released, release });
343
+ }
344
+ /** Ends the key's claim: once a base exists, whoever holds it; when a check finishes, only its own. */
345
+ private release(key: string, owner?: string): void {
346
+ const claim = this.claims.get(key);
347
+ if (!claim || (owner !== undefined && claim.owner !== owner)) return;
348
+ this.claims.delete(key); claim.release();
349
+ }
271
350
  /** Stops at the first failure: a failed mechanical check publishes no receipt. */
272
351
  async satisfyMechanical(): Promise<void> {
273
352
  const ids = await this.cheapestFirst(this.context.config.scopedExecution!.mechanicalProviders);
@@ -277,8 +356,11 @@ export class ScopedChecks {
277
356
  const provider = this.context.config.providers.find(p => p.id === id)!;
278
357
  await this.execute(provider, this.context.config.obligations.filter(o => o.providers.includes(id)).map(o => o.id), true, lane);
279
358
  };
280
- // The first declared check guards the rest, so it finishes before any other starts.
359
+ // The first declared check guards the rest, so it finishes before any other starts, or sets up.
360
+ const executing = async (some: readonly string[]) => { const out: string[] = []; for (const id of some) if ((await this.selected(id))?.attempt.status !== "passed") out.push(id); return out; };
361
+ await this.prewarm(await executing(ids.slice(0, 1)));
281
362
  await this.pool(ids.slice(0, 1), satisfy);
363
+ await this.prewarm(await executing(ids.slice(1)));
282
364
  await this.pool(ids.slice(1), satisfy);
283
365
  }
284
366
  /**
@@ -305,18 +387,24 @@ export class ScopedChecks {
305
387
  if (failFast && firstFailure === undefined) {
306
388
  firstFailure = id;
307
389
  for (const [other, controller] of running) if (other !== id) controller.abort();
390
+ // A worker waiting for a base takes the rest now, which never start.
391
+ this.wakePools();
308
392
  }
309
393
  } finally { running.delete(id); }
310
394
  };
395
+ const halted = () => firstFailure !== undefined;
396
+ const expected = await this.expectedCosts(requests.map(r => r.provider.id));
311
397
  const resource = requests.find(r => this.acceptsApplicability(r.provider, r.obligationIds));
398
+ // V1 has one conditional consumer. Keep the first configured check ahead of
399
+ // it, and execute both on the capture worker before releasing the rest to
400
+ // the pool. Other workers never inherit that worker's installed tree.
401
+ const serialIds = resource ? [...new Set([requests[0]!.provider.id, resource.provider.id])] : [];
402
+ // Those two hold every other worker, so their bases set up first, in that order.
403
+ await this.prewarm(requests.map(r => r.provider.id), expected, serialIds);
312
404
  if (resource) {
313
- // V1 has one conditional consumer. Keep the first configured check ahead of
314
- // it, and execute both on the capture worker before releasing the rest to
315
- // the pool. Other workers never inherit that worker's installed tree.
316
- const serialIds = [...new Set([requests[0]!.provider.id, resource.provider.id])];
317
405
  for (const id of serialIds) await run(id, this.serial);
318
- await this.pool(requests.map(r => r.provider.id).filter(id => !serialIds.includes(id)), run);
319
- } else await this.pool(requests.map(r => r.provider.id), run);
406
+ await this.pool(requests.map(r => r.provider.id).filter(id => !serialIds.includes(id)), run, halted, expected);
407
+ } else await this.pool(requests.map(r => r.provider.id), run, halted, expected);
320
408
  if (firstFailure !== undefined) await this.cancelUnstarted(unstarted, firstFailure);
321
409
  return requests.flatMap(r => failures.has(r.provider.id) ? [failures.get(r.provider.id)!] : []);
322
410
  }
@@ -333,12 +421,98 @@ export class ScopedChecks {
333
421
  /** One acquisition path preserves setup sharing, output ownership and worker isolation. A worker's tree
334
422
  * for a setup key is built here, cloned from the warm base, or retained from its previous check.
335
423
  * `onSetup` hears how the tree is being got before any setup runs, so a failed setup is attributed. */
424
+ /** Every scoped check's profile sharing this setup may write its own outputs in the shared tree;
425
+ * only those profiles passed the overlap refusal in create(). */
426
+ private sharedOutputsFor(key: string): string[] {
427
+ return [...new Set(this.context.config.scopedExecution!.profiles
428
+ .filter(p => this.context.config.providers.some(q => q.check?.scope?.profile === p.id && setupKey(p, q.check.physicalSource) === key)).flatMap(p => p.mutableOutputs))];
429
+ }
430
+ private buildSnapshot(profile: ScopedExecutionProfile, physicalSource: SnapshotRequest["physicalSource"] | undefined, onPhase: NonNullable<SnapshotRequest["onPhase"]>, dependencyResult?: SnapshotRequest["onDependencyResult"], signal = this.context.signal): Promise<CheckSnapshot> {
431
+ return createCheckSnapshot({ rootDir: this.context.rootDir, candidate: this.candidate, outputs: this.sharedOutputsFor(setupKey(profile, physicalSource)), gitContext: profile.gitContext ?? "full", environment: { PATH: this.context.env["PATH"] ?? process.env["PATH"] ?? "/usr/bin:/bin" }, ...(physicalSource ? { physicalSource } : {}), ...(profile.dependencies ? { dependencies: profile.dependencies } : {}), ...(signal ? { signal } : {}),
432
+ onPhase, ...(dependencyResult ? { onDependencyResult: dependencyResult } : {}) });
433
+ }
434
+ /** Shares `base` for its setup key. A failed setup is not shared: it leaves `bases` as it settles, and the
435
+ * next check of this key sets up again, as it would on its own worker. */
436
+ private shareBase(key: string, base: Promise<CheckSnapshot>): void {
437
+ this.bases.set(key, base);
438
+ this.settling.set(base, base.then(() => { this.settled.add(base); }, () => { this.settled.add(base); if (this.bases.get(key) === base) this.bases.delete(key); }));
439
+ }
440
+ /**
441
+ * Sets up the warm base of each setup key among `ids` that has none yet, so the checks that clone it neither
442
+ * build it nor wait for another check to. Bases set up one at a time: concurrent dependency installs on one
443
+ * machine each take about twice as long, so a key's base is ready soonest on its own. With `expected`, the
444
+ * keys of `leading` go first, in that order, and then the key whose own critical path is longest: its last
445
+ * recorded build plus the longest expected check that needs it. Without it, the order of `ids`. Every key
446
+ * is registered at once, so no worker takes its checks before its base settles. No attempt owns this setup:
447
+ * a check cloning the base records it as reused and carries no dependency result, and a failed one leaves
448
+ * no base, so the check that next needs its key sets up itself and owns that result. Only with cloning:
449
+ * without it each worker builds its own tree, as before.
450
+ */
451
+ private async prewarm(ids: readonly string[], expected?: ReadonlyMap<string, number>, leading: readonly string[] = []): Promise<void> {
452
+ if (this.concurrency === 1 || !await (this.cloning ??= snapshotCloneMethod())) return;
453
+ this.sharing = true;
454
+ const signal = this.context.signal ? AbortSignal.any([this.context.signal, this.prewarming.signal]) : this.prewarming.signal;
455
+ const keyed = new Map<string, { id: string; check: number; order: number }>();
456
+ for (const [order, id] of ids.entries()) {
457
+ const key = this.keyOf(id), check = expected?.get(id) ?? 0, seen = keyed.get(key);
458
+ if (this.bases.has(key)) continue;
459
+ if (!seen) keyed.set(key, { id, check, order });
460
+ else if (check > seen.check) keyed.set(key, { id, check, order: seen.order });
461
+ }
462
+ const rank = (key: string) => { const index = leading.findIndex(id => this.keyOf(id) === key); return index < 0 ? leading.length : index; };
463
+ const critical = new Map<string, number>();
464
+ for (const [key, { check }] of keyed) critical.set(key, expected ? await this.lastBuild(key) + check : 0);
465
+ const keys = [...keyed].sort(([a, x], [b, y]) => rank(a) - rank(b) || critical.get(b)! - critical.get(a)! || x.order - y.order);
466
+ for (const [key, { id }] of keys) {
467
+ const profile = this.profileOf(id), physicalSource = this.context.config.providers.find(p => p.id === id)?.check?.physicalSource;
468
+ const previous = this.prewarmTail;
469
+ // A build queued when the prewarm stops sees the aborted signal and settles as interrupted.
470
+ const base = (async () => {
471
+ await previous;
472
+ const started = performance.now();
473
+ const snapshot = await this.buildSnapshot(profile, physicalSource, (phase, durationMs) => this.context.write(`prewarm ${profile.id}: ${phase} ${Math.round(durationMs)}ms`), undefined, signal);
474
+ // Recorded in the background: a base never waits on its timing.
475
+ this.timingWrites.push(this.recordBuild(key, performance.now() - started));
476
+ return snapshot;
477
+ })();
478
+ this.prewarmTail = base.then(() => undefined, () => undefined);
479
+ this.prewarmedBases.add(base);
480
+ this.shareBase(key, base);
481
+ base.catch(error => { if (!this.prewarming.signal.aborted) this.context.write(`prewarm ${profile.id}: setup failed (${error instanceof CheckSnapshotError ? error.code : "unclassified"}); its first check sets up itself`); });
482
+ }
483
+ }
484
+ /** Where the last successful prewarm of each setup key's base is timed, resolved once a session. Ordering
485
+ * only: no admission, gate or record reads it, and a missing or unreadable timing counts as no build time.
486
+ * A check-owned build is not timed: it follows a failed prewarm and may share the machine with the chain. */
487
+ private timings?: Promise<string>;
488
+ private readonly timingWrites: Promise<void>[] = [];
489
+ private async timingPath(key: string): Promise<string> {
490
+ const dir = await (this.timings ??= resolveRecordStorage(this.context.rootDir, { storageNamespace: this.context.config.storageNamespace, leaf: "scoped-setup-timings" }).then(storage => storage.storageDir));
491
+ return path.join(dir, `${digestCanonical({ gate: this.context.config.gateId, setup: key })}.json`);
492
+ }
493
+ private async lastBuild(key: string): Promise<number> {
494
+ try {
495
+ const timing = JSON.parse(await readFile(await this.timingPath(key), "utf8")) as { version?: unknown; buildMs?: unknown };
496
+ return timing.version === "scoped-setup-timing/1" && typeof timing.buildMs === "number" && Number.isFinite(timing.buildMs) && timing.buildMs >= 0 ? timing.buildMs : 0;
497
+ } catch { return 0; }
498
+ }
499
+ private async recordBuild(key: string, buildMs: number): Promise<void> {
500
+ let temporary: string | undefined;
501
+ try {
502
+ const file = await this.timingPath(key);
503
+ temporary = `${file}.${randomUUID()}`;
504
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
505
+ await writeFile(temporary, `${JSON.stringify({ version: "scoped-setup-timing/1", buildMs: Math.round(buildMs) })}\n`);
506
+ await rename(temporary, file);
507
+ } catch {
508
+ // Timing orders prewarms; it cannot fail a setup, and leaves no partial file behind.
509
+ if (temporary) await rm(temporary, { force: true }).catch(() => undefined);
510
+ }
511
+ }
512
+ private prewarmTail: Promise<void> = Promise.resolve();
336
513
  private async acquireSnapshot(profile: ScopedExecutionProfile, lane: Lane, onDependencyResult?: SnapshotRequest["onDependencyResult"], physicalSource?: SnapshotRequest["physicalSource"], onSetup: (setup: AcquiredSetup) => void = () => {}): Promise<{ snapshot: CheckSnapshot; sharedOutputs: string[]; setup: AcquiredSetup }> {
337
514
  const key = setupKey(profile, physicalSource);
338
- // Every scoped check's profile sharing this setup may write its own outputs in the shared tree;
339
- // only those profiles passed the overlap refusal in create().
340
- const sharedOutputs = [...new Set(this.context.config.scopedExecution!.profiles
341
- .filter(p => this.context.config.providers.some(q => q.check?.scope?.profile === p.id && setupKey(p, q.check.physicalSource) === key)).flatMap(p => p.mutableOutputs))];
515
+ const sharedOutputs = this.sharedOutputsFor(key);
342
516
  const method = this.concurrency === 1 ? undefined : await (this.cloning ??= snapshotCloneMethod());
343
517
  const acquired = (snapshot: CheckSnapshot, setup: AcquiredSetup) => { lane.snapshots.set(key, snapshot); onSetup(setup); return { snapshot, sharedOutputs, setup }; };
344
518
  const retained = lane.snapshots.get(key);
@@ -354,19 +528,16 @@ export class ScopedChecks {
354
528
  if (write) write(phase, durationMs);
355
529
  else lane.write(`snapshot ${profile.id}: bootstrap ${phase} ${Math.round(durationMs)}ms`);
356
530
  };
357
- const build = (dependencyResult?: SnapshotRequest["onDependencyResult"]) => createCheckSnapshot({ rootDir: this.context.rootDir, candidate: this.candidate, outputs: sharedOutputs, gitContext: profile.gitContext ?? "full", environment: { PATH: this.context.env["PATH"] ?? process.env["PATH"] ?? "/usr/bin:/bin" }, ...(physicalSource ? { physicalSource } : {}), ...(profile.dependencies ? { dependencies: profile.dependencies } : {}), ...(this.context.signal ? { signal: this.context.signal } : {}),
358
- onPhase, ...(dependencyResult ? { onDependencyResult: dependencyResult } : {}) });
531
+ const build = (dependencyResult?: SnapshotRequest["onDependencyResult"]) => this.buildSnapshot(profile, physicalSource, onPhase, dependencyResult);
359
532
  let executed = false, clone: AcquiredSetup["clone"] = this.concurrency === 1 ? "not-used" : "unavailable";
360
533
  if (method) {
361
534
  let base = this.bases.get(key);
362
535
  if (!base) {
363
536
  executed = true;
364
537
  base = build(onDependencyResult);
365
- this.bases.set(key, base);
366
- // A failed setup is not shared: the next check of this key sets up again, as it would on its own worker.
367
- const failed = base;
368
- failed.catch(() => { if (this.bases.get(key) === failed) this.bases.delete(key); });
538
+ this.shareBase(key, base);
369
539
  }
540
+ this.release(key);
370
541
  onSetup({ action: executed ? "executed" : "reused", snapshot: "cloned", clone: method });
371
542
  const waiting = performance.now();
372
543
  let warm: CheckSnapshot | undefined;
@@ -382,8 +553,13 @@ export class ScopedChecks {
382
553
  clone = "path-bound";
383
554
  lane.write(`snapshot ${profile.id}: setup wrote its own path into the tree; each worker builds its own`);
384
555
  if (executed) return acquired(warm, { action: "executed", snapshot: "built", clone });
556
+ // A prewarmed base no check owns becomes exactly one worker's own tree; every other worker builds its own.
557
+ if (this.prewarmedBases.delete(base)) return acquired(warm, { action: "reused", snapshot: "built", clone });
385
558
  } else if (warm) {
386
- try { return acquired(await warm.clone(method, onPhase), { action: executed ? "executed" : "reused", snapshot: "cloned", clone: method }); }
559
+ // Clones run one at a time; the time this one queued counts as its clone.
560
+ const queued = performance.now(), source = warm;
561
+ const cloned = () => this.cloningOne(lane.priority ?? 0, () => { const wait = performance.now() - queued; return source.clone(method, (phase, durationMs) => onPhase(phase, phase === "clone" ? durationMs + wait : durationMs)); });
562
+ try { return acquired(await cloned(), { action: executed ? "executed" : "reused", snapshot: "cloned", clone: method }); }
387
563
  catch (error) {
388
564
  if (!(error instanceof CheckSnapshotError) || this.context.signal?.aborted) throw error;
389
565
  // The base stays for other workers; this one sets up its own tree, as without cloning.
@@ -691,6 +867,9 @@ export class ScopedChecks {
691
867
  this.cleanupStarted = true;
692
868
  await this.closeCapturePhase();
693
869
  await Promise.all(this.executionTails.values());
870
+ // No check needs a base still setting up now; stop rather than wait for it.
871
+ this.prewarming.abort();
872
+ await Promise.all(this.timingWrites);
694
873
  for (const snapshots of this.workers) { for (const snapshot of snapshots.values()) await snapshot.cleanup(); snapshots.clear(); }
695
874
  const bases = [...this.bases.values()]; this.bases.clear();
696
875
  for (const base of bases) await (await base.catch(() => undefined))?.cleanup();
@@ -21,8 +21,9 @@ export interface ScopedDependencyDiagnostic {
21
21
  const setupPhases = ["materialization", "baseline-inventory", "dependencies", "post-dependency-inventory", "warm-base-wait", "clone", "pre-command-verification"] as const;
22
22
  /** How this attempt got the tree its command ran in, and what that cost up to the command.
23
23
  * `executed`: this attempt ran the setup (a tree it built, or the warm base it then cloned);
24
- * `reused`: the setup had already run (a clone of a warm base another check built, or the
25
- * worker's tree retained from its previous check). `clone` names the copy-on-write method, or says
24
+ * `reused`: the setup had already run (a clone of a warm base another check or the session's prewarm
25
+ * built, a prewarmed path-bound base taken as this worker's own tree, or the worker's tree retained
26
+ * from its previous check). `clone` names the copy-on-write method, or says
26
27
  * why each worker built its own tree: cloning is unavailable here, or the setup wrote its own path
27
28
  * into the tree (`path-bound`), or there is one worker (`not-used`). */
28
29
  export interface ScopedSetupDiagnostic {