@vzn/vx-dte 0.0.0 → 0.0.650

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.ts ADDED
@@ -0,0 +1,1018 @@
1
+ import path from 'node:path'
2
+ import {
3
+ definePlugin,
4
+ hostPlatform,
5
+ localHistory,
6
+ PLATFORMS,
7
+ refuseUnknownOptions,
8
+ taskDefinitions,
9
+ ui,
10
+ UserError,
11
+ type CommandContext,
12
+ type Platform,
13
+ type PluginCache,
14
+ type PluginOptionKinds,
15
+ type TaskNode,
16
+ type TaskResources,
17
+ } from '@vzn/vx'
18
+ import type { DteInit, DteSetup } from './init.js'
19
+ import type { Need } from './dispatch.js'
20
+ import { booked, DEFAULT_STRATEGY, STRATEGIES, type Strategy } from './strategy.js'
21
+ import type { Orchestrator } from './orchestrator.js'
22
+ import type { Proof } from './seal.js'
23
+ import type { Size } from './planner.js'
24
+
25
+ /** One kind of worker the adapter can start. */
26
+ export interface WorkerSize {
27
+ /** How many of it one run may start; absent, as many as the work needs. */
28
+ readonly max?: number
29
+ /** Its cores: what starting one adds, until it reports what its machine has. */
30
+ readonly cpu: number
31
+ /** Its memory, in bytes or as `'8GB'`, `'512MiB'`: what starting one adds, until it reports what its machine has. */
32
+ readonly memory: number | string
33
+ /** Its machines' platform, which a joining worker must report; absent, planned as this machine's and not checked. A task naming `exec.platforms` runs only on a worker of one of them. */
34
+ readonly platform?: Platform
35
+ }
36
+
37
+ /** What an adapter is asked to start: one worker of `size`. */
38
+ export interface WorkerSpawn {
39
+ readonly run: string
40
+ readonly relay: string
41
+ readonly size: string
42
+ /** Its size's `max`: `Infinity` when uncapped. */
43
+ readonly max: number
44
+ /** Its size's cores and memory (bytes), for an adapter that sets a machine's limits. */
45
+ readonly cpu: number
46
+ readonly memory: number
47
+ /** 1 for the first worker this run starts, counting every size: unique where `id` is per size. */
48
+ readonly index: number
49
+ /** Its name in the run's output and its own (`small#2`, numbered within its size), which it reports back on joining. */
50
+ readonly id: string
51
+ readonly workspaceRoot: string
52
+ /** The commit of the tracked files as the run saw them at its start, whose tree a worker must check out. */
53
+ readonly commit: string
54
+ /**
55
+ * The run's cache directory, absolute, when the workspace names one
56
+ * (`cache.dir`, `VX_CACHE_DIR`, `--cache-dir`): an adapter starting workers on this
57
+ * machine points them at it, where the run reads what they save.
58
+ */
59
+ readonly cacheDir?: string
60
+ /** What the worker's `vx dte worker` reads: `VX_DTE_RUN`, `VX_DTE_WORKER` (`id`), `VX_DTE_RELAY`, `VX_DTE_SECRET` (the run’s). */
61
+ readonly env: Readonly<Record<string, string>>
62
+ /** Says the worker's job or process ended, a success too, and why (its exit, its last stderr line). */
63
+ readonly exited: (reason: string) => void
64
+ }
65
+
66
+ export interface DteAdapter {
67
+ /** A relay of its own, for a run whose `dte()` names none. */
68
+ relay?(): Promise<string>
69
+ spawn(worker: WorkerSpawn): void | Promise<void>
70
+ /** At run end: stop what it started. */
71
+ close?(): void | Promise<void>
72
+ /**
73
+ * Workers `ids` it started are not needed: stop each that has not joined,
74
+ * if it can. Answers with those it left running (`github()`: its job had a
75
+ * runner already), which the run takes when they join. `late`: given up
76
+ * on joining, stuck or queued, so stopped whatever its job holds.
77
+ */
78
+ cancel?(ids: readonly string[], late?: boolean): void | Promise<readonly string[]>
79
+ /** The CI files that start its workers, which `vx dte init` writes; none for an adapter that starts them on this machine. */
80
+ init?(setup: DteSetup): Promise<DteInit>
81
+ /** Where it says a fault it works around (a status poll or a cancel that failed); called as the run starts. */
82
+ report?(warn: (message: string) => void): void
83
+ /** Its workers prove themselves by their CI identity (GitHub OIDC): the run makes no secret for them. */
84
+ readonly keyless?: boolean
85
+ /**
86
+ * A keyless adapter's: the CI runs (GitHub workflow run ids) whose jobs
87
+ * alone may join as its workers; `undefined` admits any job of the
88
+ * repository.
89
+ */
90
+ workerRuns?(): ReadonlySet<string> | undefined
91
+ /** A worker's name in a job its CI started for several, when `VX_DTE_WORKER` is unset. */
92
+ workerId?(): string | undefined
93
+ /** The run's id when `VX_DTE_RUN` is unset, in a CI job it starts workers from; none elsewhere. */
94
+ runId?(): string | undefined
95
+ /**
96
+ * Workers the CI started for this run before it began (`size#n`): each
97
+ * is handed to `worker`, which counts it against its size's `max` and
98
+ * returns its spawn, for the adapter to watch and stop as its own.
99
+ */
100
+ adopt?(worker: (id: string, size: string) => WorkerSpawn): void
101
+ }
102
+
103
+ export interface DteOptions {
104
+ /** What the adapter may start, by size name. */
105
+ readonly workers?: Readonly<Record<string, WorkerSize>>
106
+ /** Starts workers; absent, the run uses the workers that join it, which needs `relay`. */
107
+ readonly adapter?: DteAdapter
108
+ /** The relay's URL; absent, the adapter's own. */
109
+ readonly relay?: string
110
+ /** The bearer token for the relay (default `VX_DTE_TOKEN`, else on GitHub Actions an OIDC token for the relay's URL). */
111
+ readonly token?: () => string | undefined | Promise<string | undefined>
112
+ /** How the run orders, places, books and scales (`Strategy`); each layer absent is the default's. */
113
+ readonly scheduler?: Partial<Strategy>
114
+ /**
115
+ * What a task with no measure counts for when the run decides how many
116
+ * workers to start (default 2 cores, 4GB). It books next to nothing, so
117
+ * it runs on any free core.
118
+ */
119
+ readonly unmeasured?: { readonly cpu: number; readonly memory: number | string }
120
+ /**
121
+ * Headers every dial to the relay carries, the orchestrator's and each
122
+ * worker's, for a proxy in front of it (`{ 'CF-Access-Client-Id':
123
+ * process.env.CF_ACCESS_CLIENT_ID }`, an Access service token,
124
+ * 2026-10-25). An unset value is left out; `authorization` stays the
125
+ * token's.
126
+ */
127
+ readonly headers?: Readonly<Record<string, string | undefined>>
128
+ }
129
+
130
+ const KEYS: PluginOptionKinds<DteOptions> = {
131
+ workers: 'object',
132
+ adapter: 'object',
133
+ relay: 'string',
134
+ token: 'function',
135
+ scheduler: 'object',
136
+ unmeasured: 'object',
137
+ headers: 'object',
138
+ }
139
+
140
+ // A typo in a size or in `unmeasured` (`maxx`) was dropped, and the size ran uncapped.
141
+ const SIZE_KEYS: PluginOptionKinds<WorkerSize> = {
142
+ max: 'number',
143
+ cpu: 'number',
144
+ memory: 'any',
145
+ platform: 'string',
146
+ }
147
+ const UNMEASURED_KEYS: PluginOptionKinds<NonNullable<DteOptions['unmeasured']>> = {
148
+ cpu: 'number',
149
+ memory: 'any',
150
+ }
151
+
152
+ const USAGE = 'Usage: vx dte worker | vx dte init [--dry] [--force]'
153
+
154
+ /**
155
+ * The names a dial sets itself (wire.ts): one of them in `headers`, in
156
+ * another case, was joined to the dial's own value.
157
+ */
158
+ const WIRE_HEADERS = new Set(['authorization', 'content-type', 'relay-proof'])
159
+
160
+ /** `headers` with unset values left out; a name or value no header can carry, or `authorization`, refused. */
161
+ function proxyHeaders(headers: DteOptions['headers']): Record<string, string> {
162
+ const out: Record<string, string> = {}
163
+ for (const [name, value] of Object.entries(headers ?? {})) {
164
+ if (value === undefined) continue
165
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) || WIRE_HEADERS.has(name.toLowerCase()))
166
+ throw new UserError(`dte() option "headers" cannot set "${name}"`)
167
+ if (typeof value !== 'string' || /[\r\n\0]/.test(value))
168
+ throw new UserError(
169
+ `dte() option "headers.${name}" must be one line of text (its value is not printed)`,
170
+ )
171
+ out[name] = value
172
+ }
173
+ return out
174
+ }
175
+
176
+ export function dte(options: DteOptions = {}) {
177
+ refuseUnknownOptions('dte()', options, KEYS)
178
+ const headers = proxyHeaders(options.headers)
179
+ const how: Strategy = { ...DEFAULT_STRATEGY }
180
+ for (const [layer, choice] of Object.entries(options.scheduler ?? {})) {
181
+ const choices = STRATEGIES[layer as keyof Strategy] as readonly string[] | undefined
182
+ if (choices === undefined)
183
+ throw new UserError(
184
+ `dte() option "scheduler" has no layer ${JSON.stringify(layer)} (layers: ${Object.keys(STRATEGIES).join(', ')})`,
185
+ )
186
+ if (!choices.includes(choice as string))
187
+ refuse(`scheduler.${layer}`, `one of ${choices.join(', ')}`, choice)
188
+ ;(how as unknown as Record<string, unknown>)[layer] = choice
189
+ }
190
+ const sizes = Object.entries(options.workers ?? {}).map(([name, s]) => {
191
+ const at = `workers.${name}`
192
+ refuseUnknownOptions(`dte() option "${at}"`, s, SIZE_KEYS)
193
+ if (!(Number.isInteger(s?.cpu) && s.cpu >= 1)) refuse(`${at}.cpu`, 'a whole number ≥ 1', s?.cpu)
194
+ const memory = bytes(s.memory)
195
+ if (memory === undefined)
196
+ refuse(`${at}.memory`, "bytes above 0 or a size such as '8GB'", s.memory)
197
+ if (!(s.max === undefined || (Number.isInteger(s.max) && s.max >= 0)))
198
+ refuse(`${at}.max`, 'a whole number ≥ 0', s.max)
199
+ if (s.platform !== undefined && !PLATFORMS.includes(s.platform))
200
+ refuse(`${at}.platform`, `one of ${PLATFORMS.join(', ')}`, s.platform)
201
+ return {
202
+ name,
203
+ max: s.max ?? Infinity,
204
+ cpu: s.cpu,
205
+ memory: memory!,
206
+ platform: s.platform ?? hostPlatform(),
207
+ }
208
+ })
209
+ // Only a platform the size names refuses a worker reporting another: an
210
+ // unnamed one is planned as this machine's, and takes whatever joins.
211
+ const named = (size: string): { platform?: Platform } => {
212
+ const platform = options.workers?.[size]?.platform
213
+ return platform === undefined ? {} : { platform }
214
+ }
215
+ const home = homeOf(sizes, hostPlatform())
216
+ const unmeasured = unmeasuredOf(options.unmeasured)
217
+ const adapter = options.adapter
218
+ // An adapter's run in its own CI with a relay names itself: the CI file
219
+ // sets nothing (2026-10-23: "super simple" CI).
220
+ let made: string | undefined
221
+ /** Each task's sends under the run's id, across the runs `vx watch` makes. */
222
+ const sent = new Map<string, number>()
223
+ const runOf = (): string | undefined =>
224
+ givenRun() ??
225
+ (options.relay === undefined
226
+ ? undefined
227
+ : (made ??=
228
+ adapter?.keyless === true && onGitHub() ? crypto.randomUUID() : adapter?.runId?.()))
229
+ if (adapter !== undefined && typeof adapter.spawn !== 'function') {
230
+ throw new UserError('dte(): adapter is an adapter, such as local()')
231
+ }
232
+ // With none, no worker was ever asked for and the run waited until killed.
233
+ if (adapter !== undefined && !sizes.some((s) => s.max > 0)) {
234
+ throw new UserError(
235
+ "dte(): workers names no size the adapter may start (one with max above 0), such as small: { cpu: 2, memory: '8GB' }",
236
+ )
237
+ }
238
+ if (adapter === undefined && options.relay === undefined) {
239
+ throw new UserError('dte(): with no adapter, set relay: the workers that join it run the tasks')
240
+ }
241
+ // With no scheme the run failed in the executor as a bare "Invalid URL".
242
+ if (options.relay !== undefined && !/^https?:\/\//.test(options.relay)) {
243
+ throw new UserError(
244
+ `dte() option "relay" must be an http(s) URL, got ${JSON.stringify(options.relay)}`,
245
+ )
246
+ }
247
+ // On GitHub Actions with none given, one for the relay's URL.
248
+ const token = async (relay: string): Promise<string | undefined> =>
249
+ (await options.token?.()) ??
250
+ (process.env['VX_DTE_TOKEN'] ||
251
+ (await import('./github.js').then((g) => g.githubRelayToken(relay))))
252
+
253
+ let nodes: ReadonlyMap<string, TaskNode> | undefined
254
+ // Core prints the footer after teardown, and its ideal needs the
255
+ // dependencies: the last run's graph, kept until the next `schedule`.
256
+ let ran: ReadonlyMap<string, TaskNode> | undefined
257
+ /** The workspace names its cache dir (`cache.dir`): `config` reads it. */
258
+ let namesCache = false
259
+ let peaks: ReadonlyMap<string, TaskResources> = new Map()
260
+ /** Each size's warm-up an earlier run learned (`warm-ups.ts`), and the read of it. */
261
+ let learned: ReadonlyMap<string, number> = new Map()
262
+ let warming: Promise<void> = Promise.resolve()
263
+ /** On CI, where the run keeps the warm-ups it learned: `setup`'s cache, root and warn. */
264
+ let keep: { cache: PluginCache; root: string; warn: (m: string) => void } | undefined
265
+ /** Each task's tail by `nodes` and `peaks`, made again when either changes. */
266
+ let tails: ((id: string) => number) | undefined
267
+ /** Settles once the needs other plugins fetch are in `peaks`: `ctx.refining()`, then the read. */
268
+ let needsIn: (() => Promise<void>) | undefined
269
+ /** Called when `schedule` has read history; set by the executor, which core makes first. */
270
+ let scheduled: (() => void) | undefined
271
+ /** The cached tasks the last commit's changes make miss (`forecast.ts`): work before any worker can say. */
272
+ let likely: ReadonlySet<string> = new Set()
273
+ // Each finished task's cache key, a hit's too: what a worker restores a
274
+ // dependency by.
275
+ const keys = new Map<string, string>()
276
+ let orchestrator: Orchestrator | undefined
277
+ let closing: Promise<void> | undefined
278
+
279
+ const close = async (): Promise<void> => {
280
+ const o = orchestrator
281
+ orchestrator = undefined
282
+ ran = nodes
283
+ nodes = undefined
284
+ const ups = o?.warmUpsMs()
285
+ const k = keep
286
+ keep = undefined
287
+ if (k !== undefined && ups !== undefined) {
288
+ const { writeWarmUps } = await import('./warm-ups.js')
289
+ await writeWarmUps(k.cache, k.root, learned, ups).catch((err: unknown) =>
290
+ k.warn(`[vx] dte: warm-ups not kept: ${err instanceof Error ? err.message : String(err)}`),
291
+ )
292
+ }
293
+ learned = new Map()
294
+ warming = Promise.resolve()
295
+ peaks = new Map()
296
+ tails = undefined
297
+ needsIn = undefined
298
+ scheduled = undefined
299
+ likely = new Set()
300
+ keys.clear()
301
+ await o?.close()
302
+ await adapter?.close?.()
303
+ }
304
+
305
+ /** `vx dte init [--dry] [--force]`: the run's CI and its workers', from these sizes. */
306
+ const init = async (args: readonly string[], ctx: CommandContext): Promise<number> => {
307
+ const extra = args.find((a) => a !== '--dry' && a !== '--force')
308
+ if (extra !== undefined)
309
+ throw new UserError(
310
+ `vx dte init: ${extra.startsWith('-') ? 'unknown flag' : 'unexpected argument'}: ${extra} (see \`vx dte --help\`)`,
311
+ 'VX_E_USAGE',
312
+ )
313
+ const root = ctx.workspaceRoot
314
+ if (adapter === undefined)
315
+ throw new UserError(
316
+ 'vx dte init: dte() names no adapter, so vx starts no workers and needs no CI files',
317
+ )
318
+ if (adapter.init === undefined) {
319
+ ui.say(
320
+ 'info',
321
+ 'vx dte init: this adapter starts workers on this machine; no CI files to write',
322
+ 'out',
323
+ )
324
+ return 0
325
+ }
326
+ const { askTasks, managerOf, originOf, writeInit } = await import('./init.js')
327
+ // CI reads its files at the repository's root; a workspace in a
328
+ // subdirectory got them in its own, which no CI reads (2026-10-25).
329
+ const git = (arg: string): string =>
330
+ Bun.spawnSync(['git', 'rev-parse', arg], { cwd: root, stderr: 'ignore' })
331
+ .stdout.toString()
332
+ .trim()
333
+ const top = git('--show-toplevel') || root
334
+ const files = await adapter.init({
335
+ workspaceRoot: root,
336
+ dir: git('--show-prefix').replace(/\/$/, ''),
337
+ ...(options.relay !== undefined ? { relay: options.relay } : {}),
338
+ sizes: sizes.map((s) => ({
339
+ name: s.name,
340
+ ...(s.max === Infinity ? {} : { max: s.max }),
341
+ ...named(s.name),
342
+ })),
343
+ manager: managerOf(root),
344
+ tasks: askTasks(),
345
+ ...originOf(root),
346
+ })
347
+ const out = (line: string): void => ui.say('info', line, 'out')
348
+ if (args.includes('--dry')) {
349
+ for (const [rel, text] of Object.entries(files.files)) ui.print('out', ui.excerpt(rel, text))
350
+ for (const line of files.next) out(line)
351
+ return 0
352
+ }
353
+ return writeInit(top, files, args.includes('--force'), out)
354
+ }
355
+
356
+ return definePlugin(import.meta, {
357
+ commands: {
358
+ dte: {
359
+ description: "Write a DTE run's CI (dte init) or run its tasks (dte worker).",
360
+ async run(argv, ctx) {
361
+ // Asked for, the usage is an answer: `vx dte --help` exited 1 (2026-10-26).
362
+ if (argv.some((a) => a === '--help' || a === '-h') || argv[0] === 'help') {
363
+ ui.say('info', USAGE, 'out')
364
+ return 0
365
+ }
366
+ if (argv[0] === 'init') return init(argv.slice(1), ctx)
367
+ // The bare usage said nothing of what was wrong with the line typed.
368
+ if (argv[0] === undefined)
369
+ throw new UserError(`missing subcommand\n${USAGE}`, 'VX_E_USAGE')
370
+ if (argv[0] !== 'worker')
371
+ throw new UserError(`unknown subcommand: ${argv[0]}\n${USAGE}`, 'VX_E_USAGE')
372
+ if (argv.length > 1)
373
+ throw new UserError(
374
+ `vx dte worker: ${argv[1]!.startsWith('-') ? 'unknown flag' : 'unexpected argument'}: ${argv[1]} (see \`vx dte --help\`)`,
375
+ 'VX_E_USAGE',
376
+ )
377
+ const run = runOf()
378
+ const relay = process.env['VX_DTE_RELAY'] || options.relay
379
+ // Only what is missing: with VX_DTE_RUN set, the old line still asked for it.
380
+ if (run === undefined || relay === undefined) {
381
+ const missing = [
382
+ ...(run === undefined ? ['VX_DTE_RUN'] : []),
383
+ ...(relay === undefined ? ['VX_DTE_RELAY or dte({ relay })'] : []),
384
+ ]
385
+ throw new UserError(`vx dte worker: needs ${missing.join(', and ')}`)
386
+ }
387
+ if (!/^https?:\/\//.test(relay))
388
+ throw new UserError(`vx dte worker: VX_DTE_RELAY must be http(s), got ${relay}`)
389
+ const secret = givenSecret()
390
+ if (secret === undefined && !onGitHub()) {
391
+ throw new UserError(
392
+ 'vx dte worker: needs VX_DTE_SECRET, the run’s, or a GitHub Actions job with id-token: write',
393
+ )
394
+ }
395
+ const [{ runWorker }, seal] = await Promise.all([
396
+ import('./worker.js'),
397
+ import('./seal.js'),
398
+ ])
399
+ const id = process.env['VX_DTE_WORKER'] || adapter?.workerId?.()
400
+ return runWorker({
401
+ relay,
402
+ run,
403
+ ...(id === undefined || id === '' ? {} : { id }),
404
+ root: ctx.workspaceRoot,
405
+ token: () => token(relay),
406
+ headers,
407
+ proof: secret === undefined ? seal.githubProof() : seal.secretProof(secret),
408
+ artifacts: ctx.artifactsFor,
409
+ })
410
+ },
411
+ },
412
+ },
413
+
414
+ config(workspace) {
415
+ namesCache = workspace.cache?.dir !== undefined
416
+ },
417
+
418
+ setup(ctx) {
419
+ // A run that is not distributed subscribes nothing.
420
+ if (runOf() === undefined) return
421
+ if (ctx.ci) keep = { cache: ctx.cache, root: ctx.workspaceRoot, warn: (m) => ctx.warn(m) }
422
+ ctx.on('onTaskComplete', (node, outcome) => {
423
+ if (outcome.hash !== undefined) keys.set(node.id, outcome.hash)
424
+ })
425
+ },
426
+
427
+ async schedule(graph, ctx) {
428
+ nodes = graph
429
+ ran = undefined
430
+ tails = undefined
431
+ // A run that is not distributed reads nothing.
432
+ if (orchestrator === undefined) return undefined
433
+ const ids = [...graph.values()]
434
+ .filter((n) => n.config.exec?.command !== undefined)
435
+ .map((n) => n.id)
436
+ const read = async (): Promise<void> => {
437
+ if (orchestrator === undefined) return
438
+ try {
439
+ peaks = await localHistory(ctx.localCache, {
440
+ branch: ctx.branch,
441
+ defs: taskDefinitions(graph.values()),
442
+ }).resourcesFor(ids)
443
+ tails = undefined
444
+ } catch (err) {
445
+ ctx.warn(
446
+ `[vx] dte: tasks are packed one core each: ${err instanceof Error ? err.message : String(err)}`,
447
+ )
448
+ }
449
+ }
450
+ let reading = read()
451
+ warming = import('./warm-ups.js')
452
+ .then((w) => w.readWarmUps(ctx.cache, ctx.workspaceRoot))
453
+ .then(
454
+ (m) => void (learned = m),
455
+ (err: unknown) =>
456
+ ctx.warn(
457
+ `[vx] dte: no warm-ups learned: ${err instanceof Error ? err.message : String(err)}`,
458
+ ),
459
+ )
460
+ const guess = changedSinceParent(ctx.workspaceRoot).then(async (changed) => {
461
+ const { likelyMisses } = await import('./forecast.js')
462
+ likely = likelyMisses(graph, changed, ctx.workspaceRoot)
463
+ })
464
+ await Promise.all([reading, guess])
465
+ // Needs another plugin fetched (the default branch's timings) land
466
+ // in history after this: the tasks not yet placed pack by them, and
467
+ // the first ask for workers waits for them.
468
+ ctx.onRefine(() => void (reading = read()))
469
+ // Asked at the first ask, when every plugin's schedule has promised its
470
+ // own. Read again then: a refine that landed before `onRefine` above
471
+ // was listening left the first read's empty history, and main planned
472
+ // 9 asks on 0 measured tasks, 25% of its cores used (2026-10-23).
473
+ needsIn = () =>
474
+ Promise.all([ctx.refining().then(() => (reading = read())), warming]).then(() => {})
475
+ scheduled?.()
476
+ return undefined
477
+ },
478
+
479
+ async executor(ctx) {
480
+ await closing
481
+ const run = runOf()
482
+ if (run === undefined) return undefined
483
+ const relay = options.relay ?? (await adapter?.relay?.())
484
+ if (relay === undefined)
485
+ throw new UserError('dte(): no relay: set relay, or use an adapter that has one')
486
+ const [
487
+ { Orchestrator },
488
+ { join },
489
+ { decide, knownMisses, slowdown, tailsMs, WARM_UP_MS },
490
+ { snapshot },
491
+ seal,
492
+ ] = await Promise.all([
493
+ import('./orchestrator.js'),
494
+ import('./wire.js'),
495
+ import('./planner.js'),
496
+ import('./tree.js'),
497
+ import('./seal.js'),
498
+ ])
499
+ // VX_DTE_SECRET wins. On GitHub, workers that join on their own or
500
+ // an adapter's keyless ones prove themselves by OIDC; the workers
501
+ // another adapter starts get a secret made for this run.
502
+ let secret = givenSecret()
503
+ let proof: Proof | undefined
504
+ if (secret === undefined) {
505
+ if ((adapter === undefined || adapter.keyless === true) && onGitHub())
506
+ proof = seal.githubProof(
507
+ undefined,
508
+ adapter?.workerRuns === undefined ? undefined : () => adapter.workerRuns!(),
509
+ )
510
+ else if (adapter === undefined)
511
+ throw new UserError(
512
+ 'dte(): with no adapter, set VX_DTE_SECRET here and on each worker, or run on GitHub Actions with id-token: write',
513
+ )
514
+ else secret = seal.runSecret()
515
+ }
516
+ proof ??= seal.secretProof(secret!)
517
+ const { commit, tree } = await snapshot(ctx.workspaceRoot)
518
+ if (tree === '') throw new UserError('dte(): a DTE run needs a git commit')
519
+ const need = (id: string): Need => booked(needOf(peaks.get(id), unmeasured), how.book)
520
+ // The run plans again on it, now when `now`; set once the plan is.
521
+ let onChange = (_now?: boolean): void => {}
522
+ const o = new Orchestrator({
523
+ me: seal.sealOrchestrator(
524
+ await join(relay, run, 'orchestrator', null, () => token(relay), headers),
525
+ run,
526
+ proof,
527
+ ctx.warn,
528
+ ),
529
+ root: ctx.workspaceRoot,
530
+ tree,
531
+ nodes: () => nodes,
532
+ need,
533
+ artifacts: ctx.artifacts,
534
+ cacheFacts: () => ctx.cacheFacts(),
535
+ run,
536
+ sent,
537
+ keyOf: (id) => keys.get(id),
538
+ tail: (id) =>
539
+ (tails ??= tailsMs(nodes ?? new Map(), (x) => peaks.get(x)?.p50DurationMs))(id),
540
+ how,
541
+ home,
542
+ lost: (id) => {
543
+ const s = sizeOf.get(id)
544
+ if (s === undefined) return
545
+ sizeOf.delete(id)
546
+ started.set(s.name, started.get(s.name)! - 1)
547
+ // Before the run counts what is left to start: none would fail it.
548
+ onChange(true)
549
+ },
550
+ joined: () => onChange(),
551
+ // A worker given up on joining: its job, still queued or stuck, is stopped.
552
+ overdue: (id) => void adapter?.cancel?.([id], true),
553
+ // Before the run counts what is left to start: a worker that let a
554
+ // task's lease end failed main's macOS run with another to start
555
+ // (2026-10-23).
556
+ faulted: () => onChange(true),
557
+ stalled: (waiting) =>
558
+ ctx.warn(`[vx] dte(): no task began or ended for a minute; waiting on\n${waiting}`),
559
+ warn: ctx.warn,
560
+ })
561
+ orchestrator = o
562
+ const started = new Map<string, number>()
563
+ const sizeOf = new Map<string, Size>()
564
+ let index = 0
565
+ // Numbered within its size: one count across sizes left M asking
566
+ // where small#1 was (mac#1 had it, 2026-10-23).
567
+ const numbered = new Map<string, number>()
568
+ const nextId = (size: string): string => {
569
+ const n = (numbered.get(size) ?? 0) + 1
570
+ numbered.set(size, n)
571
+ return `${size}#${n}`
572
+ }
573
+ // A worker the run counts as started, against its size's max.
574
+ const spawnOf = (s: Size, id: string, n: number): WorkerSpawn => {
575
+ started.set(s.name, (started.get(s.name) ?? 0) + 1)
576
+ o.spawned(id, {
577
+ size: s.name,
578
+ cpu: s.cpu,
579
+ memory: s.memory,
580
+ ...named(s.name),
581
+ })
582
+ sizeOf.set(id, s)
583
+ return {
584
+ run,
585
+ relay,
586
+ size: s.name,
587
+ max: s.max,
588
+ cpu: s.cpu,
589
+ memory: s.memory,
590
+ index: n,
591
+ id,
592
+ workspaceRoot: ctx.workspaceRoot,
593
+ commit,
594
+ ...(namesCache ||
595
+ process.env['VX_CACHE_DIR'] ||
596
+ ctx.cacheDir !== path.join(ctx.workspaceRoot, '.vx', 'cache')
597
+ ? { cacheDir: ctx.cacheDir }
598
+ : {}),
599
+ env: {
600
+ VX_DTE_RUN: run,
601
+ VX_DTE_WORKER: id,
602
+ VX_DTE_RELAY: relay,
603
+ ...(secret === undefined ? {} : { VX_DTE_SECRET: secret }),
604
+ },
605
+ exited: (reason) => o.failed(id, reason),
606
+ }
607
+ }
608
+ adapter?.report?.(ctx.warn)
609
+ // Workers the CI started before the run did (`DteAdapter.adopt`).
610
+ adapter?.adopt?.((id, size) => {
611
+ const s = sizes.find((x) => x.name === size)
612
+ const n = Number(id.slice(id.lastIndexOf('#') + 1))
613
+ if (s === undefined || !Number.isInteger(n) || n < 1)
614
+ throw new UserError(`dte(): a started worker ${id} is no worker of a declared size`)
615
+ numbered.set(size, Math.max(numbered.get(size) ?? 0, n))
616
+ return spawnOf(s, id, ++index)
617
+ })
618
+ // Tasks core handed over: each missed the cache (`missedWork`).
619
+ const executed = new Set<string>()
620
+ // The run's deadline (`Plan.boundMs`), and the work its plans knew.
621
+ let deadline: number | undefined
622
+ const known = new Set<string>()
623
+ // A plan that needs a worker later plans again then.
624
+ let again: ReturnType<typeof setTimeout> | undefined
625
+ // The fewest workers that end the run's known work within a margin of
626
+ // the most would, played out (`planner.ts`); idle ones it does
627
+ // without leave. Again at each miss, finish, join and history read.
628
+ const shape = (remaining: ReadonlySet<string>): void => {
629
+ // This run's for the size, else an earlier run's, else any size's
630
+ // this run, else the default.
631
+ const ups = o.warmUpsMs()
632
+ const any = o.warmUpMs()
633
+ const warmUp = (size: string | undefined): number =>
634
+ (size === undefined ? undefined : (ups.get(size) ?? learned.get(size))) ??
635
+ any ??
636
+ WARM_UP_MS
637
+ // What is left takes as much longer than history as what ended did.
638
+ const p50 = (id: string): number | undefined => peaks.get(id)?.p50DurationMs
639
+ const slow = slowdown(o.finished(p50))
640
+ const ms = (id: string): number | undefined => {
641
+ const m = p50(id)
642
+ return m === undefined ? undefined : m * slow
643
+ }
644
+ const { workers, running } = o.planState(ms, warmUp)
645
+ const final = adapter === undefined
646
+ const p = decide({
647
+ remaining,
648
+ nodes: nodes ?? new Map(),
649
+ executed: knownMisses(likely, sure, executed, remaining),
650
+ // As the run books them: a plan of history alone asks for workers it then fills less.
651
+ need: (id) => o.booking(need(id)),
652
+ durationMs: ms,
653
+ workers,
654
+ running,
655
+ sizes,
656
+ started,
657
+ final,
658
+ warmUpMs: warmUp,
659
+ replaceable: adapter !== undefined,
660
+ limitMs: deadline === undefined ? undefined : deadline - performance.now(),
661
+ known,
662
+ how,
663
+ home,
664
+ queued: o.queued(warmUp),
665
+ })
666
+ deadline = performance.now() + p.boundMs
667
+ for (const t of p.tasks) known.add(t.id)
668
+ if ((p.start.length > 0 && !final) || p.release.length > 0)
669
+ o.planned({
670
+ start: final ? [] : p.start.map((s) => s.name),
671
+ release: p.release,
672
+ tasks: p.tasks.length,
673
+ endsInMs: p.endsInMs,
674
+ boundMs: p.boundMs,
675
+ workers,
676
+ ...(p.start.length > 0 && !final
677
+ ? { work: p.tasks, warmUps: new Map(sizes.map((s) => [s.name, warmUp(s.name)])) }
678
+ : {}),
679
+ })
680
+ const { tasks } = p
681
+ // At the end the relay closing ends them all.
682
+ if (p.release.length > 0 && remaining.size > 0) {
683
+ const cancelled = o.release(p.release)
684
+ const sized = new Map<string, Size>()
685
+ for (const id of cancelled) {
686
+ const s = sizeOf.get(id)
687
+ if (s === undefined) continue
688
+ sized.set(id, s)
689
+ sizeOf.delete(id)
690
+ started.set(s.name, started.get(s.name)! - 1)
691
+ }
692
+ // One its adapter cannot stop joins all the same: the run counts
693
+ // it again, and takes it.
694
+ if (cancelled.length > 0)
695
+ void Promise.resolve(
696
+ adapter?.cancel === undefined ? cancelled : adapter.cancel(cancelled),
697
+ )
698
+ .then((left) => {
699
+ const back = o.leftRunning(left ?? [])
700
+ for (const id of back) {
701
+ const s = sized.get(id)
702
+ if (s === undefined) continue
703
+ sizeOf.set(id, s)
704
+ started.set(s.name, (started.get(s.name) ?? 0) + 1)
705
+ }
706
+ if (back.length > 0) onChange()
707
+ })
708
+ .catch((err: unknown) =>
709
+ ctx.warn(
710
+ `[vx] dte(): stopping ${cancelled.join(', ')} failed: ${err instanceof Error ? err.message : String(err)}`,
711
+ ),
712
+ )
713
+ }
714
+ if (again !== undefined) clearTimeout(again)
715
+ again = undefined
716
+ if (p.againInMs !== undefined && !final) {
717
+ again = setTimeout(replan, p.againInMs)
718
+ again.unref?.()
719
+ }
720
+ if (p.start.length === 0 || final) return
721
+ o.asked(
722
+ p.start.length,
723
+ tasks.length,
724
+ tasks.filter((t) => measured(t.id)).length,
725
+ p.endsInMs,
726
+ tasks.filter((t) => !measured(t.id)).map((t) => t.id),
727
+ )
728
+ for (const s of p.start) {
729
+ ++index
730
+ const id = nextId(s.name)
731
+ const a = spawnOf(s, id, index)
732
+ // A sync throw too: `spawn` may return void, and one thrown here
733
+ // escaped the plan's timer and ended the process.
734
+ void new Promise<void>((done) => done(adapter.spawn(a))).catch((err: unknown) =>
735
+ o.failed(id, err instanceof Error ? err.message : String(err)),
736
+ )
737
+ }
738
+ }
739
+ // Events come in bursts (core hands over tasks ready at once): one
740
+ // plan per tick, paced by its cost.
741
+ const paced = pace(() => {
742
+ if (pending !== undefined) shape(pending)
743
+ })
744
+ const replan = (now?: boolean): void => {
745
+ if (!missed || pending === undefined || !ready()) return
746
+ if (now === true) return paced.now()
747
+ paced.soon()
748
+ }
749
+ // Until a task misses, demand waits: a run all hits starts no worker.
750
+ let missed = false
751
+ let pending: ReadonlySet<string> | undefined
752
+ // The tasks core's probe found in no cache: they run, known before any worker keys them.
753
+ let sure: ReadonlySet<string> = new Set()
754
+ const measured = (id: string): boolean => peaks.get(id)?.maxCpuParallelism !== undefined
755
+ // Needs another plugin fetches (the default branch's timings) land a
756
+ // moment into the run. Asked before them, every task books a whole
757
+ // worker: main asked 12 workers for 13 misses and 5 ran nothing
758
+ // (2026-10-23). The first ask waits for them.
759
+ // Core makes the executor and settles its demand before `schedule`
760
+ // reads history: asked then, main's first ask booked a whole worker
761
+ // for each of 122 tasks, 0 measured, and cancelled 13 of 14
762
+ // (2026-10-23). A run that never reaches `schedule` (every task a
763
+ // restore) waits for its first miss.
764
+ let waited: boolean | undefined
765
+ const ready = (): boolean => {
766
+ if (waited === true) return true
767
+ if (needsIn === undefined) return executed.size > 0
768
+ if (waited === undefined) {
769
+ waited = false
770
+ void needsIn().then(() => {
771
+ waited = true
772
+ replan()
773
+ })
774
+ }
775
+ return false
776
+ }
777
+ onChange = replan
778
+ // Work the last commit's changes make: planned before any task misses.
779
+ const forecast = (): void => {
780
+ if (likely.size > 0) missed = true
781
+ replan()
782
+ }
783
+ if (needsIn === undefined) scheduled = forecast
784
+ else forecast()
785
+ return {
786
+ name: 'dte',
787
+ remote: true,
788
+ // No task runs on the orchestrator; only hits restore here (2026-10-24).
789
+ exclusive: true,
790
+ // Each worker times a task from its start: a wait for a booting worker is not its run.
791
+ timeouts: true,
792
+ sandbox: true,
793
+ // A task's persistent dependencies start on its worker, never here.
794
+ servers: true,
795
+ // A worker answers a task's runtime probes before it runs it.
796
+ runtime: true,
797
+ // Its sizes' platforms; workers that join on their own, this machine's.
798
+ platforms:
799
+ sizes.length === 0 ? [hostPlatform()] : [...new Set(sizes.map((s) => s.platform))],
800
+ // Room for the workers' cores and their prefetch; the orchestrator
801
+ // queues the rest. Workers that join on their own are as many as join.
802
+ capacity:
803
+ adapter === undefined || sizes.some((s) => s.max === Infinity)
804
+ ? JOINED_CAPACITY
805
+ : Math.max(
806
+ 1,
807
+ sizes.reduce((n, s) => n + 2 * s.max * s.cpu, 0),
808
+ ),
809
+ execute: (req) => {
810
+ // A miss: the run's known work grows.
811
+ missed = true
812
+ executed.add(req.taskId)
813
+ replan()
814
+ return o.execute(req)
815
+ },
816
+ footer: () => o.footer(() => o.idealMs((id) => (nodes ?? ran)?.get(id)?.deps ?? [], sizes)),
817
+ machines: () => o.machines(),
818
+ idealMs: () => o.idealMs((id) => (nodes ?? ran)?.get(id)?.deps ?? [], sizes),
819
+ profile: () => o.profile(),
820
+ running: (id) => o.where(id),
821
+ demand(remaining, misses) {
822
+ if (pending === undefined) o.probed(remaining.size, misses.size)
823
+ o.missed = misses
824
+ pending = remaining
825
+ sure = misses
826
+ if (misses.size > 0) missed = true
827
+ replan()
828
+ },
829
+ }
830
+ },
831
+
832
+ async teardown() {
833
+ closing = close()
834
+ await closing
835
+ },
836
+ })
837
+ }
838
+
839
+ /** A GitHub Actions job that may ask for OIDC tokens: all githubProof() reads. */
840
+ function onGitHub(): boolean {
841
+ return Boolean(
842
+ process.env['ACTIONS_ID_TOKEN_REQUEST_URL'] &&
843
+ process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
844
+ process.env['GITHUB_REPOSITORY_ID'],
845
+ )
846
+ }
847
+
848
+ /** `VX_DTE_SECRET`: the run's, given. */
849
+ function givenSecret(): string | undefined {
850
+ return process.env['VX_DTE_SECRET'] || undefined
851
+ }
852
+
853
+ /** A plan waits this many times the last one's length after it: its share of the CPU at most one in eleven. */
854
+ const PLAN_GAP = 10
855
+
856
+ /**
857
+ * `fn` at most once a tick (`soon`), and no sooner than `PLAN_GAP` times
858
+ * its last run's length after that run; `now` runs it at once. Planned
859
+ * after each task's end, 2000 tasks made 1281 plans and 9 s of the
860
+ * orchestrator's 19: workers waited on it, and its relay found them lost
861
+ * (2026-10-25).
862
+ */
863
+ export function pace(fn: () => void): { soon(): void; now(): void } {
864
+ let pending: { cancel(): void } | undefined
865
+ let next = 0
866
+ const run = (): void => {
867
+ pending?.cancel()
868
+ pending = undefined
869
+ const start = performance.now()
870
+ try {
871
+ fn()
872
+ } finally {
873
+ const end = performance.now()
874
+ next = end + PLAN_GAP * (end - start)
875
+ }
876
+ }
877
+ return {
878
+ now: run,
879
+ soon() {
880
+ if (pending !== undefined) return
881
+ const wait = next - performance.now()
882
+ if (wait <= 0) {
883
+ const t = setImmediate(run)
884
+ pending = { cancel: () => clearImmediate(t) }
885
+ } else {
886
+ const t = setTimeout(run, wait)
887
+ t.unref?.()
888
+ pending = { cancel: () => clearTimeout(t) }
889
+ }
890
+ },
891
+ }
892
+ }
893
+
894
+ /**
895
+ * What an unmeasured task counts for in the worker ask (`dte({ unmeasured
896
+ * })`): fixed, since a plan cannot know which worker it lands on
897
+ * (2026-10-23: "by default, 2 cores, 4 gigs").
898
+ */
899
+ const FRESH: Need = { cores: 2, memory: 4e9 }
900
+ /**
901
+ * What it books: one free core, no memory (2026-10-23: "pack them
902
+ * where we have free capability"). Simulated on main's graph with a third
903
+ * of its tasks unmeasured, it ended at 161 s where a whole worker each
904
+ * ended at 195 s; a quarter core packed a cold run's tasks 16 to a
905
+ * 4-core worker and ended at 267 s.
906
+ */
907
+ const PLACED = { cores: 1, memory: 0 }
908
+
909
+ function unmeasuredOf(u: DteOptions['unmeasured']): Need {
910
+ if (u === undefined) return FRESH
911
+ refuseUnknownOptions('dte() option "unmeasured"', u, UNMEASURED_KEYS)
912
+ if (!(typeof u.cpu === 'number' && u.cpu > 0 && Number.isFinite(u.cpu)))
913
+ refuse('unmeasured.cpu', 'a number above 0', u.cpu)
914
+ const memory = bytes(u.memory)
915
+ if (memory === undefined)
916
+ refuse('unmeasured.memory', "bytes above 0 or a size such as '4GB'", u.memory)
917
+ return { cores: u.cpu, memory: memory! }
918
+ }
919
+
920
+ /** A `dte()` option it cannot use, named with the value as written (a NaN as NaN, not null). */
921
+ function refuse(option: string, want: string, got: unknown): never {
922
+ const said = typeof got === 'number' ? String(got) : (JSON.stringify(got) ?? String(got))
923
+ throw new UserError(`dte() option "${option}" must be ${want}, got ${said}`)
924
+ }
925
+
926
+ /** What a task asks of a worker, from its history. */
927
+ export function needOf(r: TaskResources | undefined, ask: Need = FRESH): Need {
928
+ // Unmeasured (new, or over before the Linux sandbox's first look, which
929
+ // leaves no CPU): any free core, counted as `ask` when the run is sized.
930
+ // A whole worker each left main's small#1 at 13% for three sub-second
931
+ // tasks (2026-10-23: "we can't waste whole worker for one task").
932
+ // One that filled its worker: a whole bigger one.
933
+ if (r?.maxCpuParallelism === undefined)
934
+ return { ...PLACED, ask: { cores: ask.cores, memory: ask.memory } }
935
+ if (r.saturatedCores !== undefined || r.saturatedMemoryBytes !== undefined)
936
+ return {
937
+ cores: Infinity,
938
+ memory: Infinity,
939
+ above: { cores: r.saturatedCores ?? 0, memory: r.saturatedMemoryBytes ?? 0 },
940
+ }
941
+ // The cores it keeps busy on average, unrounded: a suite at 0.7 booked a
942
+ // whole core and CI's workers ran at 30-50% (2026-10-22: "close to
943
+ // 100%"). A quarter at least, so a task that only waits is not free.
944
+ return { cores: Math.max(0.25, r.maxCpuParallelism), memory: r.maxPeakRssBytes ?? 0 }
945
+ }
946
+
947
+ /** The executor's pool when workers are uncapped: more than any run starts at once. */
948
+ const JOINED_CAPACITY = 1024
949
+
950
+ const UNITS: Record<string, number> = {
951
+ B: 1,
952
+ KB: 1e3,
953
+ MB: 1e6,
954
+ GB: 1e9,
955
+ TB: 1e12,
956
+ KIB: 1024,
957
+ MIB: 1024 ** 2,
958
+ GIB: 1024 ** 3,
959
+ TIB: 1024 ** 4,
960
+ }
961
+
962
+ /** `8GB`, `512MiB` or a byte count; undefined for anything else. */
963
+ export function bytes(v: unknown): number | undefined {
964
+ if (typeof v === 'number') return Number.isFinite(v) && v > 0 ? v : undefined
965
+ if (typeof v !== 'string') return undefined
966
+ const m = /^\s*(\d+(?:\.\d+)?)\s*([a-z]+)\s*$/i.exec(v)
967
+ const unit = m === null ? undefined : UNITS[m[2]!.toUpperCase()]
968
+ const n = unit === undefined ? 0 : Number(m![1]) * unit
969
+ // `'0GB'` is no more a size than `0`: a worker of no memory fits nothing beside.
970
+ return n > 0 ? n : undefined
971
+ }
972
+
973
+ /** A random (v4) UUID, as the relay takes: a run id no one can guess. */
974
+ const RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
975
+
976
+ /** `VX_DTE_RUN`, which turns distribution on. */
977
+ function givenRun(): string | undefined {
978
+ const run = process.env['VX_DTE_RUN']
979
+ if (run === undefined || run === '') return undefined
980
+ if (!RUN_ID.test(run)) {
981
+ throw new UserError(
982
+ `VX_DTE_RUN is a random UUID, such as one \`uuidgen\` prints, so no one can guess the run: not ${JSON.stringify(run)}`,
983
+ )
984
+ }
985
+ return run
986
+ }
987
+
988
+ /**
989
+ * The workspace-relative paths changed since HEAD's first parent, the
990
+ * working tree's changes included: none when git cannot say (no parent, a
991
+ * shallow clone), and the run waits for its misses as before.
992
+ */
993
+ export async function changedSinceParent(workspaceRoot: string): Promise<string[]> {
994
+ try {
995
+ const proc = Bun.spawn({
996
+ cmd: ['git', 'diff', '--name-only', '-z', '--relative', '--no-renames', 'HEAD^', '--'],
997
+ cwd: workspaceRoot,
998
+ stdout: 'pipe',
999
+ stderr: 'ignore',
1000
+ })
1001
+ const [out, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited])
1002
+ return exit === 0 ? out.split('\0').filter((l) => l !== '') : []
1003
+ // quiet: a local git read; no files is the answer
1004
+ } catch {
1005
+ return []
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * The platform a task naming none runs on: `host`, when the sizes run it
1011
+ * and another. A worker of another platform then runs only the tasks that
1012
+ * name it, so they never wait behind the rest, and leaves when they are
1013
+ * done (2026-10-23). No size of `host`: anywhere, or nothing runs them.
1014
+ */
1015
+ export function homeOf(sizes: readonly Size[], host: string): string | undefined {
1016
+ const of = new Set(sizes.filter((s) => s.max > 0).map((s) => s.platform))
1017
+ return of.has(host) && of.size > 1 ? host : undefined
1018
+ }