@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/graph/scheduler.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
isOutOfFds,
|
|
5
5
|
isUserError,
|
|
6
6
|
OUT_OF_FDS_HINT,
|
|
7
|
+
thrownMessage,
|
|
8
|
+
ui,
|
|
7
9
|
} from '../util/index.js'
|
|
8
10
|
import {
|
|
9
11
|
computeReverseDepCount,
|
|
@@ -98,6 +100,12 @@ export interface TaskOutcome {
|
|
|
98
100
|
/** v11 analytics: CPU time + peak RSS for this task's child process. */
|
|
99
101
|
cpuMs?: number
|
|
100
102
|
peakRssBytes?: number
|
|
103
|
+
/**
|
|
104
|
+
* The most cores one sampling interval burned and the mean resident
|
|
105
|
+
* memory over the looks; only when a sink samples and a look landed.
|
|
106
|
+
*/
|
|
107
|
+
peakCores?: number
|
|
108
|
+
avgRssBytes?: number
|
|
101
109
|
/**
|
|
102
110
|
* A GROUP task's own dependency outcomes — the tasks it stands for.
|
|
103
111
|
* A group has no `exec` and therefore no outputs and no cache entry: its
|
|
@@ -116,11 +124,11 @@ export interface TaskOutcome {
|
|
|
116
124
|
*/
|
|
117
125
|
unkeyed?: true
|
|
118
126
|
/**
|
|
119
|
-
*
|
|
120
|
-
* can derive that key again, so it neither read nor
|
|
121
|
-
* dependant that folds `hash` does neither.
|
|
127
|
+
* The task with no `inputs` this task's key folds (this task, or one
|
|
128
|
+
* upstream): no run can derive that key again, so it neither read nor
|
|
129
|
+
* saved an entry, and a dependant that folds `hash` does neither.
|
|
122
130
|
*/
|
|
123
|
-
|
|
131
|
+
keylessBy?: string
|
|
124
132
|
/**
|
|
125
133
|
* The run's cache policy read and wrote nothing (`--no-cache`,
|
|
126
134
|
* `--cache-policy=local:`): no cache answered for this task, so a run of it is
|
|
@@ -135,6 +143,12 @@ export interface TaskOutcome {
|
|
|
135
143
|
* read this one field (item 267).
|
|
136
144
|
*/
|
|
137
145
|
blockedBy?: string
|
|
146
|
+
/**
|
|
147
|
+
* On a `skipped` outcome: its `exec.platforms` leave out this machine.
|
|
148
|
+
* Its dependants are skipped with `blockedBy` naming it, and neither
|
|
149
|
+
* fails the run.
|
|
150
|
+
*/
|
|
151
|
+
otherPlatform?: { readonly needs: readonly string[]; readonly host: string }
|
|
138
152
|
/**
|
|
139
153
|
* On a `failed` outcome: vx's own `timeout` killed the final attempt.
|
|
140
154
|
* The exit is the shell's 143, and every label says "timed out" instead
|
|
@@ -159,6 +173,12 @@ export interface TaskOutcome {
|
|
|
159
173
|
* the spawn itself failed. Every label reads this instead of the exit.
|
|
160
174
|
*/
|
|
161
175
|
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
176
|
+
/**
|
|
177
|
+
* On a ready persistent task: its child exited on its own while the run
|
|
178
|
+
* went on — a setup step's 0 at ready, a crash's code after it — so no
|
|
179
|
+
* label calls it running.
|
|
180
|
+
*/
|
|
181
|
+
serverExit?: number
|
|
162
182
|
/**
|
|
163
183
|
* The input that changed while the command ran, failing the task under
|
|
164
184
|
* `inputStability.severity: 'error'` (workspace-relative; '' when vx
|
|
@@ -166,8 +186,15 @@ export interface TaskOutcome {
|
|
|
166
186
|
*/
|
|
167
187
|
inputMoved?: string
|
|
168
188
|
/** Executor-reported placement label (`ExecuteResult.where`) — set only
|
|
169
|
-
* when the task ran somewhere other than this host
|
|
189
|
+
* when the task ran somewhere other than this host: telemetry and the task's row. */
|
|
170
190
|
where?: string
|
|
191
|
+
/** A persistent task each dependant's executor starts where it runs: nothing ran here, and it is no execution of its own. */
|
|
192
|
+
carried?: true
|
|
193
|
+
/** The cores and memory of the machine `where` names (`ExecuteResult.machine`). */
|
|
194
|
+
whereCores?: number
|
|
195
|
+
whereMemoryBytes?: number
|
|
196
|
+
/** `ExecuteResult.machine.cpuShare`: the share of a core its threads got there. */
|
|
197
|
+
cpuShare?: number
|
|
171
198
|
/** `'deferred'` when the task's outputs were left in the remote store
|
|
172
199
|
* (`--cache-download=none`) instead of landing on this machine. Absent for
|
|
173
200
|
* every ordinary outcome. Telemetry-only. */
|
|
@@ -201,7 +228,7 @@ export interface TaskOutcome {
|
|
|
201
228
|
failedAttempts?: readonly { endedAt: number; exitCode: number; timedOut?: true }[]
|
|
202
229
|
/**
|
|
203
230
|
* Set when this run proved the task flaky: its key failed after it had
|
|
204
|
-
* passed (this run's included) or it
|
|
231
|
+
* passed (this run's included) or it passed only on a retry. Its row carries it.
|
|
205
232
|
*/
|
|
206
233
|
flaky?: { passes: number; failures: number }
|
|
207
234
|
/**
|
|
@@ -294,14 +321,16 @@ export interface ScheduleOptions {
|
|
|
294
321
|
*/
|
|
295
322
|
settledOf?: (outcome: TaskOutcome) => Promise<Partial<TaskOutcome> | void> | undefined
|
|
296
323
|
/**
|
|
297
|
-
*
|
|
298
|
-
* dispatch it through `execute`. Asked of
|
|
299
|
-
* room for, after the skip check;
|
|
300
|
-
*
|
|
301
|
-
* unkeyed group runs nothing, and the
|
|
302
|
-
* around it were ~10 µs of each of a
|
|
324
|
+
* A task the caller settles in place: a thunk returning its outcome, or
|
|
325
|
+
* undefined to dispatch it through `execute`. Asked of a task its lane
|
|
326
|
+
* has room for, after the skip check; `onStart` hears it, then the thunk
|
|
327
|
+
* runs (a hit replays its output there), and its outcome lands in the
|
|
328
|
+
* same tick, holding no slot. An unkeyed group runs nothing, and the
|
|
329
|
+
* dispatch, the slot and the promise around it were ~10 µs of each of a
|
|
330
|
+
* warm run's 1,090 groups (X-192); a hit whose tree is current does no
|
|
331
|
+
* more. A restore-tier task's `upstream` may hold undefined entries.
|
|
303
332
|
*/
|
|
304
|
-
settleNow?: (node: TaskNode, upstream: TaskOutcome[]) => TaskOutcome | undefined
|
|
333
|
+
settleNow?: (node: TaskNode, upstream: TaskOutcome[]) => (() => TaskOutcome) | undefined
|
|
305
334
|
/**
|
|
306
335
|
* Optional priority override: callers pass their own per-node weight
|
|
307
336
|
* (e.g. `computePredictedPriorities` from the orchestrator's history
|
|
@@ -344,6 +373,14 @@ export interface ScheduleOptions {
|
|
|
344
373
|
* second cache.get. When undefined/empty, behavior is byte-identical.
|
|
345
374
|
*/
|
|
346
375
|
restoreTier?: ReadonlySet<string>
|
|
376
|
+
/**
|
|
377
|
+
* Servers held back until the caller knows whether their dependants are
|
|
378
|
+
* hits a remote holds. Each resolves to the dependants it has added to
|
|
379
|
+
* `restoreTier` since the run began (the caller's set, read live); a
|
|
380
|
+
* server whose every dependant is then in the tier is idle, as above,
|
|
381
|
+
* and any other is dispatched as usual.
|
|
382
|
+
*/
|
|
383
|
+
heldServers?: ReadonlyMap<string, Promise<readonly string[]>>
|
|
347
384
|
/**
|
|
348
385
|
* Pool assignment for tasks placed on an executor with its own capacity
|
|
349
386
|
* (a remote pool). Such a task occupies one of the pool's `capacity` slots
|
|
@@ -386,7 +423,12 @@ class ReadyHeap {
|
|
|
386
423
|
// was two per heap level on every push and pop.
|
|
387
424
|
private readonly prio: number[] = []
|
|
388
425
|
private next = 0
|
|
389
|
-
|
|
426
|
+
/** Queued ids `counts` holds true for. */
|
|
427
|
+
counted = 0
|
|
428
|
+
constructor(
|
|
429
|
+
private priority: ReadonlyMap<string, number>,
|
|
430
|
+
private readonly counts?: (id: string) => boolean,
|
|
431
|
+
) {}
|
|
390
432
|
get size(): number {
|
|
391
433
|
return this.ids.length
|
|
392
434
|
}
|
|
@@ -441,6 +483,7 @@ class ReadyHeap {
|
|
|
441
483
|
* it holds without that argument.
|
|
442
484
|
*/
|
|
443
485
|
push(id: string, seq: number = this.next++): void {
|
|
486
|
+
if (this.counts?.(id) === true) this.counted++
|
|
444
487
|
this.ids.push(id)
|
|
445
488
|
this.seq.push(seq)
|
|
446
489
|
this.prio.push(this.priority.get(id) ?? 0)
|
|
@@ -460,6 +503,7 @@ class ReadyHeap {
|
|
|
460
503
|
const n = this.ids.length
|
|
461
504
|
if (n === 0) return undefined
|
|
462
505
|
const top = this.ids[0]!
|
|
506
|
+
if (this.counts?.(top) === true) this.counted--
|
|
463
507
|
const lastId = this.ids.pop()!
|
|
464
508
|
const lastSeq = this.seq.pop()!
|
|
465
509
|
const lastPrio = this.prio.pop()!
|
|
@@ -571,10 +615,50 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
571
615
|
// (`dormant`) until a dependant is demoted, which starts it, or until
|
|
572
616
|
// every dependant has finished, which settles it unspawned. Settled
|
|
573
617
|
// without an outcome: it is not part of the run, as a pruned task is not.
|
|
574
|
-
|
|
618
|
+
// A group between it and its hits is idle with it.
|
|
619
|
+
const idleGroups = new Set<string>()
|
|
620
|
+
const idle = idleServers(nodes, dependents, restoreTier, idleGroups)
|
|
621
|
+
for (const g of idleGroups) idle.add(g)
|
|
575
622
|
const dormant = new Set<string>()
|
|
623
|
+
// The idle tasks below `id`, through idle groups: those its outcome may
|
|
624
|
+
// let settle, and those its demotion wakes.
|
|
625
|
+
const idleBelow = (id: string, each: (p: string) => void): void => {
|
|
626
|
+
const stack = [...(nodes.get(id) as TaskNode).deps]
|
|
627
|
+
const seen = new Set<string>()
|
|
628
|
+
while (stack.length > 0) {
|
|
629
|
+
const p = stack.pop()!
|
|
630
|
+
if (seen.has(p) || !idle.has(p)) continue
|
|
631
|
+
seen.add(p)
|
|
632
|
+
each(p)
|
|
633
|
+
if (idleGroups.has(p)) stack.push(...(nodes.get(p) as TaskNode).deps)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Every task above an idle one, through idle groups, has finished.
|
|
637
|
+
const settledAbove = (id: string): boolean => {
|
|
638
|
+
const stack = [id]
|
|
639
|
+
const seen = new Set<string>()
|
|
640
|
+
while (stack.length > 0) {
|
|
641
|
+
for (const d of dependents.get(stack.pop()!) ?? []) {
|
|
642
|
+
if (outcomes.has(d)) continue
|
|
643
|
+
if (!idleGroups.has(d) || !idle.has(d)) return false
|
|
644
|
+
if (!seen.has(d)) {
|
|
645
|
+
seen.add(d)
|
|
646
|
+
stack.push(d)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return true
|
|
651
|
+
}
|
|
652
|
+
// Held servers not yet answered, and those of them that became ready.
|
|
653
|
+
const unanswered = new Set(options.heldServers?.keys() ?? [])
|
|
654
|
+
for (const id of idle) unanswered.delete(id)
|
|
655
|
+
const heldReady = new Set<string>()
|
|
576
656
|
let settledIdle = 0
|
|
577
|
-
|
|
657
|
+
// Counting those no pool takes: with every pool full, only one of them can start.
|
|
658
|
+
const execReady = new ReadyHeap(
|
|
659
|
+
priority,
|
|
660
|
+
options.poolOf === undefined ? undefined : (id) => options.poolOf!(id) === undefined,
|
|
661
|
+
)
|
|
578
662
|
const restoreReady = new ReadyHeap(priority)
|
|
579
663
|
options.reprioritize?.((weights) => {
|
|
580
664
|
const merged = mergePriorities(baseline, weights)
|
|
@@ -595,6 +679,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
595
679
|
else continue
|
|
596
680
|
readyAt.set(node.id, startedAt)
|
|
597
681
|
}
|
|
682
|
+
const promoted = new Set<string>()
|
|
598
683
|
|
|
599
684
|
let active = 0
|
|
600
685
|
// Restore-tier work is disk I/O — a confirmed hit's tar extract is ~8
|
|
@@ -612,8 +697,9 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
612
697
|
const restoreRoom = (): boolean =>
|
|
613
698
|
serial ? active + activeRestore < 1 : activeRestore < restoreConcurrency
|
|
614
699
|
let resolved = false
|
|
700
|
+
let stopListening = (): void => {}
|
|
615
701
|
// A refusal's text → the first task that said it. A corrupt cache index
|
|
616
|
-
// reaches every lookup as one UserError; a repeat names the first.
|
|
702
|
+
// reaches every lookup as one UserError; on bare stderr a repeat names the first.
|
|
617
703
|
const refusedBy = new Map<string, string>()
|
|
618
704
|
|
|
619
705
|
// Admission policy over the count limit (a plugin's `admit`). Inactive
|
|
@@ -633,6 +719,18 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
633
719
|
// against a real executor (and never parks).
|
|
634
720
|
const poolOf = options.poolOf
|
|
635
721
|
const poolActive = new Map<string, number>()
|
|
722
|
+
// Every pool a task of the run is placed in, by name: a task's pool is
|
|
723
|
+
// fixed for the run, so read once here.
|
|
724
|
+
const pools = new Map<string, number>()
|
|
725
|
+
if (poolOf !== undefined)
|
|
726
|
+
for (const id of nodes.keys()) {
|
|
727
|
+
const p = poolOf(id)
|
|
728
|
+
if (p !== undefined) pools.set(p.name, p.capacity)
|
|
729
|
+
}
|
|
730
|
+
const poolRoom = (): boolean => {
|
|
731
|
+
for (const [name, capacity] of pools) if ((poolActive.get(name) ?? 0) < capacity) return true
|
|
732
|
+
return false
|
|
733
|
+
}
|
|
636
734
|
// A pooled task is admitted against ITS pool's capacity, a local one
|
|
637
735
|
// against `concurrency`; pooled tasks reserve no local resources. A
|
|
638
736
|
// restore is local disk work wherever its task was placed: on the pool's
|
|
@@ -701,7 +799,9 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
701
799
|
// A dormant server is asked again: the dispatch settles it once its
|
|
702
800
|
// last dependant is in.
|
|
703
801
|
if (dormant.size > 0) {
|
|
704
|
-
|
|
802
|
+
idleBelow(id, (p) => {
|
|
803
|
+
if (dormant.delete(p)) execReady.push(p)
|
|
804
|
+
})
|
|
705
805
|
}
|
|
706
806
|
// Observer hook (the logger's taskComplete). Crash-isolated: a
|
|
707
807
|
// throwing observer must NOT break scheduling — it would otherwise
|
|
@@ -712,8 +812,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
712
812
|
try {
|
|
713
813
|
onFinish?.(outcome)
|
|
714
814
|
} catch (err) {
|
|
715
|
-
const m =
|
|
716
|
-
|
|
815
|
+
const m = thrownMessage(err)
|
|
816
|
+
ui.say('warn', `[vx] onFinish observer threw for ${id}: ${m}\n`)
|
|
717
817
|
}
|
|
718
818
|
// A restore-tier task runs before its deps, but its DEPENDENTS keep
|
|
719
819
|
// the order `dependsOn` states: they are released once its own deps
|
|
@@ -734,16 +834,19 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
734
834
|
// one carries.
|
|
735
835
|
const blockedRestores = new Map<string, string | undefined>()
|
|
736
836
|
const heldRelease = new Set<string>()
|
|
737
|
-
const
|
|
738
|
-
if (inRestoreTier(id)
|
|
837
|
+
const blockOf = (id: string): void => {
|
|
838
|
+
if (inRestoreTier(id)) {
|
|
739
839
|
for (const dep of (nodes.get(id) as TaskNode).deps) {
|
|
740
840
|
const u = outcomes.get(dep)
|
|
741
|
-
if (u?.status === 'failed' || u?.status === 'aborted') {
|
|
841
|
+
if (continueMode !== 'always' && (u?.status === 'failed' || u?.status === 'aborted')) {
|
|
742
842
|
blockedRestores.set(id, dep)
|
|
743
843
|
break
|
|
744
844
|
}
|
|
745
845
|
if (u?.status === 'skipped') {
|
|
746
|
-
blockedRestores.set(
|
|
846
|
+
blockedRestores.set(
|
|
847
|
+
id,
|
|
848
|
+
u.blockedBy ?? (u.otherPlatform !== undefined ? dep : undefined),
|
|
849
|
+
)
|
|
747
850
|
break
|
|
748
851
|
}
|
|
749
852
|
if (blockedRestores.has(dep)) {
|
|
@@ -752,16 +855,33 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
752
855
|
}
|
|
753
856
|
}
|
|
754
857
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
858
|
+
}
|
|
859
|
+
// Its block, then its dependents: one whose pending hits 0 is enqueued,
|
|
860
|
+
// or, when it is a held hit, released in turn before the next sibling.
|
|
861
|
+
// A frame stack, not recursion: a chain of held hits is released in one
|
|
862
|
+
// walk, and recursion threw RangeError on a 30,000-deep one.
|
|
863
|
+
const release = (root: string): void => {
|
|
864
|
+
const frames: { ds: readonly string[]; i: number }[] = []
|
|
865
|
+
const enter = (id: string): void => {
|
|
866
|
+
blockOf(id)
|
|
867
|
+
const ds = dependents.get(id)
|
|
868
|
+
if (ds !== undefined) frames.push({ ds, i: 0 })
|
|
869
|
+
}
|
|
870
|
+
enter(root)
|
|
871
|
+
while (frames.length > 0) {
|
|
872
|
+
const f = frames[frames.length - 1]!
|
|
873
|
+
if (f.i === f.ds.length) {
|
|
874
|
+
frames.pop()
|
|
875
|
+
continue
|
|
876
|
+
}
|
|
877
|
+
const d = f.ds[f.i++]!
|
|
758
878
|
const rem = (pending.get(d) ?? 0) - 1
|
|
759
879
|
pending.set(d, rem)
|
|
760
880
|
if (rem !== 0) continue
|
|
761
881
|
// Restore-tier dependents were already enqueued at startup (they
|
|
762
882
|
// don't wait on deps); one that finished early releases its own
|
|
763
883
|
// dependents now. Only an exec-tier dependent is enqueued.
|
|
764
|
-
if (heldRelease.delete(d))
|
|
884
|
+
if (heldRelease.delete(d)) enter(d)
|
|
765
885
|
else if (!inRestoreTier(d)) {
|
|
766
886
|
execReady.push(d)
|
|
767
887
|
readyAt.set(d, Date.now())
|
|
@@ -802,8 +922,13 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
802
922
|
return true
|
|
803
923
|
}
|
|
804
924
|
if (inRestoreTier(id)) return false
|
|
805
|
-
if (continueMode === 'always') return false
|
|
806
925
|
const node = nodes.get(id) as TaskNode
|
|
926
|
+
// `always` runs past a failed upstream, never past one that did not
|
|
927
|
+
// run on this platform: under it, a skip has no other cause.
|
|
928
|
+
if (continueMode === 'always')
|
|
929
|
+
return node.deps.some(
|
|
930
|
+
(d) => outcomes.get(d)?.status === 'skipped' || blockedRestores.has(d),
|
|
931
|
+
)
|
|
807
932
|
return node.deps.some((d) => {
|
|
808
933
|
const u = outcomes.get(d)
|
|
809
934
|
// `aborted` propagates even though it is counted nowhere: the
|
|
@@ -823,7 +948,10 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
823
948
|
}
|
|
824
949
|
|
|
825
950
|
// A throw falls back to the dispatch, whose `execute` reports it.
|
|
826
|
-
const settleInPlace = (
|
|
951
|
+
const settleInPlace = (
|
|
952
|
+
node: TaskNode,
|
|
953
|
+
upstream: TaskOutcome[],
|
|
954
|
+
): (() => TaskOutcome) | undefined => {
|
|
827
955
|
if (options.settleNow === undefined) return undefined
|
|
828
956
|
try {
|
|
829
957
|
return options.settleNow(node, upstream)
|
|
@@ -831,6 +959,14 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
831
959
|
return undefined
|
|
832
960
|
}
|
|
833
961
|
}
|
|
962
|
+
const started = (node: TaskNode): void => {
|
|
963
|
+
try {
|
|
964
|
+
onStart?.(node)
|
|
965
|
+
} catch (err) {
|
|
966
|
+
const m = thrownMessage(err)
|
|
967
|
+
ui.say('warn', `[vx] onStart observer threw for ${node.id}: ${m}\n`)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
834
970
|
|
|
835
971
|
const tick = (): void => {
|
|
836
972
|
if (resolved) return
|
|
@@ -857,12 +993,19 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
857
993
|
// with a wide frontier that is O(R) per tick, O(R²) per run (the
|
|
858
994
|
// 6,000-task scale pin went 0.5 s → 28 s when the restore lane's
|
|
859
995
|
// first cut let the scan run past a full exec lane, 2026-09-10).
|
|
860
|
-
// This is the legacy O(1) gate, kept per lane.
|
|
861
|
-
|
|
996
|
+
// This is the legacy O(1) gate, kept per lane. Every pool full is
|
|
997
|
+
// the same: a remote pool of 6 slots before 2000 ready tasks
|
|
998
|
+
// re-popped them all on each end (DTE, 2026-10-25).
|
|
999
|
+
const execAdmissible =
|
|
1000
|
+
poolOf === undefined ? execRoom() : (execRoom() && execReady.counted > 0) || poolRoom()
|
|
862
1001
|
if (execAdmissible) {
|
|
863
1002
|
while (execReady.size > 0) {
|
|
864
1003
|
const seq = execReady.peekSeq()
|
|
865
1004
|
const id = execReady.pop() as string
|
|
1005
|
+
if (unanswered.has(id) && !willSkip(id)) {
|
|
1006
|
+
heldReady.add(id)
|
|
1007
|
+
continue
|
|
1008
|
+
}
|
|
866
1009
|
if (willSkip(id) || idle.has(id)) return id
|
|
867
1010
|
if (exclusive?.has(id) === true) {
|
|
868
1011
|
if (!busy() && (!admitActive || admits(id))) return id
|
|
@@ -892,35 +1035,60 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
892
1035
|
// nothing on the restore path may.
|
|
893
1036
|
const upstream = node.deps.map((d) => outcomes.get(d) as TaskOutcome)
|
|
894
1037
|
|
|
1038
|
+
// An idle task the stop or fail-fast would skip settles with no
|
|
1039
|
+
// outcome, as it would have run: the run never meant to start it.
|
|
1040
|
+
// Below a failed dep it is skipped as before, so the hits above it
|
|
1041
|
+
// pass the block down.
|
|
1042
|
+
const settlesIdle =
|
|
1043
|
+
idle.has(id) &&
|
|
1044
|
+
(!willSkip(id) ||
|
|
1045
|
+
aborted() ||
|
|
1046
|
+
(failFastTripped &&
|
|
1047
|
+
!node.deps.some((d) => {
|
|
1048
|
+
const s = outcomes.get(d)?.status
|
|
1049
|
+
return (
|
|
1050
|
+
s === 'failed' || s === 'skipped' || s === 'aborted' || blockedRestores.has(d)
|
|
1051
|
+
)
|
|
1052
|
+
})))
|
|
1053
|
+
|
|
895
1054
|
// If any upstream failed/skipped, propagate skip synchronously
|
|
896
1055
|
// without running. Skipped tasks still flow through this queue
|
|
897
1056
|
// because dependents are pushed when `pending` hits 0 regardless
|
|
898
1057
|
// of outcome — keeps the propagation logic in one place.
|
|
899
|
-
if (willSkip(id)) {
|
|
1058
|
+
if (!settlesIdle && willSkip(id)) {
|
|
900
1059
|
// The root of the block: a failed or aborted upstream names
|
|
901
1060
|
// itself; a skipped one hands down its own root.
|
|
902
|
-
const blocker =
|
|
903
|
-
|
|
904
|
-
|
|
1061
|
+
const blocker =
|
|
1062
|
+
continueMode === 'always'
|
|
1063
|
+
? undefined
|
|
1064
|
+
: upstream.find(
|
|
1065
|
+
(u) => u !== undefined && (u.status === 'failed' || u.status === 'aborted'),
|
|
1066
|
+
)
|
|
905
1067
|
const viaSkip = upstream.find((u) => u !== undefined && u.status === 'skipped')
|
|
906
1068
|
const viaRestore = node.deps.find((d) => blockedRestores.has(d))
|
|
907
1069
|
const blockedBy =
|
|
908
1070
|
blocker?.node.id ??
|
|
909
1071
|
node.deps.map(deadServerVia).find((d) => d !== undefined) ??
|
|
910
1072
|
viaSkip?.blockedBy ??
|
|
1073
|
+
(viaSkip?.otherPlatform !== undefined ? viaSkip.node.id : undefined) ??
|
|
911
1074
|
(viaRestore === undefined ? undefined : blockedRestores.get(viaRestore))
|
|
1075
|
+
// Behind a platform skip is no failure: the run passes, so the exit does.
|
|
1076
|
+
const notHere =
|
|
1077
|
+
!aborted() &&
|
|
1078
|
+
blockedBy !== undefined &&
|
|
1079
|
+
outcomes.get(blockedBy)?.otherPlatform !== undefined
|
|
912
1080
|
finishOne(id, {
|
|
913
1081
|
node,
|
|
914
1082
|
status: aborted() ? 'aborted' : 'skipped',
|
|
915
|
-
exitCode: 1,
|
|
1083
|
+
exitCode: notHere ? 0 : 1,
|
|
916
1084
|
durationMs: 0,
|
|
917
1085
|
...(blockedBy !== undefined ? { blockedBy } : {}),
|
|
918
1086
|
})
|
|
919
1087
|
continue
|
|
920
1088
|
}
|
|
921
|
-
if (
|
|
1089
|
+
if (settlesIdle) {
|
|
922
1090
|
readyAt.delete(id)
|
|
923
|
-
if (
|
|
1091
|
+
if (settledAbove(id)) {
|
|
924
1092
|
settledIdle++
|
|
925
1093
|
release(id)
|
|
926
1094
|
} else dormant.add(id)
|
|
@@ -929,14 +1097,17 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
929
1097
|
|
|
930
1098
|
const queuedMs = Date.now() - (readyAt.get(id) ?? Date.now())
|
|
931
1099
|
readyAt.delete(id)
|
|
932
|
-
const
|
|
933
|
-
|
|
1100
|
+
const settle = settleInPlace(node, upstream)
|
|
1101
|
+
let now: TaskOutcome | undefined
|
|
1102
|
+
if (settle !== undefined) {
|
|
1103
|
+
if (!demoted.has(id)) started(node)
|
|
934
1104
|
try {
|
|
935
|
-
|
|
936
|
-
} catch
|
|
937
|
-
|
|
938
|
-
process.stderr.write(`[vx] onStart observer threw for ${id}: ${m}\n`)
|
|
1105
|
+
now = settle()
|
|
1106
|
+
} catch {
|
|
1107
|
+
// Dispatched below, already started: `execute` reports it.
|
|
939
1108
|
}
|
|
1109
|
+
}
|
|
1110
|
+
if (now !== undefined) {
|
|
940
1111
|
finishOne(id, queuedMs === 0 ? now : { ...now, queuedMs })
|
|
941
1112
|
continue
|
|
942
1113
|
}
|
|
@@ -957,15 +1128,9 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
957
1128
|
}
|
|
958
1129
|
// Crash-isolated observer hook — a throwing onStart must not abort
|
|
959
1130
|
// the dispatch loop (it would strand the tick with the slot held).
|
|
960
|
-
// A demoted task's second dispatch is the same task, already started
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
onStart?.(node)
|
|
964
|
-
} catch (err) {
|
|
965
|
-
const m = err instanceof Error ? err.message : String(err)
|
|
966
|
-
process.stderr.write(`[vx] onStart observer threw for ${id}: ${m}\n`)
|
|
967
|
-
}
|
|
968
|
-
}
|
|
1131
|
+
// A demoted task's second dispatch is the same task, already started,
|
|
1132
|
+
// as is one whose settle threw.
|
|
1133
|
+
if (!demoted.has(id) && settle === undefined) started(node)
|
|
969
1134
|
|
|
970
1135
|
// `.then(onFulfilled, onRejected)` — NOT `.then(f).catch(g)`. The
|
|
971
1136
|
// rejection arm handles ONLY `execute()` rejecting; a throw from
|
|
@@ -1005,12 +1170,13 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1005
1170
|
leave()
|
|
1006
1171
|
untrack()
|
|
1007
1172
|
demoted.add(id)
|
|
1008
|
-
|
|
1009
|
-
|
|
1173
|
+
idleBelow(id, (p) => {
|
|
1174
|
+
idle.delete(p)
|
|
1175
|
+
if (dormant.delete(p)) {
|
|
1010
1176
|
execReady.push(p)
|
|
1011
1177
|
readyAt.set(p, Date.now())
|
|
1012
1178
|
}
|
|
1013
|
-
}
|
|
1179
|
+
})
|
|
1014
1180
|
if (pending.get(id) === 0) {
|
|
1015
1181
|
execReady.push(id)
|
|
1016
1182
|
readyAt.set(id, Date.now())
|
|
@@ -1018,7 +1184,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1018
1184
|
tick()
|
|
1019
1185
|
return
|
|
1020
1186
|
}
|
|
1021
|
-
const message =
|
|
1187
|
+
const message = thrownMessage(err)
|
|
1022
1188
|
// After the run's stop a rejection is the stop's doing (a probe
|
|
1023
1189
|
// it killed, C-65): aborted, as any task it kills, and unsaid.
|
|
1024
1190
|
const stopped = aborted()
|
|
@@ -1045,8 +1211,11 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1045
1211
|
const text = isUserError(err)
|
|
1046
1212
|
? message
|
|
1047
1213
|
: `${message} — ${isOutOfFds(err) ? OUT_OF_FDS_HINT : fsRefusalHint(err)}`
|
|
1048
|
-
|
|
1049
|
-
|
|
1214
|
+
// In a task's own output block, in full: that block is read
|
|
1215
|
+
// alone, and "as <task> above" named one printed elsewhere or
|
|
1216
|
+
// not at all (2026-10-22). On bare stderr the first is above.
|
|
1217
|
+
const first = options.onError === undefined ? refusedBy.get(text) : undefined
|
|
1218
|
+
if (!refusedBy.has(text)) refusedBy.set(text, id)
|
|
1050
1219
|
line = `[vx] ${id}: ${first === undefined ? text : `as ${first} above`}\n`
|
|
1051
1220
|
} else {
|
|
1052
1221
|
const named = err instanceof Error && err.name !== 'Error' ? `${err.name}: ` : ''
|
|
@@ -1062,10 +1231,10 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1062
1231
|
line = `[vx] internal error in ${id}: ${named}${message}${because}\n`
|
|
1063
1232
|
}
|
|
1064
1233
|
try {
|
|
1065
|
-
if (options.onError === undefined)
|
|
1234
|
+
if (options.onError === undefined) ui.say('error', line)
|
|
1066
1235
|
else options.onError(node, line, err)
|
|
1067
1236
|
} catch {
|
|
1068
|
-
|
|
1237
|
+
ui.say('error', line)
|
|
1069
1238
|
}
|
|
1070
1239
|
finishOne(id, outcome)
|
|
1071
1240
|
tick()
|
|
@@ -1085,26 +1254,118 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1085
1254
|
// condition survived the whole core suite (item 644).
|
|
1086
1255
|
if (outcomes.size + settledIdle === nodes.size) {
|
|
1087
1256
|
resolved = true
|
|
1257
|
+
stopListening()
|
|
1088
1258
|
resolve(outcomes)
|
|
1089
1259
|
}
|
|
1090
1260
|
}
|
|
1091
1261
|
|
|
1262
|
+
// A held server's answer: the dependants it found go on the restore
|
|
1263
|
+
// lane, and the server is idle when they were all of them. One whose
|
|
1264
|
+
// deps all settled is already on the exec queue, running, or done:
|
|
1265
|
+
// pushed again it ran twice (its server skipped, the exec lane full).
|
|
1266
|
+
const answer = (id: string, found: readonly string[]): void => {
|
|
1267
|
+
if (resolved || !unanswered.delete(id)) return
|
|
1268
|
+
for (const d of found) {
|
|
1269
|
+
// In `readyAt`: already queued, released to the exec queue when its
|
|
1270
|
+
// server was skipped before this answer. On both lanes it ran twice.
|
|
1271
|
+
if (
|
|
1272
|
+
promoted.has(d) ||
|
|
1273
|
+
outcomes.has(d) ||
|
|
1274
|
+
readyAt.has(d) ||
|
|
1275
|
+
pending.get(d) === 0 ||
|
|
1276
|
+
!inRestoreTier(d)
|
|
1277
|
+
)
|
|
1278
|
+
continue
|
|
1279
|
+
promoted.add(d)
|
|
1280
|
+
restoreReady.push(d)
|
|
1281
|
+
readyAt.set(d, Date.now())
|
|
1282
|
+
}
|
|
1283
|
+
if (dependents.get(id)?.every((d) => inRestoreTier(d)) === true) idle.add(id)
|
|
1284
|
+
if (heldReady.delete(id)) execReady.push(id)
|
|
1285
|
+
tick()
|
|
1286
|
+
}
|
|
1287
|
+
for (const [id, found] of options.heldServers ?? []) {
|
|
1288
|
+
if (unanswered.has(id))
|
|
1289
|
+
void found.then(
|
|
1290
|
+
(ids) => answer(id, ids),
|
|
1291
|
+
() => answer(id, []),
|
|
1292
|
+
)
|
|
1293
|
+
}
|
|
1294
|
+
// A stop reaches the scheduler through a completion, and a held server
|
|
1295
|
+
// waiting on the remote has none: the run waited out the remote's answer.
|
|
1296
|
+
const signal = options.signal
|
|
1297
|
+
if (unanswered.size > 0 && signal !== undefined) {
|
|
1298
|
+
const stop = (): void => {
|
|
1299
|
+
for (const id of [...unanswered]) answer(id, [])
|
|
1300
|
+
}
|
|
1301
|
+
signal.addEventListener('abort', stop, { once: true })
|
|
1302
|
+
stopListening = () => signal.removeEventListener('abort', stop)
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1092
1305
|
tick()
|
|
1093
1306
|
})
|
|
1094
1307
|
}
|
|
1095
1308
|
|
|
1096
|
-
/**
|
|
1309
|
+
/**
|
|
1310
|
+
* Persistent tasks nobody asked for whose every dependant is a restore-tier
|
|
1311
|
+
* hit, or a group nobody asked for that only such dependants stand behind:
|
|
1312
|
+
* a group runs nothing, so `env: { dependsOn: ['dev'] }` between the server
|
|
1313
|
+
* and its hits booted the server on every warm run. Those groups go in
|
|
1314
|
+
* `groups`.
|
|
1315
|
+
*/
|
|
1097
1316
|
export function idleServers(
|
|
1098
1317
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
1099
1318
|
dependents: ReadonlyMap<string, string[]>,
|
|
1100
1319
|
restoreTier: ReadonlySet<string> | undefined,
|
|
1320
|
+
groups?: Set<string>,
|
|
1101
1321
|
): Set<string> {
|
|
1102
1322
|
const out = new Set<string>()
|
|
1103
1323
|
if (restoreTier === undefined || restoreTier.size === 0) return out
|
|
1324
|
+
const unasked = (n: TaskNode): boolean => !n.requested && n.surfaced !== true
|
|
1325
|
+
// Per group: whether only hits stand behind it. Post-order on a stack, as
|
|
1326
|
+
// groups nest as deep as the graph.
|
|
1327
|
+
const spare = new Map<string, boolean>()
|
|
1328
|
+
const spareGroup = (root: string): boolean => {
|
|
1329
|
+
const stack = [root]
|
|
1330
|
+
while (stack.length > 0) {
|
|
1331
|
+
const id = stack[stack.length - 1]!
|
|
1332
|
+
if (spare.has(id)) {
|
|
1333
|
+
stack.pop()
|
|
1334
|
+
continue
|
|
1335
|
+
}
|
|
1336
|
+
const n = nodes.get(id)
|
|
1337
|
+
const ds = dependents.get(id)
|
|
1338
|
+
if (n === undefined || !isGroupTask(n) || !unasked(n) || ds === undefined) {
|
|
1339
|
+
spare.set(id, false)
|
|
1340
|
+
stack.pop()
|
|
1341
|
+
continue
|
|
1342
|
+
}
|
|
1343
|
+
const next = ds.find((d) => !restoreTier.has(d) && !spare.has(d))
|
|
1344
|
+
if (next !== undefined) {
|
|
1345
|
+
stack.push(next)
|
|
1346
|
+
continue
|
|
1347
|
+
}
|
|
1348
|
+
stack.pop()
|
|
1349
|
+
spare.set(
|
|
1350
|
+
id,
|
|
1351
|
+
ds.every((d) => restoreTier.has(d) || spare.get(d) === true),
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1354
|
+
return spare.get(root)!
|
|
1355
|
+
}
|
|
1104
1356
|
for (const n of nodes.values()) {
|
|
1105
|
-
if (n.config.exec?.persistent === undefined || n
|
|
1357
|
+
if (n.config.exec?.persistent === undefined || !unasked(n)) continue
|
|
1106
1358
|
const ds = dependents.get(n.id)
|
|
1107
|
-
if (ds
|
|
1359
|
+
if (ds === undefined || !ds.every((d) => restoreTier.has(d) || spareGroup(d))) continue
|
|
1360
|
+
out.add(n.id)
|
|
1361
|
+
if (groups === undefined) continue
|
|
1362
|
+
const stack = ds.filter((d) => !restoreTier.has(d))
|
|
1363
|
+
while (stack.length > 0) {
|
|
1364
|
+
const g = stack.pop()!
|
|
1365
|
+
if (groups.has(g)) continue
|
|
1366
|
+
groups.add(g)
|
|
1367
|
+
for (const d of dependents.get(g)!) if (!restoreTier.has(d)) stack.push(d)
|
|
1368
|
+
}
|
|
1108
1369
|
}
|
|
1109
1370
|
return out
|
|
1110
1371
|
}
|