@vzn/vx-dte 0.0.650 → 0.0.652

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.
@@ -25,10 +25,10 @@ import {
25
25
  type SandboxConfig,
26
26
  type TaskArtifacts,
27
27
  type TaskNode,
28
+ type WorkerAdvice,
28
29
  } from '@vzn/vx'
29
30
  import {
30
31
  bytesOf,
31
- HELD_EVERY,
32
32
  HOST_ENV,
33
33
  PART_LISTS,
34
34
  type PartList,
@@ -36,14 +36,24 @@ import {
36
36
  type DepOrder,
37
37
  type RunOrder,
38
38
  type RuntimeOrder,
39
+ type SentOrder,
40
+ type WiredDep,
39
41
  type ToWorker,
40
42
  type Load,
41
43
  type ToOrchestrator,
42
44
  type WiredResult,
43
45
  } from './messages.js'
44
- import { simulate, type PlanTask, type PlanWorker, type Size, type WarmUp } from './planner.js'
46
+ import {
47
+ advise,
48
+ simulate,
49
+ type Fleets,
50
+ type PlanTask,
51
+ type PlanWorker,
52
+ type Size,
53
+ type WarmUp,
54
+ } from './planner.js'
45
55
  import { Usage, type UsageStats } from './usage.js'
46
- import { assign, ordered, type Need } from './dispatch.js'
56
+ import { type Need, Queue } from './dispatch.js'
47
57
  import { DEFAULT_STRATEGY, type Strategy } from './strategy.js'
48
58
  import { RelayError, unsent, type Participant } from './wire.js'
49
59
  import { ORCHESTRATOR, Trace } from './trace.js'
@@ -65,11 +75,13 @@ interface Job {
65
75
  booked?: number
66
76
  /** The workers that failed it after beginning it, with why: two fail it. */
67
77
  faults?: string[]
68
- /** Its worker's lease on it ends then unless the worker says it still holds it. */
69
- leaseUntil?: number
78
+ /** Its worker's `end` came: a report naming it no more is not a loss. */
79
+ ended?: true
70
80
  /** When it was sent to its worker. */
71
81
  sentAt?: number
72
- /** When its worker began it. */
82
+ /** The batch that sent it, by its worker's count: a report of that batch or later names it. */
83
+ batch?: number
84
+ /** When its worker began it: its send, then the time its worker says. */
73
85
  startedAt?: number
74
86
  /** When its worker said its command ended: it saves since. */
75
87
  ranAt?: number
@@ -83,8 +95,15 @@ interface Job {
83
95
  reread?: Set<string>
84
96
  /** Its upstreams, once the graph named it: walked again at every pump, 8000 queued spent 5 ms a pump on it (2026-10-25). */
85
97
  up?: Set<string>
98
+ /** Its direct dependencies, as placing reads them (`Ask.up`). */
99
+ deps?: readonly string[] | undefined
100
+ /** A retry: the worker its last attempt ran on, which it goes elsewhere than while another can take it. */
101
+ avoid?: string
86
102
  }
87
103
 
104
+ /** An upstream's files, as an order names them (`DepOrder`). */
105
+ type DepFiles = Pick<DepOrder, 'project' | 'outputs'>
106
+
88
107
  /** The longest a task waits on the worker holding its upstream's files before they are made again (2026-10-23: 10 s on any wait). */
89
108
  const PIN_WAIT_MS = 10_000
90
109
 
@@ -112,14 +131,18 @@ interface Worker {
112
131
  readonly usage: Usage
113
132
  /** Sent to it and not finished: running there or in its queue. */
114
133
  readonly busy: Map<string, Job>
115
- /** Of `busy`, the ones it began. */
134
+ /** Of `busy`, the ones it began: each one sent, as a worker runs what it takes at once. */
116
135
  readonly started: Set<string>
136
+ /** Batches sent it: each batch's `n`. */
137
+ batches: number
117
138
  usedCores: number
118
139
  usedMemory: number
119
140
  /** Its machine's use as its last frame said, and when this machine heard it. */
120
141
  load?: { cpu: number; memory: number; at: number }
121
142
  /** Tasks whose outputs it holds: it ran them, or was sent them. */
122
143
  readonly has: Set<string>
144
+ /** Each dependency it was sent whole, by the key it was sent under: the next order names it alone. */
145
+ readonly sentDeps: Map<string, string>
123
146
  /** Sends to it go out in order: a run's parts before the run. */
124
147
  chain: Promise<void>
125
148
  /** It was sent the run's sandbox configs. */
@@ -164,7 +187,9 @@ export interface OrchestratorOptions {
164
187
  readonly joined?: () => void
165
188
  /** Worker `id` did not join within twice its size's first join: given up, for its adapter to stop. */
166
189
  readonly overdue?: (id: string) => void
167
- /** A worker failed a task or its lease: the run plans again, now, so a replacement starts. */
190
+ /** Worker `id`, given up as late (`overdue`), joined after all and is taken. */
191
+ readonly kept?: (id: string) => void
192
+ /** A worker failed a task or dropped it: the run plans again, now, so a replacement starts. */
168
193
  readonly faulted?: () => void
169
194
  /** How much of the run waits on a task (`tailsMs`): the queue places the most first. */
170
195
  readonly tail?: (taskId: string) => number
@@ -202,6 +227,15 @@ const BATCH_BYTES = 512 * 1024
202
227
  const FEW = 2
203
228
  /** The unmeasured tasks the summary names; the rest it counts. */
204
229
  const NAMED = 40
230
+ /** The footer's label of each `WorkerAdvice`. */
231
+ const ADVICE: Record<WorkerAdvice['of'], readonly [label: string, note: string]> = {
232
+ declared: ['best', 'the fewest of its sizes'],
233
+ more: ['more', "past its sizes' max"],
234
+ any: ['any', 'of any cores, the least core time'],
235
+ }
236
+ /** `3 × small (4c/16G linux)`. */
237
+ const fleetText = (w: WorkerAdvice['workers'][number]): string =>
238
+ `${w.count} × ${w.size === undefined ? '' : `${w.size} `}(${w.cores}c/${compact(w.memoryBytes)}${w.platform === undefined ? '' : ` ${w.platform}`})`
205
239
  /** Core-time of history the run's tasks finish before their use reshapes bookings: a few tasks' worth. */
206
240
  const LEARN_MS = 60_000
207
241
  /** The most a worker's bookings may add up to, in its cores, when tasks use less than their history. */
@@ -210,12 +244,12 @@ const OVERBOOK = 1.5
210
244
  const RESTARTS = 3
211
245
 
212
246
  /** `orders` in runs of at most `max` bytes of JSON; one bigger goes alone. */
213
- function parts<T extends { order: unknown }>(orders: readonly T[], max: number): T[][] {
247
+ function parts<T extends { bytes: number }>(orders: readonly T[], max: number): T[][] {
214
248
  const out: T[][] = []
215
249
  let part: T[] = []
216
250
  let bytes = 0
217
251
  for (const o of orders) {
218
- const size = bytesOf(o.order)
252
+ const size = o.bytes
219
253
  if (part.length > 0 && bytes + size > max) {
220
254
  out.push(part)
221
255
  part = []
@@ -254,9 +288,13 @@ export class Orchestrator {
254
288
  private readonly workers = new Map<string, Worker>()
255
289
  /** The run tree's files, workspace-relative, listed when a task first asks. */
256
290
  private inTree?: Promise<Tree | undefined>
257
- private readonly queue: Job[] = []
291
+ /** Waiting tasks, by id, in the order a pump reads them (`Queue`). */
292
+ private readonly waiting = new Map<string, Job>()
293
+ private readonly queue: Queue
258
294
  /** Where a worker saved each task it ran: its cache key or its run key. */
259
295
  private readonly savedAs = new Map<string, string>()
296
+ /** The worker each task last ran on, which its retry goes elsewhere than. */
297
+ private readonly ranOn = new Map<string, string>()
260
298
  /** Keys the run's requests named for their dependencies. */
261
299
  private readonly upstreamKeys = new Map<string, string>()
262
300
  /** How many times each task was sent: its run key is one per send. */
@@ -349,9 +387,12 @@ export class Orchestrator {
349
387
  private relend: ReturnType<typeof setTimeout> | undefined
350
388
  /** The plugins any of the run's lends lent a token. */
351
389
  private readonly lenders = new Set<string>()
390
+ /** Each graph's upstreams as orders name them (`dep`). */
391
+ private readonly deps = new WeakMap<ReadonlyMap<string, TaskNode>, Map<string, DepFiles | null>>()
352
392
 
353
393
  constructor(private readonly o: OrchestratorOptions) {
354
394
  this.sent = o.sent ?? new Map()
395
+ this.queue = new Queue(o.how ?? DEFAULT_STRATEGY, o.tail, (id) => this.waiting.get(id)!.need)
355
396
  // A root that is gone fails the run's own reads first.
356
397
  this.realRoot = (() => {
357
398
  try {
@@ -361,19 +402,16 @@ export class Orchestrator {
361
402
  return o.root
362
403
  }
363
404
  })()
364
- // Leases ran on while either side was away, as a relay restarted: what
365
- // a worker holds is still its own, not run again elsewhere
366
- // (2026-10-25: "make sure on rejoin we don't repeat work").
367
- o.me.resumed((peer) => {
368
- for (const a of this.workers.values())
369
- if (peer === undefined || a.id === peer)
370
- for (const j of a.busy.values()) if (j.leaseUntil !== undefined) this.lease(j)
371
- })
372
405
  this.loop = this.listen()
373
406
  }
374
407
 
375
408
  async execute(req: ExecuteRequest): Promise<ExecuteResult> {
376
409
  if (this.lost !== undefined) throw new Error(this.lost)
410
+ // Walked once here, for every later read: walked again on the way in,
411
+ // sharing, placing and sending, a run of 8000 tasks spent 17 s of this
412
+ // machine's CPU on it (2026-11-05).
413
+ const nodes = this.o.nodes()
414
+ const up = upstream(nodes, req.taskId)
377
415
  // A worker runs the run's tree: a file its key includes that the tree
378
416
  // lacks (untracked) is not there, and the output it saved under that
379
417
  // key was a stale hit for the next run (2026-10-24).
@@ -383,25 +421,16 @@ export class Orchestrator {
383
421
  // A dependency's output is restored there before the task runs: a
384
422
  // key that reads an upstream's bytes (`inputs.tasks` leaving it out)
385
423
  // was refused (2026-10-25).
386
- const nodes = this.o.nodes()
387
- const restored = [...upstream(nodes, req.taskId)].flatMap((id) => {
388
- const n = nodes?.get(id)
389
- const outs = n?.config.cache?.outputs
390
- return n === undefined || outs === undefined
391
- ? []
392
- : [
393
- names(path.relative(this.o.root, n.projectDir), {
394
- files: outs.files,
395
- workspaceFiles: outs.workspaceFiles ?? [],
396
- }),
397
- ]
398
- })
424
+ let restored: ((file: string) => boolean)[] | undefined
425
+ const made = (file: string): boolean =>
426
+ (restored ??= [...up].flatMap((id) => {
427
+ const d = this.dep(nodes, id)
428
+ return d === undefined ? [] : [names(d.project, d.outputs)]
429
+ })).some((m) => m(file))
399
430
  const stray =
400
431
  tree === undefined
401
432
  ? undefined
402
- : req.inputs.files.find(
403
- (f) => !tree.files.has(f.path) && !restored.some((made) => made(f.path)),
404
- )
433
+ : req.inputs.files.find((f) => !tree.files.has(f.path) && !made(f.path))
405
434
  if (stray !== undefined) {
406
435
  // A committed submodule's files are not the tree's: its gitlink is.
407
436
  const link = tree?.links.find((l) => stray.path.startsWith(`${l}/`))
@@ -413,20 +442,24 @@ export class Orchestrator {
413
442
  }
414
443
  }
415
444
  for (const u of req.inputs?.upstream ?? []) this.upstreamKeys.set(u.taskId, u.hash)
416
- await this.share(req.taskId)
445
+ await this.share(up, nodes)
417
446
  return new Promise((resolve, reject) => {
418
447
  const need = this.o.need(req.taskId)
419
448
  const job: Job = {
420
449
  req,
450
+ ...(nodes?.has(req.taskId) === true ? { up } : {}),
421
451
  need: { ...need, platform: req.platform ?? this.o.home },
422
452
  queuedAt: performance.now(),
423
453
  resolve,
424
454
  reject,
425
455
  }
456
+ // A retry runs elsewhere: the worker, or what ran beside it, may be the flake.
457
+ const last = (req.attempt ?? 1) > 1 ? this.ranOn.get(req.taskId) : undefined
458
+ if (last !== undefined) job.avoid = last
426
459
  // A signal aborted already fires no `abort`.
427
460
  if (req.signal?.aborted === true) return resolve(stopped())
428
461
  req.signal?.addEventListener('abort', () => this.abort(job), { once: true })
429
- this.queue.push(job)
462
+ this.enqueue([job])
430
463
  this.pumpSoon()
431
464
  })
432
465
  }
@@ -502,9 +535,9 @@ export class Orchestrator {
502
535
  }
503
536
 
504
537
  private abort(job: Job): void {
505
- const at = this.queue.indexOf(job)
506
- if (at >= 0) {
507
- this.queue.splice(at, 1)
538
+ if (this.waiting.get(job.req.taskId) === job) {
539
+ this.waiting.delete(job.req.taskId)
540
+ this.queue.delete(job.req.taskId)
508
541
  job.resolve(stopped())
509
542
  } else if (job.worker !== undefined) {
510
543
  const a = this.workers.get(job.worker)
@@ -522,27 +555,22 @@ export class Orchestrator {
522
555
  })
523
556
  }
524
557
 
558
+ /** `jobs` wait, in order, behind the waiting tasks or, `front`, before them. */
559
+ private enqueue(jobs: readonly Job[], front = false): void {
560
+ for (const j of jobs) this.waiting.set(j.req.taskId, j)
561
+ this.queue.add(
562
+ jobs.map((j) => j.req.taskId),
563
+ front,
564
+ )
565
+ }
566
+
525
567
  private pump(): void {
526
568
  const batches = new Map<Worker, Job[]>()
527
- const left: Job[] = []
528
569
  // Core asks in its critical-path order, but only as tasks get ready: a
529
570
  // long chain ready a moment late queued behind 46 shards ready at once,
530
571
  // and its 41 s build began 47 s in, the run's end (main, 2026-10-23).
531
- const tail = this.o.tail
532
- const how = this.o.how ?? DEFAULT_STRATEGY
533
- const waiting = this.queue.splice(0)
534
572
  const workers = [...this.workers.values()].filter((a) => !a.faulty)
535
- const byId = new Map(waiting.map((j) => [j.req.taskId, j]))
536
- const queue =
537
- tail === undefined
538
- ? waiting
539
- : ordered(
540
- waiting.map((j) => j.req.taskId),
541
- how.order,
542
- tail,
543
- (id) => byId.get(id)!.need,
544
- Math.max(1, ...workers.map((a) => a.cores)),
545
- ).map((id) => byId.get(id)!)
573
+ const byId = this.waiting
546
574
  const bins = workers.map((a) => {
547
575
  let running = 0
548
576
  for (const j of a.busy.values()) if (j.held !== undefined) running++
@@ -558,39 +586,24 @@ export class Orchestrator {
558
586
  }
559
587
  })
560
588
  const nodes = this.o.nodes()
561
- const asks = []
562
- const placed: Job[] = []
563
- // The least any ask needs: once no worker takes it, the pass ends. Each
564
- // pump read every queued task, 8000 of them a pump (2026-10-25).
565
- const floor = { cores: Infinity, memory: Infinity }
566
- for (const job of queue) {
567
- const up =
568
- job.up ??
569
- (nodes?.has(job.req.taskId) === true
570
- ? (job.up = upstream(nodes, job.req.taskId))
571
- : upstream(nodes, job.req.taskId))
572
- const pin = this.pin(job, up)
573
- if (pin === 'done') continue
574
- if (pin === 'wait') {
575
- left.push(job)
576
- continue
577
- }
578
- placed.push(job)
579
- const need = this.booking(job.need)
580
- floor.cores = Math.min(floor.cores, need.cores)
581
- floor.memory = Math.min(floor.memory, need.memory)
582
- asks.push({ id: job.req.taskId, need, up, ...pin })
583
- }
584
- const to = assign(asks, bins, (above, platform) => this.bigger(above, platform), how.place, {
585
- floor,
586
- kinds: Infinity,
589
+ const to = this.queue.pass(bins, {
590
+ book: (need) => this.booking(need),
591
+ deps: (id) => (byId.get(id)!.deps ??= nodes?.get(id)?.deps),
592
+ gate: (id) => {
593
+ const job = byId.get(id)!
594
+ const up =
595
+ job.up ?? (nodes?.has(id) === true ? (job.up = upstream(nodes, id)) : upstream(nodes, id))
596
+ const pin = this.pin(job, up)
597
+ // It ended: the queue lets it go.
598
+ if (pin === 'done') byId.delete(id)
599
+ return pin
600
+ },
601
+ waits: (above, platform) => this.bigger(above, platform),
587
602
  })
603
+ const placed = [...to.keys()].map((id) => byId.get(id)!)
604
+ for (const id of to.keys()) byId.delete(id)
588
605
  for (const job of placed) {
589
- const a = to.get(job.req.taskId)?.a
590
- if (a === undefined) {
591
- left.push(job)
592
- continue
593
- }
606
+ const a = to.get(job.req.taskId)!.a
594
607
  job.worker = a.id
595
608
  job.sentAt = performance.now()
596
609
  a.busy.set(job.req.taskId, job)
@@ -613,7 +626,6 @@ export class Orchestrator {
613
626
  if (batch === undefined) batches.set(a, [job])
614
627
  else batch.push(job)
615
628
  }
616
- this.queue.push(...left)
617
629
  for (const [a, jobs] of batches) this.send(a, jobs)
618
630
  }
619
631
 
@@ -627,6 +639,23 @@ export class Orchestrator {
627
639
  private pin(
628
640
  job: Job,
629
641
  up: ReadonlySet<string>,
642
+ ): 'wait' | 'done' | { only?: string; avoid?: string } {
643
+ const where = this.pinUp(job, up)
644
+ const avoid = job.avoid
645
+ if (typeof where !== 'object' || avoid === undefined) return where
646
+ if (where.only !== undefined || where.avoid !== undefined) return where
647
+ // With no other worker that can take it, it runs where it failed rather than wait.
648
+ const platform = job.need.platform
649
+ for (const w of this.workers.values())
650
+ if (w.id !== avoid && !w.faulty && (platform === undefined || w.platform === platform))
651
+ return { avoid }
652
+ return where
653
+ }
654
+
655
+ /** Where `job` may go as its upstreams' files lie (`pin`, before a retry's own rule). */
656
+ private pinUp(
657
+ job: Job,
658
+ up: ReadonlySet<string>,
630
659
  ): 'wait' | 'done' | { only?: string; avoid?: string } {
631
660
  if (job.again !== undefined) {
632
661
  const { only } = job.again
@@ -696,24 +725,31 @@ export class Orchestrator {
696
725
  this.pumpSoon()
697
726
  }
698
727
  this.pumpSoon()
699
- this.queue.unshift({
700
- req: k.req,
701
- need: where.platform === undefined ? k.need : { ...k.need, platform: where.platform },
702
- queuedAt: performance.now(),
703
- pinnedAt: performance.now(),
704
- again: {
705
- ...(where.only === undefined ? {} : { only: where.only }),
706
- ...(where.avoid === undefined ? {} : { avoid: where.avoid }),
707
- },
708
- resolve: (r) =>
709
- done(
710
- r.exitCode === 0 ? undefined : `${id} ran again for it and failed (exit ${r.exitCode})`,
711
- ),
712
- reject: (err) =>
713
- done(
714
- `${id} ran again for it and failed: ${err instanceof Error ? err.message : String(err)}`,
715
- ),
716
- })
728
+ this.enqueue(
729
+ [
730
+ {
731
+ req: k.req,
732
+ need: where.platform === undefined ? k.need : { ...k.need, platform: where.platform },
733
+ queuedAt: performance.now(),
734
+ pinnedAt: performance.now(),
735
+ again: {
736
+ ...(where.only === undefined ? {} : { only: where.only }),
737
+ ...(where.avoid === undefined ? {} : { avoid: where.avoid }),
738
+ },
739
+ resolve: (r) =>
740
+ done(
741
+ r.exitCode === 0
742
+ ? undefined
743
+ : `${id} ran again for it and failed (exit ${r.exitCode})`,
744
+ ),
745
+ reject: (err) =>
746
+ done(
747
+ `${id} ran again for it and failed: ${err instanceof Error ? err.message : String(err)}`,
748
+ ),
749
+ },
750
+ ],
751
+ true,
752
+ )
717
753
  if (where.only !== undefined || where.avoid !== undefined)
718
754
  setTimeout(() => this.pumpSoon(), this.pinWaitMs + 50).unref?.()
719
755
  }
@@ -818,11 +854,13 @@ export class Orchestrator {
818
854
  * hit only this cache held (the remote evicted it, or this machine does
819
855
  * not write it) failed the dependant there (2026-10-24).
820
856
  */
821
- private async share(id: string): Promise<void> {
822
- const nodes = this.o.nodes()
857
+ private async share(
858
+ up: ReadonlySet<string>,
859
+ nodes: ReadonlyMap<string, TaskNode> | undefined,
860
+ ): Promise<void> {
823
861
  const keys: string[] = []
824
- for (const d of upstream(nodes, id)) {
825
- if (this.savedAs.has(d) || nodes?.get(d)?.config.cache?.outputs === undefined) continue
862
+ for (const d of up) {
863
+ if (this.savedAs.has(d) || this.dep(nodes, d) === undefined) continue
826
864
  const key = this.upstreamKeys.get(d) ?? this.o.keyOf(d)
827
865
  if (key !== undefined) keys.push(key)
828
866
  }
@@ -863,92 +901,113 @@ export class Orchestrator {
863
901
  return got.includes(true)
864
902
  }
865
903
 
904
+ /**
905
+ * Upstream `id` as an order names it, once per graph: its project and
906
+ * outputs. Undefined: it is not in the graph, or leaves no files.
907
+ */
908
+ private dep(nodes: ReadonlyMap<string, TaskNode> | undefined, id: string): DepFiles | undefined {
909
+ if (nodes === undefined) return undefined
910
+ let of = this.deps.get(nodes)
911
+ if (of === undefined) this.deps.set(nodes, (of = new Map()))
912
+ let d = of.get(id)
913
+ if (d === undefined) {
914
+ const n = nodes.get(id)
915
+ const outs = n?.config.cache === true ? n.config.outputs : undefined
916
+ d =
917
+ n === undefined || outs === undefined
918
+ ? null
919
+ : {
920
+ project: path.relative(this.o.root, n.projectDir),
921
+ outputs: { files: [...outs.files], workspaceFiles: [...(outs.workspaceFiles ?? [])] },
922
+ }
923
+ of.set(id, d)
924
+ }
925
+ return d ?? undefined
926
+ }
927
+
866
928
  private send(a: Worker, jobs: readonly Job[]): void {
867
929
  const nodes = this.o.nodes()
868
- const orders = jobs.map((job) => {
930
+ const orders = jobs.map((job): { job: Job; order: RunOrder } => {
869
931
  const { req } = job
870
932
  const deps: DepOrder[] = []
871
933
  // Every one, held there or not: the worker restores each key once, and
872
934
  // one it was sent but gave back it never ran or restored.
873
- for (const id of upstream(nodes, req.taskId)) {
874
- const n = nodes?.get(id)
875
- const outs = n?.config.cache?.outputs
935
+ for (const id of job.up ?? upstream(nodes, req.taskId)) {
936
+ const d = this.dep(nodes, id)
876
937
  const key = this.savedAs.get(id) ?? this.upstreamKeys.get(id) ?? this.o.keyOf(id)
877
- if (n === undefined || outs === undefined || key === undefined) continue
938
+ if (d === undefined || key === undefined) continue
878
939
  a.has.add(id)
879
- deps.push({
880
- task: id,
881
- key,
882
- project: path.relative(this.o.root, n.projectDir),
883
- outputs: { files: [...outs.files], workspaceFiles: [...(outs.workspaceFiles ?? [])] },
884
- })
940
+ deps.push({ task: id, key, project: d.project, outputs: d.outputs })
885
941
  }
886
942
  // A task sent here leaves its outputs here for its dependants.
887
943
  a.has.add(req.taskId)
888
944
  const node = nodes?.get(req.taskId)
889
945
  const env = defined(req.env, req.envDefine)
890
- return {
891
- job,
892
- order: {
893
- task: req.taskId,
894
- // Unsaved: under the run's own key, as an uncached task's.
895
- ...(req.cacheKey === undefined || req.unsaved === true ? {} : { key: req.cacheKey }),
896
- runKey: runKey(this.o.run, req.taskId, this.sends(req.taskId)),
897
- // A held key keeps its entry, and restored here it replaced the
898
- // forced run's fresh bytes with the ones it bypassed (2026-10-25).
899
- ...(req.refresh === true ? { fresh: true as const } : {}),
900
- // A HEAD per upload was the workers' only remote probe: the run
901
- // already knows the key is in no cache (2026-10-25).
902
- ...(req.refresh !== true && this.missed.has(req.taskId) ? { absent: true as const } : {}),
903
- deps,
904
- nested: node === undefined ? [] : nestedIn(nodes!, node.projectDir, this.o.root),
905
- command: withForwardArgs(req.command, req.forwardArgs),
906
- cwd: path.relative(this.o.root, req.cwd),
907
- project: node === undefined ? '' : path.relative(this.o.root, node.projectDir),
908
- env,
909
- ...(node?.config.exec?.env?.secret === undefined
910
- ? {}
911
- : { secret: [...node.config.exec.env.secret] }),
912
- ...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
913
- outputs: {
914
- files: [...req.outputs.files],
915
- workspaceFiles: [...req.outputs.workspaceFiles],
916
- },
917
- cores: job.held?.cores ?? 1,
918
- memory: job.held?.memory ?? 0,
919
- ...(req.sandbox === undefined || node?.config.exec?.sandbox === undefined
920
- ? {}
921
- : {
922
- sandbox: {
923
- config: node.config.exec.sandbox,
924
- ...(req.sandbox.keyed === undefined
925
- ? {}
926
- : { keyed: req.sandbox.keyed.map((d) => path.relative(this.o.root, d)) }),
927
- },
928
- }),
929
- ...runtimeOrders(req),
930
- ...(req.servers === undefined || req.servers.length === 0
931
- ? {}
932
- : {
933
- servers: req.servers.map((s) => ({
934
- task: s.taskId,
935
- command: s.command,
936
- cwd: path.relative(this.o.root, s.cwd),
937
- env: defined(s.env, {}),
938
- ...(s.readyWhen === undefined ? {} : { readyWhen: s.readyWhen }),
939
- ...(s.timeoutMs === undefined ? {} : { timeoutMs: s.timeoutMs }),
940
- ...(s.sandbox === undefined
941
- ? {}
942
- : {
943
- sandbox: {
944
- config: s.sandbox.config,
945
- nested: s.sandbox.nested.map((d) => path.relative(this.o.root, d)),
946
- },
947
- }),
948
- })),
949
- }),
950
- } as RunOrder,
951
- }
946
+ const order = {
947
+ task: req.taskId,
948
+ // Unsaved: under the run's own key, as an uncached task's.
949
+ ...(req.cacheKey === undefined || req.unsaved === true ? {} : { key: req.cacheKey }),
950
+ runKey: runKey(this.o.run, req.taskId, this.sends(req.taskId)),
951
+ // A held key keeps its entry, and restored here it replaced the
952
+ // forced run's fresh bytes with the ones it bypassed (2026-10-25).
953
+ ...(req.refresh === true ? { fresh: true as const } : {}),
954
+ // A HEAD per upload was the workers' only remote probe: the run
955
+ // already knows the key is in no cache (2026-10-25).
956
+ ...(req.refresh !== true && this.missed.has(req.taskId) ? { absent: true as const } : {}),
957
+ deps,
958
+ nested: node === undefined ? [] : nestedIn(nodes!, node.projectDir, this.o.root),
959
+ command: withForwardArgs(req.command, req.forwardArgs),
960
+ cwd: path.relative(this.o.root, req.cwd),
961
+ project: node === undefined ? '' : path.relative(this.o.root, node.projectDir),
962
+ env,
963
+ ...(node?.config.exec?.env?.secret === undefined
964
+ ? {}
965
+ : { secret: [...node.config.exec.env.secret] }),
966
+ ...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
967
+ outputs: {
968
+ files: [...req.outputs.files],
969
+ workspaceFiles: [...req.outputs.workspaceFiles],
970
+ },
971
+ cores: job.held?.cores ?? 1,
972
+ memory: job.held?.memory ?? 0,
973
+ ...(req.sandbox === undefined || node?.config.exec?.sandbox === undefined
974
+ ? {}
975
+ : {
976
+ sandbox: {
977
+ config: node.config.exec.sandbox,
978
+ ...(req.sandbox.keyed === undefined
979
+ ? {}
980
+ : { keyed: req.sandbox.keyed.map((d) => path.relative(this.o.root, d)) }),
981
+ ...(req.sandbox.dependencyReads === undefined
982
+ ? {}
983
+ : {
984
+ reads: req.sandbox.dependencyReads.map((d) => path.relative(this.o.root, d)),
985
+ }),
986
+ },
987
+ }),
988
+ ...runtimeOrders(req),
989
+ ...(req.servers === undefined || req.servers.length === 0
990
+ ? {}
991
+ : {
992
+ servers: req.servers.map((s) => ({
993
+ task: s.taskId,
994
+ command: s.command,
995
+ cwd: path.relative(this.o.root, s.cwd),
996
+ env: defined(s.env, {}),
997
+ ...(s.readyWhen === undefined ? {} : { readyWhen: s.readyWhen }),
998
+ ...(s.timeoutMs === undefined ? {} : { timeoutMs: s.timeoutMs }),
999
+ ...(s.sandbox === undefined
1000
+ ? {}
1001
+ : {
1002
+ sandbox: {
1003
+ config: s.sandbox.config,
1004
+ nested: s.sandbox.nested.map((d) => path.relative(this.o.root, d)),
1005
+ },
1006
+ }),
1007
+ })),
1008
+ }),
1009
+ } as RunOrder
1010
+ return { job, order }
952
1011
  })
953
1012
  const sandboxed = orders.some(
954
1013
  (o) => o.order.sandbox !== undefined || o.order.servers?.some((s) => s.sandbox !== undefined),
@@ -958,13 +1017,32 @@ export class Orchestrator {
958
1017
  const pending = new Set(jobs)
959
1018
  a.chain = a.chain
960
1019
  .then(async () => {
961
- for (const [i, part] of parts(orders, BATCH_BYTES).entries()) {
1020
+ // A dependency the worker was sent under the same key goes by name
1021
+ // alone: each order named every upstream whole, and 8000 tasks of a
1022
+ // deep graph sent 219 MB (2026-11-05). Read here, as sends go in
1023
+ // order: one sent before it fails is not counted as sent.
1024
+ const fresh = new Map<string, string>()
1025
+ const wired = orders.map(({ job, order }) => {
1026
+ const intro: [string, string][] = []
1027
+ const deps = order.deps.map((d): WiredDep => {
1028
+ if ((fresh.get(d.task) ?? a.sentDeps.get(d.task)) === d.key) return d.task
1029
+ fresh.set(d.task, d.key)
1030
+ intro.push([d.task, d.key])
1031
+ return d
1032
+ })
1033
+ const sent: SentOrder = { ...order, deps }
1034
+ // Measured once: a giant repo's were measured three times each (2026-11-05).
1035
+ return { job, order: sent, intro, bytes: bytesOf(sent) }
1036
+ })
1037
+ for (const [i, part] of parts(wired, BATCH_BYTES).entries()) {
962
1038
  // An order too long for a frame (a task of thousands of
963
1039
  // dependencies was refused, 413) sends its deps ahead.
964
- const tasks: RunOrder[] = []
965
- for (const { order } of part) {
966
- if (bytesOf(order) <= BATCH_BYTES) {
1040
+ const tasks: SentOrder[] = []
1041
+ let bytes = 0
1042
+ for (const { order, bytes: size } of part) {
1043
+ if (size <= BATCH_BYTES) {
967
1044
  tasks.push(order)
1045
+ bytes += size
968
1046
  continue
969
1047
  }
970
1048
  for (const deps of chunks(order.deps, BATCH_BYTES)) {
@@ -973,10 +1051,15 @@ export class Orchestrator {
973
1051
  a.usage.messagesIn++
974
1052
  a.usage.bytesIn += bytesOf(ahead)
975
1053
  }
976
- tasks.push({ ...order, deps: [] })
1054
+ const alone = { ...order, deps: [] }
1055
+ tasks.push(alone)
1056
+ bytes += bytesOf(alone)
977
1057
  }
1058
+ const n = ++a.batches
1059
+ for (const { job } of part) job.batch = n
978
1060
  const batch = {
979
1061
  type: 'batch',
1062
+ n,
980
1063
  root: this.o.root,
981
1064
  realRoot: this.realRoot,
982
1065
  tasks,
@@ -984,11 +1067,15 @@ export class Orchestrator {
984
1067
  } satisfies ToWorker
985
1068
  await this.o.me.send(a.id, batch)
986
1069
  a.usage.messagesIn++
987
- a.usage.bytesIn += bytesOf(batch)
1070
+ // Its orders' bytes, a comma between each, in the batch's own.
1071
+ a.usage.bytesIn += bytesOf({ ...batch, tasks: [] }) + bytes + tasks.length - 1
1072
+ for (const { intro } of part) for (const [t, k] of intro) a.sentDeps.set(t, k)
988
1073
  for (const { job } of part) {
989
- this.lease(job)
990
1074
  pending.delete(job)
1075
+ // Sent, it runs: its worker takes only what it has room for.
1076
+ this.begun(a, job.req.taskId)
991
1077
  }
1078
+ this.pumpSoon()
992
1079
  }
993
1080
  })
994
1081
  .catch((err: unknown) => {
@@ -1006,13 +1093,8 @@ export class Orchestrator {
1006
1093
  })
1007
1094
  }
1008
1095
 
1009
- /** Three of the worker's `held` from now. */
1010
- private lease(job: Job): void {
1011
- job.leaseUntil = performance.now() + 3 * HELD_EVERY * this.o.me.heartbeatMs
1012
- }
1013
-
1014
1096
  /**
1015
- * Worker `a` let `job` down: lost, its lease ended, or it could not run
1097
+ * Worker `a` let `job` down: lost, its report named it no more, or it could not run
1016
1098
  * it. One it had begun counts, and two workers fail it; one still in its
1017
1099
  * queue goes back free.
1018
1100
  */
@@ -1032,9 +1114,10 @@ export class Orchestrator {
1032
1114
  a.has.delete(job.req.taskId)
1033
1115
  this.unhold(a, job)
1034
1116
  delete job.worker
1035
- delete job.leaseUntil
1036
1117
  delete job.sentAt
1118
+ delete job.ended
1037
1119
  delete job.ranAt
1120
+ delete job.batch
1038
1121
  const begun = a.started.delete(job.req.taskId)
1039
1122
  // Stopped, it runs nowhere again; the cancel sent its worker has no one to answer it.
1040
1123
  if (job.req.signal?.aborted === true) {
@@ -1078,25 +1161,6 @@ export class Orchestrator {
1078
1161
  this.o.stalled?.(lines.join('\n'))
1079
1162
  }
1080
1163
 
1081
- /** Each task whose worker's lease on it ended goes back. */
1082
- private expire(): void {
1083
- // With no socket no `held` arrives: the leases wait for it (`resumed`).
1084
- if (!this.o.me.online) return
1085
- const now = performance.now()
1086
- const back: Job[] = []
1087
- for (const a of this.workers.values())
1088
- for (const j of [...a.busy.values()])
1089
- if (j.leaseUntil !== undefined && j.leaseUntil < now) {
1090
- const again = this.retry(a, j, 'lease expired')
1091
- if (again !== undefined) back.push(again)
1092
- }
1093
- if (back.length === 0) return
1094
- this.queue.unshift(...back)
1095
- this.o.faulted?.()
1096
- if (this.spawnedAt.size > 0) this.stranded(`a task's lease ended and no worker is left`)
1097
- this.pumpSoon()
1098
- }
1099
-
1100
1164
  private post(a: Worker, m: ToWorker): void {
1101
1165
  a.chain = a.chain.then(() => this.o.me.send(a.id, m)).catch(this.unsent(m.type, a.label))
1102
1166
  }
@@ -1118,16 +1182,19 @@ export class Orchestrator {
1118
1182
  }
1119
1183
 
1120
1184
  /** Worker `a` ran `task` and saved it under `key`: it is done there, and done here once restored. */
1121
- private async finish(a: Worker, task: string, key: string, head: WiredResult): Promise<void> {
1185
+ private async finish(
1186
+ a: Worker,
1187
+ task: string,
1188
+ key: string,
1189
+ head: WiredResult,
1190
+ endedAt: number | undefined,
1191
+ ): Promise<void> {
1122
1192
  const job = a.busy.get(task)
1123
1193
  if (job === undefined) return
1124
1194
  // A dependency's remote copy did not read back there (a truncated
1125
1195
  // upload): sent again from this machine, the task runs again. Taken as
1126
1196
  // the task's failure, it failed every run, the remote saying it held the
1127
1197
  // key (2026-10-25).
1128
- // Ended there, it is in no `held` again: a lease running on through a
1129
- // slow resend called its worker faulty and gave the task to another.
1130
- delete job.leaseUntil
1131
1198
  if (head.unread !== undefined) {
1132
1199
  const sent = await this.resend(job, head.unread)
1133
1200
  // Its worker was lost meanwhile: the task went back to the queue.
@@ -1139,13 +1206,13 @@ export class Orchestrator {
1139
1206
  a.started.delete(task)
1140
1207
  const again = this.retry(a, job, `could not read ${head.unread.join(', ')}`)
1141
1208
  if (again !== undefined) {
1142
- this.queue.unshift(again)
1209
+ this.enqueue([again], true)
1143
1210
  this.pumpSoon()
1144
1211
  }
1145
1212
  return
1146
1213
  }
1147
1214
  }
1148
- const endAt = performance.now()
1215
+ const endAt = endedAt ?? performance.now()
1149
1216
  this.traceTask(a, job, head, endAt)
1150
1217
  a.busy.delete(task)
1151
1218
  this.unhold(a, job)
@@ -1154,7 +1221,15 @@ export class Orchestrator {
1154
1221
  a.done.push(task)
1155
1222
  a.usage.record(head, job.sentAt)
1156
1223
  const share = shareOf(head.cpuShare)
1157
- const held = Math.max(1, job.startedAt === undefined ? head.durationMs : endAt - job.startedAt)
1224
+ // At least as long as it ran: begun here once its batch's send resolved,
1225
+ // which can be after its worker began it, it was seen for less, and its
1226
+ // CPU over that read as more cores than it had (#5036: 1.001 each, so
1227
+ // two that ran side by side on 2 cores replayed one after the other).
1228
+ const held = Math.max(
1229
+ 1,
1230
+ head.durationMs,
1231
+ job.startedAt === undefined ? 0 : endAt - job.startedAt,
1232
+ )
1158
1233
  this.took.set(task, {
1159
1234
  ms: held,
1160
1235
  cores:
@@ -1180,11 +1255,9 @@ export class Orchestrator {
1180
1255
  this.needMs += Math.min(job.need.cores, a.cores) * ms
1181
1256
  }
1182
1257
  this.savedAs.set(task, key)
1258
+ this.ranOn.set(task, a.id)
1183
1259
  const node = this.o.nodes()?.get(task)
1184
- // No remote took its upload (too big, a 429): its files are there
1185
- // alone, as an uncached task's are, and its output came with its `end`.
1186
- const unshared = head.unshared === true
1187
- if (head.exitCode === 0 && (unshared || (node !== undefined && leaves(node))))
1260
+ if (head.exitCode === 0 && node !== undefined && leaves(node))
1188
1261
  this.kept.set(task, { worker: a.id, req: job.req, need: job.need })
1189
1262
  this.pumpSoon()
1190
1263
  if (job.again !== undefined) return job.resolve(result(head, []))
@@ -1202,17 +1275,14 @@ export class Orchestrator {
1202
1275
  const deferred = job.req.remoteOnly !== true && job.req.download === 'deferred'
1203
1276
  const read = () =>
1204
1277
  job.req.remoteOnly === true || deferred ? this.o.artifacts.output(key) : restore()
1205
- const log = await (
1206
- unshared
1207
- ? Promise.resolve(wiredLog(head.output))
1208
- : // A failed download reads as a miss: asked once more, since one
1209
- // stalled download failed a CI run whose upload had landed
1210
- // (main, 2026-10-28).
1211
- read().then((got) => got ?? read())
1212
- ).catch((err: unknown) => {
1213
- job.reject(err)
1214
- return undefined
1215
- })
1278
+ // A failed download reads as a miss: asked once more, since one stalled
1279
+ // download failed a CI run whose upload had landed (main, 2026-10-28).
1280
+ const log = await read()
1281
+ .then((got) => got ?? read())
1282
+ .catch((err: unknown) => {
1283
+ job.reject(err)
1284
+ return undefined
1285
+ })
1216
1286
  this.trace.slice({
1217
1287
  track: ORCHESTRATOR,
1218
1288
  name: task,
@@ -1224,14 +1294,14 @@ export class Orchestrator {
1224
1294
  if (log === null) {
1225
1295
  job.reject(
1226
1296
  new Error(
1227
- `${task}: ${a.label} ran it, and no cache this machine reads gave its entry, asked twice: a download failed (warned above), ${a.label}'s upload failed (its log says why), or the workers and this machine share no remote cache the workers write`,
1297
+ `${task}: ${a.label} ran it, and no cache this machine reads gave its entry, asked twice: a download failed (warned above), or the workers and this machine share no remote cache the workers write`,
1228
1298
  ),
1229
1299
  )
1230
1300
  return
1231
1301
  }
1232
1302
  // An upstream's file the task removed there is gone here too: a restore
1233
1303
  // lays files, never removes one, and this disk is what core saves from.
1234
- if (head.removed !== undefined && job.req.remoteOnly !== true && !deferred && !unshared) {
1304
+ if (head.removed !== undefined && job.req.remoteOnly !== true && !deferred) {
1235
1305
  const ours = names(
1236
1306
  path.relative(this.o.root, node?.projectDir ?? job.req.cwd),
1237
1307
  job.req.outputs,
@@ -1267,29 +1337,17 @@ export class Orchestrator {
1267
1337
  for (const c of log) (c.err ? job.req.onStderr : job.req.onStdout)(c.text)
1268
1338
  job.resolve({
1269
1339
  ...result(head, log),
1270
- ...(unshared
1340
+ ...(deferred
1271
1341
  ? {
1272
1342
  outputs: {
1273
1343
  kind: 'deferred' as const,
1274
- materialize: () =>
1275
- Promise.reject(
1276
- new UserError(
1277
- `${task}: its files are on ${a.label} alone: no remote cache took its upload (its log says why)`,
1278
- ),
1279
- ),
1344
+ materialize: async () => {
1345
+ if ((await restore()) === null)
1346
+ throw new Error(`${task}'s entry ${key} is in no cache this machine reads`)
1347
+ },
1280
1348
  },
1281
1349
  }
1282
- : deferred
1283
- ? {
1284
- outputs: {
1285
- kind: 'deferred' as const,
1286
- materialize: async () => {
1287
- if ((await restore()) === null)
1288
- throw new Error(`${task}'s entry ${key} is in no cache this machine reads`)
1289
- },
1290
- },
1291
- }
1292
- : {}),
1350
+ : {}),
1293
1351
  where: a.label,
1294
1352
  machine: {
1295
1353
  cores: a.reported.cores,
@@ -1374,6 +1432,7 @@ export class Orchestrator {
1374
1432
  if (info.id !== undefined && this.late.delete(info.id)) {
1375
1433
  this.cancelled.delete(info.id)
1376
1434
  this.unstarted.delete(info.id)
1435
+ this.o.kept?.(info.id)
1377
1436
  }
1378
1437
  if (info.id !== undefined && this.cancelled.has(info.id)) {
1379
1438
  this.turnedAway.add(info.id)
@@ -1422,7 +1481,9 @@ export class Orchestrator {
1422
1481
  label,
1423
1482
  busy: new Map(),
1424
1483
  started: new Set(),
1484
+ batches: 0,
1425
1485
  has: new Set(),
1486
+ sentDeps: new Map(),
1426
1487
  // Its name first, before any batch.
1427
1488
  chain: this.cache()
1428
1489
  .then((cache) =>
@@ -1459,7 +1520,7 @@ export class Orchestrator {
1459
1520
  const again = this.retry(a, j, 'lost')
1460
1521
  if (again !== undefined) back.push(again)
1461
1522
  }
1462
- this.queue.unshift(...back)
1523
+ this.enqueue(back, true)
1463
1524
  this.o.lost?.(a.label)
1464
1525
  // Workers that join on their own may yet join; ones this run starts
1465
1526
  // are all it gets.
@@ -1487,67 +1548,99 @@ export class Orchestrator {
1487
1548
  }
1488
1549
  const a = this.workers.get(from)
1489
1550
  if (a === undefined) continue
1490
- const m = body as ToOrchestrator
1491
- if ('load' in m) this.heard(a, m.load)
1492
- if (m.type !== 'held') this.movedAt = performance.now()
1493
- if (m.type === 'start') {
1494
- this.begun(a, m.task)
1495
- this.pumpSoon()
1496
- } else if (m.type === 'held') {
1497
- for (const t of m.running) {
1498
- const j = a.busy.get(t)
1499
- if (j?.leaseUntil !== undefined) this.lease(j)
1500
- this.begun(a, t)
1501
- }
1502
- this.pumpSoon()
1503
- } else if (m.type === 'ran') {
1504
- // Its command ended: its cores are the next task's while it saves.
1505
- const j = a.busy.get(m.task)
1506
- if (j === undefined) continue
1507
- this.begun(a, m.task)
1508
- this.unhold(a, j)
1509
- // Its worker still says it holds it while it saves.
1510
- j.ranAt = performance.now()
1511
- this.pumpSoon()
1512
- } else if (m.type === 'fault') {
1513
- const j = a.busy.get(m.task)
1514
- if (j === undefined) continue
1515
- // It began it whether or not its `start` came first.
1516
- this.begun(a, m.task)
1517
- if (this.retry(a, j, m.reason) !== undefined) this.queue.unshift(j)
1518
- if (a.faulty) this.o.faulted?.()
1519
- if (a.faulty && this.spawnedAt.size > 0)
1520
- this.stranded(`${a.label} could not run ${m.task} (${m.reason}) and no worker is left`)
1521
- this.pumpSoon()
1522
- } else if (m.type === 'part') {
1523
- a.usage.messagesOut++
1524
- a.usage.bytesOut += bytesOf(m)
1525
- const k = `${from}\0${m.task}`
1526
- const held = this.parts.get(k) ?? {}
1527
- for (const list of PART_LISTS) {
1528
- const items = m[list] as readonly unknown[] | undefined
1529
- if (items !== undefined) held[list] = [...(held[list] ?? []), ...items]
1530
- }
1531
- this.parts.set(k, held)
1532
- } else if (m.type === 'end') {
1533
- a.usage.messagesOut++
1534
- a.usage.bytesOut += bytesOf(m)
1535
- const k = `${from}\0${m.task}`
1536
- const held = this.parts.get(k)
1537
- this.parts.delete(k)
1538
- const result: Record<string, unknown> = { ...m.result }
1539
- for (const list of PART_LISTS) {
1540
- const items = m.result[list] as readonly unknown[] | undefined
1541
- if (held?.[list] !== undefined) result[list] = [...held[list], ...(items ?? [])]
1551
+ const said = body as ToOrchestrator
1552
+ const frame = said.type === 'together' ? said.messages : [said]
1553
+ // A worker's times, read against its clock as this frame left it:
1554
+ // no two machines' clocks agree.
1555
+ const now = performance.now()
1556
+ let at: number | undefined
1557
+ for (const m of frame) if ('load' in m && m.load?.at !== undefined) at = Number(m.load.at)
1558
+ const here = (t: unknown): number | undefined =>
1559
+ at === undefined || typeof t !== 'number' || !Number.isFinite(at - t)
1560
+ ? undefined
1561
+ : Math.min(now, now - (at - t))
1562
+ for (const m of frame) {
1563
+ if ('load' in m && m.load !== undefined) this.heard(a, m.load)
1564
+ if (m.type !== 'report') this.movedAt = performance.now()
1565
+ if (m.type === 'report') {
1566
+ // Every task of the batches it read that has not ended is in its
1567
+ // report: one missing it no longer runs, and goes back at once.
1568
+ const running = new Set(m.running)
1569
+ const read = Number(m.batch) || 0
1570
+ const back: Job[] = []
1571
+ for (const [t, j] of [...a.busy])
1572
+ if (
1573
+ a.started.has(t) &&
1574
+ j.ended !== true &&
1575
+ (j.batch ?? 0) <= read &&
1576
+ !running.has(t)
1577
+ ) {
1578
+ const again = this.retry(a, j, 'not in its report')
1579
+ if (again !== undefined) back.push(again)
1580
+ }
1581
+ if (back.length > 0) {
1582
+ this.enqueue(back, true)
1583
+ this.o.faulted?.()
1584
+ if (this.spawnedAt.size > 0)
1585
+ this.stranded(`${a.label} no longer runs a task it began and no worker is left`)
1586
+ }
1587
+ this.pumpSoon()
1588
+ } else if (m.type === 'ran') {
1589
+ // Its command ended: its cores are the next task's while it saves.
1590
+ const j = a.busy.get(m.task)
1591
+ if (j === undefined) continue
1592
+ this.begun(a, m.task)
1593
+ this.unhold(a, j)
1594
+ // Its worker still says it holds it while it saves.
1595
+ j.startedAt = here(m.startedAt) ?? j.startedAt ?? now
1596
+ j.ranAt = here(m.ranAt) ?? now
1597
+ this.pumpSoon()
1598
+ } else if (m.type === 'fault') {
1599
+ const j = a.busy.get(m.task)
1600
+ if (j === undefined) continue
1601
+ // It began it whether or not its `start` came first.
1602
+ this.begun(a, m.task)
1603
+ if (this.retry(a, j, m.reason) !== undefined) this.enqueue([j], true)
1604
+ if (a.faulty) this.o.faulted?.()
1605
+ if (a.faulty && this.spawnedAt.size > 0)
1606
+ this.stranded(
1607
+ `${a.label} could not run ${m.task} (${m.reason}) and no worker is left`,
1608
+ )
1609
+ this.pumpSoon()
1610
+ } else if (m.type === 'part') {
1611
+ a.usage.messagesOut++
1612
+ a.usage.bytesOut += bytesOf(m)
1613
+ const k = `${from}\0${m.task}`
1614
+ const held = this.parts.get(k) ?? {}
1615
+ for (const list of PART_LISTS) {
1616
+ const items = m[list] as readonly unknown[] | undefined
1617
+ if (items !== undefined) held[list] = [...(held[list] ?? []), ...items]
1618
+ }
1619
+ this.parts.set(k, held)
1620
+ } else if (m.type === 'end') {
1621
+ a.usage.messagesOut++
1622
+ a.usage.bytesOut += bytesOf(m)
1623
+ const k = `${from}\0${m.task}`
1624
+ const held = this.parts.get(k)
1625
+ this.parts.delete(k)
1626
+ const result: Record<string, unknown> = { ...m.result }
1627
+ for (const list of PART_LISTS) {
1628
+ const items = m.result[list] as readonly unknown[] | undefined
1629
+ if (held?.[list] !== undefined) result[list] = [...held[list], ...(items ?? [])]
1630
+ }
1631
+ // Not awaited: one task's restore holds no other message.
1632
+ const j = a.busy.get(m.task)
1633
+ if (j !== undefined) {
1634
+ j.ended = true
1635
+ const began = here(m.startedAt)
1636
+ const ran = here(m.ranAt)
1637
+ if (began !== undefined) j.startedAt = began
1638
+ if (ran !== undefined) j.ranAt = ran
1639
+ }
1640
+ void this.finish(a, m.task, m.key, result as WiredResult, here(m.endedAt))
1542
1641
  }
1543
- // Not awaited: one task's restore holds no other message.
1544
- void this.finish(a, m.task, m.key, result as WiredResult)
1545
1642
  }
1546
1643
  }
1547
- // After the batch: a `held` in it renews its lease first. Expired
1548
- // before, a stalled loop's waiting `held` came too late and the task
1549
- // ran again on another worker (2026-10-27).
1550
- this.expire()
1551
1644
  this.stall()
1552
1645
  }
1553
1646
  }
@@ -1556,7 +1649,7 @@ export class Orchestrator {
1556
1649
  where(taskId: string): string | null | undefined {
1557
1650
  for (const a of this.workers.values())
1558
1651
  if (a.busy.has(taskId)) return a.started.has(taskId) ? a.label : null
1559
- return this.queue.some((j) => j.req.taskId === taskId) ? null : undefined
1652
+ return this.waiting.has(taskId) ? null : undefined
1560
1653
  }
1561
1654
 
1562
1655
  /**
@@ -1729,7 +1822,8 @@ export class Orchestrator {
1729
1822
  id: j.req.taskId,
1730
1823
  cores: Math.min(a.cores, Math.max((j.held?.cores ?? 0) * over, j.need.ask?.cores ?? 0)),
1731
1824
  memory: Math.max(j.held?.memory ?? 0, Math.min(a.memory, j.need.ask?.memory ?? 0)),
1732
- endsIn: Math.max(0, (ms(j.req.taskId) ?? 0) - (now - j.startedAt!)),
1825
+ // Below 0: past its time, which the plan weighs (`decide`).
1826
+ endsIn: (ms(j.req.taskId) ?? 0) - (now - j.startedAt!),
1733
1827
  })),
1734
1828
  })
1735
1829
  }
@@ -1877,7 +1971,11 @@ export class Orchestrator {
1877
1971
  private fail(reason: string): void {
1878
1972
  for (const a of this.workers.values())
1879
1973
  for (const j of a.busy.values()) j.reject(new Error(reason))
1880
- for (const j of this.queue.splice(0)) j.reject(new Error(reason))
1974
+ for (const [id, j] of this.waiting) {
1975
+ this.queue.delete(id)
1976
+ j.reject(new Error(reason))
1977
+ }
1978
+ this.waiting.clear()
1881
1979
  this.workers.clear()
1882
1980
  this.lost = reason
1883
1981
  }
@@ -1901,13 +1999,8 @@ export class Orchestrator {
1901
1999
  * waits on its deps through those no worker ran (groups, hits).
1902
2000
  */
1903
2001
  idealMs(depsOf: (id: string) => readonly string[], sizes: readonly Size[]): number | undefined {
1904
- if (this.took.size === 0) return undefined
1905
- const all = [...this.workers.values(), ...this.gone]
1906
- const warm = new Map<string | undefined, number>()
1907
- for (const a of all)
1908
- if (a.warmUpMs !== undefined && !(warm.get(a.platform)! <= a.warmUpMs))
1909
- warm.set(a.platform, a.warmUpMs)
1910
- const least = this.warmUpMs() ?? 0
2002
+ const r = this.replay(depsOf)
2003
+ if (r === undefined) return undefined
1911
2004
  const pool: PlanWorker[] = (
1912
2005
  sizes.length > 0
1913
2006
  ? sizes.flatMap((s) =>
@@ -1917,14 +2010,104 @@ export class Orchestrator {
1917
2010
  platform: s.platform,
1918
2011
  })),
1919
2012
  )
1920
- : all.map((a) => ({ cores: a.cores, memory: a.memory, platform: a.platform }))
2013
+ : [...this.workers.values(), ...this.gone].map((a) => ({
2014
+ cores: a.cores,
2015
+ memory: a.memory,
2016
+ platform: a.platform,
2017
+ }))
1921
2018
  ).map((w, i) => ({
1922
2019
  ...w,
1923
2020
  id: `ideal#${i}`,
1924
- at: warm.get(w.platform) ?? least,
2021
+ at: r.warm(w.platform),
1925
2022
  running: [],
1926
2023
  idle: false,
1927
2024
  }))
2025
+ const end = simulate(r.tasks, pool)
2026
+ if (!Number.isFinite(end)) return undefined
2027
+ return r.from + end
2028
+ }
2029
+
2030
+ /**
2031
+ * The workers that would have run this run best (`advise`), from the
2032
+ * same replay as `idealMs`: on `sizes`, past their `max`, and of any
2033
+ * cores. With no sizes (workers that join on their own), the shapes
2034
+ * that joined stand for them, as many as joined.
2035
+ */
2036
+ advice(depsOf: (id: string) => readonly string[], sizes: readonly Size[]): WorkerAdvice[] {
2037
+ // The footer and the summary each ask once the run ended: played once.
2038
+ if (this.ended && this.advised !== undefined) return this.advised
2039
+ const out = this.advise(depsOf, sizes)
2040
+ if (this.ended) this.advised = out
2041
+ return out
2042
+ }
2043
+
2044
+ private advised: WorkerAdvice[] | undefined
2045
+
2046
+ private advise(
2047
+ depsOf: (id: string) => readonly string[],
2048
+ sizes: readonly Size[],
2049
+ ): WorkerAdvice[] {
2050
+ const r = this.replay(depsOf)
2051
+ if (r === undefined) return []
2052
+ const shapes: Size[] =
2053
+ sizes.length > 0
2054
+ ? [...sizes]
2055
+ : [
2056
+ ...[...this.workers.values(), ...this.gone]
2057
+ .reduce((by, a) => {
2058
+ const name = `${a.cores}c/${a.platform ?? ''}`
2059
+ const had = by.get(name)
2060
+ by.set(name, {
2061
+ name,
2062
+ max: (had?.max ?? 0) + 1,
2063
+ cpu: a.cores,
2064
+ memory: a.memory,
2065
+ platform: a.platform,
2066
+ })
2067
+ return by
2068
+ }, new Map<string, Size>())
2069
+ .values(),
2070
+ ]
2071
+ const a = advise(r.tasks, shapes, r.warm)
2072
+ const wire = (of: WorkerAdvice['of'], f: Fleets | undefined): WorkerAdvice[] =>
2073
+ f === undefined
2074
+ ? []
2075
+ : [
2076
+ {
2077
+ of,
2078
+ endMs: Math.round(r.from + f.endMs),
2079
+ workers: f.fleet.map((w) => ({
2080
+ count: w.count,
2081
+ cores: w.cores,
2082
+ memoryBytes: w.memory,
2083
+ ...(w.platform === undefined ? {} : { platform: w.platform }),
2084
+ ...(w.size === undefined || sizes.length === 0 ? {} : { size: w.size }),
2085
+ })),
2086
+ },
2087
+ ]
2088
+ return [...wire('declared', a.declared), ...wire('more', a.more), ...wire('any', a.any)]
2089
+ }
2090
+
2091
+ /**
2092
+ * Each task a worker ran, as long as it held its worker and with the
2093
+ * cores it used meanwhile, waiting on its deps through those no worker
2094
+ * ran (groups, hits); each platform's fastest real warm-up; and when
2095
+ * the run first knew its work.
2096
+ */
2097
+ private replay(depsOf: (id: string) => readonly string[]):
2098
+ | {
2099
+ tasks: PlanTask[]
2100
+ warm: (platform: string | undefined) => number
2101
+ from: number
2102
+ }
2103
+ | undefined {
2104
+ if (this.took.size === 0) return undefined
2105
+ const all = [...this.workers.values(), ...this.gone]
2106
+ const warm = new Map<string | undefined, number>()
2107
+ for (const a of all)
2108
+ if (a.warmUpMs !== undefined && !(warm.get(a.platform)! <= a.warmUpMs))
2109
+ warm.set(a.platform, a.warmUpMs)
2110
+ const least = this.warmUpMs() ?? 0
1928
2111
  const seen = new Map<string, string[]>()
1929
2112
  const ran = (id: string): string[] => {
1930
2113
  const known = seen.get(id)
@@ -1934,10 +2117,11 @@ export class Orchestrator {
1934
2117
  seen.set(id, out)
1935
2118
  return out
1936
2119
  }
1937
- const tasks: PlanTask[] = [...this.took].map(([id, t]) => ({ id, ...t, deps: ran(id) }))
1938
- const end = simulate(tasks, pool)
1939
- if (!Number.isFinite(end)) return undefined
1940
- return (this.probe?.at ?? this.asks[0]?.at ?? 0) + end
2120
+ return {
2121
+ tasks: [...this.took].map(([id, t]) => ({ id, ...t, deps: ran(id) })),
2122
+ warm: (platform) => warm.get(platform) ?? least,
2123
+ from: this.probe?.at ?? this.asks[0]?.at ?? 0,
2124
+ }
1941
2125
  }
1942
2126
 
1943
2127
  /**
@@ -1996,7 +2180,10 @@ export class Orchestrator {
1996
2180
  * each, the cores and memory its size declares and its machine reports;
1997
2181
  * and `ideal`, the run's `idealMs`, asked once it ended.
1998
2182
  */
1999
- footer(ideal?: () => number | undefined): [string, ui.Inline][] {
2183
+ footer(
2184
+ ideal?: () => number | undefined,
2185
+ advice?: () => readonly WorkerAdvice[],
2186
+ ): [string, ui.Inline][] {
2000
2187
  // Over with none, it says so: core's own `N workers` (this machine's
2001
2188
  // pool) read as the run's when no task needed one (2026-10-25).
2002
2189
  if (this.spawnedAt.size === 0 && this.workers.size === 0 && this.gone.length === 0)
@@ -2127,6 +2314,17 @@ export class Orchestrator {
2127
2314
  ),
2128
2315
  ] as [string, ui.Inline],
2129
2316
  ])(ideal?.()),
2317
+ // What to declare next time (2026-11-05: "So I can add more
2318
+ // or change their specs").
2319
+ ...(advice?.() ?? []).map(
2320
+ (a) =>
2321
+ [
2322
+ '',
2323
+ ui.dim(
2324
+ `${ADVICE[a.of][0].padEnd(width)} ${a.workers.map(fleetText).join(' + ')}: ${seconds(a.endMs)}, ${ADVICE[a.of][1]}`,
2325
+ ),
2326
+ ] as [string, ui.Inline],
2327
+ ),
2130
2328
  ...(this.unmeasured.length === 0
2131
2329
  ? []
2132
2330
  : [
@@ -2155,7 +2353,7 @@ export class Orchestrator {
2155
2353
  }
2156
2354
 
2157
2355
  function result(r: WiredResult, log: readonly CapturedChunk[]): ExecuteResult {
2158
- const { cpuShare: _, removed: __, unread: ___, unshared: ____, output: _____, ...rest } = r
2356
+ const { cpuShare: _, removed: __, unread: ___, ...rest } = r
2159
2357
  const violations = (r.violations ?? []).map((v) => ({ ...v, timestamp: new Date(v.timestamp) }))
2160
2358
  const of = (err: boolean) =>
2161
2359
  log
@@ -2165,18 +2363,6 @@ function result(r: WiredResult, log: readonly CapturedChunk[]): ExecuteResult {
2165
2363
  return { ...rest, stdout: of(false), stderr: of(true), violations, outputs: { kind: 'disk' } }
2166
2364
  }
2167
2365
 
2168
- /** A worker's `output`, read at the boundary: its chunks of text. */
2169
- function wiredLog(output: unknown): CapturedChunk[] {
2170
- return Array.isArray(output)
2171
- ? output
2172
- .filter(
2173
- (c): c is CapturedChunk =>
2174
- typeof c === 'object' && c !== null && typeof c.text === 'string',
2175
- )
2176
- .map((c) => ({ text: c.text, err: c.err === true }))
2177
- : []
2178
- }
2179
-
2180
2366
  /**
2181
2367
  * A worker's `cpuShare`, read at the boundary: in (0, 1], else 1. A twentieth
2182
2368
  * at least: a share that small is a misread, and dividing by it would book
@@ -2204,15 +2390,28 @@ export function nestedIn(
2204
2390
  ): string[] {
2205
2391
  let of = nested.get(nodes)
2206
2392
  if (of === undefined) {
2207
- of = { dirs: [...new Set([...nodes.values()].map((n) => n.projectDir))], by: new Map() }
2393
+ of = {
2394
+ dirs: [...new Set([...nodes.values()].map((n) => n.projectDir))].sort(),
2395
+ by: new Map(),
2396
+ }
2208
2397
  nested.set(nodes, of)
2209
2398
  }
2210
2399
  let out = of.by.get(dir)
2211
2400
  if (out === undefined) {
2212
- out = of.dirs
2213
- .filter((d) => d.startsWith(dir + path.sep))
2214
- .map((d) => path.relative(root, d))
2215
- .sort()
2401
+ // Sorted, the ones under `dir` lie together: each project read every
2402
+ // project, 16 million reads for 4000 (2026-11-05).
2403
+ const under = dir + path.sep
2404
+ let lo = 0
2405
+ let hi = of.dirs.length
2406
+ while (lo < hi) {
2407
+ const mid = (lo + hi) >> 1
2408
+ if (of.dirs[mid]! < under) lo = mid + 1
2409
+ else hi = mid
2410
+ }
2411
+ const found: string[] = []
2412
+ for (let i = lo; i < of.dirs.length && of.dirs[i]!.startsWith(under); i++)
2413
+ found.push(path.relative(root, of.dirs[i]!))
2414
+ out = found.sort()
2216
2415
  of.by.set(dir, out)
2217
2416
  }
2218
2417
  return [...out]
@@ -2264,7 +2463,7 @@ function sandboxConfigs(nodes: ReadonlyMap<string, TaskNode> | undefined): Sandb
2264
2463
  * pinning every dependant to its worker held a run's shards on one.
2265
2464
  */
2266
2465
  function leaves(node: TaskNode): boolean {
2267
- if (node.config.cache !== undefined) return false
2466
+ if (node.config.cache === true) return false
2268
2467
  const sandbox = node.config.exec?.sandbox
2269
2468
  return sandbox === undefined || (sandbox.allow?.write?.length ?? 0) > 0
2270
2469
  }