@vzn/vx-dte 0.0.651 → 0.0.653

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,6 +25,7 @@ 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,
@@ -35,14 +36,24 @@ import {
35
36
  type DepOrder,
36
37
  type RunOrder,
37
38
  type RuntimeOrder,
39
+ type SentOrder,
40
+ type WiredDep,
38
41
  type ToWorker,
39
42
  type Load,
40
43
  type ToOrchestrator,
41
44
  type WiredResult,
42
45
  } from './messages.js'
43
- 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'
44
55
  import { Usage, type UsageStats } from './usage.js'
45
- import { assign, ordered, type Need } from './dispatch.js'
56
+ import { type Need, Queue } from './dispatch.js'
46
57
  import { DEFAULT_STRATEGY, type Strategy } from './strategy.js'
47
58
  import { RelayError, unsent, type Participant } from './wire.js'
48
59
  import { ORCHESTRATOR, Trace } from './trace.js'
@@ -52,7 +63,8 @@ import { names } from './outputs.js'
52
63
 
53
64
  interface Job {
54
65
  readonly req: ExecuteRequest
55
- readonly need: Need
66
+ /** Read as it joined the queue, and again while it waits when history changes (`rerank`). */
67
+ need: Need
56
68
  /** The cores and memory it holds on its worker: its need, or the whole worker for one that filled its worker. */
57
69
  held?: Need
58
70
  readonly resolve: (r: ExecuteResult) => void
@@ -84,8 +96,15 @@ interface Job {
84
96
  reread?: Set<string>
85
97
  /** Its upstreams, once the graph named it: walked again at every pump, 8000 queued spent 5 ms a pump on it (2026-10-25). */
86
98
  up?: Set<string>
99
+ /** Its direct dependencies, as placing reads them (`Ask.up`). */
100
+ deps?: readonly string[] | undefined
101
+ /** A retry: the worker its last attempt ran on, which it goes elsewhere than while another can take it. */
102
+ avoid?: string
87
103
  }
88
104
 
105
+ /** An upstream's files, as an order names them (`DepOrder`). */
106
+ type DepFiles = Pick<DepOrder, 'project' | 'outputs'>
107
+
89
108
  /** 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). */
90
109
  const PIN_WAIT_MS = 10_000
91
110
 
@@ -123,6 +142,14 @@ interface Worker {
123
142
  load?: { cpu: number; memory: number; at: number }
124
143
  /** Tasks whose outputs it holds: it ran them, or was sent them. */
125
144
  readonly has: Set<string>
145
+ /**
146
+ * The persistent tasks it runs, each with the base it books there
147
+ * (`Need.servers`): booked once, by the first task placed there that
148
+ * needs it, and given back when the run says none needs it.
149
+ */
150
+ readonly serving: Map<string, number>
151
+ /** Each dependency it was sent whole, by the key it was sent under: the next order names it alone. */
152
+ readonly sentDeps: Map<string, string>
126
153
  /** Sends to it go out in order: a run's parts before the run. */
127
154
  chain: Promise<void>
128
155
  /** It was sent the run's sandbox configs. */
@@ -138,6 +165,8 @@ export interface Declared {
138
165
  readonly memory: number
139
166
  /** Its size's platform: a worker reporting another is refused. None: planned as this machine's. */
140
167
  readonly platform?: string
168
+ /** Where tasks match it when the run has several worker types: `<type>:<platform>`. */
169
+ readonly place?: string
141
170
  }
142
171
 
143
172
  export interface OrchestratorOptions {
@@ -147,6 +176,8 @@ export interface OrchestratorOptions {
147
176
  readonly tree: string
148
177
  readonly nodes: () => ReadonlyMap<string, TaskNode> | undefined
149
178
  readonly need: (taskId: string) => Need
179
+ /** Each server's base, cores, as workers fit them: updated in place. */
180
+ readonly bases?: Map<string, number>
150
181
  /** The run's cache, by key: what workers save to and this machine restores from. */
151
182
  readonly artifacts: TaskArtifacts
152
183
  /** What workers read the cache under (`ExecutorContext.cacheFacts`). */
@@ -177,6 +208,11 @@ export interface OrchestratorOptions {
177
208
  readonly how?: Strategy
178
209
  /** The platform a task naming none runs on (`homeOf`). */
179
210
  readonly home?: string | undefined
211
+ /** A joined worker's place, by its size and the platform it reported: absent, the platform. */
212
+ readonly joinedPlace?: (
213
+ size: string | undefined,
214
+ platform: string | undefined,
215
+ ) => string | undefined
180
216
  /** No task began or ended for `stallMs`: what the run waits on, one line a task. */
181
217
  readonly stalled?: (waiting: string) => void
182
218
  /** A fault the run works around (a message the relay would not take); default stderr. */
@@ -207,6 +243,17 @@ const BATCH_BYTES = 512 * 1024
207
243
  const FEW = 2
208
244
  /** The unmeasured tasks the summary names; the rest it counts. */
209
245
  const NAMED = 40
246
+ /** The footer's label of each `WorkerAdvice`. */
247
+ const ADVICE: Record<WorkerAdvice['of'], readonly [label: string, note: string]> = {
248
+ declared: ['best', 'the fewest of its sizes'],
249
+ more: ['more', "past its sizes' max"],
250
+ any: ['any', 'of any cores, the least core time'],
251
+ }
252
+ /** A platform as shown: a worker type's place (`ubuntu:linux-x64`) without its type, which its size names. */
253
+ const bare = (platform: string): string => platform.slice(platform.indexOf(':') + 1)
254
+ /** `3 × small (4c/16G linux)`. */
255
+ const fleetText = (w: WorkerAdvice['workers'][number]): string =>
256
+ `${w.count} × ${w.size === undefined ? '' : `${w.size} `}(${w.cores}c/${compact(w.memoryBytes)}${w.platform === undefined ? '' : ` ${w.platform}`})`
210
257
  /** Core-time of history the run's tasks finish before their use reshapes bookings: a few tasks' worth. */
211
258
  const LEARN_MS = 60_000
212
259
  /** The most a worker's bookings may add up to, in its cores, when tasks use less than their history. */
@@ -215,12 +262,12 @@ const OVERBOOK = 1.5
215
262
  const RESTARTS = 3
216
263
 
217
264
  /** `orders` in runs of at most `max` bytes of JSON; one bigger goes alone. */
218
- function parts<T extends { order: unknown }>(orders: readonly T[], max: number): T[][] {
265
+ function parts<T extends { bytes: number }>(orders: readonly T[], max: number): T[][] {
219
266
  const out: T[][] = []
220
267
  let part: T[] = []
221
268
  let bytes = 0
222
269
  for (const o of orders) {
223
- const size = bytesOf(o.order)
270
+ const size = o.bytes
224
271
  if (part.length > 0 && bytes + size > max) {
225
272
  out.push(part)
226
273
  part = []
@@ -259,9 +306,15 @@ export class Orchestrator {
259
306
  private readonly workers = new Map<string, Worker>()
260
307
  /** The run tree's files, workspace-relative, listed when a task first asks. */
261
308
  private inTree?: Promise<Tree | undefined>
262
- private readonly queue: Job[] = []
309
+ /** Waiting tasks, by id, in the order a pump reads them (`Queue`). */
310
+ private readonly waiting = new Map<string, Job>()
311
+ /** Handed by core and not yet waiting: held for a machine as the waiting are. */
312
+ private readonly handed = new Set<string>()
313
+ private readonly queue: Queue
263
314
  /** Where a worker saved each task it ran: its cache key or its run key. */
264
315
  private readonly savedAs = new Map<string, string>()
316
+ /** The worker each task last ran on, which its retry goes elsewhere than. */
317
+ private readonly ranOn = new Map<string, string>()
265
318
  /** Keys the run's requests named for their dependencies. */
266
319
  private readonly upstreamKeys = new Map<string, string>()
267
320
  /** How many times each task was sent: its run key is one per send. */
@@ -288,6 +341,10 @@ export class Orchestrator {
288
341
  private readonly retired = new Set<Worker>()
289
342
  /** Workers asked for that never joined, with why. */
290
343
  private readonly unstarted = new Map<string, string>()
344
+ /** A task waited here on a worker: one ended with none joined needed one. */
345
+ private waited = false
346
+ /** Why each worker this run refused was refused, in order. */
347
+ private readonly refused: string[] = []
291
348
  /** Started, then not needed before they joined (`release`), or given up as late. */
292
349
  private readonly cancelled = new Set<string>()
293
350
  /** Of `cancelled`, those given up past their join limit, not let go: taken if they join. */
@@ -354,9 +411,12 @@ export class Orchestrator {
354
411
  private relend: ReturnType<typeof setTimeout> | undefined
355
412
  /** The plugins any of the run's lends lent a token. */
356
413
  private readonly lenders = new Set<string>()
414
+ /** Each graph's upstreams as orders name them (`dep`). */
415
+ private readonly deps = new WeakMap<ReadonlyMap<string, TaskNode>, Map<string, DepFiles | null>>()
357
416
 
358
417
  constructor(private readonly o: OrchestratorOptions) {
359
418
  this.sent = o.sent ?? new Map()
419
+ this.queue = new Queue(o.how ?? DEFAULT_STRATEGY, o.tail, (id) => this.waiting.get(id)!.need)
360
420
  // A root that is gone fails the run's own reads first.
361
421
  this.realRoot = (() => {
362
422
  try {
@@ -371,6 +431,32 @@ export class Orchestrator {
371
431
 
372
432
  async execute(req: ExecuteRequest): Promise<ExecuteResult> {
373
433
  if (this.lost !== undefined) throw new Error(this.lost)
434
+ this.handed.add(req.taskId)
435
+ try {
436
+ return await this.take(req)
437
+ } finally {
438
+ this.handed.delete(req.taskId)
439
+ }
440
+ }
441
+
442
+ private async take(req: ExecuteRequest): Promise<ExecuteResult> {
443
+ // Walked once here, for every later read: walked again on the way in,
444
+ // sharing, placing and sending, a run of 8000 tasks spent 17 s of this
445
+ // machine's CPU on it (2026-11-05).
446
+ const nodes = this.o.nodes()
447
+ const up = upstream(nodes, req.taskId)
448
+ // A worker runs a task in its project's directory of the run's tree: a
449
+ // project whose config is untracked is not there, and every worker
450
+ // failed to spawn it, exit 127 (2026-11-07).
451
+ const dir = path.relative(this.o.root, req.cwd)
452
+ if (dir !== '' && !dir.startsWith('..')) {
453
+ this.inTree ??= listTree(this.o.root, this.o.tree)
454
+ const tree = await this.inTree
455
+ if (tree !== undefined && !holds(tree, dir))
456
+ throw new UserError(
457
+ `${req.taskId} runs in ${dir}, which is not in the commit workers check out: commit its project (its vx.config file is untracked)`,
458
+ )
459
+ }
374
460
  // A worker runs the run's tree: a file its key includes that the tree
375
461
  // lacks (untracked) is not there, and the output it saved under that
376
462
  // key was a stale hit for the next run (2026-10-24).
@@ -380,25 +466,16 @@ export class Orchestrator {
380
466
  // A dependency's output is restored there before the task runs: a
381
467
  // key that reads an upstream's bytes (`inputs.tasks` leaving it out)
382
468
  // was refused (2026-10-25).
383
- const nodes = this.o.nodes()
384
- const restored = [...upstream(nodes, req.taskId)].flatMap((id) => {
385
- const n = nodes?.get(id)
386
- const outs = n?.config.cache?.outputs
387
- return n === undefined || outs === undefined
388
- ? []
389
- : [
390
- names(path.relative(this.o.root, n.projectDir), {
391
- files: outs.files,
392
- workspaceFiles: outs.workspaceFiles ?? [],
393
- }),
394
- ]
395
- })
469
+ let restored: ((file: string) => boolean)[] | undefined
470
+ const made = (file: string): boolean =>
471
+ (restored ??= [...up].flatMap((id) => {
472
+ const d = this.dep(nodes, id)
473
+ return d === undefined ? [] : [names(d.project, d.outputs)]
474
+ })).some((m) => m(file))
396
475
  const stray =
397
476
  tree === undefined
398
477
  ? undefined
399
- : req.inputs.files.find(
400
- (f) => !tree.files.has(f.path) && !restored.some((made) => made(f.path)),
401
- )
478
+ : req.inputs.files.find((f) => !tree.files.has(f.path) && !made(f.path))
402
479
  if (stray !== undefined) {
403
480
  // A committed submodule's files are not the tree's: its gitlink is.
404
481
  const link = tree?.links.find((l) => stray.path.startsWith(`${l}/`))
@@ -408,22 +485,42 @@ export class Orchestrator {
408
485
  : `${req.taskId} keys ${stray.path}, in submodule ${link}, whose files workers do not check out: leave it out of the task's inputs`,
409
486
  )
410
487
  }
488
+ // A worker runs the commit's bytes and modes (`unfilter`): a file on
489
+ // disk otherwise (CRLF a git filter wrote, an expanded ident, an LFS
490
+ // file's content) is not what it builds on, and the output saved
491
+ // under the key of this disk's bytes was a stale hit (2026-11-08).
492
+ const off =
493
+ tree === undefined
494
+ ? undefined
495
+ : req.inputs.files.find((f) => {
496
+ const has = tree.files.get(f.path)
497
+ return has !== undefined && has !== f.digest && !made(f.path)
498
+ })
499
+ if (off !== undefined)
500
+ throw new UserError(
501
+ `${req.taskId} keys ${off.path}, which is on disk other than the commit workers check out holds it (a git filter's text, eol, ident or LFS bytes, or core.autocrlf): leave it out of the task's inputs, or run the task without dte`,
502
+ )
411
503
  }
412
504
  for (const u of req.inputs?.upstream ?? []) this.upstreamKeys.set(u.taskId, u.hash)
413
- await this.share(req.taskId)
505
+ await this.share(up, nodes)
414
506
  return new Promise((resolve, reject) => {
415
507
  const need = this.o.need(req.taskId)
416
508
  const job: Job = {
417
509
  req,
510
+ ...(nodes?.has(req.taskId) === true ? { up } : {}),
418
511
  need: { ...need, platform: req.platform ?? this.o.home },
419
512
  queuedAt: performance.now(),
420
513
  resolve,
421
514
  reject,
422
515
  }
516
+ // A retry runs elsewhere: the worker, or what ran beside it, may be the flake.
517
+ const last = (req.attempt ?? 1) > 1 ? this.ranOn.get(req.taskId) : undefined
518
+ if (last !== undefined) job.avoid = last
519
+ this.handed.delete(req.taskId)
423
520
  // A signal aborted already fires no `abort`.
424
521
  if (req.signal?.aborted === true) return resolve(stopped())
425
522
  req.signal?.addEventListener('abort', () => this.abort(job), { once: true })
426
- this.queue.push(job)
523
+ this.enqueue([job])
427
524
  this.pumpSoon()
428
525
  })
429
526
  }
@@ -498,10 +595,21 @@ export class Orchestrator {
498
595
  return this.trace.events(this.ended ? this.endedAt : performance.now())
499
596
  }
500
597
 
598
+ /** No task left in the run uses server `task`: each worker that started it stops it. */
599
+ unneeded(task: string): void {
600
+ for (const a of this.workers.values()) {
601
+ const base = a.serving.get(task)
602
+ if (base === undefined) continue
603
+ a.serving.delete(task)
604
+ a.usedCores -= base
605
+ this.post(a, { type: 'unserve', task })
606
+ }
607
+ }
608
+
501
609
  private abort(job: Job): void {
502
- const at = this.queue.indexOf(job)
503
- if (at >= 0) {
504
- this.queue.splice(at, 1)
610
+ if (this.waiting.get(job.req.taskId) === job) {
611
+ this.waiting.delete(job.req.taskId)
612
+ this.queue.delete(job.req.taskId)
505
613
  job.resolve(stopped())
506
614
  } else if (job.worker !== undefined) {
507
615
  const a = this.workers.get(job.worker)
@@ -519,27 +627,36 @@ export class Orchestrator {
519
627
  })
520
628
  }
521
629
 
630
+ /**
631
+ * History changed (`need`, `tail`): the waiting book and rank by it. On
632
+ * CI the default branch's timings land a moment after the roots joined
633
+ * the queue, each booked as unmeasured and ranked as an unknown task:
634
+ * shards ready later went first, and test.wire waited 129 s (2026-11-07).
635
+ */
636
+ rerank(): void {
637
+ for (const [id, job] of this.waiting)
638
+ job.need = { ...this.o.need(id), platform: job.need.platform }
639
+ this.queue.rerank()
640
+ this.pumpSoon()
641
+ }
642
+
643
+ /** `jobs` wait, in order, behind the waiting tasks or, `front`, before them. */
644
+ private enqueue(jobs: readonly Job[], front = false): void {
645
+ for (const j of jobs) this.waiting.set(j.req.taskId, j)
646
+ if (jobs.length > 0) this.waited = true
647
+ this.queue.add(
648
+ jobs.map((j) => j.req.taskId),
649
+ front,
650
+ )
651
+ }
652
+
522
653
  private pump(): void {
523
654
  const batches = new Map<Worker, Job[]>()
524
- const left: Job[] = []
525
655
  // Core asks in its critical-path order, but only as tasks get ready: a
526
656
  // long chain ready a moment late queued behind 46 shards ready at once,
527
657
  // and its 41 s build began 47 s in, the run's end (main, 2026-10-23).
528
- const tail = this.o.tail
529
- const how = this.o.how ?? DEFAULT_STRATEGY
530
- const waiting = this.queue.splice(0)
531
658
  const workers = [...this.workers.values()].filter((a) => !a.faulty)
532
- const byId = new Map(waiting.map((j) => [j.req.taskId, j]))
533
- const queue =
534
- tail === undefined
535
- ? waiting
536
- : ordered(
537
- waiting.map((j) => j.req.taskId),
538
- how.order,
539
- tail,
540
- (id) => byId.get(id)!.need,
541
- Math.max(1, ...workers.map((a) => a.cores)),
542
- ).map((id) => byId.get(id)!)
659
+ const byId = this.waiting
543
660
  const bins = workers.map((a) => {
544
661
  let running = 0
545
662
  for (const j of a.busy.values()) if (j.held !== undefined) running++
@@ -551,45 +668,32 @@ export class Orchestrator {
551
668
  running,
552
669
  has: a.has,
553
670
  platform: a.platform,
671
+ serving: new Set(a.serving.keys()),
554
672
  a,
555
673
  }
556
674
  })
557
675
  const nodes = this.o.nodes()
558
- const asks = []
559
- const placed: Job[] = []
560
- // The least any ask needs: once no worker takes it, the pass ends. Each
561
- // pump read every queued task, 8000 of them a pump (2026-10-25).
562
- const floor = { cores: Infinity, memory: Infinity }
563
- for (const job of queue) {
564
- const up =
565
- job.up ??
566
- (nodes?.has(job.req.taskId) === true
567
- ? (job.up = upstream(nodes, job.req.taskId))
568
- : upstream(nodes, job.req.taskId))
569
- const pin = this.pin(job, up)
570
- if (pin === 'done') continue
571
- if (pin === 'wait') {
572
- left.push(job)
573
- continue
574
- }
575
- placed.push(job)
576
- const need = this.booking(job.need)
577
- floor.cores = Math.min(floor.cores, need.cores)
578
- floor.memory = Math.min(floor.memory, need.memory)
579
- asks.push({ id: job.req.taskId, need, up, ...pin })
580
- }
581
- const to = assign(asks, bins, (above, platform) => this.bigger(above, platform), how.place, {
582
- floor,
583
- kinds: Infinity,
676
+ const to = this.queue.pass(bins, {
677
+ book: (need) => this.booking(need),
678
+ deps: (id) => (byId.get(id)!.deps ??= nodes?.get(id)?.deps),
679
+ gate: (id) => {
680
+ const job = byId.get(id)!
681
+ const up =
682
+ job.up ?? (nodes?.has(id) === true ? (job.up = upstream(nodes, id)) : upstream(nodes, id))
683
+ const pin = this.pin(job, up)
684
+ // It ended: the queue lets it go.
685
+ if (pin === 'done') byId.delete(id)
686
+ return pin
687
+ },
688
+ waits: (above, platform) => this.bigger(above, platform),
584
689
  })
690
+ const placed = [...to.keys()].map((id) => byId.get(id)!)
691
+ for (const id of to.keys()) byId.delete(id)
585
692
  for (const job of placed) {
586
- const a = to.get(job.req.taskId)?.a
587
- if (a === undefined) {
588
- left.push(job)
589
- continue
590
- }
693
+ const a = to.get(job.req.taskId)!.a
591
694
  job.worker = a.id
592
695
  job.sentAt = performance.now()
696
+ a.usage.sent(job.sentAt)
593
697
  a.busy.set(job.req.taskId, job)
594
698
  job.held = {
595
699
  cores: Math.min(this.booking(job.need).cores, a.cores),
@@ -606,11 +710,16 @@ export class Orchestrator {
606
710
  })
607
711
  a.usedCores += job.held.cores
608
712
  a.usedMemory += job.held.memory
713
+ // A server it starts there books its base once, while it runs.
714
+ for (const sv of job.need.servers ?? [])
715
+ if (!a.serving.has(sv.id)) {
716
+ a.serving.set(sv.id, sv.cores)
717
+ a.usedCores += sv.cores
718
+ }
609
719
  const batch = batches.get(a)
610
720
  if (batch === undefined) batches.set(a, [job])
611
721
  else batch.push(job)
612
722
  }
613
- this.queue.push(...left)
614
723
  for (const [a, jobs] of batches) this.send(a, jobs)
615
724
  }
616
725
 
@@ -624,6 +733,23 @@ export class Orchestrator {
624
733
  private pin(
625
734
  job: Job,
626
735
  up: ReadonlySet<string>,
736
+ ): 'wait' | 'done' | { only?: string; avoid?: string } {
737
+ const where = this.pinUp(job, up)
738
+ const avoid = job.avoid
739
+ if (typeof where !== 'object' || avoid === undefined) return where
740
+ if (where.only !== undefined || where.avoid !== undefined) return where
741
+ // With no other worker that can take it, it runs where it failed rather than wait.
742
+ const platform = job.need.platform
743
+ for (const w of this.workers.values())
744
+ if (w.id !== avoid && !w.faulty && (platform === undefined || w.platform === platform))
745
+ return { avoid }
746
+ return where
747
+ }
748
+
749
+ /** Where `job` may go as its upstreams' files lie (`pin`, before a retry's own rule). */
750
+ private pinUp(
751
+ job: Job,
752
+ up: ReadonlySet<string>,
627
753
  ): 'wait' | 'done' | { only?: string; avoid?: string } {
628
754
  if (job.again !== undefined) {
629
755
  const { only } = job.again
@@ -677,7 +803,11 @@ export class Orchestrator {
677
803
  return this.o.pinWaitMs ?? PIN_WAIT_MS
678
804
  }
679
805
 
680
- /** `ids`, uncached tasks that ran, run again where `where` says, for a dependant. */
806
+ /**
807
+ * `ids`, uncached tasks that ran, run again where `where` says, for a
808
+ * dependant: on its place, which the plan refused when one names another
809
+ * worker type or platform (`apart`).
810
+ */
681
811
  private remake(
682
812
  ids: readonly string[],
683
813
  where: { only?: string; avoid?: string; platform?: string | undefined },
@@ -693,24 +823,31 @@ export class Orchestrator {
693
823
  this.pumpSoon()
694
824
  }
695
825
  this.pumpSoon()
696
- this.queue.unshift({
697
- req: k.req,
698
- need: where.platform === undefined ? k.need : { ...k.need, platform: where.platform },
699
- queuedAt: performance.now(),
700
- pinnedAt: performance.now(),
701
- again: {
702
- ...(where.only === undefined ? {} : { only: where.only }),
703
- ...(where.avoid === undefined ? {} : { avoid: where.avoid }),
704
- },
705
- resolve: (r) =>
706
- done(
707
- r.exitCode === 0 ? undefined : `${id} ran again for it and failed (exit ${r.exitCode})`,
708
- ),
709
- reject: (err) =>
710
- done(
711
- `${id} ran again for it and failed: ${err instanceof Error ? err.message : String(err)}`,
712
- ),
713
- })
826
+ this.enqueue(
827
+ [
828
+ {
829
+ req: k.req,
830
+ need: where.platform === undefined ? k.need : { ...k.need, platform: where.platform },
831
+ queuedAt: performance.now(),
832
+ pinnedAt: performance.now(),
833
+ again: {
834
+ ...(where.only === undefined ? {} : { only: where.only }),
835
+ ...(where.avoid === undefined ? {} : { avoid: where.avoid }),
836
+ },
837
+ resolve: (r) =>
838
+ done(
839
+ r.exitCode === 0
840
+ ? undefined
841
+ : `${id} ran again for it and failed (exit ${r.exitCode})`,
842
+ ),
843
+ reject: (err) =>
844
+ done(
845
+ `${id} ran again for it and failed: ${err instanceof Error ? err.message : String(err)}`,
846
+ ),
847
+ },
848
+ ],
849
+ true,
850
+ )
714
851
  if (where.only !== undefined || where.avoid !== undefined)
715
852
  setTimeout(() => this.pumpSoon(), this.pinWaitMs + 50).unref?.()
716
853
  }
@@ -774,7 +911,15 @@ export class Orchestrator {
774
911
  if (load === undefined) return
775
912
  a.load = { cpu: Number(load.cpu) || 0, memory: Number(load.memory) || 0, at: performance.now() }
776
913
  const avg = Number(load.avg)
777
- if (Number.isFinite(avg) && avg >= 0) a.usage.machineAvg = avg
914
+ const self = Number(load.self)
915
+ if (Number.isFinite(avg) && avg >= 0)
916
+ a.usage.machine(avg, Number.isFinite(self) && self >= 0 ? self : undefined, a.load.at)
917
+ // Its servers' bases as it fits them (`server-fit.ts`): what the next
918
+ // task needing one books, and the next run.
919
+ if (typeof load.servers === 'object' && load.servers !== null)
920
+ for (const [id, cores] of Object.entries(load.servers))
921
+ if (typeof cores === 'number' && Number.isFinite(cores) && cores >= 0 && cores <= a.cores)
922
+ this.o.bases?.set(id, cores)
778
923
  const tasks = Number(load.tasks)
779
924
  this.trace.load(a.label, a.load.cpu, a.load.at, Number.isFinite(tasks) ? tasks : undefined)
780
925
  }
@@ -790,7 +935,8 @@ export class Orchestrator {
790
935
  !joined.has(id) &&
791
936
  !this.unstarted.has(id) &&
792
937
  !this.cancelled.has(id) &&
793
- (platform === undefined || (declared.platform ?? hostPlatform()) === platform) &&
938
+ (platform === undefined ||
939
+ (declared.place ?? declared.platform ?? hostPlatform()) === platform) &&
794
940
  (declared.cpu > above.cores || declared.memory > above.memory)
795
941
  )
796
942
  return true
@@ -815,11 +961,13 @@ export class Orchestrator {
815
961
  * hit only this cache held (the remote evicted it, or this machine does
816
962
  * not write it) failed the dependant there (2026-10-24).
817
963
  */
818
- private async share(id: string): Promise<void> {
819
- const nodes = this.o.nodes()
964
+ private async share(
965
+ up: ReadonlySet<string>,
966
+ nodes: ReadonlyMap<string, TaskNode> | undefined,
967
+ ): Promise<void> {
820
968
  const keys: string[] = []
821
- for (const d of upstream(nodes, id)) {
822
- if (this.savedAs.has(d) || nodes?.get(d)?.config.cache?.outputs === undefined) continue
969
+ for (const d of up) {
970
+ if (this.savedAs.has(d) || this.dep(nodes, d) === undefined) continue
823
971
  const key = this.upstreamKeys.get(d) ?? this.o.keyOf(d)
824
972
  if (key !== undefined) keys.push(key)
825
973
  }
@@ -860,92 +1008,123 @@ export class Orchestrator {
860
1008
  return got.includes(true)
861
1009
  }
862
1010
 
1011
+ /**
1012
+ * Upstream `id` as an order names it, once per graph: its project and
1013
+ * outputs. Undefined: it is not in the graph, or leaves no files.
1014
+ */
1015
+ private dep(nodes: ReadonlyMap<string, TaskNode> | undefined, id: string): DepFiles | undefined {
1016
+ if (nodes === undefined) return undefined
1017
+ let of = this.deps.get(nodes)
1018
+ if (of === undefined) this.deps.set(nodes, (of = new Map()))
1019
+ let d = of.get(id)
1020
+ if (d === undefined) {
1021
+ const n = nodes.get(id)
1022
+ const outs = n?.config.cache === true ? n.config.outputs : undefined
1023
+ d =
1024
+ n === undefined || outs === undefined
1025
+ ? null
1026
+ : {
1027
+ project: path.relative(this.o.root, n.projectDir),
1028
+ outputs: { files: [...outs.files], workspaceFiles: [...(outs.workspaceFiles ?? [])] },
1029
+ }
1030
+ of.set(id, d)
1031
+ }
1032
+ return d ?? undefined
1033
+ }
1034
+
863
1035
  private send(a: Worker, jobs: readonly Job[]): void {
864
1036
  const nodes = this.o.nodes()
865
- const orders = jobs.map((job) => {
1037
+ const orders = jobs.map((job): { job: Job; order: RunOrder } => {
866
1038
  const { req } = job
867
1039
  const deps: DepOrder[] = []
868
1040
  // Every one, held there or not: the worker restores each key once, and
869
1041
  // one it was sent but gave back it never ran or restored.
870
- for (const id of upstream(nodes, req.taskId)) {
871
- const n = nodes?.get(id)
872
- const outs = n?.config.cache?.outputs
1042
+ for (const id of job.up ?? upstream(nodes, req.taskId)) {
1043
+ const d = this.dep(nodes, id)
873
1044
  const key = this.savedAs.get(id) ?? this.upstreamKeys.get(id) ?? this.o.keyOf(id)
874
- if (n === undefined || outs === undefined || key === undefined) continue
1045
+ if (d === undefined || key === undefined) continue
875
1046
  a.has.add(id)
876
- deps.push({
877
- task: id,
878
- key,
879
- project: path.relative(this.o.root, n.projectDir),
880
- outputs: { files: [...outs.files], workspaceFiles: [...(outs.workspaceFiles ?? [])] },
881
- })
1047
+ deps.push({ task: id, key, project: d.project, outputs: d.outputs })
882
1048
  }
883
1049
  // A task sent here leaves its outputs here for its dependants.
884
1050
  a.has.add(req.taskId)
885
1051
  const node = nodes?.get(req.taskId)
886
- const env = defined(req.env, req.envDefine)
887
- return {
888
- job,
889
- order: {
890
- task: req.taskId,
891
- // Unsaved: under the run's own key, as an uncached task's.
892
- ...(req.cacheKey === undefined || req.unsaved === true ? {} : { key: req.cacheKey }),
893
- runKey: runKey(this.o.run, req.taskId, this.sends(req.taskId)),
894
- // A held key keeps its entry, and restored here it replaced the
895
- // forced run's fresh bytes with the ones it bypassed (2026-10-25).
896
- ...(req.refresh === true ? { fresh: true as const } : {}),
897
- // A HEAD per upload was the workers' only remote probe: the run
898
- // already knows the key is in no cache (2026-10-25).
899
- ...(req.refresh !== true && this.missed.has(req.taskId) ? { absent: true as const } : {}),
900
- deps,
901
- nested: node === undefined ? [] : nestedIn(nodes!, node.projectDir, this.o.root),
902
- command: withForwardArgs(req.command, req.forwardArgs),
903
- cwd: path.relative(this.o.root, req.cwd),
904
- project: node === undefined ? '' : path.relative(this.o.root, node.projectDir),
905
- env,
906
- ...(node?.config.exec?.env?.secret === undefined
907
- ? {}
908
- : { secret: [...node.config.exec.env.secret] }),
909
- ...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
910
- outputs: {
911
- files: [...req.outputs.files],
912
- workspaceFiles: [...req.outputs.workspaceFiles],
913
- },
914
- cores: job.held?.cores ?? 1,
915
- memory: job.held?.memory ?? 0,
916
- ...(req.sandbox === undefined || node?.config.exec?.sandbox === undefined
917
- ? {}
918
- : {
919
- sandbox: {
920
- config: node.config.exec.sandbox,
921
- ...(req.sandbox.keyed === undefined
922
- ? {}
923
- : { keyed: req.sandbox.keyed.map((d) => path.relative(this.o.root, d)) }),
924
- },
925
- }),
926
- ...runtimeOrders(req),
927
- ...(req.servers === undefined || req.servers.length === 0
928
- ? {}
929
- : {
930
- servers: req.servers.map((s) => ({
931
- task: s.taskId,
932
- command: s.command,
933
- cwd: path.relative(this.o.root, s.cwd),
934
- env: defined(s.env, {}),
935
- ...(s.readyWhen === undefined ? {} : { readyWhen: s.readyWhen }),
936
- ...(s.timeoutMs === undefined ? {} : { timeoutMs: s.timeoutMs }),
937
- ...(s.sandbox === undefined
938
- ? {}
939
- : {
940
- sandbox: {
941
- config: s.sandbox.config,
942
- nested: s.sandbox.nested.map((d) => path.relative(this.o.root, d)),
943
- },
944
- }),
945
- })),
946
- }),
947
- } as RunOrder,
948
- }
1052
+ const keyed = hostKeyed(req)
1053
+ const env = defined(req.env, req.envDefine, keyed)
1054
+ const unset = keyed.filter((k) => req.env[k] === undefined && !(k in req.envDefine))
1055
+ const order = {
1056
+ task: req.taskId,
1057
+ // Unsaved: under the run's own key, as an uncached task's.
1058
+ ...(req.cacheKey === undefined || req.unsaved === true ? {} : { key: req.cacheKey }),
1059
+ runKey: runKey(this.o.run, req.taskId, this.sends(req.taskId)),
1060
+ // A held key keeps its entry, and restored here it replaced the
1061
+ // forced run's fresh bytes with the ones it bypassed (2026-10-25).
1062
+ ...(req.refresh === true ? { fresh: true as const } : {}),
1063
+ // A HEAD per upload was the workers' only remote probe: the run
1064
+ // already knows the key is in no cache (2026-10-25).
1065
+ ...(req.refresh !== true && this.missed.has(req.taskId) ? { absent: true as const } : {}),
1066
+ deps,
1067
+ nested: node === undefined ? [] : nestedIn(nodes!, node.projectDir, this.o.root),
1068
+ command: withForwardArgs(req.command, req.forwardArgs),
1069
+ cwd: path.relative(this.o.root, req.cwd),
1070
+ project: node === undefined ? '' : path.relative(this.o.root, node.projectDir),
1071
+ env,
1072
+ ...(unset.length === 0 ? {} : { unset }),
1073
+ ...(node?.config.exec?.env?.secret === undefined
1074
+ ? {}
1075
+ : { secret: [...node.config.exec.env.secret] }),
1076
+ ...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
1077
+ outputs: {
1078
+ files: [...req.outputs.files],
1079
+ workspaceFiles: [...req.outputs.workspaceFiles],
1080
+ },
1081
+ cores: job.held?.cores ?? 1,
1082
+ memory: job.held?.memory ?? 0,
1083
+ ...(req.sandbox === undefined || node?.config.exec?.sandbox === undefined
1084
+ ? {}
1085
+ : {
1086
+ sandbox: {
1087
+ config: node.config.exec.sandbox,
1088
+ ...(req.sandbox.keyed === undefined
1089
+ ? {}
1090
+ : { keyed: req.sandbox.keyed.map((d) => path.relative(this.o.root, d)) }),
1091
+ ...(req.sandbox.dependencyReads === undefined
1092
+ ? {}
1093
+ : {
1094
+ reads: req.sandbox.dependencyReads.map((d) => path.relative(this.o.root, d)),
1095
+ }),
1096
+ ...(req.sandbox.dependencyReported === undefined
1097
+ ? {}
1098
+ : {
1099
+ reported: req.sandbox.dependencyReported.map((d) =>
1100
+ path.relative(this.o.root, d),
1101
+ ),
1102
+ }),
1103
+ },
1104
+ }),
1105
+ ...runtimeOrders(req),
1106
+ ...(req.servers === undefined || req.servers.length === 0
1107
+ ? {}
1108
+ : {
1109
+ servers: req.servers.map((s) => ({
1110
+ task: s.taskId,
1111
+ command: s.command,
1112
+ cwd: path.relative(this.o.root, s.cwd),
1113
+ env: defined(s.env, {}),
1114
+ ...(s.readyWhen === undefined ? {} : { readyWhen: s.readyWhen }),
1115
+ ...(s.timeoutMs === undefined ? {} : { timeoutMs: s.timeoutMs }),
1116
+ ...(s.sandbox === undefined
1117
+ ? {}
1118
+ : {
1119
+ sandbox: {
1120
+ config: s.sandbox.config,
1121
+ nested: s.sandbox.nested.map((d) => path.relative(this.o.root, d)),
1122
+ },
1123
+ }),
1124
+ })),
1125
+ }),
1126
+ } as RunOrder
1127
+ return { job, order }
949
1128
  })
950
1129
  const sandboxed = orders.some(
951
1130
  (o) => o.order.sandbox !== undefined || o.order.servers?.some((s) => s.sandbox !== undefined),
@@ -955,13 +1134,32 @@ export class Orchestrator {
955
1134
  const pending = new Set(jobs)
956
1135
  a.chain = a.chain
957
1136
  .then(async () => {
958
- for (const [i, part] of parts(orders, BATCH_BYTES).entries()) {
1137
+ // A dependency the worker was sent under the same key goes by name
1138
+ // alone: each order named every upstream whole, and 8000 tasks of a
1139
+ // deep graph sent 219 MB (2026-11-05). Read here, as sends go in
1140
+ // order: one sent before it fails is not counted as sent.
1141
+ const fresh = new Map<string, string>()
1142
+ const wired = orders.map(({ job, order }) => {
1143
+ const intro: [string, string][] = []
1144
+ const deps = order.deps.map((d): WiredDep => {
1145
+ if ((fresh.get(d.task) ?? a.sentDeps.get(d.task)) === d.key) return d.task
1146
+ fresh.set(d.task, d.key)
1147
+ intro.push([d.task, d.key])
1148
+ return d
1149
+ })
1150
+ const sent: SentOrder = { ...order, deps }
1151
+ // Measured once: a giant repo's were measured three times each (2026-11-05).
1152
+ return { job, order: sent, intro, bytes: bytesOf(sent) }
1153
+ })
1154
+ for (const [i, part] of parts(wired, BATCH_BYTES).entries()) {
959
1155
  // An order too long for a frame (a task of thousands of
960
1156
  // dependencies was refused, 413) sends its deps ahead.
961
- const tasks: RunOrder[] = []
962
- for (const { order } of part) {
963
- if (bytesOf(order) <= BATCH_BYTES) {
1157
+ const tasks: SentOrder[] = []
1158
+ let bytes = 0
1159
+ for (const { order, bytes: size } of part) {
1160
+ if (size <= BATCH_BYTES) {
964
1161
  tasks.push(order)
1162
+ bytes += size
965
1163
  continue
966
1164
  }
967
1165
  for (const deps of chunks(order.deps, BATCH_BYTES)) {
@@ -970,7 +1168,9 @@ export class Orchestrator {
970
1168
  a.usage.messagesIn++
971
1169
  a.usage.bytesIn += bytesOf(ahead)
972
1170
  }
973
- tasks.push({ ...order, deps: [] })
1171
+ const alone = { ...order, deps: [] }
1172
+ tasks.push(alone)
1173
+ bytes += bytesOf(alone)
974
1174
  }
975
1175
  const n = ++a.batches
976
1176
  for (const { job } of part) job.batch = n
@@ -984,7 +1184,12 @@ export class Orchestrator {
984
1184
  } satisfies ToWorker
985
1185
  await this.o.me.send(a.id, batch)
986
1186
  a.usage.messagesIn++
987
- a.usage.bytesIn += bytesOf(batch)
1187
+ // Its orders' bytes, a comma between each, in the batch's own.
1188
+ a.usage.bytesIn += bytesOf({ ...batch, tasks: [] }) + bytes + tasks.length - 1
1189
+ for (const { intro } of part) for (const [t, k] of intro) a.sentDeps.set(t, k)
1190
+ for (const { order } of part)
1191
+ for (const sv of order.servers ?? [])
1192
+ if (!a.serving.has(sv.task)) a.serving.set(sv.task, 0)
988
1193
  for (const { job } of part) {
989
1194
  pending.delete(job)
990
1195
  // Sent, it runs: its worker takes only what it has room for.
@@ -1121,7 +1326,7 @@ export class Orchestrator {
1121
1326
  a.started.delete(task)
1122
1327
  const again = this.retry(a, job, `could not read ${head.unread.join(', ')}`)
1123
1328
  if (again !== undefined) {
1124
- this.queue.unshift(again)
1329
+ this.enqueue([again], true)
1125
1330
  this.pumpSoon()
1126
1331
  }
1127
1332
  return
@@ -1136,7 +1341,15 @@ export class Orchestrator {
1136
1341
  a.done.push(task)
1137
1342
  a.usage.record(head, job.sentAt)
1138
1343
  const share = shareOf(head.cpuShare)
1139
- const held = Math.max(1, job.startedAt === undefined ? head.durationMs : endAt - job.startedAt)
1344
+ // At least as long as it ran: begun here once its batch's send resolved,
1345
+ // which can be after its worker began it, it was seen for less, and its
1346
+ // CPU over that read as more cores than it had (#5036: 1.001 each, so
1347
+ // two that ran side by side on 2 cores replayed one after the other).
1348
+ const held = Math.max(
1349
+ 1,
1350
+ head.durationMs,
1351
+ job.startedAt === undefined ? 0 : endAt - job.startedAt,
1352
+ )
1140
1353
  this.took.set(task, {
1141
1354
  ms: held,
1142
1355
  cores:
@@ -1162,6 +1375,7 @@ export class Orchestrator {
1162
1375
  this.needMs += Math.min(job.need.cores, a.cores) * ms
1163
1376
  }
1164
1377
  this.savedAs.set(task, key)
1378
+ this.ranOn.set(task, a.id)
1165
1379
  const node = this.o.nodes()?.get(task)
1166
1380
  if (head.exitCode === 0 && node !== undefined && leaves(node))
1167
1381
  this.kept.set(task, { worker: a.id, req: job.req, need: job.need })
@@ -1309,7 +1523,11 @@ export class Orchestrator {
1309
1523
  if (body.type === 'joined') {
1310
1524
  const info = body.info as Partial<WorkerInfo> | null
1311
1525
  const id = body.id!
1526
+ // Only the worker heard why: the run sat silent and its footer said
1527
+ // none joined (2026-11-07).
1312
1528
  const refuse = (reason: string): void => {
1529
+ this.refused.push(reason)
1530
+ this.warn(`dte: refused worker ${typeof info?.id === 'string' ? info.id : id}: ${reason}`)
1313
1531
  void this.o.me
1314
1532
  .send(id, { type: 'refused', reason } satisfies ToWorker)
1315
1533
  .catch(this.unsent('refused', id))
@@ -1381,7 +1599,7 @@ export class Orchestrator {
1381
1599
  cores,
1382
1600
  memory,
1383
1601
  reported: { cores: Number(info.cores), memory: Number(info.memory) },
1384
- platform,
1602
+ platform: this.o.joinedPlace?.(spawn?.declared.size, platform) ?? platform,
1385
1603
  usedCores: 0,
1386
1604
  usedMemory: 0,
1387
1605
  label,
@@ -1389,6 +1607,8 @@ export class Orchestrator {
1389
1607
  started: new Set(),
1390
1608
  batches: 0,
1391
1609
  has: new Set(),
1610
+ serving: new Map(),
1611
+ sentDeps: new Map(),
1392
1612
  // Its name first, before any batch.
1393
1613
  chain: this.cache()
1394
1614
  .then((cache) =>
@@ -1419,13 +1639,15 @@ export class Orchestrator {
1419
1639
  a.usage.to = performance.now()
1420
1640
  this.trace.left(a.label, 'lost', a.usage.to)
1421
1641
  this.gone.push(a)
1642
+ // Its results' `end`s never come.
1643
+ for (const k of this.parts.keys()) if (k.startsWith(`${a.id}\0`)) this.parts.delete(k)
1422
1644
  // Its tasks run again: first, as they were first to go.
1423
1645
  const back: Job[] = []
1424
1646
  for (const j of [...a.busy.values()]) {
1425
1647
  const again = this.retry(a, j, 'lost')
1426
1648
  if (again !== undefined) back.push(again)
1427
1649
  }
1428
- this.queue.unshift(...back)
1650
+ this.enqueue(back, true)
1429
1651
  this.o.lost?.(a.label)
1430
1652
  // Workers that join on their own may yet join; ones this run starts
1431
1653
  // are all it gets.
@@ -1484,7 +1706,7 @@ export class Orchestrator {
1484
1706
  if (again !== undefined) back.push(again)
1485
1707
  }
1486
1708
  if (back.length > 0) {
1487
- this.queue.unshift(...back)
1709
+ this.enqueue(back, true)
1488
1710
  this.o.faulted?.()
1489
1711
  if (this.spawnedAt.size > 0)
1490
1712
  this.stranded(`${a.label} no longer runs a task it began and no worker is left`)
@@ -1505,7 +1727,7 @@ export class Orchestrator {
1505
1727
  if (j === undefined) continue
1506
1728
  // It began it whether or not its `start` came first.
1507
1729
  this.begun(a, m.task)
1508
- if (this.retry(a, j, m.reason) !== undefined) this.queue.unshift(j)
1730
+ if (this.retry(a, j, m.reason) !== undefined) this.enqueue([j], true)
1509
1731
  if (a.faulty) this.o.faulted?.()
1510
1732
  if (a.faulty && this.spawnedAt.size > 0)
1511
1733
  this.stranded(
@@ -1554,7 +1776,7 @@ export class Orchestrator {
1554
1776
  where(taskId: string): string | null | undefined {
1555
1777
  for (const a of this.workers.values())
1556
1778
  if (a.busy.has(taskId)) return a.started.has(taskId) ? a.label : null
1557
- return this.queue.some((j) => j.req.taskId === taskId) ? null : undefined
1779
+ return this.waiting.has(taskId) || this.handed.has(taskId) ? null : undefined
1558
1780
  }
1559
1781
 
1560
1782
  /**
@@ -1610,7 +1832,7 @@ export class Orchestrator {
1610
1832
  }): void {
1611
1833
  const work = p.work?.map(
1612
1834
  (t) =>
1613
- `${t.id} ${t.ms === undefined ? '?' : Math.round(t.ms / 100) / 10}s ${Math.round(t.cores * 100) / 100}c${t.platform === undefined ? '' : ` ${t.platform}`}`,
1835
+ `${t.id} ${t.ms === undefined ? '?' : Math.round(t.ms / 100) / 10}s ${Math.round(t.cores * 100) / 100}c${t.platform === undefined ? '' : ` ${bare(t.platform)}`}`,
1614
1836
  )
1615
1837
  const workers = p.workers.map((w) =>
1616
1838
  w.starting === true
@@ -1716,6 +1938,7 @@ export class Orchestrator {
1716
1938
  cores: a.cores,
1717
1939
  memory: a.memory,
1718
1940
  platform: a.platform,
1941
+ ...(a.serving.size === 0 ? {} : { serving: new Map(a.serving) }),
1719
1942
  ...(a.declared === undefined ? {} : { size: a.declared.size }),
1720
1943
  at: 0,
1721
1944
  idle: a.busy.size === 0,
@@ -1727,7 +1950,8 @@ export class Orchestrator {
1727
1950
  id: j.req.taskId,
1728
1951
  cores: Math.min(a.cores, Math.max((j.held?.cores ?? 0) * over, j.need.ask?.cores ?? 0)),
1729
1952
  memory: Math.max(j.held?.memory ?? 0, Math.min(a.memory, j.need.ask?.memory ?? 0)),
1730
- endsIn: Math.max(0, (ms(j.req.taskId) ?? 0) - (now - j.startedAt!)),
1953
+ // Below 0: past its time, which the plan weighs (`decide`).
1954
+ endsIn: (ms(j.req.taskId) ?? 0) - (now - j.startedAt!),
1731
1955
  })),
1732
1956
  })
1733
1957
  }
@@ -1741,7 +1965,7 @@ export class Orchestrator {
1741
1965
  // A size naming none is planned as this machine's, as `dte()`
1742
1966
  // planned it: on no platform, no task naming one fit it, and the
1743
1967
  // next plan cancelled it.
1744
- platform: declared.platform ?? hostPlatform(),
1968
+ platform: declared.place ?? declared.platform ?? hostPlatform(),
1745
1969
  size: declared.size,
1746
1970
  at: Math.max(
1747
1971
  0,
@@ -1873,9 +2097,19 @@ export class Orchestrator {
1873
2097
 
1874
2098
  /** Every task sent here and not done fails with `reason`; later ones too. */
1875
2099
  private fail(reason: string): void {
1876
- for (const a of this.workers.values())
2100
+ const now = performance.now()
2101
+ for (const a of this.workers.values()) {
1877
2102
  for (const j of a.busy.values()) j.reject(new Error(reason))
1878
- for (const j of this.queue.splice(0)) j.reject(new Error(reason))
2103
+ // Lost, keeping what it ran: dropped, the footer read it as never joined.
2104
+ a.usage.to = now
2105
+ this.trace.left(a.label, 'lost', now)
2106
+ this.gone.push(a)
2107
+ }
2108
+ for (const [id, j] of this.waiting) {
2109
+ this.queue.delete(id)
2110
+ j.reject(new Error(reason))
2111
+ }
2112
+ this.waiting.clear()
1879
2113
  this.workers.clear()
1880
2114
  this.lost = reason
1881
2115
  }
@@ -1899,13 +2133,8 @@ export class Orchestrator {
1899
2133
  * waits on its deps through those no worker ran (groups, hits).
1900
2134
  */
1901
2135
  idealMs(depsOf: (id: string) => readonly string[], sizes: readonly Size[]): number | undefined {
1902
- if (this.took.size === 0) return undefined
1903
- const all = [...this.workers.values(), ...this.gone]
1904
- const warm = new Map<string | undefined, number>()
1905
- for (const a of all)
1906
- if (a.warmUpMs !== undefined && !(warm.get(a.platform)! <= a.warmUpMs))
1907
- warm.set(a.platform, a.warmUpMs)
1908
- const least = this.warmUpMs() ?? 0
2136
+ const r = this.replay(depsOf)
2137
+ if (r === undefined) return undefined
1909
2138
  const pool: PlanWorker[] = (
1910
2139
  sizes.length > 0
1911
2140
  ? sizes.flatMap((s) =>
@@ -1915,14 +2144,104 @@ export class Orchestrator {
1915
2144
  platform: s.platform,
1916
2145
  })),
1917
2146
  )
1918
- : all.map((a) => ({ cores: a.cores, memory: a.memory, platform: a.platform }))
2147
+ : [...this.workers.values(), ...this.gone].map((a) => ({
2148
+ cores: a.cores,
2149
+ memory: a.memory,
2150
+ platform: a.platform,
2151
+ }))
1919
2152
  ).map((w, i) => ({
1920
2153
  ...w,
1921
2154
  id: `ideal#${i}`,
1922
- at: warm.get(w.platform) ?? least,
2155
+ at: r.warm(w.platform),
1923
2156
  running: [],
1924
2157
  idle: false,
1925
2158
  }))
2159
+ const end = simulate(r.tasks, pool)
2160
+ if (!Number.isFinite(end)) return undefined
2161
+ return r.from + end
2162
+ }
2163
+
2164
+ /**
2165
+ * The workers that would have run this run best (`advise`), from the
2166
+ * same replay as `idealMs`: on `sizes`, past their `max`, and of any
2167
+ * cores. With no sizes (workers that join on their own), the shapes
2168
+ * that joined stand for them, as many as joined.
2169
+ */
2170
+ advice(depsOf: (id: string) => readonly string[], sizes: readonly Size[]): WorkerAdvice[] {
2171
+ // The footer and the summary each ask once the run ended: played once.
2172
+ if (this.ended && this.advised !== undefined) return this.advised
2173
+ const out = this.advise(depsOf, sizes)
2174
+ if (this.ended) this.advised = out
2175
+ return out
2176
+ }
2177
+
2178
+ private advised: WorkerAdvice[] | undefined
2179
+
2180
+ private advise(
2181
+ depsOf: (id: string) => readonly string[],
2182
+ sizes: readonly Size[],
2183
+ ): WorkerAdvice[] {
2184
+ const r = this.replay(depsOf)
2185
+ if (r === undefined) return []
2186
+ const shapes: Size[] =
2187
+ sizes.length > 0
2188
+ ? [...sizes]
2189
+ : [
2190
+ ...[...this.workers.values(), ...this.gone]
2191
+ .reduce((by, a) => {
2192
+ const name = `${a.cores}c/${a.platform ?? ''}`
2193
+ const had = by.get(name)
2194
+ by.set(name, {
2195
+ name,
2196
+ max: (had?.max ?? 0) + 1,
2197
+ cpu: a.cores,
2198
+ memory: a.memory,
2199
+ platform: a.platform,
2200
+ })
2201
+ return by
2202
+ }, new Map<string, Size>())
2203
+ .values(),
2204
+ ]
2205
+ const a = advise(r.tasks, shapes, r.warm)
2206
+ const wire = (of: WorkerAdvice['of'], f: Fleets | undefined): WorkerAdvice[] =>
2207
+ f === undefined
2208
+ ? []
2209
+ : [
2210
+ {
2211
+ of,
2212
+ endMs: Math.round(r.from + f.endMs),
2213
+ workers: f.fleet.map((w) => ({
2214
+ count: w.count,
2215
+ cores: w.cores,
2216
+ memoryBytes: w.memory,
2217
+ ...(w.platform === undefined ? {} : { platform: bare(w.platform) }),
2218
+ ...(w.size === undefined || sizes.length === 0 ? {} : { size: w.size }),
2219
+ })),
2220
+ },
2221
+ ]
2222
+ return [...wire('declared', a.declared), ...wire('more', a.more), ...wire('any', a.any)]
2223
+ }
2224
+
2225
+ /**
2226
+ * Each task a worker ran, as long as it held its worker and with the
2227
+ * cores it used meanwhile, waiting on its deps through those no worker
2228
+ * ran (groups, hits); each platform's fastest real warm-up; and when
2229
+ * the run first knew its work.
2230
+ */
2231
+ private replay(depsOf: (id: string) => readonly string[]):
2232
+ | {
2233
+ tasks: PlanTask[]
2234
+ warm: (platform: string | undefined) => number
2235
+ from: number
2236
+ }
2237
+ | undefined {
2238
+ if (this.took.size === 0) return undefined
2239
+ const all = [...this.workers.values(), ...this.gone]
2240
+ const warm = new Map<string | undefined, number>()
2241
+ for (const a of all)
2242
+ if (a.warmUpMs !== undefined && !(warm.get(a.platform)! <= a.warmUpMs))
2243
+ warm.set(a.platform, a.warmUpMs)
2244
+ const least = this.warmUpMs() ?? 0
1926
2245
  const seen = new Map<string, string[]>()
1927
2246
  const ran = (id: string): string[] => {
1928
2247
  const known = seen.get(id)
@@ -1932,10 +2251,11 @@ export class Orchestrator {
1932
2251
  seen.set(id, out)
1933
2252
  return out
1934
2253
  }
1935
- const tasks: PlanTask[] = [...this.took].map(([id, t]) => ({ id, ...t, deps: ran(id) }))
1936
- const end = simulate(tasks, pool)
1937
- if (!Number.isFinite(end)) return undefined
1938
- return (this.probe?.at ?? this.asks[0]?.at ?? 0) + end
2254
+ return {
2255
+ tasks: [...this.took].map(([id, t]) => ({ id, ...t, deps: ran(id) })),
2256
+ warm: (platform) => warm.get(platform) ?? least,
2257
+ from: this.probe?.at ?? this.asks[0]?.at ?? 0,
2258
+ }
1939
2259
  }
1940
2260
 
1941
2261
  /**
@@ -1994,11 +2314,30 @@ export class Orchestrator {
1994
2314
  * each, the cores and memory its size declares and its machine reports;
1995
2315
  * and `ideal`, the run's `idealMs`, asked once it ended.
1996
2316
  */
1997
- footer(ideal?: () => number | undefined): [string, ui.Inline][] {
2317
+ footer(
2318
+ ideal?: () => number | undefined,
2319
+ advice?: () => readonly WorkerAdvice[],
2320
+ ): [string, ui.Inline][] {
1998
2321
  // Over with none, it says so: core's own `N workers` (this machine's
1999
- // pool) read as the run's when no task needed one (2026-10-25).
2322
+ // pool) read as the run's when no task needed one (2026-10-25), and
2323
+ // says when one did and none joined (2026-11-06), and why any it
2324
+ // refused was (2026-11-07).
2000
2325
  if (this.spawnedAt.size === 0 && this.workers.size === 0 && this.gone.length === 0)
2001
- return this.ended ? [['workers', ui.dim('none started: no task needed one')]] : []
2326
+ return this.ended
2327
+ ? [
2328
+ [
2329
+ 'workers',
2330
+ this.refused.length > 0
2331
+ ? ui.tone(
2332
+ 'warn',
2333
+ `none joined: ${this.refused.length} refused (${this.refused[0]})`,
2334
+ )
2335
+ : this.waited
2336
+ ? ui.tone('warn', 'none joined: a task waited on one')
2337
+ : ui.dim('none started: no task needed one'),
2338
+ ],
2339
+ ]
2340
+ : []
2002
2341
  const gone = new Set(this.gone)
2003
2342
  const all = [...this.workers.values(), ...this.gone]
2004
2343
  const joined = new Set(all.map((a) => a.label))
@@ -2068,13 +2407,18 @@ export class Orchestrator {
2068
2407
  // Once over, what each did, under its line.
2069
2408
  const usage = new Map<string, readonly string[]>()
2070
2409
  let cpuMs = 0
2410
+ let tasksMs = 0
2411
+ let workerMs: number | undefined
2071
2412
  let coreMs = 0
2072
2413
  if (this.ended)
2073
2414
  for (const a of all) {
2074
2415
  resources(a.label, a.reported.cores, a.reported.memory, a.declared)
2075
2416
  const st = a.usage.stats(a.cores, this.endedAt)
2076
- cpuMs += st.cpuMs
2077
- coreMs += a.cores * st.onlineMs
2417
+ cpuMs += st.workCoreMs
2418
+ tasksMs += st.tasksUtilization * a.cores * st.workMs
2419
+ if (st.workerUtilization !== undefined)
2420
+ workerMs = (workerMs ?? 0) + st.workerUtilization * a.cores * st.workMs
2421
+ coreMs += a.cores * st.workMs
2078
2422
  usage.set(a.label, describe(st, a.usage))
2079
2423
  }
2080
2424
  return [
@@ -2125,6 +2469,17 @@ export class Orchestrator {
2125
2469
  ),
2126
2470
  ] as [string, ui.Inline],
2127
2471
  ])(ideal?.()),
2472
+ // What to declare next time (2026-11-05: "So I can add more
2473
+ // or change their specs").
2474
+ ...(advice?.() ?? []).map(
2475
+ (a) =>
2476
+ [
2477
+ '',
2478
+ ui.dim(
2479
+ `${ADVICE[a.of][0].padEnd(width)} ${a.workers.map(fleetText).join(' + ')}: ${seconds(a.endMs)}, ${ADVICE[a.of][1]}`,
2480
+ ),
2481
+ ] as [string, ui.Inline],
2482
+ ),
2128
2483
  ...(this.unmeasured.length === 0
2129
2484
  ? []
2130
2485
  : [
@@ -2141,7 +2496,7 @@ export class Orchestrator {
2141
2496
  ? [
2142
2497
  [
2143
2498
  '',
2144
- `${'total'.padEnd(width)} cpu utilization ${percent(cpuMs / coreMs)}${this.needMs < LEARN_MS ? '' : ` · tasks used ${percent(this.usedMs / this.needMs)} of their history`}`,
2499
+ `${'total'.padEnd(width)} ${utilization(cpuMs / coreMs, tasksMs / coreMs, workerMs === undefined ? undefined : workerMs / coreMs)}${this.needMs < LEARN_MS ? '' : ` · tasks used ${percent(this.usedMs / this.needMs)} of their history`}`,
2145
2500
  ] as [string, ui.Inline],
2146
2501
  ]
2147
2502
  : []),
@@ -2190,15 +2545,28 @@ export function nestedIn(
2190
2545
  ): string[] {
2191
2546
  let of = nested.get(nodes)
2192
2547
  if (of === undefined) {
2193
- of = { dirs: [...new Set([...nodes.values()].map((n) => n.projectDir))], by: new Map() }
2548
+ of = {
2549
+ dirs: [...new Set([...nodes.values()].map((n) => n.projectDir))].sort(),
2550
+ by: new Map(),
2551
+ }
2194
2552
  nested.set(nodes, of)
2195
2553
  }
2196
2554
  let out = of.by.get(dir)
2197
2555
  if (out === undefined) {
2198
- out = of.dirs
2199
- .filter((d) => d.startsWith(dir + path.sep))
2200
- .map((d) => path.relative(root, d))
2201
- .sort()
2556
+ // Sorted, the ones under `dir` lie together: each project read every
2557
+ // project, 16 million reads for 4000 (2026-11-05).
2558
+ const under = dir + path.sep
2559
+ let lo = 0
2560
+ let hi = of.dirs.length
2561
+ while (lo < hi) {
2562
+ const mid = (lo + hi) >> 1
2563
+ if (of.dirs[mid]! < under) lo = mid + 1
2564
+ else hi = mid
2565
+ }
2566
+ const found: string[] = []
2567
+ for (let i = lo; i < of.dirs.length && of.dirs[i]!.startsWith(under); i++)
2568
+ found.push(path.relative(root, of.dirs[i]!))
2569
+ out = found.sort()
2202
2570
  of.by.set(dir, out)
2203
2571
  }
2204
2572
  return [...out]
@@ -2208,27 +2576,49 @@ const nested = new WeakMap<
2208
2576
  { dirs: string[]; by: Map<string, readonly string[]> }
2209
2577
  >()
2210
2578
 
2211
- /** A tree's file paths, and its submodules' (gitlinks). */
2212
- type Tree = { files: Set<string>; links: string[] }
2579
+ /** A tree's files, each path with its identity as core keys it, and its submodules' (gitlinks); `dirs` once asked. */
2580
+ type Tree = { files: Map<string, string>; links: string[]; dirs?: Set<string> }
2581
+
2582
+ /** Whether `tree` has a file under `dir`, or `dir` in a submodule (checked out or not, its own say). */
2583
+ function holds(tree: Tree, dir: string): boolean {
2584
+ if (tree.dirs === undefined) {
2585
+ const dirs = (tree.dirs = new Set<string>())
2586
+ const above = (f: string): void => {
2587
+ for (let d = path.posix.dirname(f); d !== '.' && !dirs.has(d); d = path.posix.dirname(d))
2588
+ dirs.add(d)
2589
+ }
2590
+ for (const f of tree.files.keys()) above(f)
2591
+ for (const l of tree.links) above(l)
2592
+ }
2593
+ return tree.dirs.has(dir) || tree.links.some((l) => dir === l || dir.startsWith(`${l}/`))
2594
+ }
2213
2595
 
2214
2596
  /** `tree`'s files and submodules under `root`, relative to it; undefined when git cannot list it. */
2215
2597
  async function listTree(root: string, tree: string): Promise<Tree | undefined> {
2216
- const p = Bun.spawn(['git', 'ls-tree', '-r', '-z', tree], {
2217
- cwd: root,
2218
- stdin: 'ignore',
2219
- stdout: 'pipe',
2220
- stderr: 'ignore',
2221
- })
2598
+ let p
2599
+ try {
2600
+ p = Bun.spawn(['git', 'ls-tree', '-r', '-z', tree], {
2601
+ cwd: root,
2602
+ stdin: 'ignore',
2603
+ stdout: 'pipe',
2604
+ stderr: 'ignore',
2605
+ })
2606
+ } catch {
2607
+ // quiet: no git, or no root, is git unable to say.
2608
+ return undefined
2609
+ }
2222
2610
  const [out, code] = await Promise.all([new Response(p.stdout).text(), p.exited])
2223
2611
  if (code !== 0) return undefined
2224
- const files = new Set<string>()
2612
+ const files = new Map<string, string>()
2225
2613
  const links: string[] = []
2226
2614
  for (const e of out.split('\0')) {
2227
2615
  const tab = e.indexOf('\t')
2228
2616
  if (tab === -1) continue
2229
2617
  const name = e.slice(tab + 1)
2230
- if (e.startsWith('160000 ')) links.push(name)
2231
- else files.add(name)
2618
+ // `<mode> <type> <oid>`: core's identity is the OID, the mode before it unless a plain file's.
2619
+ const [mode, , oid] = e.slice(0, tab).split(' ')
2620
+ if (mode === '160000') links.push(name)
2621
+ else files.set(name, mode === '100644' ? oid! : `${mode}:${oid}`)
2232
2622
  }
2233
2623
  return { files, links }
2234
2624
  }
@@ -2250,11 +2640,52 @@ function sandboxConfigs(nodes: ReadonlyMap<string, TaskNode> | undefined): Sandb
2250
2640
  * pinning every dependant to its worker held a run's shards on one.
2251
2641
  */
2252
2642
  function leaves(node: TaskNode): boolean {
2253
- if (node.config.cache !== undefined) return false
2643
+ if (node.config.cache === true) return false
2254
2644
  const sandbox = node.config.exec?.sandbox
2255
2645
  return sandbox === undefined || (sandbox.allow?.write?.length ?? 0) > 0
2256
2646
  }
2257
2647
 
2648
+ /**
2649
+ * A task and an upstream of it that `leaves` files, which `bound` says
2650
+ * cannot run where the task does (its `exec.worker` or `exec.platforms`):
2651
+ * the task reads those files where the upstream ran, or runs it again
2652
+ * where it runs itself. Undefined when none.
2653
+ */
2654
+ export function apart(
2655
+ nodes: ReadonlyMap<string, TaskNode>,
2656
+ bound: (upstream: TaskNode, dependant: TaskNode) => boolean,
2657
+ ): { upstream: string; dependant: string } | undefined {
2658
+ const runs = (n: TaskNode): boolean =>
2659
+ n.config.exec?.command !== undefined && n.config.exec.persistent === undefined
2660
+ const named = [...nodes.values()].filter(
2661
+ (n) =>
2662
+ runs(n) &&
2663
+ leaves(n) &&
2664
+ (n.config.exec?.worker !== undefined || n.config.exec?.platforms !== undefined),
2665
+ )
2666
+ if (named.length === 0) return undefined
2667
+ const below = new Map<string, string[]>()
2668
+ for (const n of nodes.values())
2669
+ for (const d of n.deps) {
2670
+ const of = below.get(d)
2671
+ if (of === undefined) below.set(d, [n.id])
2672
+ else of.push(n.id)
2673
+ }
2674
+ for (const u of named) {
2675
+ const seen = new Set<string>()
2676
+ const walk = [...(below.get(u.id) ?? [])]
2677
+ for (let id = walk.pop(); id !== undefined; id = walk.pop()) {
2678
+ if (seen.has(id)) continue
2679
+ seen.add(id)
2680
+ const d = nodes.get(id)
2681
+ if (d === undefined) continue
2682
+ if (runs(d) && bound(u, d)) return { upstream: u.id, dependant: id }
2683
+ walk.push(...(below.get(id) ?? []))
2684
+ }
2685
+ }
2686
+ return undefined
2687
+ }
2688
+
2258
2689
  /**
2259
2690
  * Every task `id` depends on, transitively, each after its own
2260
2691
  * dependencies: the order a worker restores them in. Dependant first, an
@@ -2276,11 +2707,17 @@ function upstream(nodes: ReadonlyMap<string, TaskNode> | undefined, id: string):
2276
2707
  return out
2277
2708
  }
2278
2709
 
2710
+ /** Core-time busy over core-time had, from first send to last end: its machine, its tasks, its worker. */
2711
+ function utilization(machine: number, tasks: number, worker: number | undefined): string {
2712
+ return `cpu utilization machine ${percent(machine)} · tasks ${percent(tasks)}${worker === undefined ? '' : ` · worker ${percent(worker)}`}`
2713
+ }
2714
+
2279
2715
  /** A worker's run, spelled out: time, CPU, memory and the relay. */
2280
2716
  function describe(st: UsageStats, u: Usage): string[] {
2281
2717
  return [
2282
2718
  `online ${seconds(st.onlineMs)} · busy ${seconds(st.busyMs)} · idle ${seconds(Math.max(0, st.onlineMs - st.busyMs))}`,
2283
- `cpu peak ${st.cpuPeak.toFixed(1)} · used avg ${st.cpuAvg.toFixed(1)} cores${st.machineAvg === undefined ? '' : ` · machine avg ${st.machineAvg.toFixed(1)}`} · booked avg ${st.bookedAvg.toFixed(1)} · cpu utilization ${percent(st.utilization)}`,
2719
+ `cpu peak ${st.cpuPeak.toFixed(1)} · used avg ${st.cpuAvg.toFixed(1)} cores${st.machineAvg === undefined ? '' : ` · machine avg ${st.machineAvg.toFixed(1)}`} · booked avg ${st.bookedAvg.toFixed(1)}`,
2720
+ utilization(st.utilization, st.tasksUtilization, st.workerUtilization),
2284
2721
  // No task said what it held: `memory peak 0 B` would be a claim.
2285
2722
  ...(st.memoryPeak > 0
2286
2723
  ? [`memory peak ${bytes(st.memoryPeak)} · avg ${bytes(st.memoryAvg)}`]
@@ -2330,16 +2767,24 @@ function runtimeOrders(req: ExecuteRequest): { runtime?: RuntimeOrder[] } {
2330
2767
 
2331
2768
  /**
2332
2769
  * `env` as it travels: no unset name, and no name whose value is the
2333
- * worker's (`HOST_ENV`) unless `define` sets it. A defined `HOME` ran
2334
- * with the worker's own, saved under a key that folded the defined one
2335
- * (2026-10-24).
2770
+ * worker's (`HOST_ENV`) unless `define` sets it or the task keys it
2771
+ * (`keyed`). A defined `HOME` ran with the worker's own, saved under a key
2772
+ * that folded the defined one (2026-10-24); a keyed `XDG_CONFIG_HOME` did
2773
+ * the same (2026-11-07).
2336
2774
  */
2337
2775
  function defined(
2338
2776
  env: NodeJS.ProcessEnv,
2339
2777
  define: Readonly<Record<string, string>>,
2778
+ keyed: readonly string[] = [],
2340
2779
  ): Record<string, string> {
2341
- const out: Record<string, string> = {}
2780
+ // No prototype: on `{}`, a name `__proto__` hit the setter and was not sent.
2781
+ const out: Record<string, string> = Object.create(null)
2342
2782
  for (const [k, v] of Object.entries(env))
2343
- if (v !== undefined && (!HOST_ENV.includes(k) || k in define)) out[k] = v
2783
+ if (v !== undefined && (!HOST_ENV.includes(k) || k in define || keyed.includes(k))) out[k] = v
2344
2784
  return out
2345
2785
  }
2786
+
2787
+ /** The `HOST_ENV` names the task's `inputs.env` keys: their value is this machine's, as keyed. */
2788
+ function hostKeyed(req: ExecuteRequest): string[] {
2789
+ return (req.inputs?.env ?? []).map((e) => e.name).filter((k) => HOST_ENV.includes(k))
2790
+ }