@vzn/vx 0.0.595 → 0.0.597
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 +1 -1
- package/launcher.cjs +7 -3
- package/package.json +5 -5
- package/src/cache/archive.ts +35 -6
- package/src/cache/cache.ts +170 -99
- package/src/cache/chained-cache.ts +25 -7
- package/src/cache/git-inputs.ts +23 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +99 -44
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +9 -6
- package/src/cache/policy.ts +5 -0
- package/src/cache/zstd.ts +10 -4
- package/src/cli/cache.ts +12 -6
- package/src/cli/foreign-flags.ts +57 -3
- package/src/cli/info.ts +1 -1
- package/src/cli/init.ts +2 -2
- package/src/cli/last.ts +10 -4
- package/src/cli/lock.ts +5 -3
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/run.ts +73 -30
- package/src/cli/select.ts +6 -3
- package/src/cli/show.ts +8 -2
- package/src/cli/upgrade.ts +1 -1
- package/src/cli/watch-filter.ts +63 -12
- package/src/cli/watch-fs.ts +11 -3
- package/src/cli/watch-judge.ts +15 -8
- package/src/cli/watch.ts +18 -11
- package/src/cli/workspace-config.ts +3 -1
- package/src/config.ts +42 -21
- package/src/exec/executor.ts +1 -1
- package/src/exec/kill-tree.ts +89 -18
- package/src/exec/proc-sample.ts +1 -1
- package/src/exec/runner.ts +74 -36
- package/src/exec/sandbox-paths.ts +12 -1
- package/src/exec/sandbox-runtime.ts +283 -88
- package/src/exec/sandbox-violations.ts +115 -15
- package/src/graph/scheduler.ts +30 -14
- package/src/graph/task-graph.ts +265 -44
- package/src/orchestrator/admission.ts +12 -3
- package/src/orchestrator/affected-tasks.ts +6 -2
- package/src/orchestrator/deferred-outputs.ts +3 -2
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/events.ts +10 -1
- package/src/orchestrator/excluded-keys.ts +2 -13
- package/src/orchestrator/execute-task.ts +85 -39
- package/src/orchestrator/failure-mode.ts +43 -28
- package/src/orchestrator/framed-output.ts +8 -8
- package/src/orchestrator/index.ts +0 -1
- package/src/orchestrator/keyed-projects.ts +6 -3
- package/src/orchestrator/logger.ts +21 -9
- package/src/orchestrator/metrics.ts +19 -8
- package/src/orchestrator/miss-save.ts +9 -13
- package/src/orchestrator/options.ts +7 -0
- package/src/orchestrator/plan.ts +7 -23
- package/src/orchestrator/plugin.ts +37 -16
- package/src/orchestrator/prepare.ts +13 -4
- package/src/orchestrator/run-artifacts.ts +4 -3
- package/src/orchestrator/run-lock.ts +25 -24
- package/src/orchestrator/run.ts +22 -5
- package/src/orchestrator/sandbox-request.ts +96 -18
- package/src/orchestrator/save-lane.ts +5 -5
- package/src/orchestrator/signals.ts +22 -4
- package/src/orchestrator/stable-keys.ts +3 -11
- package/src/orchestrator/summary.ts +7 -13
- package/src/orchestrator/task-hash.ts +13 -3
- package/src/orchestrator/telemetry.ts +16 -7
- package/src/orchestrator/upstream.ts +11 -0
- package/src/util/hangup.ts +42 -0
- package/src/util/index.ts +2 -0
- package/src/util/paths.ts +24 -4
- package/src/workspace/affected.ts +79 -42
- package/src/workspace/config-eval.ts +27 -6
- package/src/workspace/config-imports.ts +8 -53
- package/src/workspace/config-schema.ts +56 -9
- package/src/workspace/filter.ts +19 -0
- package/src/workspace/lockfile.ts +2 -1
- package/src/workspace/migrate-scripts.ts +5 -3
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/project-loader.ts +19 -4
- package/src/workspace/repo-id.ts +42 -4
- package/src/workspace/workspace.ts +53 -24
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// task's owner can act on — inside the project, minus the loopback denial
|
|
5
5
|
// no grant can avoid, minus what the task chose to ignore.
|
|
6
6
|
|
|
7
|
-
import { statSync } from 'node:fs'
|
|
7
|
+
import { existsSync, lstatSync, statSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { absolutize, atOrUnder, isUnderAny, localBindingOn, toRealPath } from './sandbox-paths.js'
|
|
10
10
|
import { bindableReads, bindableWrites } from './sandbox-binds.js'
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
* <pid> openat(AT_FDCWD, "<path>", <flags>) = -1 ENOENT (...)
|
|
25
25
|
* <pid> access("<path>", <mode>) = -1 EACCES (...)
|
|
26
26
|
* <pid> statx(AT_FDCWD, "<path>", <flags>, <mask>, ...) = -1 ENOENT (...)
|
|
27
|
+
* <pid> execve("<path>", ["<argv0>", ...], <envp>) = -1 ENOENT (...)
|
|
27
28
|
*
|
|
28
29
|
* …but ONLY when the syscall completes without another traced process
|
|
29
30
|
* interleaving. Under `-f` strace splits an interrupted call across two
|
|
@@ -40,21 +41,26 @@ import type {
|
|
|
40
41
|
* (`cStringPath` decodes it). Paths that are relative resolve against the
|
|
41
42
|
* task's cwd (set by Bun.spawn).
|
|
42
43
|
*/
|
|
43
|
-
const SYSCALLS = 'openat|access|statx|newfstatat'
|
|
44
|
+
const SYSCALLS = 'openat|execve|faccessat2|faccessat|access|statx|newfstatat'
|
|
44
45
|
const QUOTED = '"((?:[^"\\\\]|\\\\.)+)"'
|
|
45
46
|
// The directory descriptor a call names, `-y`'s path included: taken up to
|
|
46
47
|
// the `, "` that opens the path argument, since a directory's name may hold
|
|
47
48
|
// a quote (`4</ws/q"d>`). Captured: a relative path is in that directory.
|
|
48
49
|
const DIRFD = '(?:AT_FDCWD|\\d+)(?:<(.*?)>)?, '
|
|
50
|
+
// The rest of the call: an `execve`'s argv strings may hold a `)`
|
|
51
|
+
// (`sh -c "f()"`), so it reads to the result at the line's end.
|
|
52
|
+
const ARGS = '.*'
|
|
49
53
|
const STRACE_DONE_RE = new RegExp(
|
|
50
|
-
`^(\\d+)\\s+(${SYSCALLS})\\((?:${DIRFD})?${QUOTED}
|
|
54
|
+
`^(\\d+)\\s+(${SYSCALLS})\\((?:${DIRFD})?${QUOTED}${ARGS}\\)\\s*=\\s*-1\\s+(ENOENT|EACCES|EPERM)`,
|
|
51
55
|
)
|
|
52
56
|
const STRACE_UNFINISHED_RE = new RegExp(
|
|
53
|
-
`^(\\d+)\\s+(${SYSCALLS})\\((?:${DIRFD})?${QUOTED}
|
|
57
|
+
`^(\\d+)\\s+(${SYSCALLS})\\((?:${DIRFD})?${QUOTED}${ARGS}<unfinished`,
|
|
54
58
|
)
|
|
55
59
|
const STRACE_RESUMED_RE = new RegExp(
|
|
56
60
|
`^(\\d+)\\s+<\\.\\.\\. (${SYSCALLS}) resumed>.*?=\\s*-1\\s+(ENOENT|EACCES|EPERM)`,
|
|
57
61
|
)
|
|
62
|
+
/** The calls that ask whether a path is there rather than read it. */
|
|
63
|
+
const PROBES = new Set(['execve', 'faccessat2', 'faccessat', 'access', 'statx', 'newfstatat'])
|
|
58
64
|
/** A resumed call that SUCCEEDED — clears the pending entry, emits nothing. */
|
|
59
65
|
const STRACE_RESUMED_OK_RE = new RegExp(`^(\\d+)\\s+<\\.\\.\\. (${SYSCALLS}) resumed>`)
|
|
60
66
|
/**
|
|
@@ -352,7 +358,16 @@ export async function parseStraceViolations(
|
|
|
352
358
|
...args.config.allowWrite.filter((w) => bindableWrites([w]).length > 0),
|
|
353
359
|
].map((p) => toRealPath(absolutize(p))),
|
|
354
360
|
)
|
|
361
|
+
// A write glob no mount holds is a write grant too (`refusedWrites`
|
|
362
|
+
// counts its scratch): cp's probe of its destination there failed a task
|
|
363
|
+
// on its own output. One a read bind holds is readable; one outside the
|
|
364
|
+
// workspace, never reported.
|
|
365
|
+
const pendingWrites = (args.config.pendingWrites ?? []).map((g) => new Bun.Glob(g))
|
|
355
366
|
const denyAnchors = baselines.denyRead.map((p) => toRealPath(absolutize(p)))
|
|
367
|
+
const mounted = new Set([
|
|
368
|
+
...allowAbs,
|
|
369
|
+
...bindableWrites(args.config.allowWrite).map((p) => toRealPath(absolutize(p))),
|
|
370
|
+
])
|
|
356
371
|
// A read under a widened write grant's directory is never refused, so it
|
|
357
372
|
// is reported when it succeeds: an entry that was there at the start and
|
|
358
373
|
// no grant covers is an input the key never saw, and so is the
|
|
@@ -377,13 +392,20 @@ export async function parseStraceViolations(
|
|
|
377
392
|
baselines.cwd,
|
|
378
393
|
widened.size > 0,
|
|
379
394
|
)) {
|
|
380
|
-
const
|
|
395
|
+
const lexical = absolutize(rawPath, dir ?? baselines.cwd)
|
|
396
|
+
const abs = toRealPath(lexical)
|
|
381
397
|
// Only report paths under the workspace-root deny anchor — system
|
|
382
398
|
// libs / /proc / /sys / etc. probes are not interesting violations.
|
|
383
399
|
if (!denyAnchors.some((root) => atOrUnder(abs, root))) continue
|
|
384
400
|
// Skip paths the user explicitly allowed (and their descendants).
|
|
385
|
-
if (isUnderAny(abs, allowAbs)) continue
|
|
401
|
+
if (isUnderAny(abs, allowAbs) && !hiddenByLink(lexical, abs, mounted, denyAnchors)) continue
|
|
402
|
+
if (underGlob(abs, pendingWrites)) continue
|
|
386
403
|
if (read === true && !undeclared(abs)) continue
|
|
404
|
+
// A hidden path answers ENOENT, as a missing one does: a probe
|
|
405
|
+
// (`./gen.sh || …`, `test -x`, a PATH walk) is a refusal only where
|
|
406
|
+
// the host has the file.
|
|
407
|
+
if (errno === 'ENOENT' && PROBES.has(syscall) && !lstatSync(abs, { throwIfNoEntry: false }))
|
|
408
|
+
continue
|
|
387
409
|
const key = `${syscall}|${abs}`
|
|
388
410
|
if (seen.has(key)) continue
|
|
389
411
|
seen.add(key)
|
|
@@ -401,9 +423,9 @@ export async function parseStraceViolations(
|
|
|
401
423
|
timestamp: new Date(),
|
|
402
424
|
target: abs,
|
|
403
425
|
path: abs,
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
426
|
+
// An openat is a read or a write depending on flags the parse
|
|
427
|
+
// does not keep, and `access` may ask either — so either list
|
|
428
|
+
// can silence it.
|
|
407
429
|
ignorable: ['read', 'write'],
|
|
408
430
|
},
|
|
409
431
|
)
|
|
@@ -411,6 +433,39 @@ export async function parseStraceViolations(
|
|
|
411
433
|
return out
|
|
412
434
|
}
|
|
413
435
|
|
|
436
|
+
/**
|
|
437
|
+
* A granted file the task could not open because a link on the path it
|
|
438
|
+
* opened is not in the sandbox. A grant is mounted at its real path and
|
|
439
|
+
* bwrap mounts no link, so `read: ['config.json']` over `config.json ->
|
|
440
|
+
* conf/real.json` bound the target and left the name the task opens out;
|
|
441
|
+
* judged by its real path, the ENOENT was granted and dropped, and a tool
|
|
442
|
+
* that fell back on the missing file was cached green. The link is there
|
|
443
|
+
* when the directory holding it is mounted; a target missing on the host
|
|
444
|
+
* is missing outside the sandbox too, and outside the deny anchor the
|
|
445
|
+
* host is mounted, links and all.
|
|
446
|
+
*/
|
|
447
|
+
function hiddenByLink(
|
|
448
|
+
lexical: string,
|
|
449
|
+
real: string,
|
|
450
|
+
mounted: Set<string>,
|
|
451
|
+
anchors: readonly string[],
|
|
452
|
+
): boolean {
|
|
453
|
+
if (lexical === real || !existsSync(real)) return false
|
|
454
|
+
let at = lexical
|
|
455
|
+
const links: string[] = []
|
|
456
|
+
for (let up = path.dirname(at); up !== at; at = up, up = path.dirname(at)) {
|
|
457
|
+
try {
|
|
458
|
+
if (lstatSync(at).isSymbolicLink()) links.push(at)
|
|
459
|
+
} catch {
|
|
460
|
+
// Not on the host by this spelling: no link to judge here.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return links.some((l) => {
|
|
464
|
+
const holder = toRealPath(path.dirname(l))
|
|
465
|
+
return anchors.some((a) => atOrUnder(holder, a)) && !isUnderAny(holder, mounted)
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
414
469
|
/**
|
|
415
470
|
* Does a violation line match something the task said to ignore?
|
|
416
471
|
*
|
|
@@ -418,17 +473,23 @@ export async function parseStraceViolations(
|
|
|
418
473
|
* /path/x`, `deny(1) system-info vfs.disk-space` — so the operation picks
|
|
419
474
|
* the list and the target is matched against its patterns. Anything
|
|
420
475
|
* unparseable is NOT ignored: a record we cannot classify is exactly the
|
|
421
|
-
* one worth seeing.
|
|
476
|
+
* one worth seeing. A file list matches the record's `path`, where it
|
|
477
|
+
* lands, as `read` and `write` patterns are anchored and as the report's
|
|
478
|
+
* own boundary judges it: a seatbelt record named through a link was
|
|
479
|
+
* reported and its pattern never matched.
|
|
422
480
|
*/
|
|
423
481
|
function matchesIgnore(
|
|
424
482
|
v: SandboxViolation,
|
|
425
483
|
ignore: NonNullable<ResolvedSandboxConfig['ignore']>,
|
|
426
484
|
): boolean {
|
|
427
485
|
if (v.target === undefined || v.ignorable === undefined) return false
|
|
486
|
+
const proxy = PROXY_DENY_RE.test(v.line)
|
|
428
487
|
for (const which of v.ignorable) {
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
431
|
-
|
|
488
|
+
const listed = ignore[which]
|
|
489
|
+
if (listed === undefined) continue
|
|
490
|
+
const patterns = proxy ? listed.map(hostKey) : listed
|
|
491
|
+
const subject = which === 'read' || which === 'write' ? (v.path ?? v.target) : v.target
|
|
492
|
+
if (patterns.some((pat) => pat === subject || new Bun.Glob(pat).match(subject))) return true
|
|
432
493
|
}
|
|
433
494
|
return false
|
|
434
495
|
}
|
|
@@ -440,7 +501,7 @@ function matchesIgnore(
|
|
|
440
501
|
*/
|
|
441
502
|
function describeMacViolation(line: string): Partial<SandboxViolation> {
|
|
442
503
|
const proxy = PROXY_DENY_RE.exec(line)
|
|
443
|
-
if (proxy !== null) return { target: proxy[1]
|
|
504
|
+
if (proxy !== null) return { target: hostKey(proxy[1]!), ignorable: ['network'] }
|
|
444
505
|
const m = /deny\(\d+\)\s+(\S+)\s+(.+?)\s*$/.exec(line)
|
|
445
506
|
if (m === null) return {}
|
|
446
507
|
const [op, target] = [m[1]!, m[2]!]
|
|
@@ -657,6 +718,18 @@ function filterIgnored(
|
|
|
657
718
|
*/
|
|
658
719
|
const PROXY_DENY_RE = /^deny network-outbound (\S+) \([^)]*\)$/
|
|
659
720
|
|
|
721
|
+
/**
|
|
722
|
+
* `<host>[:<port>]` as the proxy matches it: case-blind, no trailing dot.
|
|
723
|
+
* The record keeps the client's spelling (curl sends `EXAMPLE.Com`, curl
|
|
724
|
+
* and Bun keep `example.com.`), so an exact match against the ignore list
|
|
725
|
+
* left a refusal the task named in its report.
|
|
726
|
+
*/
|
|
727
|
+
function hostKey(target: string): string {
|
|
728
|
+
const colon = target.lastIndexOf(':')
|
|
729
|
+
const host = colon === -1 ? target : target.slice(0, colon)
|
|
730
|
+
return host.toLowerCase().replace(/\.$/, '') + (colon === -1 ? '' : target.slice(colon))
|
|
731
|
+
}
|
|
732
|
+
|
|
660
733
|
/**
|
|
661
734
|
* Linux: the connections the proxy refused, from the store records. Only
|
|
662
735
|
* the write observer's records were read there, so a task denied a host
|
|
@@ -685,6 +758,8 @@ export function refusedWrites(
|
|
|
685
758
|
writable: readonly string[],
|
|
686
759
|
/** Write globs whose writes land in the sandbox's scratch (`scratchWrites`): granted. */
|
|
687
760
|
scratch: readonly string[] = [],
|
|
761
|
+
/** What the sandbox mounts readable, canonical: a mkdir of what is there is no write. */
|
|
762
|
+
readable: readonly string[] = [],
|
|
688
763
|
): SandboxViolation[] {
|
|
689
764
|
const binds = new Set(writable.map((w) => toRealPath(absolutize(w))))
|
|
690
765
|
const globs = scratch.map((g) => new Bun.Glob(g))
|
|
@@ -705,7 +780,9 @@ export function refusedWrites(
|
|
|
705
780
|
// the bind: `mkdir -p node_modules/.cache/tool` under a grant of
|
|
706
781
|
// `node_modules/.cache/` met EEXIST on `node_modules`, wrote nothing,
|
|
707
782
|
// and the attempt failed a clean task (2026-10-03).
|
|
708
|
-
|
|
783
|
+
// So does one a read grant shows, or holds: `mkdir -p src` under
|
|
784
|
+
// `read: ['src']` failed a clean task the same way (X-82).
|
|
785
|
+
if (syscall.startsWith('mkdir') && inSandbox(abs, binds, readable)) continue
|
|
709
786
|
const key = `${syscall}|${abs}`
|
|
710
787
|
if (seen.has(key)) continue
|
|
711
788
|
seen.add(key)
|
|
@@ -720,6 +797,29 @@ export function refusedWrites(
|
|
|
720
797
|
return out
|
|
721
798
|
}
|
|
722
799
|
|
|
800
|
+
/**
|
|
801
|
+
* `abs` exists inside the sandbox: a write bind lies at or under it (vx
|
|
802
|
+
* made each before the spawn), a read grant that exists does, or a read
|
|
803
|
+
* grant holds it and it exists. A read grant naming nothing is not
|
|
804
|
+
* mounted, so neither it nor its ancestors are there.
|
|
805
|
+
*/
|
|
806
|
+
function inSandbox(abs: string, binds: Set<string>, readable: readonly string[]): boolean {
|
|
807
|
+
return (
|
|
808
|
+
[...binds].some((b) => atOrUnder(b, abs)) ||
|
|
809
|
+
readable.some((r) => atOrUnder(r, abs) && exists(r)) ||
|
|
810
|
+
(readable.some((r) => atOrUnder(abs, r)) && exists(abs))
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function exists(p: string): boolean {
|
|
815
|
+
try {
|
|
816
|
+
lstatSync(p)
|
|
817
|
+
return true
|
|
818
|
+
} catch {
|
|
819
|
+
return false
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
723
823
|
/** `p`, or a directory holding it, matches one of `globs`. */
|
|
724
824
|
function underGlob(p: string, globs: readonly InstanceType<typeof Bun.Glob>[]): boolean {
|
|
725
825
|
if (globs.length === 0) return false
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -120,6 +120,12 @@ export interface TaskOutcome {
|
|
|
120
120
|
* so a dependant that folds `hash` saves nothing either (A-12).
|
|
121
121
|
*/
|
|
122
122
|
unkeyed?: true
|
|
123
|
+
/**
|
|
124
|
+
* The run's cache policy read and wrote nothing (`--no-cache`,
|
|
125
|
+
* `--cache=local:`): no cache answered for this task, so a run of it is
|
|
126
|
+
* `no-cache`, not a miss. Set by `run()`; `ranNoCache` reads it.
|
|
127
|
+
*/
|
|
128
|
+
cacheOff?: true
|
|
123
129
|
/**
|
|
124
130
|
* On a `skipped` outcome: the task at the ROOT of what blocked it — the
|
|
125
131
|
* failed (or aborted) upstream, followed through any chain of skips
|
|
@@ -173,8 +179,8 @@ export interface TaskOutcome {
|
|
|
173
179
|
/** The attempts that failed and were run again, in order: when each ended (epoch ms) and how. */
|
|
174
180
|
failedAttempts?: readonly { endedAt: number; exitCode: number; timedOut?: true }[]
|
|
175
181
|
/**
|
|
176
|
-
* Set when this run proved the task flaky: its key
|
|
177
|
-
*
|
|
182
|
+
* Set when this run proved the task flaky: its key failed after it had
|
|
183
|
+
* passed (this run's included) or it needed a retry. Its row carries it.
|
|
178
184
|
*/
|
|
179
185
|
flaky?: { passes: number; failures: number }
|
|
180
186
|
/**
|
|
@@ -292,9 +298,10 @@ export interface ScheduleOptions {
|
|
|
292
298
|
* Admission over the worker count: asked for every exec-tier task about
|
|
293
299
|
* to start on this machine, with the ids of the exec-tier tasks running
|
|
294
300
|
* here right now (added on dispatch, so two asks in one tick see each
|
|
295
|
-
* other
|
|
296
|
-
*
|
|
297
|
-
*
|
|
301
|
+
* other; removed when `execute` resolves, which for a persistent task is
|
|
302
|
+
* at ready). `false` parks the task until something finishes. Undefined →
|
|
303
|
+
* count-only, the legacy path byte for byte. Restore-tier hits, pooled
|
|
304
|
+
* tasks and groups hold no local resources: never asked, never listed.
|
|
298
305
|
*/
|
|
299
306
|
admit?: (id: string, running: ReadonlySet<string>) => boolean
|
|
300
307
|
/**
|
|
@@ -394,19 +401,26 @@ class ReadyHeap {
|
|
|
394
401
|
/**
|
|
395
402
|
* The dead server a dependency stands for: itself, or one a group reaches.
|
|
396
403
|
* A group is a name for its deps, and it finished the moment the server
|
|
397
|
-
* was ready, so a task behind it ran against the dead one.
|
|
404
|
+
* was ready, so a task behind it ran against the dead one. Walked with a
|
|
405
|
+
* stack and a seen set: recursion threw `RangeError` on a deep chain of
|
|
406
|
+
* groups, and re-walked a group each path reached it by.
|
|
398
407
|
*/
|
|
399
408
|
export function deadServerBehind(
|
|
400
409
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
401
410
|
serverDied: (id: string) => boolean,
|
|
402
411
|
id: string,
|
|
403
412
|
): string | undefined {
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
413
|
+
const stack = [id]
|
|
414
|
+
const seen = new Set<string>()
|
|
415
|
+
while (stack.length > 0) {
|
|
416
|
+
const at = stack.pop()!
|
|
417
|
+
if (seen.has(at)) continue
|
|
418
|
+
seen.add(at)
|
|
419
|
+
if (serverDied(at)) return at
|
|
420
|
+
const n = nodes.get(at)
|
|
421
|
+
if (n === undefined || !isGroupTask(n)) continue
|
|
422
|
+
// Reversed, so the first dep is walked first, as the order it names them.
|
|
423
|
+
for (let i = n.deps.length - 1; i >= 0; i--) stack.push(n.deps[i]!)
|
|
410
424
|
}
|
|
411
425
|
return undefined
|
|
412
426
|
}
|
|
@@ -582,8 +596,10 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
582
596
|
}
|
|
583
597
|
}
|
|
584
598
|
// The policy sees exec-tier local tasks only; a restore is a tar
|
|
585
|
-
// extract
|
|
586
|
-
|
|
599
|
+
// extract, a pooled task runs on someone else's capacity, and a group
|
|
600
|
+
// runs nothing, so a packing policy that counted one parked work for it.
|
|
601
|
+
const local = (id: string): boolean =>
|
|
602
|
+
!inRestoreTier(id) && poolOf?.(id) === undefined && !isGroupTask(nodes.get(id) as TaskNode)
|
|
587
603
|
const admits = (id: string): boolean => !local(id) || admitPolicy!(id, running)
|
|
588
604
|
// With no policy nothing reads `running`, so nothing is tracked: the
|
|
589
605
|
// count-only dispatch allocates no closure and touches no set per task.
|