@vzn/vx 0.0.646 → 0.0.649
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 +29 -22
- 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 +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -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/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- 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 +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- 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 +270 -60
- 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 +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- 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 +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- 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 +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- 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 +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- 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 +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- 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 +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- 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 +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- 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 +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -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 +61 -3
- 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 +17 -11
- 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 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
|
@@ -5,10 +5,11 @@
|
|
|
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'
|
|
@@ -34,13 +35,16 @@ export function pinnedLocalSet(
|
|
|
34
35
|
servers = false,
|
|
35
36
|
/** `false` for a `runtime` executor: it checks the probes where it runs the task. */
|
|
36
37
|
runtime = true,
|
|
38
|
+
/** For an `exclusive` executor: it takes what pins elsewhere, servers included. */
|
|
39
|
+
exclusive = false,
|
|
37
40
|
): Set<string> {
|
|
38
41
|
const pinned = new Set<string>()
|
|
39
42
|
for (const node of nodes.values()) {
|
|
40
43
|
if (
|
|
41
|
-
(node.config.exec?.persistent !== undefined && !(servers && carriable(node))) ||
|
|
42
|
-
(
|
|
43
|
-
|
|
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))
|
|
44
48
|
) {
|
|
45
49
|
pinned.add(node.id)
|
|
46
50
|
}
|
|
@@ -71,17 +75,17 @@ export function pinnedLocalSet(
|
|
|
71
75
|
/**
|
|
72
76
|
* A persistent task a `servers` executor can start where it runs a
|
|
73
77
|
* dependant: one the run holds for its dependants only, with no sandbox
|
|
74
|
-
* (this machine's machinery) and no `exec.remote: false
|
|
78
|
+
* (this machine's machinery) and no `exec.remote: false`, which an
|
|
79
|
+
* `exclusive` executor takes all the same.
|
|
75
80
|
*/
|
|
76
|
-
function carriable(node: TaskNode): boolean {
|
|
81
|
+
function carriable(node: TaskNode, exclusive = false): boolean {
|
|
77
82
|
const exec = node.config.exec
|
|
78
83
|
return (
|
|
79
84
|
exec?.persistent !== undefined &&
|
|
80
85
|
!node.requested &&
|
|
81
86
|
node.surfaced !== true &&
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
exec.interactive !== true
|
|
87
|
+
(exclusive ||
|
|
88
|
+
(exec.sandbox === undefined && exec.remote !== false && exec.interactive !== true))
|
|
85
89
|
)
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -154,6 +158,17 @@ export function terminalHolders(nodes: Map<string, TaskNode>, tty: boolean): Set
|
|
|
154
158
|
stack.push(...nodes.get(id)!.deps)
|
|
155
159
|
}
|
|
156
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
|
+
}
|
|
157
172
|
if (clash.length > 0) {
|
|
158
173
|
throw new UserError(
|
|
159
174
|
`${server} is persistent and interactive, so it holds the terminal until the run ends, ` +
|
|
@@ -165,6 +180,20 @@ export function terminalHolders(nodes: Map<string, TaskNode>, tty: boolean): Set
|
|
|
165
180
|
return holders
|
|
166
181
|
}
|
|
167
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
|
+
|
|
168
197
|
export interface Placements {
|
|
169
198
|
executors: Map<string, TaskExecutor>
|
|
170
199
|
/**
|
|
@@ -181,6 +210,16 @@ export interface Placements {
|
|
|
181
210
|
servers: Map<string, TaskNode[]>
|
|
182
211
|
/** Persistent tasks every dependant of which carries them: never started here. */
|
|
183
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[]>
|
|
184
223
|
}
|
|
185
224
|
|
|
186
225
|
/**
|
|
@@ -208,33 +247,88 @@ export function placeTasks(
|
|
|
208
247
|
): Placements {
|
|
209
248
|
// Each executor's pins, computed only when one reads them.
|
|
210
249
|
const pins = new Map<string, Set<string>>()
|
|
211
|
-
const pinsOf = (
|
|
212
|
-
|
|
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}`
|
|
213
254
|
let set = pins.get(k)
|
|
214
255
|
if (set === undefined) {
|
|
215
|
-
set = pinnedLocalSet(
|
|
256
|
+
set = pinnedLocalSet(
|
|
257
|
+
nodes,
|
|
258
|
+
e.sandbox !== true,
|
|
259
|
+
e.servers === true,
|
|
260
|
+
e.runtime !== true,
|
|
261
|
+
e.exclusive === true,
|
|
262
|
+
)
|
|
216
263
|
pins.set(k, set)
|
|
217
264
|
}
|
|
218
265
|
return set
|
|
219
266
|
}
|
|
220
|
-
const pinned = pinsOf({})
|
|
221
267
|
const placements: Placements = {
|
|
222
268
|
executors: new Map(),
|
|
223
269
|
remoteOnlyNoop: new Set(),
|
|
224
270
|
remoteOnly: new Set(),
|
|
225
271
|
servers: new Map(),
|
|
226
272
|
carried: new Set(),
|
|
273
|
+
otherPlatform: new Map(),
|
|
274
|
+
unroutable: new Map(),
|
|
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)
|
|
227
283
|
}
|
|
228
284
|
/** Persistent tasks a dependant placed off a `servers` executor needs here. */
|
|
229
285
|
const heldHere = new Set<string>()
|
|
230
286
|
// `resolveExecutors` puts it last in every list.
|
|
231
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')
|
|
232
319
|
for (const node of nodes.values()) {
|
|
320
|
+
delete node.platform
|
|
233
321
|
if (isGroupTask(node) || node.config.exec?.persistent !== undefined) continue
|
|
234
|
-
if (node.config.exec?.interactive === true) {
|
|
322
|
+
if (node.config.exec?.interactive === true && sole === undefined) {
|
|
235
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)
|
|
236
327
|
continue
|
|
237
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
|
|
238
332
|
const executor = selectExecutor(
|
|
239
333
|
executors,
|
|
240
334
|
{
|
|
@@ -246,9 +340,33 @@ export function placeTasks(
|
|
|
246
340
|
cacheable: node.config.cache !== undefined,
|
|
247
341
|
},
|
|
248
342
|
executorLabel,
|
|
249
|
-
(e) => pinsOf(e).has(node.id),
|
|
343
|
+
(e) => pinsOf(e).has(node.id) || !routes(e),
|
|
250
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
|
+
}
|
|
251
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
|
+
}
|
|
252
370
|
const servers = serversBehind(nodes, node)
|
|
253
371
|
if (servers.length > 0) {
|
|
254
372
|
if (executor.servers === true) placements.servers.set(node.id, servers)
|
|
@@ -263,6 +381,11 @@ export function placeTasks(
|
|
|
263
381
|
}
|
|
264
382
|
for (const servers of placements.servers.values())
|
|
265
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
|
+
}
|
|
266
389
|
return placements
|
|
267
390
|
}
|
|
268
391
|
|
|
@@ -275,16 +398,20 @@ export function placeTasks(
|
|
|
275
398
|
* inspection command and must not fail over a label. The error is still
|
|
276
399
|
* said, on the status line, in the plugin's name: the run this plan
|
|
277
400
|
* previews would refuse on it, and a plan that hid that would read as
|
|
278
|
-
* "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.
|
|
279
404
|
*/
|
|
280
405
|
export async function planExecutorOf(
|
|
281
406
|
prepared: Awaited<ReturnType<typeof prepareRun>>,
|
|
282
407
|
log: Logger,
|
|
283
408
|
policy: 'all' | 'toplevel' | 'none',
|
|
409
|
+
ci: boolean,
|
|
284
410
|
): Promise<{
|
|
285
411
|
executorOf?: (id: string) => string | undefined
|
|
286
412
|
downloadOf?: (id: string) => 'eager' | 'deferred' | 'never' | undefined
|
|
287
413
|
downloadDowngrades?: ReadonlyArray<{ taskId: string; reason: string }>
|
|
414
|
+
otherPlatform?: ReadonlyMap<string, readonly string[]>
|
|
288
415
|
}> {
|
|
289
416
|
let executors: readonly TaskExecutor[]
|
|
290
417
|
try {
|
|
@@ -303,6 +430,9 @@ export async function planExecutorOf(
|
|
|
303
430
|
return {}
|
|
304
431
|
}
|
|
305
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)
|
|
306
436
|
// Download modes need placement regardless of how many executors there
|
|
307
437
|
// are (a single REMOTE one still defers); executor LABELS only earn their
|
|
308
438
|
// column when there is a choice to report.
|
|
@@ -316,6 +446,7 @@ export async function planExecutorOf(
|
|
|
316
446
|
remoteOnly: placements.remoteOnly,
|
|
317
447
|
})
|
|
318
448
|
return {
|
|
449
|
+
...(placements.otherPlatform.size > 0 ? { otherPlatform: placements.otherPlatform } : {}),
|
|
319
450
|
...(executors.length < 2 && placements.remoteOnlyNoop.size === 0
|
|
320
451
|
? {}
|
|
321
452
|
: {
|
|
@@ -380,3 +511,40 @@ export function poolOfPlacement(
|
|
|
380
511
|
return executor === undefined ? undefined : pools.get(executor)
|
|
381
512
|
}
|
|
382
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 }>
|