@vzn/vx 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.
- package/README.md +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/graph/scheduler.ts
CHANGED
|
@@ -33,6 +33,81 @@ export class RestoreDemoted extends Error {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Thrown by `execute` when restoring a cache hit threw (a full disk, a
|
|
38
|
+
* path in the way, a link out of the project). The run reports `error`
|
|
39
|
+
* as any throw, and the failed outcome says `restoreFailed`: a failed
|
|
40
|
+
* task, counted as neither a miss nor a hit.
|
|
41
|
+
*/
|
|
42
|
+
export class RestoreFailed extends Error {
|
|
43
|
+
constructor(readonly error: unknown) {
|
|
44
|
+
super(thrownMessage(error))
|
|
45
|
+
this.name = 'RestoreFailed'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by `execute` when deriving the task's key threw (a runtime probe
|
|
51
|
+
* that exited non-zero, a refused `inputs.env` value). The command never
|
|
52
|
+
* ran: the failed outcome is marked (`isKeyFailed`), and the line says so.
|
|
53
|
+
*/
|
|
54
|
+
export class KeyFailed extends Error {
|
|
55
|
+
constructor(readonly error: unknown) {
|
|
56
|
+
super(thrownMessage(error))
|
|
57
|
+
this.name = 'KeyFailed'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A mark, not a field: `TaskOutcome` and `RunPlan` are public types. A symbol
|
|
62
|
+
// rides a spread copy and never reaches JSON.
|
|
63
|
+
const KEY_FAILED = Symbol('keyFailed')
|
|
64
|
+
|
|
65
|
+
/** Mark a failed outcome (or a plan holding one) as having no derivable key. */
|
|
66
|
+
export function markKeyFailed<T extends object>(o: T): T {
|
|
67
|
+
;(o as Record<symbol, unknown>)[KEY_FAILED] = true
|
|
68
|
+
return o
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether `markKeyFailed` marked it: no key, so no command ran and no cache was asked. */
|
|
72
|
+
export function isKeyFailed(o: object): boolean {
|
|
73
|
+
return (o as Record<symbol, unknown>)[KEY_FAILED] === true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Thrown by `execute` when vx's own work on a miss's outputs threw: the
|
|
78
|
+
* clean before an attempt, or the save after an exit 0 (a name that is
|
|
79
|
+
* not UTF-8). The outcome keeps `exitCode` and `durationMs` (what the
|
|
80
|
+
* command returned and took so far) and is marked (`outputsFailedOf`),
|
|
81
|
+
* so no label names an exit the command never returned.
|
|
82
|
+
*/
|
|
83
|
+
export class OutputsFailed extends Error {
|
|
84
|
+
constructor(
|
|
85
|
+
readonly error: unknown,
|
|
86
|
+
readonly step: OutputsStep,
|
|
87
|
+
readonly exitCode: number,
|
|
88
|
+
readonly durationMs: number,
|
|
89
|
+
) {
|
|
90
|
+
super(thrownMessage(error))
|
|
91
|
+
this.name = 'OutputsFailed'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type OutputsStep = 'clean' | 'save'
|
|
96
|
+
|
|
97
|
+
// A mark, as KEY_FAILED: `TaskOutcome` is a public type.
|
|
98
|
+
const OUTPUTS_FAILED = Symbol('outputsFailed')
|
|
99
|
+
|
|
100
|
+
/** Mark a failed outcome (or its view) with the output step that failed it. */
|
|
101
|
+
export function markOutputsFailed<T extends object>(o: T, step: OutputsStep): T {
|
|
102
|
+
;(o as Record<symbol, unknown>)[OUTPUTS_FAILED] = step
|
|
103
|
+
return o
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The output step `markOutputsFailed` marked, if any. */
|
|
107
|
+
export function outputsFailedOf(o: object): OutputsStep | undefined {
|
|
108
|
+
return (o as Record<symbol, OutputsStep | undefined>)[OUTPUTS_FAILED]
|
|
109
|
+
}
|
|
110
|
+
|
|
36
111
|
/** What moved in a missed task's key since the last entry saved for it. */
|
|
37
112
|
export interface InputChanges {
|
|
38
113
|
/** How many components changed, appeared or went. 0: the same key, its entry gone. */
|
|
@@ -128,6 +203,13 @@ export interface TaskOutcome {
|
|
|
128
203
|
* so a dependant that folds `hash` saves nothing either (A-12).
|
|
129
204
|
*/
|
|
130
205
|
unkeyed?: true
|
|
206
|
+
/**
|
|
207
|
+
* The input this task's own re-check found moved while it ran (absolute),
|
|
208
|
+
* or null when the key re-derived before the command already differed.
|
|
209
|
+
* The command read a state of it no `vx watch` judgement saw, so the
|
|
210
|
+
* loop counts its next settled state as a change.
|
|
211
|
+
*/
|
|
212
|
+
driftedInput?: string | null
|
|
131
213
|
/**
|
|
132
214
|
* The task with no `inputs` this task's key folds (this task, or one
|
|
133
215
|
* upstream): no run can derive that key again, so it neither read nor
|
|
@@ -148,6 +230,14 @@ export interface TaskOutcome {
|
|
|
148
230
|
* read this one field (item 267).
|
|
149
231
|
*/
|
|
150
232
|
blockedBy?: string
|
|
233
|
+
/**
|
|
234
|
+
* On an outcome that ran behind a failed dependency (`continueMode:
|
|
235
|
+
* 'always'`), one `--exclude-dependencies` dropped, or a server that died
|
|
236
|
+
* while it ran: the task at the root of it. Its result is not its key's, so no flaky verdict reads it and
|
|
237
|
+
* its run row names the root as a skip's `blockedBy` does. Set by `run()`
|
|
238
|
+
* before any observer (a plugin's `onTaskComplete`) hears the outcome.
|
|
239
|
+
*/
|
|
240
|
+
taintedBy?: string
|
|
151
241
|
/**
|
|
152
242
|
* On a `skipped` outcome: its `exec.platforms` leave out this machine.
|
|
153
243
|
* Its dependants are skipped with `blockedBy` naming it, and neither
|
|
@@ -186,7 +276,7 @@ export interface TaskOutcome {
|
|
|
186
276
|
serverExit?: number
|
|
187
277
|
/**
|
|
188
278
|
* The input that changed while the command ran, failing the task under
|
|
189
|
-
* `
|
|
279
|
+
* `inputs.stability.severity: 'error'` (workspace-relative; '' when vx
|
|
190
280
|
* could not name it). The exit is vx's 1; the label reads this instead.
|
|
191
281
|
*/
|
|
192
282
|
inputMoved?: string
|
|
@@ -206,6 +296,12 @@ export interface TaskOutcome {
|
|
|
206
296
|
outputs?: 'deferred'
|
|
207
297
|
/** The task ran here and its result went to the cache. The footer's `↑`. */
|
|
208
298
|
saved?: true
|
|
299
|
+
/**
|
|
300
|
+
* On a `failed` outcome: a cache hit whose restore threw, so no command
|
|
301
|
+
* ran. It counts as failed only, in no cache column: not a miss, not
|
|
302
|
+
* its layer's hit.
|
|
303
|
+
*/
|
|
304
|
+
restoreFailed?: true
|
|
209
305
|
/**
|
|
210
306
|
* v11 analytics: hrtime span relative to the parent run's t=0.
|
|
211
307
|
* Lets downstream analytics reconstruct the actual parallel timeline
|
|
@@ -231,6 +327,8 @@ export interface TaskOutcome {
|
|
|
231
327
|
attempts?: number
|
|
232
328
|
/** The attempts that failed and were run again, in order: when each ended (epoch ms) and how. */
|
|
233
329
|
failedAttempts?: readonly { endedAt: number; exitCode: number; timedOut?: true }[]
|
|
330
|
+
/** Each of `failedAttempts`' output, as `failedOutput` encodes it: what `vx last --log` shows. */
|
|
331
|
+
attemptOutputs?: readonly string[]
|
|
234
332
|
/**
|
|
235
333
|
* Set when this run proved the task flaky: its key failed after it had
|
|
236
334
|
* passed (this run's included) or it passed only on a retry. Its row carries it.
|
|
@@ -334,6 +432,8 @@ export interface ScheduleOptions {
|
|
|
334
432
|
* dispatch, the slot and the promise around it were ~10 µs of each of a
|
|
335
433
|
* warm run's 1,090 groups (X-192); a hit whose tree is current does no
|
|
336
434
|
* more. A restore-tier task's `upstream` may hold undefined entries.
|
|
435
|
+
* The thunk's outcome is the scheduler's: its queue time is set on it,
|
|
436
|
+
* not on a copy (a copy per hit, 2026-11-07).
|
|
337
437
|
*/
|
|
338
438
|
settleNow?: (node: TaskNode, upstream: TaskOutcome[]) => (() => TaskOutcome) | undefined
|
|
339
439
|
/**
|
|
@@ -429,23 +529,23 @@ export interface ScheduleOptions {
|
|
|
429
529
|
class ReadyHeap {
|
|
430
530
|
private readonly ids: string[] = []
|
|
431
531
|
private readonly seq: number[] = []
|
|
432
|
-
// Each slot's priority, read once at push: a
|
|
532
|
+
// Each slot's priority, read once at push: a lookup per comparison
|
|
433
533
|
// was two per heap level on every push and pop.
|
|
434
534
|
private readonly prio: number[] = []
|
|
435
535
|
private next = 0
|
|
436
536
|
/** Queued ids `counts` holds true for. */
|
|
437
537
|
counted = 0
|
|
438
538
|
constructor(
|
|
439
|
-
private priority:
|
|
539
|
+
private priority: (id: string) => number,
|
|
440
540
|
private readonly counts?: (id: string) => boolean,
|
|
441
541
|
) {}
|
|
442
542
|
get size(): number {
|
|
443
543
|
return this.ids.length
|
|
444
544
|
}
|
|
445
545
|
/** Re-rank every queued id under `priority`, and every later push. */
|
|
446
|
-
reprioritize(priority:
|
|
546
|
+
reprioritize(priority: (id: string) => number): void {
|
|
447
547
|
this.priority = priority
|
|
448
|
-
for (let i = 0; i < this.ids.length; i++) this.prio[i] = priority
|
|
548
|
+
for (let i = 0; i < this.ids.length; i++) this.prio[i] = priority(this.ids[i]!)
|
|
449
549
|
for (let i = (this.ids.length >> 1) - 1; i >= 0; i--) this.down(i)
|
|
450
550
|
}
|
|
451
551
|
private down(i: number): void {
|
|
@@ -496,7 +596,7 @@ class ReadyHeap {
|
|
|
496
596
|
if (this.counts?.(id) === true) this.counted++
|
|
497
597
|
this.ids.push(id)
|
|
498
598
|
this.seq.push(seq)
|
|
499
|
-
this.prio.push(this.priority
|
|
599
|
+
this.prio.push(this.priority(id))
|
|
500
600
|
let i = this.ids.length - 1
|
|
501
601
|
while (i > 0) {
|
|
502
602
|
const parent = (i - 1) >> 1
|
|
@@ -528,11 +628,13 @@ class ReadyHeap {
|
|
|
528
628
|
}
|
|
529
629
|
|
|
530
630
|
/**
|
|
531
|
-
* The dead server a dependency stands for: itself, or one a group
|
|
532
|
-
* A group is a name for its deps, and it finished the
|
|
533
|
-
* was ready, so a task behind it ran against the dead
|
|
534
|
-
*
|
|
535
|
-
*
|
|
631
|
+
* The dead server a dependency stands for: itself, or one a group or a
|
|
632
|
+
* server reaches. A group is a name for its deps, and it finished the
|
|
633
|
+
* moment the server was ready, so a task behind it ran against the dead
|
|
634
|
+
* one; a server stays up on the servers it depends on, so a task behind
|
|
635
|
+
* `web` runs against the `api` under it too. Walked with a stack and a
|
|
636
|
+
* seen set: recursion threw `RangeError` on a deep chain of groups, and
|
|
637
|
+
* re-walked a group each path reached it by.
|
|
536
638
|
*/
|
|
537
639
|
export function deadServerBehind(
|
|
538
640
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
@@ -547,7 +649,7 @@ export function deadServerBehind(
|
|
|
547
649
|
seen.add(at)
|
|
548
650
|
if (serverDied(at)) return at
|
|
549
651
|
const n = nodes.get(at)
|
|
550
|
-
if (n === undefined || !isGroupTask(n)) continue
|
|
652
|
+
if (n === undefined || (!isGroupTask(n) && n.config.exec?.persistent === undefined)) continue
|
|
551
653
|
// Reversed, so the first dep is walked first, as the order it names them.
|
|
552
654
|
for (let i = n.deps.length - 1; i >= 0; i--) stack.push(n.deps[i]!)
|
|
553
655
|
}
|
|
@@ -604,9 +706,9 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
604
706
|
: options.restoreTier !== undefined && options.restoreTier.size > 0
|
|
605
707
|
? tieredReverseDepCount(nodes, options.restoreTier)
|
|
606
708
|
: (options.reverseDeps?.() ?? computeReverseDepCount(nodes))
|
|
607
|
-
const priority:
|
|
709
|
+
const priority: (id: string) => number = options.priorities
|
|
608
710
|
? mergePriorities(baseline, options.priorities)
|
|
609
|
-
: baseline
|
|
711
|
+
: (id) => baseline.get(id) ?? 0
|
|
610
712
|
|
|
611
713
|
// Two ready queues — exec-tier (cache misses + unstable tasks) and
|
|
612
714
|
// restore-tier (confirmed stable local hits). Each is a max-heap keyed by
|
|
@@ -625,7 +727,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
625
727
|
// (`dormant`) until a dependant is demoted, which starts it, or until
|
|
626
728
|
// every dependant has finished, which settles it unspawned. Settled
|
|
627
729
|
// without an outcome: it is not part of the run, as a pruned task is not.
|
|
628
|
-
// A group between it and its hits is idle with it.
|
|
730
|
+
// A group or a server between it and its hits is idle with it.
|
|
629
731
|
const idleGroups = new Set<string>()
|
|
630
732
|
const idle = idleServers(nodes, dependents, restoreTier, idleGroups)
|
|
631
733
|
for (const g of idleGroups) idle.add(g)
|
|
@@ -682,7 +784,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
682
784
|
// complete.
|
|
683
785
|
// When each queued id became ready; dispatch takes it out.
|
|
684
786
|
const readyAt = new Map<string, number>()
|
|
685
|
-
|
|
787
|
+
// Elapsed-time stamps (queue and hold waits), immune to wall clock steps.
|
|
788
|
+
const startedAt = performance.now()
|
|
686
789
|
for (const node of nodes.values()) {
|
|
687
790
|
if (restoreTier?.has(node.id)) restoreReady.push(node.id)
|
|
688
791
|
else if (node.deps.length === 0) execReady.push(node.id)
|
|
@@ -894,7 +997,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
894
997
|
if (heldRelease.delete(d)) enter(d)
|
|
895
998
|
else if (!inRestoreTier(d)) {
|
|
896
999
|
execReady.push(d)
|
|
897
|
-
readyAt.set(d,
|
|
1000
|
+
readyAt.set(d, performance.now())
|
|
898
1001
|
}
|
|
899
1002
|
}
|
|
900
1003
|
}
|
|
@@ -1025,7 +1128,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1025
1128
|
if (hasRoom(id)) {
|
|
1026
1129
|
if (!admitActive || admits(id)) return id
|
|
1027
1130
|
// A free worker, refused by the policy: the hold starts now.
|
|
1028
|
-
if (!heldSince.has(id)) heldSince.set(id,
|
|
1131
|
+
if (!heldSince.has(id)) heldSince.set(id, performance.now())
|
|
1029
1132
|
}
|
|
1030
1133
|
parked.push([id, seq])
|
|
1031
1134
|
}
|
|
@@ -1105,7 +1208,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1105
1208
|
continue
|
|
1106
1209
|
}
|
|
1107
1210
|
|
|
1108
|
-
const
|
|
1211
|
+
const readySince = readyAt.get(id)
|
|
1212
|
+
const queuedMs = readySince === undefined ? 0 : Math.round(performance.now() - readySince)
|
|
1109
1213
|
readyAt.delete(id)
|
|
1110
1214
|
const settle = settleInPlace(node, upstream)
|
|
1111
1215
|
let now: TaskOutcome | undefined
|
|
@@ -1118,7 +1222,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1118
1222
|
}
|
|
1119
1223
|
}
|
|
1120
1224
|
if (now !== undefined) {
|
|
1121
|
-
|
|
1225
|
+
if (queuedMs !== 0) now.queuedMs = queuedMs
|
|
1226
|
+
finishOne(id, now)
|
|
1122
1227
|
continue
|
|
1123
1228
|
}
|
|
1124
1229
|
const leave = admit(id)
|
|
@@ -1126,7 +1231,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1126
1231
|
// tick sees it; the completion callbacks unlist it.
|
|
1127
1232
|
const untrack = track(id)
|
|
1128
1233
|
const since = admitActive ? heldSince.get(id) : undefined
|
|
1129
|
-
const heldMs = since === undefined ? 0 : Math.max(1,
|
|
1234
|
+
const heldMs = since === undefined ? 0 : Math.max(1, Math.round(performance.now() - since))
|
|
1130
1235
|
if (since !== undefined) heldSince.delete(id)
|
|
1131
1236
|
const withHold = (o: TaskOutcome): TaskOutcome =>
|
|
1132
1237
|
heldMs === 0 && queuedMs === 0
|
|
@@ -1175,8 +1280,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1175
1280
|
}
|
|
1176
1281
|
void settled.then(done, () => done())
|
|
1177
1282
|
},
|
|
1178
|
-
(
|
|
1179
|
-
if (
|
|
1283
|
+
(thrown: unknown) => {
|
|
1284
|
+
if (thrown instanceof RestoreDemoted) {
|
|
1180
1285
|
leave()
|
|
1181
1286
|
untrack()
|
|
1182
1287
|
demoted.add(id)
|
|
@@ -1184,16 +1289,22 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1184
1289
|
idle.delete(p)
|
|
1185
1290
|
if (dormant.delete(p)) {
|
|
1186
1291
|
execReady.push(p)
|
|
1187
|
-
readyAt.set(p,
|
|
1292
|
+
readyAt.set(p, performance.now())
|
|
1188
1293
|
}
|
|
1189
1294
|
})
|
|
1190
1295
|
if (pending.get(id) === 0) {
|
|
1191
1296
|
execReady.push(id)
|
|
1192
|
-
readyAt.set(id,
|
|
1297
|
+
readyAt.set(id, performance.now())
|
|
1193
1298
|
}
|
|
1194
1299
|
tick()
|
|
1195
1300
|
return
|
|
1196
1301
|
}
|
|
1302
|
+
const err =
|
|
1303
|
+
thrown instanceof RestoreFailed ||
|
|
1304
|
+
thrown instanceof KeyFailed ||
|
|
1305
|
+
thrown instanceof OutputsFailed
|
|
1306
|
+
? thrown.error
|
|
1307
|
+
: thrown
|
|
1197
1308
|
const message = thrownMessage(err)
|
|
1198
1309
|
// After the run's stop a rejection is the stop's doing (a probe
|
|
1199
1310
|
// it killed, C-65): aborted, as any task it kills, and unsaid.
|
|
@@ -1201,9 +1312,12 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1201
1312
|
const outcome: TaskOutcome = withHold({
|
|
1202
1313
|
node,
|
|
1203
1314
|
status: stopped ? 'aborted' : 'failed',
|
|
1204
|
-
exitCode: 1,
|
|
1205
|
-
durationMs: 0,
|
|
1315
|
+
exitCode: thrown instanceof OutputsFailed ? thrown.exitCode : 1,
|
|
1316
|
+
durationMs: thrown instanceof OutputsFailed ? thrown.durationMs : 0,
|
|
1317
|
+
...(thrown instanceof RestoreFailed && !stopped ? { restoreFailed: true } : {}),
|
|
1206
1318
|
})
|
|
1319
|
+
if (thrown instanceof KeyFailed && !stopped) markKeyFailed(outcome)
|
|
1320
|
+
if (thrown instanceof OutputsFailed && !stopped) markOutputsFailed(outcome, thrown.step)
|
|
1207
1321
|
leave()
|
|
1208
1322
|
untrack()
|
|
1209
1323
|
if (stopped) {
|
|
@@ -1218,9 +1332,10 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1218
1332
|
// Out of descriptors is the environment's limit too: a stack
|
|
1219
1333
|
// naming a file that was fine sent the user after the file.
|
|
1220
1334
|
if (isUserError(err) || isFsRefusal(err) || isOutOfFds(err)) {
|
|
1221
|
-
const
|
|
1335
|
+
const said = isUserError(err)
|
|
1222
1336
|
? message
|
|
1223
1337
|
: `${message} — ${isOutOfFds(err) ? OUT_OF_FDS_HINT : fsRefusalHint(err)}`
|
|
1338
|
+
const text = thrown instanceof KeyFailed ? `cache key not derived: ${said}` : said
|
|
1224
1339
|
// In a task's own output block, in full: that block is read
|
|
1225
1340
|
// alone, and "as <task> above" named one printed elsewhere or
|
|
1226
1341
|
// not at all (2026-10-22). On bare stderr the first is above.
|
|
@@ -1288,9 +1403,25 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1288
1403
|
continue
|
|
1289
1404
|
promoted.add(d)
|
|
1290
1405
|
restoreReady.push(d)
|
|
1291
|
-
readyAt.set(d,
|
|
1406
|
+
readyAt.set(d, performance.now())
|
|
1407
|
+
}
|
|
1408
|
+
// Idle as `idleServers` finds it: through the groups and servers
|
|
1409
|
+
// nobody asked for above it. The servers beneath it are held on the
|
|
1410
|
+
// same keys, so the walk idles them with it; a server already
|
|
1411
|
+
// answered, or never held, keeps its course.
|
|
1412
|
+
if (found.length > 0) {
|
|
1413
|
+
const tier =
|
|
1414
|
+
demoted.size === 0 ? restoreTier : new Set([...restoreTier!].filter(inRestoreTier))
|
|
1415
|
+
const between = new Set<string>()
|
|
1416
|
+
const holdsHere = (s: string): boolean =>
|
|
1417
|
+
s === id || unanswered.has(s) || isGroupTask(nodes.get(s) as TaskNode)
|
|
1418
|
+
for (const s of idleServers(nodes, dependents, tier, between)) if (holdsHere(s)) idle.add(s)
|
|
1419
|
+
for (const g of between)
|
|
1420
|
+
if (holdsHere(g) && !idleGroups.has(g)) {
|
|
1421
|
+
idleGroups.add(g)
|
|
1422
|
+
idle.add(g)
|
|
1423
|
+
}
|
|
1292
1424
|
}
|
|
1293
|
-
if (dependents.get(id)?.every((d) => inRestoreTier(d)) === true) idle.add(id)
|
|
1294
1425
|
if (heldReady.delete(id)) execReady.push(id)
|
|
1295
1426
|
tick()
|
|
1296
1427
|
}
|
|
@@ -1318,10 +1449,11 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1318
1449
|
|
|
1319
1450
|
/**
|
|
1320
1451
|
* Persistent tasks nobody asked for whose every dependant is a restore-tier
|
|
1321
|
-
* hit, or a group nobody asked for that only such dependants
|
|
1322
|
-
* a group runs nothing, so `env: { dependsOn: ['dev'] }`
|
|
1323
|
-
* and its hits booted the server on every warm run
|
|
1324
|
-
* `groups
|
|
1452
|
+
* hit, or a group or server nobody asked for that only such dependants
|
|
1453
|
+
* stand behind: a group runs nothing, so `env: { dependsOn: ['dev'] }`
|
|
1454
|
+
* between the server and its hits booted the server on every warm run, and
|
|
1455
|
+
* `api` under an idle `web` serves nobody either. Those groups and servers
|
|
1456
|
+
* go in `groups`: the scheduler settles them as it settles a group.
|
|
1325
1457
|
*/
|
|
1326
1458
|
export function idleServers(
|
|
1327
1459
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
@@ -1332,10 +1464,10 @@ export function idleServers(
|
|
|
1332
1464
|
const out = new Set<string>()
|
|
1333
1465
|
if (restoreTier === undefined || restoreTier.size === 0) return out
|
|
1334
1466
|
const unasked = (n: TaskNode): boolean => !n.requested && n.surfaced !== true
|
|
1335
|
-
// Per group: whether only hits stand behind it. Post-order on a
|
|
1336
|
-
// groups nest as deep as the graph.
|
|
1467
|
+
// Per group or server: whether only hits stand behind it. Post-order on a
|
|
1468
|
+
// stack, as groups and servers nest as deep as the graph.
|
|
1337
1469
|
const spare = new Map<string, boolean>()
|
|
1338
|
-
const
|
|
1470
|
+
const spareNode = (root: string): boolean => {
|
|
1339
1471
|
const stack = [root]
|
|
1340
1472
|
while (stack.length > 0) {
|
|
1341
1473
|
const id = stack[stack.length - 1]!
|
|
@@ -1345,7 +1477,12 @@ export function idleServers(
|
|
|
1345
1477
|
}
|
|
1346
1478
|
const n = nodes.get(id)
|
|
1347
1479
|
const ds = dependents.get(id)
|
|
1348
|
-
if (
|
|
1480
|
+
if (
|
|
1481
|
+
n === undefined ||
|
|
1482
|
+
(!isGroupTask(n) && n.config.exec?.persistent === undefined) ||
|
|
1483
|
+
!unasked(n) ||
|
|
1484
|
+
ds === undefined
|
|
1485
|
+
) {
|
|
1349
1486
|
spare.set(id, false)
|
|
1350
1487
|
stack.pop()
|
|
1351
1488
|
continue
|
|
@@ -1366,7 +1503,7 @@ export function idleServers(
|
|
|
1366
1503
|
for (const n of nodes.values()) {
|
|
1367
1504
|
if (n.config.exec?.persistent === undefined || !unasked(n)) continue
|
|
1368
1505
|
const ds = dependents.get(n.id)
|
|
1369
|
-
if (ds === undefined || !ds.every((d) => restoreTier.has(d) ||
|
|
1506
|
+
if (ds === undefined || !ds.every((d) => restoreTier.has(d) || spareNode(d))) continue
|
|
1370
1507
|
out.add(n.id)
|
|
1371
1508
|
if (groups === undefined) continue
|
|
1372
1509
|
const stack = ds.filter((d) => !restoreTier.has(d))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// What an edge past a default build on a package cycle stands in for. Such
|
|
2
|
+
// a build folds nothing of its cycle, so the walk takes edges past it
|
|
3
|
+
// (`goPast`, `walkHolders` in task-graph.ts), and `inputs.tasks` selecting
|
|
4
|
+
// it selects them (`selectFoldedDeps`).
|
|
5
|
+
//
|
|
6
|
+
// Kept off the public `TaskNode` shape under a symbol key: a spread copy
|
|
7
|
+
// (the scheduler's restore view, an executor's `accepts` copy) carries it,
|
|
8
|
+
// and `applyGraphStage` re-attaches it by id to a node a hook rebuilt.
|
|
9
|
+
|
|
10
|
+
import type { TaskNode } from './task-graph.js'
|
|
11
|
+
|
|
12
|
+
/** Each edge's id, to the ids of the edges it went past. */
|
|
13
|
+
export type StandIns = Readonly<Record<string, readonly string[]>>
|
|
14
|
+
|
|
15
|
+
const STAND_INS = Symbol('vx.standIns')
|
|
16
|
+
type Carrier = TaskNode & { [STAND_INS]?: Record<string, string[]> }
|
|
17
|
+
|
|
18
|
+
/** The node's stand-ins; undefined off a package cycle. */
|
|
19
|
+
export function standInsOf(node: TaskNode): StandIns | undefined {
|
|
20
|
+
return (node as Carrier)[STAND_INS]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Give `node` its stand-ins: a graph read back, or a node a hook rebuilt. */
|
|
24
|
+
export function setStandIns(node: TaskNode, standIns: StandIns): void {
|
|
25
|
+
const own: Record<string, string[]> = {}
|
|
26
|
+
for (const [edge, past] of Object.entries(standIns)) own[edge] = [...past]
|
|
27
|
+
;(node as Carrier)[STAND_INS] = own
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Record that `node`'s edge to `id` stands in for the edges in `past`. */
|
|
31
|
+
export function addStandIn(node: TaskNode, id: string, past: readonly string[]): void {
|
|
32
|
+
const map = ((node as Carrier)[STAND_INS] ??= {})
|
|
33
|
+
const known = map[id]
|
|
34
|
+
if (known === undefined) map[id] = [...past]
|
|
35
|
+
else for (const p of past) if (!known.includes(p)) known.push(p)
|
|
36
|
+
}
|