@vzn/vx 0.0.646 → 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 +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
|
@@ -18,8 +18,15 @@ import {
|
|
|
18
18
|
type ResolvedRemote,
|
|
19
19
|
} from '../cache/index.js'
|
|
20
20
|
import { localExecutor, type TaskExecutor } from '../exec/index.js'
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import {
|
|
22
|
+
errorCode,
|
|
23
|
+
maskedLine,
|
|
24
|
+
settleWithin,
|
|
25
|
+
teardownTimeoutMs,
|
|
26
|
+
thrownMessage,
|
|
27
|
+
UserError,
|
|
28
|
+
} from '../util/index.js'
|
|
29
|
+
import { PLATFORMS, type ProjectConfig, type WorkspaceConfig } from '../config.js'
|
|
23
30
|
import { checkGraph, type GraphRules, type TaskNode } from '../graph/index.js'
|
|
24
31
|
import type {
|
|
25
32
|
CacheContext,
|
|
@@ -58,6 +65,8 @@ function isRecord(v: unknown): v is Record<string, unknown> {
|
|
|
58
65
|
/** Every method a `cache` hook's `RemoteCacheLayer` requires (`hasMany` is optional). */
|
|
59
66
|
export const REMOTE_CACHE_METHODS: readonly string[] = ['has', 'get', 'put']
|
|
60
67
|
|
|
68
|
+
const OPTIONAL_REMOTE_METHODS = ['open', 'lend', 'hasMany', 'missing'] as const
|
|
69
|
+
|
|
61
70
|
/** What a capability hook handed back must be what the seam runs. */
|
|
62
71
|
function assertShape(
|
|
63
72
|
plugin: VxPlugin,
|
|
@@ -91,10 +100,11 @@ async function safe<T>(plugin: VxPlugin, hook: string, fn: () => T | Promise<T>)
|
|
|
91
100
|
// Masked here, not by whoever prints it: an embedder of `run()` and
|
|
92
101
|
// a plugin's verb print it as they get it, and the throw quotes what the
|
|
93
102
|
// plugin was told (a remote's reply, a header) (L-11).
|
|
103
|
+
// A plugin's own code survives (cli.md § Error codes): an agent
|
|
104
|
+
// branches on it.
|
|
94
105
|
throw new UserError(
|
|
95
|
-
maskedLine(
|
|
96
|
-
|
|
97
|
-
),
|
|
106
|
+
maskedLine(`plugin '${plugin.name}' failed in ${hook}: ${thrownMessage(err)}`),
|
|
107
|
+
errorCode(err) === 'VX_E_INTERNAL' ? 'VX_E_REFUSED' : errorCode(err),
|
|
98
108
|
)
|
|
99
109
|
}
|
|
100
110
|
}
|
|
@@ -267,6 +277,14 @@ function checkNodeShapes(nodes: Map<string, TaskNode>): void {
|
|
|
267
277
|
throw new Error(`${key}'s ${field} is ${shown(fields[field])}, not a name`)
|
|
268
278
|
}
|
|
269
279
|
}
|
|
280
|
+
// A node copied under a new id kept the old taskName: the key read
|
|
281
|
+
// the id, `--exclude-dependencies` the name, and the two disagreed.
|
|
282
|
+
const id = (node as { id?: unknown }).id
|
|
283
|
+
if (id !== `${fields.projectName as string}#${fields.taskName as string}`) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`${key}'s id is ${shown(id)}, not its projectName#taskName (${fields.projectName as string}#${fields.taskName as string})`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
270
288
|
const dir = fields.projectDir
|
|
271
289
|
if (typeof dir !== 'string' || !path.isAbsolute(dir)) {
|
|
272
290
|
throw new Error(`${key}'s projectDir is ${shown(dir)}, not an absolute path`)
|
|
@@ -274,6 +292,10 @@ function checkNodeShapes(nodes: Map<string, TaskNode>): void {
|
|
|
274
292
|
}
|
|
275
293
|
}
|
|
276
294
|
|
|
295
|
+
// With `u`, a well-formed pair is one code point, so `Cs` matches only a lone half.
|
|
296
|
+
const LONE_SURROGATE = /\p{Cs}/u
|
|
297
|
+
const LONE_SURROGATES = /\p{Cs}/gu
|
|
298
|
+
|
|
277
299
|
/**
|
|
278
300
|
* `key` stage: every plugin may add `{ name: value }` material to every
|
|
279
301
|
* task. Stored on the node as sorted `[plugin/name, value]` pairs so the
|
|
@@ -313,6 +335,13 @@ export async function applyKeyHooks(
|
|
|
313
335
|
`plugin '${plugin.name}' failed in key: a name on ${node.id} holds a NUL, the fold's delimiter`,
|
|
314
336
|
)
|
|
315
337
|
}
|
|
338
|
+
// The fold hashes a part's UTF-8, which writes every lone surrogate
|
|
339
|
+
// as U+FFFD: `'\ud800'` and `'\ud801'` folded one key.
|
|
340
|
+
if (LONE_SURROGATE.test(name) || LONE_SURROGATE.test(value)) {
|
|
341
|
+
throw new UserError(
|
|
342
|
+
`plugin '${plugin.name}' failed in key: part '${name.replace(LONE_SURROGATES, '�')}' on ${node.id} holds a lone UTF-16 surrogate, which the key cannot tell from another`,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
316
345
|
parts.push([`${plugin.name}/${name}`, value])
|
|
317
346
|
}
|
|
318
347
|
}
|
|
@@ -364,18 +393,35 @@ export async function claimedAffected(
|
|
|
364
393
|
): Promise<Set<string> | undefined> {
|
|
365
394
|
const answer = await safe(plugin, 'fingerprint', () => plugin.fingerprint!.affected(change, ctx))
|
|
366
395
|
if (answer === undefined) return undefined
|
|
367
|
-
|
|
396
|
+
// A plain object is no list: `for…of` threw "is not iterable", naming
|
|
397
|
+
// neither the plugin nor the hook.
|
|
398
|
+
if (
|
|
399
|
+
typeof answer === 'string' ||
|
|
400
|
+
typeof answer !== 'object' ||
|
|
401
|
+
answer === null ||
|
|
402
|
+
typeof (answer as Partial<Iterable<unknown>>)[Symbol.iterator] !== 'function'
|
|
403
|
+
) {
|
|
368
404
|
throw new UserError(
|
|
369
405
|
`plugin '${plugin.name}' failed in fingerprint: returned ${describeValue(answer)}, not a list of project names`,
|
|
370
406
|
)
|
|
371
407
|
}
|
|
372
408
|
const names = new Set<string>()
|
|
373
|
-
|
|
409
|
+
// A name no project carries selected nothing, and the tasks it meant to
|
|
410
|
+
// select did not run, with no word said.
|
|
411
|
+
const known = new Set(ctx.projects.map((p) => p.name))
|
|
412
|
+
// A lazy list (a generator) runs the plugin's code as it is read.
|
|
413
|
+
const read = await safe(plugin, 'fingerprint', () => [...(answer as Iterable<unknown>)])
|
|
414
|
+
for (const name of read) {
|
|
374
415
|
if (typeof name !== 'string') {
|
|
375
416
|
throw new UserError(
|
|
376
417
|
`plugin '${plugin.name}' failed in fingerprint: affected project ${describeValue(name)} is not a name`,
|
|
377
418
|
)
|
|
378
419
|
}
|
|
420
|
+
if (!known.has(name)) {
|
|
421
|
+
throw new UserError(
|
|
422
|
+
`plugin '${plugin.name}' failed in fingerprint: affected project ${JSON.stringify(name)} is not a project in this workspace`,
|
|
423
|
+
)
|
|
424
|
+
}
|
|
379
425
|
names.add(name)
|
|
380
426
|
}
|
|
381
427
|
return names
|
|
@@ -386,21 +432,20 @@ export async function claimedAffected(
|
|
|
386
432
|
* (`FingerprintClaim.extraFiles`); `undefined` when it names none it can
|
|
387
433
|
* answer for, and a plugin that throws is refused by its name.
|
|
388
434
|
*/
|
|
389
|
-
export function claimedExtraFiles(
|
|
435
|
+
export async function claimedExtraFiles(
|
|
390
436
|
plugin: VxPlugin,
|
|
391
437
|
file: string,
|
|
392
438
|
bytes: Uint8Array,
|
|
393
|
-
|
|
439
|
+
ctx: FingerprintContext,
|
|
440
|
+
): Promise<readonly string[] | undefined> {
|
|
394
441
|
const read = plugin.fingerprint?.extraFiles
|
|
395
442
|
if (read === undefined) return undefined
|
|
396
443
|
let names: unknown
|
|
397
444
|
try {
|
|
398
|
-
names = read.call(plugin.fingerprint, file, bytes)
|
|
445
|
+
names = await read.call(plugin.fingerprint, file, bytes, ctx)
|
|
399
446
|
} catch (err) {
|
|
400
447
|
throw new UserError(
|
|
401
|
-
maskedLine(
|
|
402
|
-
`plugin '${plugin.name}' failed in fingerprint: ${err instanceof Error ? err.message : String(err)}`,
|
|
403
|
-
),
|
|
448
|
+
maskedLine(`plugin '${plugin.name}' failed in fingerprint: ${thrownMessage(err)}`),
|
|
404
449
|
)
|
|
405
450
|
}
|
|
406
451
|
if (!Array.isArray(names) || names.some((n) => typeof n !== 'string')) {
|
|
@@ -427,12 +472,16 @@ export interface ScheduleRun {
|
|
|
427
472
|
export async function applyScheduleHooks(
|
|
428
473
|
plugins: readonly VxPlugin[],
|
|
429
474
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
430
|
-
ctx: Omit<ScheduleHookContext, 'cache' | 'refine' | 'onRefine'>,
|
|
475
|
+
ctx: Omit<ScheduleHookContext, 'cache' | 'refine' | 'onRefine' | 'refining'>,
|
|
431
476
|
cacheFor: (plugin: string) => PluginCache = () => absentPluginCache,
|
|
432
477
|
run: ScheduleRun = {},
|
|
433
478
|
): Promise<ReadonlyMap<string, number>> {
|
|
434
479
|
const sets: (ReadonlyMap<string, number> | undefined)[] = []
|
|
435
480
|
const listeners: { plugin: VxPlugin; i: number; listener: () => void }[] = []
|
|
481
|
+
const promised = new Set<Promise<void>>()
|
|
482
|
+
const refining = async (): Promise<void> => {
|
|
483
|
+
while (promised.size > 0) await Promise.all(promised)
|
|
484
|
+
}
|
|
436
485
|
const merged = (): ReadonlyMap<string, number> => {
|
|
437
486
|
const out = new Map<string, number>()
|
|
438
487
|
for (const set of sets) if (set !== undefined) for (const [id, w] of set) out.set(id, w)
|
|
@@ -440,12 +489,27 @@ export async function applyScheduleHooks(
|
|
|
440
489
|
}
|
|
441
490
|
for (const [i, plugin] of plugins.entries()) {
|
|
442
491
|
if (plugin.schedule === undefined) continue
|
|
443
|
-
const refine = (
|
|
492
|
+
const refine = (
|
|
493
|
+
weights: ReadonlyMap<string, number> | Promise<ReadonlyMap<string, number> | undefined>,
|
|
494
|
+
): void => {
|
|
495
|
+
if (weights instanceof Promise) {
|
|
496
|
+
const p: Promise<void> = weights
|
|
497
|
+
.then(
|
|
498
|
+
(w) => {
|
|
499
|
+
if (w !== undefined) refine(w)
|
|
500
|
+
},
|
|
501
|
+
(err: unknown) =>
|
|
502
|
+
ctx.warn(`[vx] plugin '${plugin.name}' failed to refine: ${thrownMessage(err)}`),
|
|
503
|
+
)
|
|
504
|
+
.finally(() => promised.delete(p))
|
|
505
|
+
promised.add(p)
|
|
506
|
+
return
|
|
507
|
+
}
|
|
444
508
|
if (run.refined === undefined) return
|
|
445
509
|
try {
|
|
446
510
|
sets[i] = scheduleWeights(plugin, weights, nodes)
|
|
447
511
|
} catch (err) {
|
|
448
|
-
ctx.warn(`[vx] ${
|
|
512
|
+
ctx.warn(`[vx] ${thrownMessage(err)}; refine ignored`)
|
|
449
513
|
return
|
|
450
514
|
}
|
|
451
515
|
run.refined(merged())
|
|
@@ -454,16 +518,14 @@ export async function applyScheduleHooks(
|
|
|
454
518
|
try {
|
|
455
519
|
l.listener()
|
|
456
520
|
} catch (err) {
|
|
457
|
-
ctx.warn(
|
|
458
|
-
`[vx] plugin '${l.plugin.name}' failed in onRefine: ${err instanceof Error ? err.message : String(err)}`,
|
|
459
|
-
)
|
|
521
|
+
ctx.warn(`[vx] plugin '${l.plugin.name}' failed in onRefine: ${thrownMessage(err)}`)
|
|
460
522
|
}
|
|
461
523
|
}
|
|
462
524
|
}
|
|
463
525
|
const onRefine = (listener: () => void): void => {
|
|
464
526
|
if (run.refined !== undefined) listeners.push({ plugin, i, listener })
|
|
465
527
|
}
|
|
466
|
-
const own = { ...ctx, cache: cacheFor(plugin.name), refine, onRefine }
|
|
528
|
+
const own = { ...ctx, cache: cacheFor(plugin.name), refine, onRefine, refining }
|
|
467
529
|
const call = (): Promise<ReadonlyMap<string, number> | undefined> =>
|
|
468
530
|
safe(plugin, 'schedule', () => plugin.schedule!(nodes, own))
|
|
469
531
|
const weights = await (run.step === undefined
|
|
@@ -543,6 +605,11 @@ export function buildAdmission(
|
|
|
543
605
|
verdict.catch(() => {})
|
|
544
606
|
throw new Error('returned a Promise; admit is synchronous')
|
|
545
607
|
}
|
|
608
|
+
// `0` or `42` admitted beside a running task with no word: a policy
|
|
609
|
+
// answering a count enforced nothing.
|
|
610
|
+
if (verdict !== undefined && typeof verdict !== 'boolean') {
|
|
611
|
+
throw new Error(`returned ${describeValue(verdict)}, not true or false`)
|
|
612
|
+
}
|
|
546
613
|
if (verdict !== false) continue
|
|
547
614
|
if (running.size > 0) return false
|
|
548
615
|
if (!overridden.has(plugin)) {
|
|
@@ -553,7 +620,7 @@ export function buildAdmission(
|
|
|
553
620
|
}
|
|
554
621
|
} catch (err) {
|
|
555
622
|
broken.add(plugin)
|
|
556
|
-
const m =
|
|
623
|
+
const m = thrownMessage(err)
|
|
557
624
|
warn(
|
|
558
625
|
`[vx] plugin '${plugin.name}' failed in admit: ${m}; admitting every task from here on`,
|
|
559
626
|
)
|
|
@@ -578,8 +645,8 @@ export interface CacheHost extends CacheContext {
|
|
|
578
645
|
/**
|
|
579
646
|
* Collect every plugin's remote (`cache` hook) in declaration order and
|
|
580
647
|
* layer each over the local store: a lookup asks the local store first,
|
|
581
|
-
* then each remote in order (
|
|
582
|
-
* remote. The run's policy alone gates each remote (
|
|
648
|
+
* then each remote in order (2026-10-13), and a save reaches every
|
|
649
|
+
* remote. The run's policy alone gates each remote (2026-10-16: a
|
|
583
650
|
* plugin moves bytes, it never decides reads or writes); a remote's throw is a miss and a warning naming its plugin. No
|
|
584
651
|
* remote → the local store alone.
|
|
585
652
|
*/
|
|
@@ -596,6 +663,17 @@ export async function resolveCache(
|
|
|
596
663
|
const remote = await safe(plugin, 'cache', () => plugin.cache!(ctx))
|
|
597
664
|
if (remote === undefined) continue
|
|
598
665
|
assertShape(plugin, 'cache', remote, REMOTE_CACHE_METHODS, 'a remote cache layer')
|
|
666
|
+
// `hasMany: true` failed every batch probe, and `missing: true` every
|
|
667
|
+
// upload, each said per call as the remote's own fault.
|
|
668
|
+
const odd = OPTIONAL_REMOTE_METHODS.filter(
|
|
669
|
+
(m) => remote[m] !== undefined && typeof remote[m] !== 'function',
|
|
670
|
+
)
|
|
671
|
+
if (odd.length > 0) {
|
|
672
|
+
throw new UserError(
|
|
673
|
+
`plugin '${plugin.name}' returned from cache something that is not a remote cache layer: ` +
|
|
674
|
+
`${odd.join(', ')} ${odd.length === 1 ? 'not a function' : 'not functions'}`,
|
|
675
|
+
)
|
|
676
|
+
}
|
|
599
677
|
const entry: ResolvedRemote = {
|
|
600
678
|
remote,
|
|
601
679
|
plugin: plugin.name,
|
|
@@ -626,9 +704,7 @@ const executorPlugin = new WeakMap<TaskExecutor, string>()
|
|
|
626
704
|
*/
|
|
627
705
|
export function nameExecutorFailure(executor: TaskExecutor, err: unknown): unknown {
|
|
628
706
|
if (!executorPlugin.has(executor)) return err
|
|
629
|
-
const named = new UserError(
|
|
630
|
-
`${executorLabel(executor)} failed in execute: ${err instanceof Error ? err.message : String(err)}`,
|
|
631
|
-
)
|
|
707
|
+
const named = new UserError(`${executorLabel(executor)} failed in execute: ${thrownMessage(err)}`)
|
|
632
708
|
named.cause = err
|
|
633
709
|
return named
|
|
634
710
|
}
|
|
@@ -641,23 +717,47 @@ export function executorLabel(executor: TaskExecutor): string {
|
|
|
641
717
|
: `plugin '${plugin}' (executor '${executor.name}')`
|
|
642
718
|
}
|
|
643
719
|
|
|
720
|
+
/** What `executorDemand` returns: the run's three calls into it. */
|
|
721
|
+
export interface ExecutorDemand {
|
|
722
|
+
/** Whether any executor asked: with none, every call does nothing. */
|
|
723
|
+
readonly any: boolean
|
|
724
|
+
/** A task finished: dropped, and its executor told once demand settled. */
|
|
725
|
+
finished(id: string): void
|
|
726
|
+
/**
|
|
727
|
+
* The placed tasks the run's cache answers: dropped, and each executor
|
|
728
|
+
* told its demand for the first time. Before it a finish narrows
|
|
729
|
+
* silently, so an executor never sizes for what restores. The first
|
|
730
|
+
* call counts; the rest do nothing.
|
|
731
|
+
*/
|
|
732
|
+
settle(hits: Iterable<string>, missed?: Iterable<string>): void
|
|
733
|
+
/** A task about to execute: back in if `settle` dropped it (its artifact was gone). */
|
|
734
|
+
executing(id: string): void
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const NO_DEMAND: ExecutorDemand = {
|
|
738
|
+
any: false,
|
|
739
|
+
finished: () => {},
|
|
740
|
+
settle: () => {},
|
|
741
|
+
executing: () => {},
|
|
742
|
+
}
|
|
743
|
+
|
|
644
744
|
/**
|
|
645
745
|
* Demand: what each executor still has left to run. An executor that
|
|
646
746
|
* provisions per task (a container, an allocation, a pod) otherwise has to
|
|
647
747
|
* guess from `capacity` alone and hold everything until teardown. Built
|
|
648
748
|
* ONLY for executors that asked: with none declared there is no map, no
|
|
649
|
-
* set, and
|
|
749
|
+
* set, and every call does nothing.
|
|
650
750
|
*
|
|
651
751
|
* The SAME set is handed back every time, narrowed in place: a consumer
|
|
652
752
|
* reading it later sees the truth rather than a stale snapshot. A hint,
|
|
653
753
|
* as `admit` is: one that throws is named once and asked no more, where
|
|
654
754
|
* it surfaced as a bare stack, or mid-run from the completion path
|
|
655
|
-
* (item 1022).
|
|
755
|
+
* (item 1022).
|
|
656
756
|
*/
|
|
657
757
|
export function executorDemand(
|
|
658
758
|
placed: ReadonlyMap<string, TaskExecutor>,
|
|
659
759
|
warn: (message: string) => void,
|
|
660
|
-
):
|
|
760
|
+
): ExecutorDemand {
|
|
661
761
|
const demandOf = new Map<TaskExecutor, Set<string>>()
|
|
662
762
|
for (const [id, executor] of placed) {
|
|
663
763
|
if (executor.demand === undefined) continue
|
|
@@ -665,31 +765,72 @@ export function executorDemand(
|
|
|
665
765
|
if (remaining === undefined) demandOf.set(executor, (remaining = new Set()))
|
|
666
766
|
remaining.add(id)
|
|
667
767
|
}
|
|
668
|
-
if (demandOf.size === 0) return
|
|
768
|
+
if (demandOf.size === 0) return NO_DEMAND
|
|
769
|
+
// Of the placed tasks, those the cache was asked and holds nowhere.
|
|
770
|
+
const missed = new Set<string>()
|
|
669
771
|
const tell = (executor: TaskExecutor, remaining: ReadonlySet<string>): void => {
|
|
670
772
|
const failed = (err: unknown): void => {
|
|
671
773
|
if (!demandOf.delete(executor)) return
|
|
672
|
-
const m =
|
|
774
|
+
const m = thrownMessage(err)
|
|
673
775
|
warn(`[vx] ${executorLabel(executor)} failed in demand: ${m}; not asked again this run`)
|
|
674
776
|
}
|
|
675
777
|
try {
|
|
676
778
|
// An `async demand` that rejects did so where no one listened: an
|
|
677
779
|
// unhandled rejection, a stack, the run killed. Its rejection is a
|
|
678
780
|
// throw like any other.
|
|
679
|
-
const ret: unknown = executor.demand!(remaining)
|
|
781
|
+
const ret: unknown = executor.demand!(remaining, missed)
|
|
680
782
|
if (ret instanceof Promise) ret.catch(failed)
|
|
681
783
|
} catch (err) {
|
|
682
784
|
failed(err)
|
|
683
785
|
}
|
|
684
786
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
787
|
+
let settled = false
|
|
788
|
+
const done = new Set<string>()
|
|
789
|
+
const ran = new Set<string>()
|
|
790
|
+
const dropped = new Set<string>()
|
|
791
|
+
return {
|
|
792
|
+
any: true,
|
|
793
|
+
finished(id) {
|
|
794
|
+
done.add(id)
|
|
795
|
+
missed.delete(id)
|
|
796
|
+
for (const [executor, remaining] of demandOf) {
|
|
797
|
+
if (remaining.delete(id) && settled) tell(executor, remaining)
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
settle(hits, misses = []) {
|
|
801
|
+
if (settled) return
|
|
802
|
+
settled = true
|
|
803
|
+
for (const id of misses) if (placed.has(id) && !done.has(id)) missed.add(id)
|
|
804
|
+
for (const id of hits) {
|
|
805
|
+
if (ran.has(id) || done.has(id)) continue
|
|
806
|
+
const executor = placed.get(id)
|
|
807
|
+
if (executor !== undefined && demandOf.get(executor)?.delete(id)) dropped.add(id)
|
|
808
|
+
}
|
|
809
|
+
for (const [executor, remaining] of demandOf) tell(executor, remaining)
|
|
810
|
+
},
|
|
811
|
+
executing(id) {
|
|
812
|
+
ran.add(id)
|
|
813
|
+
if (!dropped.delete(id)) return
|
|
814
|
+
const executor = placed.get(id)!
|
|
815
|
+
const remaining = demandOf.get(executor)
|
|
816
|
+
if (remaining === undefined) return
|
|
817
|
+
remaining.add(id)
|
|
818
|
+
tell(executor, remaining)
|
|
819
|
+
},
|
|
690
820
|
}
|
|
691
821
|
}
|
|
692
822
|
|
|
823
|
+
const EXECUTOR_FLAGS = ['remote', 'sandbox', 'servers', 'runtime', 'exclusive', 'timeouts'] as const
|
|
824
|
+
const EXECUTOR_HOOKS = [
|
|
825
|
+
'accepts',
|
|
826
|
+
'demand',
|
|
827
|
+
'footer',
|
|
828
|
+
'machines',
|
|
829
|
+
'idealMs',
|
|
830
|
+
'profile',
|
|
831
|
+
'running',
|
|
832
|
+
] as const
|
|
833
|
+
|
|
693
834
|
/**
|
|
694
835
|
* Collect every plugin's `executor`, in declaration order, with core's
|
|
695
836
|
* own appended at the tail: per task, `selectExecutor` takes the first
|
|
@@ -716,6 +857,35 @@ export async function resolveExecutors(
|
|
|
716
857
|
`plugin '${plugin.name}' returned executor '${executor.name}' with capacity ${typeof capacity === 'number' ? String(capacity) : JSON.stringify(capacity)}: it must be a positive integer`,
|
|
717
858
|
)
|
|
718
859
|
}
|
|
860
|
+
// Read as `=== true`: `remote: 'yes'` ran a remote executor as a local one.
|
|
861
|
+
for (const flag of EXECUTOR_FLAGS) {
|
|
862
|
+
const v: unknown = executor[flag]
|
|
863
|
+
if (v !== undefined && typeof v !== 'boolean') {
|
|
864
|
+
throw new UserError(
|
|
865
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with ${flag} ${typeof v === 'string' ? JSON.stringify(v) : typeof v === 'object' && v !== null ? describeValue(v) : String(v as number)}: it must be true or false`,
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const platforms: unknown = executor.platforms
|
|
870
|
+
if (
|
|
871
|
+
platforms !== undefined &&
|
|
872
|
+
!(
|
|
873
|
+
Array.isArray(platforms) &&
|
|
874
|
+
platforms.length > 0 &&
|
|
875
|
+
platforms.every((p) => (PLATFORMS as readonly unknown[]).includes(p))
|
|
876
|
+
)
|
|
877
|
+
) {
|
|
878
|
+
throw new UserError(
|
|
879
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with platforms ${JSON.stringify(platforms)}: it must be a non-empty array of ${PLATFORMS.join(', ')}`,
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
for (const hook of EXECUTOR_HOOKS) {
|
|
883
|
+
const v: unknown = executor[hook]
|
|
884
|
+
if (v !== undefined && typeof v !== 'function')
|
|
885
|
+
throw new UserError(
|
|
886
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with ${hook} that is not a function`,
|
|
887
|
+
)
|
|
888
|
+
}
|
|
719
889
|
executorPlugin.set(executor, plugin.name)
|
|
720
890
|
executors.push(executor)
|
|
721
891
|
}
|
|
@@ -728,7 +898,10 @@ export async function resolveExecutors(
|
|
|
728
898
|
|
|
729
899
|
/**
|
|
730
900
|
* End-of-run plugin lifecycle: each plugin's optional `teardown()`, in
|
|
731
|
-
* declaration order
|
|
901
|
+
* declaration order, a plugin that supplies a remote cache after every
|
|
902
|
+
* other: a teardown may write through the cache (`RunReport`), and one
|
|
903
|
+
* that revoked its token first refused the write (main 7bdbf01: the
|
|
904
|
+
* schedule's timings, put after the Vercel token's revoke). Crash-isolated — a throwing teardown is logged and
|
|
732
905
|
* skipped, never propagated — and each call is time-bounded by
|
|
733
906
|
* {@link teardownTimeoutMs}. Runs on every exit of a run once its plugins'
|
|
734
907
|
* factories ran, and of a plan (item 1021); the finally-path disposers
|
|
@@ -749,7 +922,8 @@ export async function teardownPlugins(
|
|
|
749
922
|
report?: RunReport,
|
|
750
923
|
step: <T>(id: string, work: () => Promise<T>) => Promise<T> = (_id, work) => work(),
|
|
751
924
|
): Promise<void> {
|
|
752
|
-
|
|
925
|
+
const last = (p: VxPlugin): number => (p.cache === undefined ? 0 : 1)
|
|
926
|
+
for (const plugin of plugins.toSorted((a, b) => last(a) - last(b))) {
|
|
753
927
|
if (plugin.teardown === undefined) continue
|
|
754
928
|
const ms = teardownTimeoutMs()
|
|
755
929
|
try {
|
|
@@ -763,9 +937,7 @@ export async function teardownPlugins(
|
|
|
763
937
|
warn(`[vx] plugin '${plugin.name}' teardown timed out after ${ms}ms; it did not complete`)
|
|
764
938
|
}
|
|
765
939
|
} catch (err) {
|
|
766
|
-
warn(
|
|
767
|
-
`[vx] plugin '${plugin.name}' failed in teardown: ${err instanceof Error ? err.message : String(err)}`,
|
|
768
|
-
)
|
|
940
|
+
warn(`[vx] plugin '${plugin.name}' failed in teardown: ${thrownMessage(err)}`)
|
|
769
941
|
}
|
|
770
942
|
}
|
|
771
943
|
}
|