@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
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
// Where each task runs: the placement of a graph over the resolved
|
|
2
2
|
// executors, and the plan-mode view of it. Split from run.ts on 2026-09-10
|
|
3
3
|
// (pure motion). A task is pinned to this machine when it is persistent,
|
|
4
|
-
// depends on a persistent task, is sandboxed, says `exec.remote: false`, or
|
|
4
|
+
// depends on a persistent task no `servers` executor carries, is sandboxed, says `exec.remote: false`, or
|
|
5
5
|
// folds a runtime probe into its key; an `exec.interactive` task goes to the
|
|
6
6
|
// local floor directly, as only it can hand over this terminal;
|
|
7
7
|
// everything else asks the executors in declaration order, and the local
|
|
8
|
-
// floor takes what nothing claimed.
|
|
8
|
+
// floor takes what nothing claimed. An `exclusive` executor lifts every
|
|
9
|
+
// pin and leaves the floor nothing: a task that would land here refuses.
|
|
9
10
|
|
|
10
11
|
import { machineParallelism, UserError } from '../util/index.js'
|
|
11
|
-
import { isLocalExecutor, selectExecutor, type TaskExecutor } from '../exec/index.js'
|
|
12
|
+
import { hostPlatform, isLocalExecutor, selectExecutor, type TaskExecutor } from '../exec/index.js'
|
|
12
13
|
import { isGroupTask, type TaskNode } from '../graph/index.js'
|
|
13
14
|
import { resolveDownloadModes } from './download-policy.js'
|
|
14
15
|
import { type Logger, warnOf } from '../report/index.js'
|
|
15
16
|
import { executorLabel, resolveExecutors } from './plugin-host.js'
|
|
16
17
|
import type { prepareRun } from './prepare.js'
|
|
18
|
+
import { cacheFacts, taskArtifacts } from './task-artifacts.js'
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* A task is pinned to this machine when it is persistent, transitively
|
|
@@ -22,20 +24,32 @@ import type { prepareRun } from './prepare.js'
|
|
|
22
24
|
* machinery — a worker has none of it, and a boundary "verified" where it
|
|
23
25
|
* is not enforced passes vacuously), or declares `exec.remote: false`. A
|
|
24
26
|
* dependant of a pinned task is pinned with it. A task whose key folds a
|
|
25
|
-
* runtime probe is pinned alone (`withProbedRuntime`).
|
|
27
|
+
* runtime probe is pinned alone (`withProbedRuntime`). `sandbox: false`
|
|
28
|
+
* leaves `exec.sandbox` out: the set for an executor that enforces it where
|
|
29
|
+
* it runs (`TaskExecutor.sandbox`).
|
|
26
30
|
*/
|
|
27
|
-
export function pinnedLocalSet(
|
|
31
|
+
export function pinnedLocalSet(
|
|
32
|
+
nodes: Map<string, TaskNode>,
|
|
33
|
+
sandbox = true,
|
|
34
|
+
/** For a `servers` executor: a carriable persistent task pins nothing. */
|
|
35
|
+
servers = false,
|
|
36
|
+
/** `false` for a `runtime` executor: it checks the probes where it runs the task. */
|
|
37
|
+
runtime = true,
|
|
38
|
+
/** For an `exclusive` executor: it takes what pins elsewhere, servers included. */
|
|
39
|
+
exclusive = false,
|
|
40
|
+
): Set<string> {
|
|
28
41
|
const pinned = new Set<string>()
|
|
29
42
|
for (const node of nodes.values()) {
|
|
30
43
|
if (
|
|
31
|
-
node.config.exec?.persistent !== undefined ||
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
(node.config.exec?.persistent !== undefined && !(servers && carriable(node, exclusive))) ||
|
|
45
|
+
(!exclusive &&
|
|
46
|
+
((sandbox && node.config.exec?.sandbox !== undefined) ||
|
|
47
|
+
node.config.exec?.remote === false))
|
|
34
48
|
) {
|
|
35
49
|
pinned.add(node.id)
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
|
-
if (pinned.size === 0) return withProbedRuntime(nodes, pinned)
|
|
52
|
+
if (pinned.size === 0) return runtime ? withProbedRuntime(nodes, pinned) : pinned
|
|
39
53
|
// Pinning flows up the dependant edges from what pins itself, one walk on
|
|
40
54
|
// an explicit stack: a chain is as deep as the graph, and a recursion
|
|
41
55
|
// per edge threw `RangeError` on the 50,000 the builder takes (item 737).
|
|
@@ -55,14 +69,50 @@ export function pinnedLocalSet(nodes: Map<string, TaskNode>): Set<string> {
|
|
|
55
69
|
stack.push(up)
|
|
56
70
|
}
|
|
57
71
|
}
|
|
58
|
-
return withProbedRuntime(nodes, pinned)
|
|
72
|
+
return runtime ? withProbedRuntime(nodes, pinned) : pinned
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A persistent task a `servers` executor can start where it runs a
|
|
77
|
+
* dependant: one the run holds for its dependants only, with no sandbox
|
|
78
|
+
* (this machine's machinery) and no `exec.remote: false`, which an
|
|
79
|
+
* `exclusive` executor takes all the same.
|
|
80
|
+
*/
|
|
81
|
+
function carriable(node: TaskNode, exclusive = false): boolean {
|
|
82
|
+
const exec = node.config.exec
|
|
83
|
+
return (
|
|
84
|
+
exec?.persistent !== undefined &&
|
|
85
|
+
!node.requested &&
|
|
86
|
+
node.surfaced !== true &&
|
|
87
|
+
(exclusive ||
|
|
88
|
+
(exec.sandbox === undefined && exec.remote !== false && exec.interactive !== true))
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The persistent tasks `node` depends on through groups and servers, each after its own. */
|
|
93
|
+
function serversBehind(nodes: ReadonlyMap<string, TaskNode>, node: TaskNode): TaskNode[] {
|
|
94
|
+
const out: TaskNode[] = []
|
|
95
|
+
const seen = new Set<string>()
|
|
96
|
+
const visit = (id: string): void => {
|
|
97
|
+
if (seen.has(id)) return
|
|
98
|
+
seen.add(id)
|
|
99
|
+
const dep = nodes.get(id)
|
|
100
|
+
if (dep === undefined) return
|
|
101
|
+
const server = dep.config.exec?.persistent !== undefined
|
|
102
|
+
if (!server && !isGroupTask(dep)) return
|
|
103
|
+
for (const d of dep.deps) visit(d)
|
|
104
|
+
if (server) out.push(dep)
|
|
105
|
+
}
|
|
106
|
+
for (const d of node.deps) visit(d)
|
|
107
|
+
return out
|
|
59
108
|
}
|
|
60
109
|
|
|
61
110
|
/**
|
|
62
111
|
* A key that folds `inputs.runtime` / `workspaceRuntime` holds what
|
|
63
112
|
* THIS machine answered (`node -v`); a worker runs its own runtime, which no
|
|
64
113
|
* executor can prove equal, and its output saved under this key is a stale
|
|
65
|
-
* hit here (C-2). Such a task runs here
|
|
114
|
+
* hit here (C-2). Such a task runs here, or on a `runtime` executor, which
|
|
115
|
+
* fails it where its probes answer otherwise. Its dependants are not pinned with
|
|
66
116
|
* it: they fold the probe through its input key, and their own output does
|
|
67
117
|
* not depend on this machine's runtime.
|
|
68
118
|
*/
|
|
@@ -108,6 +158,17 @@ export function terminalHolders(nodes: Map<string, TaskNode>, tty: boolean): Set
|
|
|
108
158
|
stack.push(...nodes.get(id)!.deps)
|
|
109
159
|
}
|
|
110
160
|
const clash = [...holders].filter((id) => id !== server && !before.has(id))
|
|
161
|
+
// One that runs after the server cannot run before it: the advice to make
|
|
162
|
+
// the server depend on it wrote a cycle.
|
|
163
|
+
const after = clash.filter((id) => reaches(nodes, id, server))
|
|
164
|
+
if (after.length > 0) {
|
|
165
|
+
const one = after.length === 1
|
|
166
|
+
throw new UserError(
|
|
167
|
+
`${server} is persistent and interactive, so it holds the terminal until the run ends, ` +
|
|
168
|
+
`and ${after.join(', ')}, which ${one ? 'depends' : 'depend'} on it, would ask for it too: ` +
|
|
169
|
+
`run ${one ? 'it' : 'them'} on ${one ? 'its' : 'their'} own`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
111
172
|
if (clash.length > 0) {
|
|
112
173
|
throw new UserError(
|
|
113
174
|
`${server} is persistent and interactive, so it holds the terminal until the run ends, ` +
|
|
@@ -119,6 +180,20 @@ export function terminalHolders(nodes: Map<string, TaskNode>, tty: boolean): Set
|
|
|
119
180
|
return holders
|
|
120
181
|
}
|
|
121
182
|
|
|
183
|
+
/** Does `from` depend on `to`, directly or through others? */
|
|
184
|
+
function reaches(nodes: Map<string, TaskNode>, from: string, to: string): boolean {
|
|
185
|
+
const seen = new Set<string>()
|
|
186
|
+
const stack = [...nodes.get(from)!.deps]
|
|
187
|
+
while (stack.length > 0) {
|
|
188
|
+
const id = stack.pop()!
|
|
189
|
+
if (id === to) return true
|
|
190
|
+
if (seen.has(id)) continue
|
|
191
|
+
seen.add(id)
|
|
192
|
+
stack.push(...(nodes.get(id)?.deps ?? []))
|
|
193
|
+
}
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
122
197
|
export interface Placements {
|
|
123
198
|
executors: Map<string, TaskExecutor>
|
|
124
199
|
/**
|
|
@@ -131,6 +206,20 @@ export interface Placements {
|
|
|
131
206
|
remoteOnlyNoop: Set<string>
|
|
132
207
|
/** `'only'` tasks a remote executor DID take — executed remotely, outputs stay remote. */
|
|
133
208
|
remoteOnly: Set<string>
|
|
209
|
+
/** Each task placed on a `servers` executor → the persistent tasks it starts there. */
|
|
210
|
+
servers: Map<string, TaskNode[]>
|
|
211
|
+
/** Persistent tasks every dependant of which carries them: never started here. */
|
|
212
|
+
carried: Set<string>
|
|
213
|
+
/**
|
|
214
|
+
* Tasks whose `exec.platforms` leave out this machine, placed here: the
|
|
215
|
+
* run skips them and their dependants. Each → its platforms.
|
|
216
|
+
*/
|
|
217
|
+
otherPlatform: Map<string, readonly string[]>
|
|
218
|
+
/**
|
|
219
|
+
* Of those, the ones an executor that routes by platform could not take:
|
|
220
|
+
* none of its `platforms` is theirs. Each → the platforms on offer.
|
|
221
|
+
*/
|
|
222
|
+
unroutable: Map<string, readonly string[]>
|
|
134
223
|
}
|
|
135
224
|
|
|
136
225
|
/**
|
|
@@ -156,20 +245,90 @@ export function placeTasks(
|
|
|
156
245
|
nodes: Map<string, TaskNode>,
|
|
157
246
|
executors: readonly TaskExecutor[],
|
|
158
247
|
): Placements {
|
|
159
|
-
|
|
248
|
+
// Each executor's pins, computed only when one reads them.
|
|
249
|
+
const pins = new Map<string, Set<string>>()
|
|
250
|
+
const pinsOf = (
|
|
251
|
+
e: Pick<TaskExecutor, 'sandbox' | 'servers' | 'runtime' | 'exclusive'>,
|
|
252
|
+
): Set<string> => {
|
|
253
|
+
const k = `${e.sandbox === true}${e.servers === true}${e.runtime === true}${e.exclusive === true}`
|
|
254
|
+
let set = pins.get(k)
|
|
255
|
+
if (set === undefined) {
|
|
256
|
+
set = pinnedLocalSet(
|
|
257
|
+
nodes,
|
|
258
|
+
e.sandbox !== true,
|
|
259
|
+
e.servers === true,
|
|
260
|
+
e.runtime !== true,
|
|
261
|
+
e.exclusive === true,
|
|
262
|
+
)
|
|
263
|
+
pins.set(k, set)
|
|
264
|
+
}
|
|
265
|
+
return set
|
|
266
|
+
}
|
|
160
267
|
const placements: Placements = {
|
|
161
268
|
executors: new Map(),
|
|
162
269
|
remoteOnlyNoop: new Set(),
|
|
163
270
|
remoteOnly: new Set(),
|
|
271
|
+
servers: new Map(),
|
|
272
|
+
carried: new Set(),
|
|
273
|
+
otherPlatform: new Map(),
|
|
274
|
+
unroutable: new Map(),
|
|
164
275
|
}
|
|
276
|
+
const host = hostPlatform()
|
|
277
|
+
/** Runs `node` here: on this machine's platform, or skipped if it is not one of its. */
|
|
278
|
+
const here = (node: TaskNode): void => {
|
|
279
|
+
const wants: readonly string[] | undefined = node.config.exec?.platforms
|
|
280
|
+
if (wants === undefined) return
|
|
281
|
+
if (wants.includes(host)) node.platform = host
|
|
282
|
+
else placements.otherPlatform.set(node.id, wants)
|
|
283
|
+
}
|
|
284
|
+
/** Persistent tasks a dependant placed off a `servers` executor needs here. */
|
|
285
|
+
const heldHere = new Set<string>()
|
|
165
286
|
// `resolveExecutors` puts it last in every list.
|
|
166
287
|
const floor = executors.find(isLocalExecutor)!
|
|
288
|
+
// The floor alone takes every task and starts no server: nothing to pin or
|
|
289
|
+
// select. The walks below were 2.8 ms of a 1,200-task warm run.
|
|
290
|
+
if (executors.length === 1 && executors[0] === floor) {
|
|
291
|
+
for (const node of nodes.values()) {
|
|
292
|
+
delete node.platform
|
|
293
|
+
if (isGroupTask(node)) continue
|
|
294
|
+
here(node)
|
|
295
|
+
if (node.config.exec?.persistent !== undefined) continue
|
|
296
|
+
placements.executors.set(node.id, floor)
|
|
297
|
+
if (node.config.exec?.remote === 'only') placements.remoteOnlyNoop.add(node.id)
|
|
298
|
+
}
|
|
299
|
+
return placements
|
|
300
|
+
}
|
|
301
|
+
const pinned = pinsOf({})
|
|
302
|
+
const routers = executors.filter((e) => e.platforms !== undefined)
|
|
303
|
+
const sole = executors.find((e) => e.exclusive === true)
|
|
304
|
+
/** A task an `exclusive` executor leaves to this machine: the run refuses. */
|
|
305
|
+
const refuse = (id: string, why: string): never => {
|
|
306
|
+
throw new UserError(
|
|
307
|
+
`${id} would run on this machine, and ${executorLabel(sole!)} runs every task elsewhere: ${why}`,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
// A server it cannot carry first: one asked for pins its dependants, and
|
|
311
|
+
// the run named a dependant, as folding a runtime probe (2026-10-24).
|
|
312
|
+
if (sole !== undefined)
|
|
313
|
+
for (const node of nodes.values())
|
|
314
|
+
if (
|
|
315
|
+
node.config.exec?.persistent !== undefined &&
|
|
316
|
+
!(sole.servers === true && carriable(node, true))
|
|
317
|
+
)
|
|
318
|
+
refuse(node.id, 'it starts a server only beside a task that needs it')
|
|
167
319
|
for (const node of nodes.values()) {
|
|
320
|
+
delete node.platform
|
|
168
321
|
if (isGroupTask(node) || node.config.exec?.persistent !== undefined) continue
|
|
169
|
-
if (node.config.exec?.interactive === true) {
|
|
322
|
+
if (node.config.exec?.interactive === true && sole === undefined) {
|
|
170
323
|
placements.executors.set(node.id, floor)
|
|
324
|
+
here(node)
|
|
325
|
+
// Its servers run here with it: carried, none started under it.
|
|
326
|
+
for (const s of serversBehind(nodes, node)) heldHere.add(s.id)
|
|
171
327
|
continue
|
|
172
328
|
}
|
|
329
|
+
const wants: readonly string[] | undefined = node.config.exec?.platforms
|
|
330
|
+
const routes = (e: TaskExecutor): boolean =>
|
|
331
|
+
wants === undefined || e.platforms?.some((p) => wants.includes(p)) === true
|
|
173
332
|
const executor = selectExecutor(
|
|
174
333
|
executors,
|
|
175
334
|
{
|
|
@@ -181,8 +340,38 @@ export function placeTasks(
|
|
|
181
340
|
cacheable: node.config.cache !== undefined,
|
|
182
341
|
},
|
|
183
342
|
executorLabel,
|
|
343
|
+
(e) => pinsOf(e).has(node.id) || !routes(e),
|
|
184
344
|
)
|
|
345
|
+
if (sole !== undefined && executor === floor) {
|
|
346
|
+
refuse(
|
|
347
|
+
node.id,
|
|
348
|
+
pinsOf(sole).has(node.id)
|
|
349
|
+
? 'its key includes a runtime probe, which it does not check'
|
|
350
|
+
: routes(sole)
|
|
351
|
+
? 'it declined the task'
|
|
352
|
+
: `it offers ${sole.platforms?.join(', ') ?? 'no platform'}, and exec.platforms asks for ${wants!.join(', ')}`,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
185
355
|
placements.executors.set(node.id, executor)
|
|
356
|
+
if (wants !== undefined) {
|
|
357
|
+
const offered = executor.platforms
|
|
358
|
+
if (offered !== undefined && routes(executor)) {
|
|
359
|
+
node.platform =
|
|
360
|
+
wants.includes(host) && offered.includes(host)
|
|
361
|
+
? host
|
|
362
|
+
: wants.find((p) => offered.includes(p))!
|
|
363
|
+
} else {
|
|
364
|
+
here(node)
|
|
365
|
+
if (placements.otherPlatform.has(node.id) && routers.length > 0) {
|
|
366
|
+
placements.unroutable.set(node.id, [...new Set(routers.flatMap((e) => e.platforms!))])
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const servers = serversBehind(nodes, node)
|
|
371
|
+
if (servers.length > 0) {
|
|
372
|
+
if (executor.servers === true) placements.servers.set(node.id, servers)
|
|
373
|
+
else for (const s of servers) heldHere.add(s.id)
|
|
374
|
+
}
|
|
186
375
|
if (node.config.exec?.remote === 'only') {
|
|
187
376
|
// A pinned 'only' task (it transitively depends on a persistent one)
|
|
188
377
|
// lands here too: pinning wins, so it noops rather than shipping.
|
|
@@ -190,6 +379,13 @@ export function placeTasks(
|
|
|
190
379
|
else placements.remoteOnlyNoop.add(node.id)
|
|
191
380
|
}
|
|
192
381
|
}
|
|
382
|
+
for (const servers of placements.servers.values())
|
|
383
|
+
for (const s of servers) if (!heldHere.has(s.id)) placements.carried.add(s.id)
|
|
384
|
+
for (const node of nodes.values()) {
|
|
385
|
+
if (node.config.exec?.persistent === undefined || placements.carried.has(node.id)) continue
|
|
386
|
+
if (sole !== undefined) refuse(node.id, 'it starts a server only beside a task that needs it')
|
|
387
|
+
here(node)
|
|
388
|
+
}
|
|
193
389
|
return placements
|
|
194
390
|
}
|
|
195
391
|
|
|
@@ -202,16 +398,20 @@ export function placeTasks(
|
|
|
202
398
|
* inspection command and must not fail over a label. The error is still
|
|
203
399
|
* said, on the status line, in the plugin's name: the run this plan
|
|
204
400
|
* previews would refuse on it, and a plan that hid that would read as
|
|
205
|
-
* "everything lands locally".
|
|
401
|
+
* "everything lands locally". The tasks `exec.platforms` skip here ride
|
|
402
|
+
* along, and a platform refusal (CI, an executor that cannot route) throws
|
|
403
|
+
* as the run's does.
|
|
206
404
|
*/
|
|
207
405
|
export async function planExecutorOf(
|
|
208
406
|
prepared: Awaited<ReturnType<typeof prepareRun>>,
|
|
209
407
|
log: Logger,
|
|
210
408
|
policy: 'all' | 'toplevel' | 'none',
|
|
409
|
+
ci: boolean,
|
|
211
410
|
): Promise<{
|
|
212
411
|
executorOf?: (id: string) => string | undefined
|
|
213
412
|
downloadOf?: (id: string) => 'eager' | 'deferred' | 'never' | undefined
|
|
214
413
|
downloadDowngrades?: ReadonlyArray<{ taskId: string; reason: string }>
|
|
414
|
+
otherPlatform?: ReadonlyMap<string, readonly string[]>
|
|
215
415
|
}> {
|
|
216
416
|
let executors: readonly TaskExecutor[]
|
|
217
417
|
try {
|
|
@@ -220,6 +420,8 @@ export async function planExecutorOf(
|
|
|
220
420
|
cacheDir: prepared.cacheDir,
|
|
221
421
|
warn: warnOf(log),
|
|
222
422
|
concurrency: machineParallelism(),
|
|
423
|
+
artifacts: taskArtifacts(prepared.cache, prepared.workspaceRoot),
|
|
424
|
+
cacheFacts: () => cacheFacts(prepared.remotes, prepared.cacheRefs),
|
|
223
425
|
})
|
|
224
426
|
} catch (err) {
|
|
225
427
|
warnOf(log)(
|
|
@@ -228,6 +430,9 @@ export async function planExecutorOf(
|
|
|
228
430
|
return {}
|
|
229
431
|
}
|
|
230
432
|
const placements = placeTasks(prepared.nodes, executors)
|
|
433
|
+
// What the run refuses on, the plan refuses on: on CI a skip fails the run.
|
|
434
|
+
const platforms = platformVerdict(placements, ci)
|
|
435
|
+
if (platforms !== undefined && 'refuse' in platforms) throw new UserError(platforms.refuse)
|
|
231
436
|
// Download modes need placement regardless of how many executors there
|
|
232
437
|
// are (a single REMOTE one still defers); executor LABELS only earn their
|
|
233
438
|
// column when there is a choice to report.
|
|
@@ -241,6 +446,7 @@ export async function planExecutorOf(
|
|
|
241
446
|
remoteOnly: placements.remoteOnly,
|
|
242
447
|
})
|
|
243
448
|
return {
|
|
449
|
+
...(placements.otherPlatform.size > 0 ? { otherPlatform: placements.otherPlatform } : {}),
|
|
244
450
|
...(executors.length < 2 && placements.remoteOnlyNoop.size === 0
|
|
245
451
|
? {}
|
|
246
452
|
: {
|
|
@@ -305,3 +511,40 @@ export function poolOfPlacement(
|
|
|
305
511
|
return executor === undefined ? undefined : pools.get(executor)
|
|
306
512
|
}
|
|
307
513
|
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* What a run says of its `otherPlatform` tasks: a refusal when an executor
|
|
517
|
+
* that routes by platform could not take one, or the run is on CI (each
|
|
518
|
+
* task runs there or the run fails, 2026-10-23), else the warning a
|
|
519
|
+
* local run prints as it skips them and what depends on them.
|
|
520
|
+
*/
|
|
521
|
+
export function platformVerdict(
|
|
522
|
+
placements: Placements,
|
|
523
|
+
ci: boolean,
|
|
524
|
+
): { refuse: string } | { warn: string } | undefined {
|
|
525
|
+
const skipped = placements.otherPlatform
|
|
526
|
+
if (skipped.size === 0) return undefined
|
|
527
|
+
const host = hostPlatform()
|
|
528
|
+
const needs = (id: string): string => `${id} needs ${skipped.get(id)!.join(' or ')}`
|
|
529
|
+
if (placements.unroutable.size > 0) {
|
|
530
|
+
const offered = [...new Set([...placements.unroutable.values()].flat())]
|
|
531
|
+
return {
|
|
532
|
+
refuse:
|
|
533
|
+
`no executor runs ${placements.unroutable.size === 1 ? 'this task' : 'these tasks'} on a platform ${placements.unroutable.size === 1 ? 'it lists' : 'they list'} ` +
|
|
534
|
+
`(this machine is ${host}; executors run ${offered.join(', ')}):\n` +
|
|
535
|
+
[...placements.unroutable.keys()].map((id) => ` ${needs(id)}`).join('\n') +
|
|
536
|
+
`\ngive the executor a worker of each platform listed`,
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (ci) {
|
|
540
|
+
return {
|
|
541
|
+
refuse:
|
|
542
|
+
`on CI every task runs or the run fails, and ${skipped.size === 1 ? 'this task' : 'these tasks'} cannot run on ${host}:\n` +
|
|
543
|
+
[...skipped.keys()].map((id) => ` ${needs(id)}`).join('\n') +
|
|
544
|
+
`\nrun a target that holds only ${host} tasks, or one DTE run with a worker of each platform`,
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
warn: `[vx] skipped on ${host}, with what depends on them: ${[...skipped.keys()].map(needs).join('; ')}`,
|
|
549
|
+
}
|
|
550
|
+
}
|
package/src/orchestrator/plan.ts
CHANGED
|
@@ -17,8 +17,9 @@ import { ranNoCache } from '../report/index.js'
|
|
|
17
17
|
import { idleServers, isGroupTask, runGraph, type TaskNode } from '../graph/index.js'
|
|
18
18
|
import type { AffectedReason } from './affected-tasks.js'
|
|
19
19
|
import type { HistoryProvider } from '../history/index.js'
|
|
20
|
+
import { hostPlatform } from '../exec/index.js'
|
|
20
21
|
import { computeGroupKey, computeTaskHash } from './task-hash.js'
|
|
21
|
-
import { keyedOutcome, keyUpstream } from './upstream.js'
|
|
22
|
+
import { dependedOn, keyedOutcome, keylessServer, keyUpstream } from './upstream.js'
|
|
22
23
|
|
|
23
24
|
export type CacheStatus =
|
|
24
25
|
| 'hit-local' // entry exists in local cache
|
|
@@ -27,6 +28,7 @@ export type CacheStatus =
|
|
|
27
28
|
| 'no-cache' // task opts out of caching (no `cache` block) or --no-cache
|
|
28
29
|
| 'group' // no `exec`; just an aggregator
|
|
29
30
|
| 'not-started' // a server every dependant of which restores early: the run never spawns it
|
|
31
|
+
| 'skipped' // `exec.platforms` leave this machine out, or a task it depends on is skipped
|
|
30
32
|
|
|
31
33
|
export interface PlannedTask {
|
|
32
34
|
node: TaskNode
|
|
@@ -58,6 +60,10 @@ export interface PlannedTask {
|
|
|
58
60
|
remoteBytes?: number
|
|
59
61
|
/** Why `--affected` kept this requested task; absent without `--affected` and on a dependency it pulled in. */
|
|
60
62
|
affected?: AffectedReason
|
|
63
|
+
/** A `skipped` task's own platforms, and this machine's. */
|
|
64
|
+
otherPlatform?: { readonly needs: readonly string[]; readonly host: string }
|
|
65
|
+
/** A `skipped` task's root: the task off this machine's platform it depends on. */
|
|
66
|
+
blockedBy?: string
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
/**
|
|
@@ -121,10 +127,13 @@ export interface PlanArgs {
|
|
|
121
127
|
executorOf?: (id: string) => string | undefined
|
|
122
128
|
/**
|
|
123
129
|
* The tasks the run's short-circuit would restore ahead of their deps on
|
|
124
|
-
* a
|
|
125
|
-
*
|
|
130
|
+
* a hit (stable key, no workspace-output reach). A server whose every
|
|
131
|
+
* dependant is one of those and a hit, local or remote, is never spawned:
|
|
132
|
+
* the run holds it for the remote's answer (`held-servers.ts`).
|
|
126
133
|
*/
|
|
127
134
|
restorable?: ReadonlySet<string>
|
|
135
|
+
/** Tasks whose `exec.platforms` leave this machine out (`placement.ts`), each → its platforms. */
|
|
136
|
+
otherPlatform?: ReadonlyMap<string, readonly string[]>
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
/**
|
|
@@ -145,6 +154,7 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
145
154
|
// Keyed first, probed after: a remote that can batch (`hasMany`) is
|
|
146
155
|
// asked once for every key, one that cannot is asked per key.
|
|
147
156
|
const probed = new Map<string, string>()
|
|
157
|
+
const depended = dependedOn(args.nodes.values())
|
|
148
158
|
|
|
149
159
|
const outcomes = await runGraph({
|
|
150
160
|
nodes: args.nodes,
|
|
@@ -170,7 +180,7 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
170
180
|
return keyedOutcome(node, await computeGroupKey(hashArgs))
|
|
171
181
|
}
|
|
172
182
|
|
|
173
|
-
const hash = await computeTaskHash(hashArgs)
|
|
183
|
+
const hash = keylessServer(node, depended) ? undefined : await computeTaskHash(hashArgs)
|
|
174
184
|
|
|
175
185
|
// Prediction keys off READS: a no-read policy re-executes every task,
|
|
176
186
|
// so nothing is probed. Whether that run is a miss or no cache at all
|
|
@@ -181,7 +191,7 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
181
191
|
const policy = args.cachePolicy ?? FULL_CACHE_POLICY
|
|
182
192
|
const reads = policy.localRead || policy.remoteRead
|
|
183
193
|
let status: CacheStatus
|
|
184
|
-
if (ranNoCache({ node, cacheOff: cachesNothing(policy) })) {
|
|
194
|
+
if (hash === undefined || ranNoCache({ node, cacheOff: cachesNothing(policy) })) {
|
|
185
195
|
status = 'no-cache'
|
|
186
196
|
} else if (!reads) {
|
|
187
197
|
status = 'miss'
|
|
@@ -208,7 +218,10 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
208
218
|
|
|
209
219
|
if (args.restorable !== undefined) {
|
|
210
220
|
const restoreTier = new Set(
|
|
211
|
-
[...args.restorable].filter((id) =>
|
|
221
|
+
[...args.restorable].filter((id) => {
|
|
222
|
+
const status = cacheStatusById.get(id)
|
|
223
|
+
return status === 'hit-local' || status === 'hit-remote'
|
|
224
|
+
}),
|
|
212
225
|
)
|
|
213
226
|
const dependents = new Map<string, string[]>()
|
|
214
227
|
for (const node of args.nodes.values())
|
|
@@ -217,6 +230,7 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
217
230
|
cacheStatusById.set(id, 'not-started')
|
|
218
231
|
}
|
|
219
232
|
|
|
233
|
+
const skips = platformSkips(args.nodes, args.otherPlatform)
|
|
220
234
|
const tasks: PlannedTask[] = []
|
|
221
235
|
for (const id of args.nodes.keys()) {
|
|
222
236
|
const o = outcomes.get(id)
|
|
@@ -226,11 +240,12 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
226
240
|
tasks.push({
|
|
227
241
|
node,
|
|
228
242
|
hash: o.hash ?? '',
|
|
229
|
-
cacheStatus: cacheStatusById.get(id) ?? 'no-cache',
|
|
243
|
+
cacheStatus: skips.has(id) ? 'skipped' : (cacheStatusById.get(id) ?? 'no-cache'),
|
|
230
244
|
deps: node.deps,
|
|
231
245
|
...(executor !== undefined ? { executor } : {}),
|
|
232
246
|
...(args.downloadOf?.(id) === 'deferred' ? { download: 'deferred' as const } : {}),
|
|
233
247
|
...(remoteBytesById.has(id) ? { remoteBytes: remoteBytesById.get(id)! } : {}),
|
|
248
|
+
...skips.get(id),
|
|
234
249
|
})
|
|
235
250
|
}
|
|
236
251
|
|
|
@@ -258,6 +273,48 @@ export async function plan(args: PlanArgs): Promise<RunPlan> {
|
|
|
258
273
|
return { tasks, ...downgradeField(args) }
|
|
259
274
|
}
|
|
260
275
|
|
|
276
|
+
/**
|
|
277
|
+
* What the run skips here, as its scheduler does: a task its `exec.platforms`
|
|
278
|
+
* leave this machine out of, and every task that depends on one, named by
|
|
279
|
+
* the root of the block. In every `--continue` mode: a skip is no failure.
|
|
280
|
+
*/
|
|
281
|
+
function platformSkips(
|
|
282
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
283
|
+
otherPlatform: ReadonlyMap<string, readonly string[]> | undefined,
|
|
284
|
+
): Map<string, Pick<PlannedTask, 'otherPlatform' | 'blockedBy'>> {
|
|
285
|
+
const skips = new Map<string, Pick<PlannedTask, 'otherPlatform' | 'blockedBy'>>()
|
|
286
|
+
if (otherPlatform === undefined || otherPlatform.size === 0) return skips
|
|
287
|
+
const host = hostPlatform()
|
|
288
|
+
const settled = new Set<string>()
|
|
289
|
+
const visit = (id: string): void => {
|
|
290
|
+
const stack = [id]
|
|
291
|
+
while (stack.length > 0) {
|
|
292
|
+
const top = stack[stack.length - 1]!
|
|
293
|
+
if (settled.has(top)) {
|
|
294
|
+
stack.pop()
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
const node = nodes.get(top)!
|
|
298
|
+
const open = node.deps.filter((d) => !settled.has(d))
|
|
299
|
+
if (open.length > 0) {
|
|
300
|
+
stack.push(...open)
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
stack.pop()
|
|
304
|
+
settled.add(top)
|
|
305
|
+
const needs = otherPlatform.get(top)
|
|
306
|
+
if (needs !== undefined) {
|
|
307
|
+
skips.set(top, { otherPlatform: { needs, host } })
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
const via = node.deps.find((d) => skips.has(d))
|
|
311
|
+
if (via !== undefined) skips.set(top, { blockedBy: skips.get(via)!.blockedBy ?? via })
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const id of nodes.keys()) visit(id)
|
|
315
|
+
return skips
|
|
316
|
+
}
|
|
317
|
+
|
|
261
318
|
/** Carried onto the plan only when the gate actually refused something. */
|
|
262
319
|
function downgradeField(args: PlanArgs): {
|
|
263
320
|
downloadDowngrades?: ReadonlyArray<{ taskId: string; reason: string }>
|