@vzn/vx 0.0.650 → 0.0.652
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 +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/index.ts
CHANGED
|
@@ -41,9 +41,10 @@ export type {
|
|
|
41
41
|
TaskConfig,
|
|
42
42
|
ExecConfig,
|
|
43
43
|
ExecEnv,
|
|
44
|
-
CacheConfig,
|
|
45
44
|
InputsConfig,
|
|
46
|
-
|
|
45
|
+
TaskOutputs,
|
|
46
|
+
WorkspaceInputs,
|
|
47
|
+
WorkspaceOutputs,
|
|
47
48
|
InputStability,
|
|
48
49
|
SandboxConfig,
|
|
49
50
|
SandboxGrants,
|
|
@@ -145,7 +146,7 @@ export {
|
|
|
145
146
|
taskNameProblem,
|
|
146
147
|
} from './workspace/index.js'
|
|
147
148
|
// Output components: a plugin draws its lines with core's, so they look alike.
|
|
148
|
-
export { ui } from './
|
|
149
|
+
export { ui } from './ui/index.js'
|
|
149
150
|
export type {
|
|
150
151
|
ApplyMigrationArgs,
|
|
151
152
|
MigrationFormat,
|
|
@@ -186,10 +187,12 @@ export type {
|
|
|
186
187
|
CacheFacts,
|
|
187
188
|
TaskArtifacts,
|
|
188
189
|
FingerprintChange,
|
|
190
|
+
FingerprintDigests,
|
|
189
191
|
FingerprintClaim,
|
|
190
192
|
FingerprintContext,
|
|
191
193
|
GraphHookContext,
|
|
192
194
|
KeyHookContext,
|
|
195
|
+
LockedFile,
|
|
193
196
|
PluginCommand,
|
|
194
197
|
ProjectHookContext,
|
|
195
198
|
ScheduleHookContext,
|
|
@@ -243,6 +246,7 @@ export type {
|
|
|
243
246
|
ServerSpec,
|
|
244
247
|
TaskExecutor,
|
|
245
248
|
TaskPlacement,
|
|
249
|
+
WorkerAdvice,
|
|
246
250
|
} from './exec/index.js'
|
|
247
251
|
|
|
248
252
|
// Telemetry — THE canonical, versioned data-export contract every exporter
|
|
@@ -186,7 +186,7 @@ export function admitTasks(
|
|
|
186
186
|
const cacheable =
|
|
187
187
|
!isGroupTask(node) &&
|
|
188
188
|
node.config.exec?.persistent === undefined &&
|
|
189
|
-
node.config.cache
|
|
189
|
+
node.config.cache === true &&
|
|
190
190
|
canRead &&
|
|
191
191
|
canWrite
|
|
192
192
|
// A restore-tier task (confirmed local hit, may run before its deps)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// When each command of the run ran and where it may write, so
|
|
2
|
-
// `
|
|
2
|
+
// `inputs.stability.strategy: 'mutable'` can tell a task's own identical
|
|
3
3
|
// rewrite from another task's write and restore (STATUS item 28): a file
|
|
4
4
|
// whose bytes match the key after a write is the task's own only if no
|
|
5
5
|
// other command that may write it ran between the describe and that write.
|
|
@@ -115,7 +115,7 @@ export class DeferredOutputs {
|
|
|
115
115
|
// Project outputs only: a task declaring `outputs.workspaceFiles` is
|
|
116
116
|
// never deferred (`deferralEligibility` forces it eager), so nothing
|
|
117
117
|
// workspace-anchored can reach this path.
|
|
118
|
-
const outputs = producer.config.
|
|
118
|
+
const outputs = producer.config.outputs?.files ?? []
|
|
119
119
|
const nestedProjectDirs = this.args.nestedDirsByProject.get(producer.projectName) ?? []
|
|
120
120
|
const cleanArgs = {
|
|
121
121
|
projectDir: producer.projectDir,
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
machineMemoryBytes,
|
|
20
20
|
machineParallelism,
|
|
21
21
|
treeBytes,
|
|
22
|
-
ui,
|
|
23
22
|
} from '../util/index.js'
|
|
23
|
+
import * as ui from '../ui/index.js'
|
|
24
24
|
import { vxPath } from '../handoff.js'
|
|
25
25
|
import { VERSION } from '../version.js'
|
|
26
26
|
import { probeSandbox, resetSandbox, untracedReason } from '../exec/index.js'
|
|
@@ -44,14 +44,12 @@ export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, s
|
|
|
44
44
|
if (list) list.push({ taskId: n.id, prefixes, via })
|
|
45
45
|
else readersByProject.set(n.projectName, [{ taskId: n.id, prefixes, via }])
|
|
46
46
|
}
|
|
47
|
-
// A task
|
|
47
|
+
// A task without `cache: true` keys on its inputs, and a cached task that
|
|
48
48
|
// depends on it folds that key: deferring a producer there moved both
|
|
49
49
|
// keys with the transfer flag. Only such a task is a reader — one nothing
|
|
50
50
|
// cached folds keys nothing that lasts.
|
|
51
51
|
const folded = new Set<string>()
|
|
52
|
-
const stack = [...nodes.values()]
|
|
53
|
-
.filter((n) => n.config.cache !== undefined)
|
|
54
|
-
.flatMap((n) => n.deps)
|
|
52
|
+
const stack = [...nodes.values()].filter((n) => n.config.cache === true).flatMap((n) => n.deps)
|
|
55
53
|
for (let id = stack.pop(); id !== undefined; id = stack.pop()) {
|
|
56
54
|
if (folded.has(id)) continue
|
|
57
55
|
folded.add(id)
|
|
@@ -63,7 +61,7 @@ export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, s
|
|
|
63
61
|
const inputs = n.config.inputs
|
|
64
62
|
// A task with no inputs has no key to read; an uncached one with inputs
|
|
65
63
|
// is read only for the cached tasks that fold its key.
|
|
66
|
-
if (inputs === undefined || (n.config.cache
|
|
64
|
+
if (inputs === undefined || (n.config.cache !== true && !folded.has(n.id))) continue
|
|
67
65
|
if ((inputs?.workspaceFiles?.length ?? 0) > 0) workspaceReader ??= n.id
|
|
68
66
|
// A `runtime` input is a SHELL COMMAND whose reads are unknowable —
|
|
69
67
|
// the same reason vx refuses to infer inputs by tracing. It can `cat` a
|
|
@@ -86,11 +84,11 @@ export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, s
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
for (const n of nodes.values()) {
|
|
89
|
-
const outputs = n.config.
|
|
87
|
+
const outputs = n.config.outputs
|
|
90
88
|
if ((outputs?.workspaceFiles?.length ?? 0) > 0) {
|
|
91
89
|
ineligible.set(
|
|
92
90
|
n.id,
|
|
93
|
-
'declares
|
|
91
|
+
'declares outputs.workspaceFiles — root-anchored outputs can land where any project-relative input reads',
|
|
94
92
|
)
|
|
95
93
|
continue
|
|
96
94
|
}
|
|
@@ -117,7 +117,7 @@ export function excludedTaint(nodes: ReadonlyMap<string, TaskNode>): {
|
|
|
117
117
|
let unsaved = 0
|
|
118
118
|
for (const id of reached) {
|
|
119
119
|
const cfg = nodes.get(id)!.config
|
|
120
|
-
if (cfg.cache
|
|
120
|
+
if (cfg.cache === true && cfg.exec?.persistent === undefined) unsaved++
|
|
121
121
|
}
|
|
122
122
|
return { seeds, unsaved }
|
|
123
123
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import type { ExecConfig, TaskConfig,
|
|
2
|
+
import type { ExecConfig, TaskConfig, InputsConfig, InputStability } from '../config.js'
|
|
3
3
|
import {
|
|
4
4
|
ArtifactVanishedError,
|
|
5
5
|
type CacheEntry,
|
|
@@ -58,6 +58,7 @@ import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
|
58
58
|
import type { CommandWindows } from './command-windows.js'
|
|
59
59
|
import {
|
|
60
60
|
commandWriteReach,
|
|
61
|
+
dependencyReads,
|
|
61
62
|
mayWriteFingerprint,
|
|
62
63
|
type Placeholder,
|
|
63
64
|
reachedWithheld,
|
|
@@ -72,6 +73,7 @@ import {
|
|
|
72
73
|
bracketLiteralHint,
|
|
73
74
|
markUnsaved,
|
|
74
75
|
saveMiss,
|
|
76
|
+
unsavedOutputBytes,
|
|
75
77
|
type OutputDirSnapshot,
|
|
76
78
|
type SaveFacts,
|
|
77
79
|
} from './miss-save.js'
|
|
@@ -182,17 +184,23 @@ export interface ExecuteArgs {
|
|
|
182
184
|
* byte-identical with and without it.
|
|
183
185
|
*/
|
|
184
186
|
retries?: number
|
|
187
|
+
/**
|
|
188
|
+
* Whether the run history knows the task as flaky (`knownFlaky`): a keyed
|
|
189
|
+
* task with no `retries` of its own or of the run that fails gets ONE more
|
|
190
|
+
* attempt when it does. Decided at run time, so never in the key.
|
|
191
|
+
*/
|
|
192
|
+
knownFlaky?: (node: TaskNode, hash: string) => boolean
|
|
185
193
|
/**
|
|
186
194
|
* `run.timeout` (ms), already resolved from flag, variable and
|
|
187
195
|
* workspace. Per-task `exec.timeout` wins. Never hashed.
|
|
188
196
|
*/
|
|
189
197
|
timeout?: number
|
|
190
198
|
/**
|
|
191
|
-
* The run's `
|
|
199
|
+
* The run's `inputs.stability` layers around the task's own: `run` from
|
|
192
200
|
* the env vars (above the task), `workspace` from `.vx/workspace` (below).
|
|
193
201
|
*/
|
|
194
202
|
inputStability?: { run: InputStability; workspace?: InputStability }
|
|
195
|
-
/** `
|
|
203
|
+
/** `outputs.clean` for tasks without their own: `.vx/workspace`. */
|
|
196
204
|
cleanOutputs?: boolean
|
|
197
205
|
/** Per-run memo for `git ls-files` (one entry per project dir). */
|
|
198
206
|
gitFilesCache?: GitFilesCache
|
|
@@ -235,6 +243,11 @@ export interface ExecuteArgs {
|
|
|
235
243
|
* granted a linked workspace package only when it is in this set.
|
|
236
244
|
*/
|
|
237
245
|
keyedProjects: (node: TaskNode) => ReadonlySet<string>
|
|
246
|
+
/**
|
|
247
|
+
* The run's lookup of the tasks whose outputs a sandboxed task may read
|
|
248
|
+
* (keyed-projects.ts `readableDeps`). Absent: none.
|
|
249
|
+
*/
|
|
250
|
+
readableDeps?: (node: TaskNode) => readonly TaskNode[]
|
|
238
251
|
/**
|
|
239
252
|
* Run-scoped list the miss path appends its output-directory snapshot
|
|
240
253
|
* request to, taken at run end (run.ts) instead of right after the save:
|
|
@@ -344,7 +357,7 @@ function readsCache(args: ExecuteArgs): boolean {
|
|
|
344
357
|
const policy = args.cachePolicy ?? FULL_CACHE_POLICY
|
|
345
358
|
return (
|
|
346
359
|
args.remoteOnly !== true &&
|
|
347
|
-
args.node.config.cache
|
|
360
|
+
args.node.config.cache === true &&
|
|
348
361
|
(policy.localRead || policy.remoteRead)
|
|
349
362
|
)
|
|
350
363
|
}
|
|
@@ -396,16 +409,16 @@ async function restoreOrMiss(
|
|
|
396
409
|
|
|
397
410
|
/**
|
|
398
411
|
* What a write to an input during the command means, each field highest
|
|
399
|
-
* first: the env var (`layers.run`), the task's `
|
|
412
|
+
* first: the env var (`layers.run`), the task's `inputs.stability`,
|
|
400
413
|
* the workspace's, then `'immutable'` and `'warn'` (2026-10-12), so
|
|
401
414
|
* by default any such write withholds the save, on CI too, since a
|
|
402
415
|
* concurrent task can change an input and put it back.
|
|
403
416
|
*/
|
|
404
417
|
function inputStabilityOf(
|
|
405
|
-
|
|
418
|
+
inputs: InputsConfig | undefined,
|
|
406
419
|
layers: ExecuteArgs['inputStability'],
|
|
407
420
|
): Required<InputStability> {
|
|
408
|
-
const task =
|
|
421
|
+
const task = inputs?.stability
|
|
409
422
|
return {
|
|
410
423
|
strategy: layers?.run.strategy ?? task?.strategy ?? layers?.workspace?.strategy ?? 'immutable',
|
|
411
424
|
severity: layers?.run.severity ?? task?.severity ?? layers?.workspace?.severity ?? 'warn',
|
|
@@ -517,9 +530,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
517
530
|
const { node, upstream, cache, log } = args
|
|
518
531
|
const cfg: TaskConfig = node.config
|
|
519
532
|
const step = cfg.exec as ExecConfig // dispatcher guarantees exec is present
|
|
520
|
-
const
|
|
533
|
+
const cfgOut = cfg.outputs
|
|
521
534
|
const policy = args.cachePolicy ?? FULL_CACHE_POLICY
|
|
522
|
-
const cfgCacheable =
|
|
535
|
+
const cfgCacheable = cfg.cache === true
|
|
523
536
|
// We may READ when at least one read axis is on (the cache layer
|
|
524
537
|
// refines local vs remote), and WRITE when at least one write axis is
|
|
525
538
|
// on. Output management (clean before exec) keys off writes — so
|
|
@@ -570,8 +583,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
570
583
|
const deferralRequested = args.download === 'deferred'
|
|
571
584
|
const capture: CaptureConfig = { stdout: false, stderr: false }
|
|
572
585
|
|
|
573
|
-
const outputs =
|
|
574
|
-
const wsOutputs =
|
|
586
|
+
const outputs = cfgOut?.files ?? []
|
|
587
|
+
const wsOutputs = cfgOut?.workspaceFiles ?? []
|
|
575
588
|
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
576
589
|
// Per-task `exec.timeout` beats `run.timeout` (flag, variable,
|
|
577
590
|
// workspace; resolved into `args.timeout` by run.ts).
|
|
@@ -734,12 +747,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
734
747
|
const additive = (node.addsToOutputsOf?.length ?? 0) > 0
|
|
735
748
|
let stampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
736
749
|
let wsStampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
737
|
-
// `
|
|
750
|
+
// `outputs.clean: false` leaves the outputs to the first attempt's
|
|
738
751
|
// command, and the save stores whatever they hold after it, a leftover
|
|
739
752
|
// included: the user opted out of the clean-tree guarantee
|
|
740
753
|
// (2026-10-15). A retry still starts clean. An additive task never cleans
|
|
741
754
|
// by glob anyway.
|
|
742
|
-
const keepOutputs = !additive && (
|
|
755
|
+
const keepOutputs = !additive && (cfgOut?.clean ?? args.cleanOutputs ?? true) === false
|
|
743
756
|
|
|
744
757
|
// Cache lookup. On hit, time the user-perceived restore op
|
|
745
758
|
// (clean+restore+log-replay) — that's what the framed-block footer
|
|
@@ -888,7 +901,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
888
901
|
// Cache miss path (or caching disabled), up to `1 + retries` attempts.
|
|
889
902
|
// Explicit config wins over the run-level `--retries` default, including
|
|
890
903
|
// an explicit `retries: 0`.
|
|
891
|
-
const
|
|
904
|
+
const retriesSet = step.retries !== undefined || args.retries !== undefined
|
|
905
|
+
let maxAttempts = 1 + (step.retries ?? args.retries ?? 0)
|
|
892
906
|
let attempt = 0
|
|
893
907
|
let result: ExecuteResult
|
|
894
908
|
let effectiveExitCode: number
|
|
@@ -1172,6 +1186,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1172
1186
|
const storeMask = secrets ?? undefined
|
|
1173
1187
|
const storedOutput = (): string => encodeOutputLog(output?.chunks() ?? [], storeMask)
|
|
1174
1188
|
const failedAttempts: { endedAt: number; exitCode: number; timedOut?: true }[] = []
|
|
1189
|
+
// Each failed attempt's output, beside `failedAttempts`: kept for `vx last --log`.
|
|
1190
|
+
const attemptOutputs: string[] = []
|
|
1175
1191
|
// The sampling of the attempt's process tree, stopped once it settles.
|
|
1176
1192
|
let untrack: (() => TreeLoad | undefined) | undefined
|
|
1177
1193
|
// What the last attempt's samples saw, for the run history.
|
|
@@ -1210,6 +1226,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1210
1226
|
if (a.withdrawn) {
|
|
1211
1227
|
attempt--
|
|
1212
1228
|
failedAttempts.pop()
|
|
1229
|
+
attemptOutputs.pop()
|
|
1213
1230
|
break
|
|
1214
1231
|
}
|
|
1215
1232
|
forgetUndeclaredWrites(args, writeReach)
|
|
@@ -1251,12 +1268,27 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1251
1268
|
}
|
|
1252
1269
|
}
|
|
1253
1270
|
|
|
1271
|
+
// A known flake gets the one retry its config did not ask for; a
|
|
1272
|
+
// timeout always fails, and a task whose key names nothing (no cache
|
|
1273
|
+
// block, or keyed on a keyless upstream) has no record to know it by.
|
|
1274
|
+
if (
|
|
1275
|
+
effectiveExitCode !== 0 &&
|
|
1276
|
+
attempt === 1 &&
|
|
1277
|
+
!retriesSet &&
|
|
1278
|
+
result.timedOut !== true &&
|
|
1279
|
+
cfgCacheable &&
|
|
1280
|
+
!keyless &&
|
|
1281
|
+
args.failFast?.aborted !== true &&
|
|
1282
|
+
args.knownFlaky?.(node, hash) === true
|
|
1283
|
+
)
|
|
1284
|
+
maxAttempts = 2
|
|
1254
1285
|
if (effectiveExitCode === 0 || attempt >= maxAttempts || args.failFast?.aborted === true) break
|
|
1255
1286
|
failedAttempts.push({
|
|
1256
1287
|
endedAt: Date.now(),
|
|
1257
1288
|
exitCode: effectiveExitCode,
|
|
1258
1289
|
...(result.timedOut === true ? { timedOut: true as const } : {}),
|
|
1259
1290
|
})
|
|
1291
|
+
attemptOutputs.push(output === undefined ? '' : failedOutputLog(output, true, storeMask))
|
|
1260
1292
|
log.taskStderr(
|
|
1261
1293
|
node,
|
|
1262
1294
|
`${midLine ? '\n' : ''}vx: retrying ${node.id} (attempt ${attempt + 1}/${maxAttempts}) after ${result.timedOut === true ? 'a timeout' : `exit ${effectiveExitCode}`}\n`,
|
|
@@ -1284,6 +1316,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1284
1316
|
flushMasked = () => masked?.end()
|
|
1285
1317
|
const base: ExecuteRequest = {
|
|
1286
1318
|
taskId: node.id,
|
|
1319
|
+
...(attempt > 1 ? { attempt } : {}),
|
|
1287
1320
|
workspaceRoot: args.workspaceRoot,
|
|
1288
1321
|
command: step.command,
|
|
1289
1322
|
forwardArgs: effectiveForwardArgs,
|
|
@@ -1335,6 +1368,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1335
1368
|
cfgCacheable ? args.keyedProjects(node) : undefined,
|
|
1336
1369
|
args.nestedProjectDirs,
|
|
1337
1370
|
args.cacheDir,
|
|
1371
|
+
await dependencyReads(args.readableDeps?.(node) ?? [], args.workspaceRoot),
|
|
1338
1372
|
)
|
|
1339
1373
|
placeholders = sb.placeholders
|
|
1340
1374
|
withheld = sb.withheld
|
|
@@ -1343,10 +1377,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1343
1377
|
|
|
1344
1378
|
const wallclockEndNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
1345
1379
|
|
|
1346
|
-
const stability = inputStabilityOf(args.node.config.
|
|
1380
|
+
const stability = inputStabilityOf(args.node.config.inputs, args.inputStability)
|
|
1347
1381
|
let movedFails: string | undefined
|
|
1348
1382
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1349
1383
|
let saved = false
|
|
1384
|
+
// An unsaved miss's outputs, as a save's are counted: its row's `outputs`.
|
|
1385
|
+
let unsavedBytes: number | undefined
|
|
1350
1386
|
if (
|
|
1351
1387
|
effectiveExitCode === 0 &&
|
|
1352
1388
|
willSave &&
|
|
@@ -1408,7 +1444,17 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1408
1444
|
// entry is in; without one, the save is taken as done.
|
|
1409
1445
|
if (args.deferredSaves !== undefined) args.deferredSaves.set(node.id, landed)
|
|
1410
1446
|
else saved = true
|
|
1411
|
-
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants
|
|
1447
|
+
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants === true) {
|
|
1448
|
+
// Nothing reads after it, so nothing to mark: only its row's `outputs`.
|
|
1449
|
+
if (effectiveExitCode === 0)
|
|
1450
|
+
unsavedBytes = await unsavedOutputBytes({
|
|
1451
|
+
node,
|
|
1452
|
+
workspaceRoot: args.workspaceRoot,
|
|
1453
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
1454
|
+
outputs,
|
|
1455
|
+
wsOutputs,
|
|
1456
|
+
})
|
|
1457
|
+
} else if (cfgCacheable && !remoteOnly && !deferralRequested) {
|
|
1412
1458
|
// Ran here and saves nothing — failed, a read-only policy, a tainted
|
|
1413
1459
|
// upstream, a key that no longer held — yet it wrote what a save would
|
|
1414
1460
|
// have marked, and may have rewritten an input the re-check before a
|
|
@@ -1422,7 +1468,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1422
1468
|
if (moved !== undefined) {
|
|
1423
1469
|
forgetUndeclaredWrites(args, wsOutputs.length > 0 ? 'workspace' : 'project')
|
|
1424
1470
|
} else {
|
|
1425
|
-
await markUnsaved({
|
|
1471
|
+
unsavedBytes = await markUnsaved({
|
|
1426
1472
|
node,
|
|
1427
1473
|
workspaceRoot: args.workspaceRoot,
|
|
1428
1474
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -1527,11 +1573,11 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1527
1573
|
const hint =
|
|
1528
1574
|
moved === null || rewroteBy !== undefined
|
|
1529
1575
|
? ''
|
|
1530
|
-
: '; if the task writes it, declare it in
|
|
1576
|
+
: '; if the task writes it, declare it in outputs'
|
|
1531
1577
|
if (stability.severity === 'error') {
|
|
1532
1578
|
warnOf(log)(
|
|
1533
1579
|
`[vx] ${node.id}: ${what} — the result is not saved, ` +
|
|
1534
|
-
`and
|
|
1580
|
+
`and inputs.stability.severity is 'error', so the task fails${hint}`,
|
|
1535
1581
|
)
|
|
1536
1582
|
movedFails = moved === null ? '' : printable(relPosix(args.workspaceRoot, moved))
|
|
1537
1583
|
} else {
|
|
@@ -1564,7 +1610,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1564
1610
|
durationMs: spentMs,
|
|
1565
1611
|
hash,
|
|
1566
1612
|
...(attempt > 1 ? { attempts: attempt } : {}),
|
|
1567
|
-
...(failedAttempts.length > 0 ? { failedAttempts } : {}),
|
|
1613
|
+
...(failedAttempts.length > 0 ? { failedAttempts, attemptOutputs } : {}),
|
|
1568
1614
|
...(described !== undefined
|
|
1569
1615
|
? { inputFiles: captured.reduce((n, c) => (c.kind === 'file' ? n + 1 : n), 0) }
|
|
1570
1616
|
: {}),
|
|
@@ -1572,6 +1618,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1572
1618
|
...(unkeyed ? { unkeyed: true as const } : {}),
|
|
1573
1619
|
...(keylessBy !== undefined ? { keylessBy } : {}),
|
|
1574
1620
|
...(saved ? { saved: true as const } : {}),
|
|
1621
|
+
...(unsavedBytes !== undefined ? { outputBytes: unsavedBytes } : {}),
|
|
1575
1622
|
...(effectiveExitCode !== 0 && output !== undefined
|
|
1576
1623
|
? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined, storeMask) }
|
|
1577
1624
|
: {}),
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
// it is never remembered.
|
|
13
13
|
|
|
14
14
|
import type { FoldMemo } from '../cache/index.js'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildTaskGraph,
|
|
17
|
+
computeReverseDepCount,
|
|
18
|
+
declaredTask,
|
|
19
|
+
type TaskNode,
|
|
20
|
+
} from '../graph/index.js'
|
|
16
21
|
import { xxh3hex } from '../util/index.js'
|
|
17
22
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
18
23
|
import { codeIdentity } from './run-keys.js'
|
|
@@ -138,3 +143,47 @@ function rebuilt(
|
|
|
138
143
|
}
|
|
139
144
|
return nodes
|
|
140
145
|
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The run's dependents count, the scheduler's baseline and every row's
|
|
149
|
+
* `blocks`, read back with the graph it was counted on: the closure over a
|
|
150
|
+
* 9,603-task graph (11k nodes) was ~100 ms of every warm run, the same each
|
|
151
|
+
* time (2026-10-29). `token` names the graph's edges (`memoGraph`, undefined
|
|
152
|
+
* once anything edited them), the node ids which of them this run kept
|
|
153
|
+
* (`--affected` prunes under the same token).
|
|
154
|
+
*/
|
|
155
|
+
export function graphCounts(
|
|
156
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
157
|
+
folds: FoldMemo | undefined,
|
|
158
|
+
token: string | undefined,
|
|
159
|
+
): Map<string, number> {
|
|
160
|
+
if (folds === undefined || token === undefined) return computeReverseDepCount(nodes)
|
|
161
|
+
// Not `counts`: those rows held a second count the rows no longer show.
|
|
162
|
+
const id = `\0dependents\0${token}`
|
|
163
|
+
const probe = xxh3hex([...nodes.keys()].join('\0'))
|
|
164
|
+
const row = folds.get(id, probe)
|
|
165
|
+
if (row !== undefined) {
|
|
166
|
+
const back = countsOf(row, nodes)
|
|
167
|
+
if (back !== undefined) return back
|
|
168
|
+
}
|
|
169
|
+
const counts = computeReverseDepCount(nodes)
|
|
170
|
+
folds.set(id, probe, JSON.stringify([...nodes.keys()].map((k) => counts.get(k)!)))
|
|
171
|
+
return counts
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function countsOf(
|
|
175
|
+
row: string,
|
|
176
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
177
|
+
): Map<string, number> | undefined {
|
|
178
|
+
let all: number[]
|
|
179
|
+
try {
|
|
180
|
+
all = JSON.parse(row) as number[]
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
if (all?.length !== nodes.size) return undefined
|
|
185
|
+
const counts = new Map<string, number>()
|
|
186
|
+
let i = 0
|
|
187
|
+
for (const id of nodes.keys()) counts.set(id, all[i++]!)
|
|
188
|
+
return counts
|
|
189
|
+
}
|
|
@@ -29,6 +29,12 @@ export async function applyGraphStage(
|
|
|
29
29
|
// keyed and cleaned its outputs there: past the hard project boundary.
|
|
30
30
|
const dirs = new Map(args.projectMetas.map((m) => [m.name, path.resolve(m.dir)]))
|
|
31
31
|
const shown = (dir: string): string => relPosix(args.workspaceRoot, dir) || '.'
|
|
32
|
+
// The lock shares a task config two tasks declare alike: an edit in place
|
|
33
|
+
// works on this node's own copy.
|
|
34
|
+
if (plugins.some((p) => p.graph !== undefined)) {
|
|
35
|
+
for (const n of nodes.values())
|
|
36
|
+
if (!isDefaultBuild(n.config)) n.config = structuredClone(n.config)
|
|
37
|
+
}
|
|
32
38
|
await applyGraphHooks(
|
|
33
39
|
plugins,
|
|
34
40
|
nodes,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// 2026-09-10 as pure code motion, the mirror of miss-save.ts.
|
|
10
10
|
|
|
11
11
|
import path from 'node:path'
|
|
12
|
-
import type { CacheConfig } from '../config.js'
|
|
13
12
|
import {
|
|
14
13
|
type CacheEntry,
|
|
15
14
|
cleanOutputPaths,
|
|
@@ -58,9 +57,9 @@ export interface RestoreHitArgs {
|
|
|
58
57
|
export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
59
58
|
const { args, hash } = given
|
|
60
59
|
const { node } = args
|
|
61
|
-
const
|
|
62
|
-
const outputs =
|
|
63
|
-
const wsOutputs =
|
|
60
|
+
const cfgOut = node.config.outputs
|
|
61
|
+
const outputs = cfgOut?.files ?? []
|
|
62
|
+
const wsOutputs = cfgOut?.workspaceFiles ?? []
|
|
64
63
|
const anyOutputs = outputs.length > 0 || wsOutputs.length > 0
|
|
65
64
|
// A layer that handed no rows: one SELECT, read by both paths below.
|
|
66
65
|
const restore =
|
|
@@ -399,8 +398,8 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
399
398
|
export function currentHit(restore: RestoreHitArgs): (() => TaskOutcome) | undefined {
|
|
400
399
|
const { args, hash, hit } = restore
|
|
401
400
|
const { node } = args
|
|
402
|
-
const outputs = node.config.
|
|
403
|
-
if ((node.config.
|
|
401
|
+
const outputs = node.config.outputs?.files ?? []
|
|
402
|
+
if ((node.config.outputs?.workspaceFiles?.length ?? 0) > 0) return undefined
|
|
404
403
|
if (outputs.length > 0) {
|
|
405
404
|
const dirPrefixes = dirPrefixesOf(outputs)
|
|
406
405
|
if (dirPrefixes === null || args.cache.outputDirsCurrent === undefined) return undefined
|
|
@@ -60,6 +60,7 @@ export {
|
|
|
60
60
|
type CommandContext,
|
|
61
61
|
type GraphHookContext,
|
|
62
62
|
type KeyHookContext,
|
|
63
|
+
type LockedFile,
|
|
63
64
|
type PluginCommand,
|
|
64
65
|
type ProjectHookContext,
|
|
65
66
|
type ScheduleHookContext,
|
|
@@ -78,6 +79,7 @@ export {
|
|
|
78
79
|
type RunReport,
|
|
79
80
|
type RunReportTask,
|
|
80
81
|
type FingerprintChange,
|
|
82
|
+
type FingerprintDigests,
|
|
81
83
|
type FingerprintClaim,
|
|
82
84
|
type FingerprintContext,
|
|
83
85
|
type VxPlugin,
|
|
@@ -95,6 +97,7 @@ export {
|
|
|
95
97
|
applyScheduleHooks,
|
|
96
98
|
claimedAffected,
|
|
97
99
|
claimedExtraFiles,
|
|
100
|
+
claimedDigests,
|
|
98
101
|
fingerprintClaims,
|
|
99
102
|
hasHook,
|
|
100
103
|
REMOTE_CACHE_METHODS,
|
|
@@ -63,3 +63,31 @@ export function keyedProjects(
|
|
|
63
63
|
return below.get(root.id)!
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A run's lookup of the exec tasks whose outputs a sandboxed task may read:
|
|
69
|
+
* those its key folds, through a group to its members, never past an exec
|
|
70
|
+
* task. Their outputs are what their keys answer for, and this key folds
|
|
71
|
+
* those keys, so a read of them can never replay stale (the toolchain
|
|
72
|
+
* rule: a `tools` project builds Node, every dependant runs it). A
|
|
73
|
+
* dependency `inputs.tasks` leaves out is ordered, not read.
|
|
74
|
+
*/
|
|
75
|
+
export function readableDeps(
|
|
76
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
77
|
+
keyOnly: ReadonlyMap<string, TaskNode> = new Map(),
|
|
78
|
+
): (node: TaskNode) => readonly TaskNode[] {
|
|
79
|
+
const nodeOf = (id: string): TaskNode => nodes.get(id) ?? keyOnly.get(id)!
|
|
80
|
+
return (root) => {
|
|
81
|
+
const out = new Map<string, TaskNode>()
|
|
82
|
+
const seen = new Set<string>()
|
|
83
|
+
const stack = foldedDeps(root, nodeOf).map((c) => c.node)
|
|
84
|
+
while (stack.length > 0) {
|
|
85
|
+
const dep = stack.pop()!
|
|
86
|
+
if (seen.has(dep.id)) continue
|
|
87
|
+
seen.add(dep.id)
|
|
88
|
+
if (!isGroupTask(dep)) out.set(dep.id, dep)
|
|
89
|
+
else for (const { node } of foldedDeps(dep, nodeOf)) stack.push(node)
|
|
90
|
+
}
|
|
91
|
+
return [...out.values()]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
// The live list's `cpu` and `mem`:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The live list's `cpu` and `mem`: each running task's process tree's
|
|
2
|
+
// average cores since it started and its peak memory so far, the values
|
|
3
|
+
// its results row shows once it ends (2026-10-29: the live row showed the
|
|
4
|
+
// last half second's, and a task's row changed meaning as it finished).
|
|
5
|
+
// Looked at on a timer only once the list asks for a row, so a run with
|
|
6
|
+
// no live list (a pipe, CI) registers pids and reads nothing.
|
|
4
7
|
// The look is async and off the dispatch path: a slow one skips a tick.
|
|
5
8
|
|
|
6
9
|
import { sampleTrees, type TreeUsage } from '../exec/index.js'
|
|
@@ -19,7 +22,10 @@ const everyInterval: Every = (ms, fn) => {
|
|
|
19
22
|
|
|
20
23
|
interface Tracked {
|
|
21
24
|
pid: number
|
|
22
|
-
|
|
25
|
+
startedAt: number
|
|
26
|
+
/** The most CPU time the tree has shown: a descendant that exits takes its share out of the sum. */
|
|
27
|
+
cpuMs: number
|
|
28
|
+
peakRssBytes: number
|
|
23
29
|
now?: LiveUsage
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -51,13 +57,12 @@ export function createLiveUsage(
|
|
|
51
57
|
for (const [id, t] of entries) {
|
|
52
58
|
const u = usage.get(t.pid)
|
|
53
59
|
if (u === undefined || tracked.get(id) !== t) continue
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
const cores =
|
|
58
|
-
t.now = { cores: Math.round(cores * 100) / 100, rssBytes:
|
|
60
|
+
t.cpuMs = Math.max(t.cpuMs, u.cpuMs)
|
|
61
|
+
t.peakRssBytes = Math.max(t.peakRssBytes, u.rssBytes)
|
|
62
|
+
if (ts > t.startedAt) {
|
|
63
|
+
const cores = t.cpuMs / (ts - t.startedAt)
|
|
64
|
+
t.now = { cores: Math.round(cores * 100) / 100, rssBytes: t.peakRssBytes }
|
|
59
65
|
}
|
|
60
|
-
t.last = { ts, cpuMs: u.cpuMs }
|
|
61
66
|
}
|
|
62
67
|
} catch {
|
|
63
68
|
// A failed look is a missing point, never a broken run.
|
|
@@ -71,7 +76,7 @@ export function createLiveUsage(
|
|
|
71
76
|
|
|
72
77
|
return {
|
|
73
78
|
track(taskId, pid) {
|
|
74
|
-
const t: Tracked = { pid }
|
|
79
|
+
const t: Tracked = { pid, startedAt: now(), cpuMs: 0, peakRssBytes: 0 }
|
|
75
80
|
tracked.set(taskId, t)
|
|
76
81
|
start()
|
|
77
82
|
return () => {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// - Only STABLE-key, cacheable, local-read tasks are classified. A
|
|
33
33
|
// stable miss stays in the normal (dep-gated) schedule; an unstable
|
|
34
34
|
// task is never probed here.
|
|
35
|
-
// - A task that declares `
|
|
35
|
+
// - A task that declares `outputs.workspaceFiles` (the
|
|
36
36
|
// boundary-ignoring escape hatch) can write where a restore touches,
|
|
37
37
|
// so every task whose project directory a workspace-output glob's
|
|
38
38
|
// static prefix REACHES stays out of the restore tier, and so does
|
|
@@ -261,7 +261,7 @@ export function restoreTierExclusions(
|
|
|
261
261
|
const prefixes: string[] = []
|
|
262
262
|
let everything = false
|
|
263
263
|
for (const node of nodes.values()) {
|
|
264
|
-
for (const raw of node.config.
|
|
264
|
+
for (const raw of node.config.outputs?.workspaceFiles ?? []) {
|
|
265
265
|
// A `!` entry needs no skip: its prefix (`!gen`) names no directory,
|
|
266
266
|
// so it excludes nothing (A-44; the `continue` that stood here was
|
|
267
267
|
// dropped in item 640).
|