@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
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
committedFiles,
|
|
17
17
|
OUTPUT_DIRS_OVER_CAP,
|
|
18
18
|
outputWalkTakes,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
resolveOutputTree,
|
|
20
|
+
resolveWorkspaceOutputTree,
|
|
21
21
|
sweepRestoreTemps,
|
|
22
22
|
WORKSPACE_OUTPUT_PREFIX,
|
|
23
23
|
} from '../cache/index.js'
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from '../util/index.js'
|
|
34
34
|
import type { ExecuteArgs } from './execute-task.js'
|
|
35
35
|
import { entryHolds } from './miss-save.js'
|
|
36
|
-
import { decodeOutputLog } from './output-log.js'
|
|
36
|
+
import { decodeOutputLog, maskStreams } from './output-log.js'
|
|
37
37
|
|
|
38
38
|
export interface RestoreHitArgs {
|
|
39
39
|
args: ExecuteArgs
|
|
@@ -55,28 +55,33 @@ export interface RestoreHitArgs {
|
|
|
55
55
|
* schedule using the IDENTICAL logic — there is one restore path, not
|
|
56
56
|
* two that could drift.
|
|
57
57
|
*/
|
|
58
|
-
export async function restoreHit(
|
|
59
|
-
const { args, hash
|
|
60
|
-
const { node
|
|
58
|
+
export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
59
|
+
const { args, hash } = given
|
|
60
|
+
const { node } = args
|
|
61
61
|
const cacheCfg: CacheConfig | undefined = node.config.cache
|
|
62
62
|
const outputs = cacheCfg?.outputs.files ?? []
|
|
63
63
|
const wsOutputs = cacheCfg?.outputs.workspaceFiles ?? []
|
|
64
64
|
const anyOutputs = outputs.length > 0 || wsOutputs.length > 0
|
|
65
|
+
// A layer that handed no rows: one SELECT, read by both paths below.
|
|
66
|
+
const restore =
|
|
67
|
+
anyOutputs && given.hit.outputRows === undefined
|
|
68
|
+
? {
|
|
69
|
+
...given,
|
|
70
|
+
hit: {
|
|
71
|
+
...given.hit,
|
|
72
|
+
outputRows: args.cache.loadOutputFilesBatch([hash]).get(hash) ?? [],
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
: given
|
|
76
|
+
const { hit } = restore
|
|
77
|
+
const now = currentHit(restore)
|
|
78
|
+
if (now !== undefined) return now()
|
|
65
79
|
// The directories the positive globs cover whole. A `!` entry inside one
|
|
66
80
|
// leaves the snapshot sound: a file added or removed there still bumps a
|
|
67
81
|
// recorded directory, and the walk that follows applies the `!` (A-46).
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
outputs,
|
|
72
|
-
nestedProjectDirs: args.nestedProjectDirs,
|
|
73
|
-
committed: committedFiles(args.gitFilesCache, node.projectDir),
|
|
74
|
-
}
|
|
75
|
-
const wsCleanArgs = {
|
|
76
|
-
workspaceRoot: args.workspaceRoot,
|
|
77
|
-
outputs: wsOutputs,
|
|
78
|
-
committed: committedFiles(args.gitFilesCache, args.workspaceRoot),
|
|
79
|
-
}
|
|
82
|
+
// Asked only of a task that declares outputs: most hits declare none,
|
|
83
|
+
// and a warm run of 1,000 such hits paid this set-up for each.
|
|
84
|
+
const dirPrefixes = anyOutputs ? dirPrefixesOf(outputs) : null
|
|
80
85
|
// The two sides of an overlapping pair (item 588). An ADDITIVE task owns
|
|
81
86
|
// only its recorded rows: its glob also selects the upstream's files it
|
|
82
87
|
// adds beside, so "no strays" is not a claim it can make, and its clean
|
|
@@ -85,9 +90,10 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
85
90
|
// addition below is not a stray — while its glob clean still takes them:
|
|
86
91
|
// the dependant restores or runs after it, by the edge.
|
|
87
92
|
const additive = (node.addsToOutputsOf?.length ?? 0) > 0
|
|
88
|
-
const addedGlobs =
|
|
89
|
-
.
|
|
90
|
-
|
|
93
|
+
const addedGlobs =
|
|
94
|
+
anyOutputs && node.outputsAddedToBy !== undefined
|
|
95
|
+
? node.outputsAddedToBy.flatMap((g) => asTrees([g])).map((g) => taskGlob(g))
|
|
96
|
+
: []
|
|
91
97
|
const isAddition = (rel: string): boolean => addedGlobs.some((g) => g.match(rel))
|
|
92
98
|
|
|
93
99
|
// "Tree is already current" short-circuit — skip cleanOutputs
|
|
@@ -114,11 +120,9 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
114
120
|
// The walk that found the tree stale, handed to the clean when nothing
|
|
115
121
|
// was awaited between them: the clean's own walk would only repeat it
|
|
116
122
|
// (X-161).
|
|
117
|
-
let walked: { proj:
|
|
123
|
+
let walked: { proj: Tree; ws: Tree } | undefined
|
|
118
124
|
if (anyOutputs) {
|
|
119
|
-
const
|
|
120
|
-
const expected = hit.outputRows ?? args.cache.loadOutputFilesBatch([hash]).get(hash) ?? []
|
|
121
|
-
endRows()
|
|
125
|
+
const expected = hit.outputRows ?? []
|
|
122
126
|
if (expected.length > 0) {
|
|
123
127
|
// Two namespaces in the rows: bare rels are project outputs,
|
|
124
128
|
// `workspace-outputs/<rel>` rows anchor at the workspace root.
|
|
@@ -126,91 +130,92 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
126
130
|
const wsExpected = expected
|
|
127
131
|
.filter((e) => e.path.startsWith(WORKSPACE_OUTPUT_PREFIX))
|
|
128
132
|
.map((e) => ({ ...e, path: e.path.slice(WORKSPACE_OUTPUT_PREFIX.length) }))
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// each row to a path and back was ~5 ms per 1,000 warm hits.
|
|
155
|
-
treeMatches = wsExpected.length === 0
|
|
156
|
-
} else {
|
|
157
|
-
const endGlob = span('output glob')
|
|
158
|
-
const actualAbs = await resolveOutputs({
|
|
159
|
-
projectDir: node.projectDir,
|
|
160
|
-
outputs,
|
|
161
|
-
nestedProjectDirs: args.nestedProjectDirs,
|
|
162
|
-
})
|
|
163
|
-
const actualWsAbs = await resolveWorkspaceOutputs({
|
|
164
|
-
workspaceRoot: args.workspaceRoot,
|
|
165
|
-
outputs: wsOutputs,
|
|
166
|
-
})
|
|
167
|
-
endGlob()
|
|
168
|
-
const setsMatch = (
|
|
169
|
-
actual: readonly string[],
|
|
170
|
-
exp: ReadonlyArray<{ path: string }>,
|
|
171
|
-
): boolean => {
|
|
172
|
-
const expSet = new Set(exp.map((e) => e.path).filter((p) => !p.endsWith('/')))
|
|
173
|
-
return actual.length === expSet.size && actual.every((r) => expSet.has(r))
|
|
174
|
-
}
|
|
175
|
-
// A stray a dependant's glob could have added is not a stray; a path
|
|
176
|
-
// this entry recorded is never dropped, even where that glob covers
|
|
177
|
-
// the whole tree (strapi's shape: both on `dist`).
|
|
178
|
-
const expectedRels = new Set(projExpected.map((e) => e.path))
|
|
179
|
-
const expectedWsRels = new Set(wsExpected.map((e) => e.path))
|
|
180
|
-
const actualRels = actualAbs
|
|
181
|
-
.map((p) => relPosix(node.projectDir, p))
|
|
182
|
-
.filter((rel) => expectedRels.has(rel) || !isAddition(rel))
|
|
183
|
-
const actualWsRels = actualWsAbs
|
|
184
|
-
.map((p) => relPosix(args.workspaceRoot, p))
|
|
185
|
-
.filter((rel) => expectedWsRels.has(rel) || !isAddition(rel))
|
|
186
|
-
const rowsPresent = (
|
|
187
|
-
actual: readonly string[],
|
|
188
|
-
exp: ReadonlyArray<{ path: string }>,
|
|
189
|
-
): boolean => {
|
|
190
|
-
const have = new Set(actual)
|
|
191
|
-
return exp.every((e) => e.path.endsWith('/') || have.has(e.path))
|
|
133
|
+
const endGlob = span('output glob')
|
|
134
|
+
const projTree = await resolveOutputTree({
|
|
135
|
+
projectDir: node.projectDir,
|
|
136
|
+
outputs,
|
|
137
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
138
|
+
})
|
|
139
|
+
const wsTree = await resolveWorkspaceOutputTree({
|
|
140
|
+
workspaceRoot: args.workspaceRoot,
|
|
141
|
+
outputs: wsOutputs,
|
|
142
|
+
})
|
|
143
|
+
endGlob()
|
|
144
|
+
const actualAbs = projTree.files
|
|
145
|
+
const actualWsAbs = wsTree.files
|
|
146
|
+
// An empty directory is an entry row (`dist/empty/`): one the entry
|
|
147
|
+
// does not hold is a stray, as a file is. A directory row with a row
|
|
148
|
+
// under it is one kept for its mode (`DIR_MODE`), and the walk lists
|
|
149
|
+
// no such directory: only the files and empty directories it holds.
|
|
150
|
+
const setsMatch = (
|
|
151
|
+
actual: readonly string[],
|
|
152
|
+
exp: ReadonlyArray<{ path: string }>,
|
|
153
|
+
): boolean => {
|
|
154
|
+
const held = new Set<string>()
|
|
155
|
+
for (const { path: p } of exp) {
|
|
156
|
+
for (let i = p.lastIndexOf('/', p.length - 2); i > 0; i = p.lastIndexOf('/', i - 1))
|
|
157
|
+
held.add(p.slice(0, i + 1))
|
|
192
158
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
159
|
+
const expSet = new Set(
|
|
160
|
+
exp.map((e) => e.path).filter((p) => !p.endsWith('/') || !held.has(p)),
|
|
161
|
+
)
|
|
162
|
+
return actual.length === expSet.size && actual.every((r) => expSet.has(r))
|
|
163
|
+
}
|
|
164
|
+
const dirRels = (root: string, dirs: readonly string[]): string[] =>
|
|
165
|
+
dirs.map((d) => `${relPosix(root, d)}/`)
|
|
166
|
+
// A stray a dependant's glob could have added is not a stray; a path
|
|
167
|
+
// this entry recorded is never dropped, even where that glob covers
|
|
168
|
+
// the whole tree (strapi's shape: both on `dist`).
|
|
169
|
+
const expectedRels = new Set(projExpected.map((e) => e.path))
|
|
170
|
+
const expectedWsRels = new Set(wsExpected.map((e) => e.path))
|
|
171
|
+
const actualRels = actualAbs
|
|
172
|
+
.map((p) => relPosix(node.projectDir, p))
|
|
173
|
+
.filter((rel) => expectedRels.has(rel) || !isAddition(rel))
|
|
174
|
+
const actualWsRels = actualWsAbs
|
|
175
|
+
.map((p) => relPosix(args.workspaceRoot, p))
|
|
176
|
+
.filter((rel) => expectedWsRels.has(rel) || !isAddition(rel))
|
|
177
|
+
const actualDirRels = dirRels(node.projectDir, projTree.emptyDirs).filter(
|
|
178
|
+
(rel) => expectedRels.has(rel) || !isAddition(rel.slice(0, -1)),
|
|
179
|
+
)
|
|
180
|
+
const actualWsDirRels = dirRels(args.workspaceRoot, wsTree.emptyDirs).filter(
|
|
181
|
+
(rel) => expectedWsRels.has(rel) || !isAddition(rel.slice(0, -1)),
|
|
182
|
+
)
|
|
183
|
+
const rowsPresent = (
|
|
184
|
+
actual: readonly string[],
|
|
185
|
+
exp: ReadonlyArray<{ path: string }>,
|
|
186
|
+
): boolean => {
|
|
187
|
+
const have = new Set(actual)
|
|
188
|
+
return exp.every((e) => e.path.endsWith('/') || have.has(e.path))
|
|
197
189
|
}
|
|
190
|
+
const treeMatches = additive
|
|
191
|
+
? rowsPresent(actualRels, projExpected) && rowsPresent(actualWsRels, wsExpected)
|
|
192
|
+
: setsMatch([...actualRels, ...actualDirRels], projExpected) &&
|
|
193
|
+
setsMatch([...actualWsRels, ...actualWsDirRels], wsExpected)
|
|
194
|
+
if (!treeMatches) walked = { proj: projTree, ws: wsTree }
|
|
198
195
|
if (treeMatches) {
|
|
199
196
|
const endStat = span('output stat')
|
|
200
197
|
skipRestore =
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
args.cache.isOutputsCurrent(node.projectDir, projExpected) &&
|
|
199
|
+
args.cache.isOutputsCurrent(args.workspaceRoot, wsExpected)
|
|
203
200
|
endStat()
|
|
204
201
|
// The walk ran and proved the tree current (rows absent, or a
|
|
205
202
|
// directory had moved — a benign touch): snapshot the directories
|
|
206
203
|
// so the next hit skips the walk.
|
|
207
204
|
if (
|
|
208
205
|
skipRestore &&
|
|
209
|
-
!setKnown &&
|
|
210
206
|
dirPrefixes !== null &&
|
|
211
207
|
hit.outputDirRows?.some((r) => r.mtimeMs === OUTPUT_DIRS_OVER_CAP) !== true
|
|
212
208
|
) {
|
|
213
|
-
|
|
209
|
+
// Held to the rows like the run-end snapshot (item 1087): a stray
|
|
210
|
+
// written since the walk listed its directory, more than a racy
|
|
211
|
+
// window before this, is not the entry's.
|
|
212
|
+
const holds = entryHolds(
|
|
213
|
+
node,
|
|
214
|
+
outputWalkTakes(node.projectDir, outputs, args.nestedProjectDirs),
|
|
215
|
+
)
|
|
216
|
+
await args.cache.recordOutputDirs?.(hash, node.projectDir, dirPrefixes, (files) =>
|
|
217
|
+
holds(files, expected),
|
|
218
|
+
)
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
}
|
|
@@ -221,24 +226,37 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
221
226
|
// of the tar is spared. A stray under the glob still falls through to
|
|
222
227
|
// the clean, as strict ownership requires (item 589).
|
|
223
228
|
if (anyOutputs && !skipRestore) {
|
|
224
|
-
const expected = hit.outputRows ??
|
|
229
|
+
const expected = hit.outputRows ?? []
|
|
225
230
|
if (expected.length === 0) {
|
|
226
231
|
const endGlob = span('output glob')
|
|
227
|
-
const actual = await
|
|
232
|
+
const actual = await resolveOutputTree({
|
|
228
233
|
projectDir: node.projectDir,
|
|
229
234
|
outputs,
|
|
230
235
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
231
236
|
})
|
|
232
|
-
const actualWs = await
|
|
237
|
+
const actualWs = await resolveWorkspaceOutputTree({
|
|
233
238
|
workspaceRoot: args.workspaceRoot,
|
|
234
239
|
outputs: wsOutputs,
|
|
235
240
|
})
|
|
236
241
|
endGlob()
|
|
237
|
-
skipRestore =
|
|
242
|
+
skipRestore =
|
|
243
|
+
actual.files.length + actual.emptyDirs.length + actualWs.files.length === 0 &&
|
|
244
|
+
actualWs.emptyDirs.length === 0
|
|
238
245
|
walked = { proj: actual, ws: actualWs }
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
if (!skipRestore) {
|
|
249
|
+
const cleanArgs = {
|
|
250
|
+
projectDir: node.projectDir,
|
|
251
|
+
outputs,
|
|
252
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
253
|
+
committed: committedFiles(args.gitFilesCache, node.projectDir),
|
|
254
|
+
}
|
|
255
|
+
const wsCleanArgs = {
|
|
256
|
+
workspaceRoot: args.workspaceRoot,
|
|
257
|
+
outputs: wsOutputs,
|
|
258
|
+
committed: committedFiles(args.gitFilesCache, args.workspaceRoot),
|
|
259
|
+
}
|
|
242
260
|
const heldRels = hit.outputFiles.filter((p) => !p.startsWith(WORKSPACE_OUTPUT_PREFIX))
|
|
243
261
|
const heldWsRels = hit.outputFiles
|
|
244
262
|
.filter((p) => p.startsWith(WORKSPACE_OUTPUT_PREFIX))
|
|
@@ -255,34 +273,42 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
255
273
|
let foundWsRels: string[] = []
|
|
256
274
|
let staleRels: string[] = []
|
|
257
275
|
let staleWsRels: string[] = []
|
|
276
|
+
let staleDirs: string[] = []
|
|
277
|
+
let staleWsDirs: string[] = []
|
|
258
278
|
if (!additive) {
|
|
259
|
-
sweepRestoreTemps(node.projectDir, outputs)
|
|
260
|
-
sweepRestoreTemps(args.workspaceRoot, wsOutputs)
|
|
279
|
+
sweepRestoreTemps(node.projectDir, outputs, cleanArgs.committed)
|
|
280
|
+
sweepRestoreTemps(args.workspaceRoot, wsOutputs, wsCleanArgs.committed)
|
|
261
281
|
if (outputs.length > 0) {
|
|
262
|
-
const
|
|
263
|
-
foundRels =
|
|
282
|
+
const tree = walked?.proj ?? (await resolveOutputTree(cleanArgs))
|
|
283
|
+
foundRels = tree.files.map((f) => relPosix(node.projectDir, f))
|
|
264
284
|
const blocking = inTheWay(foundRels, heldRels)
|
|
265
285
|
staleRels = notHeld(foundRels, heldRels).filter((f) => !blocking.has(f))
|
|
266
|
-
|
|
286
|
+
const dirs = splitDirs(node.projectDir, tree.emptyDirs, heldRels)
|
|
287
|
+
staleDirs = dirs.stale
|
|
288
|
+
if (blocking.size > 0 || dirs.blocking.length > 0) {
|
|
267
289
|
await cleanOutputPaths({
|
|
268
290
|
projectDir: node.projectDir,
|
|
269
291
|
rels: [...blocking],
|
|
270
292
|
outputs,
|
|
271
293
|
committed: cleanArgs.committed,
|
|
294
|
+
emptyDirs: dirs.blocking,
|
|
272
295
|
})
|
|
273
296
|
}
|
|
274
297
|
}
|
|
275
298
|
if (wsOutputs.length > 0) {
|
|
276
|
-
const
|
|
277
|
-
foundWsRels =
|
|
299
|
+
const tree = walked?.ws ?? (await resolveWorkspaceOutputTree(wsCleanArgs))
|
|
300
|
+
foundWsRels = tree.files.map((f) => relPosix(args.workspaceRoot, f))
|
|
278
301
|
const blocking = inTheWay(foundWsRels, heldWsRels)
|
|
279
302
|
staleWsRels = notHeld(foundWsRels, heldWsRels).filter((f) => !blocking.has(f))
|
|
280
|
-
|
|
303
|
+
const dirs = splitDirs(args.workspaceRoot, tree.emptyDirs, heldWsRels)
|
|
304
|
+
staleWsDirs = dirs.stale
|
|
305
|
+
if (blocking.size > 0 || dirs.blocking.length > 0) {
|
|
281
306
|
await cleanOutputPaths({
|
|
282
307
|
projectDir: args.workspaceRoot,
|
|
283
308
|
rels: [...blocking],
|
|
284
309
|
outputs: wsOutputs,
|
|
285
310
|
committed: wsCleanArgs.committed,
|
|
311
|
+
emptyDirs: dirs.blocking,
|
|
286
312
|
})
|
|
287
313
|
}
|
|
288
314
|
}
|
|
@@ -291,10 +317,11 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
291
317
|
files: outputs,
|
|
292
318
|
workspaceFiles: wsOutputs,
|
|
293
319
|
})
|
|
294
|
-
if (staleRels.length > 0) {
|
|
320
|
+
if (staleRels.length > 0 || staleDirs.length > 0) {
|
|
295
321
|
await cleanOutputPaths({
|
|
296
322
|
projectDir: node.projectDir,
|
|
297
323
|
rels: staleRels,
|
|
324
|
+
emptyDirs: staleDirs,
|
|
298
325
|
outputs,
|
|
299
326
|
committed: cleanArgs.committed,
|
|
300
327
|
// A stale file in an output directory the entry holds empty takes
|
|
@@ -306,12 +333,18 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
306
333
|
),
|
|
307
334
|
})
|
|
308
335
|
}
|
|
309
|
-
if (staleWsRels.length > 0) {
|
|
336
|
+
if (staleWsRels.length > 0 || staleWsDirs.length > 0) {
|
|
310
337
|
await cleanOutputPaths({
|
|
311
338
|
projectDir: args.workspaceRoot,
|
|
312
339
|
rels: staleWsRels,
|
|
340
|
+
emptyDirs: staleWsDirs,
|
|
313
341
|
outputs: wsOutputs,
|
|
314
342
|
committed: wsCleanArgs.committed,
|
|
343
|
+
keep: new Set(
|
|
344
|
+
heldWsRels
|
|
345
|
+
.filter((r) => r.endsWith('/'))
|
|
346
|
+
.map((r) => path.join(args.workspaceRoot, r.slice(0, -1))),
|
|
347
|
+
),
|
|
315
348
|
})
|
|
316
349
|
}
|
|
317
350
|
// The tree now holds this entry's bytes: stamp them for the next hit
|
|
@@ -351,6 +384,47 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
351
384
|
])
|
|
352
385
|
}
|
|
353
386
|
}
|
|
387
|
+
return hitOutcome(restore, !skipRestore && anyOutputs)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The hit's outcome now, when its tree is provably current without a walk
|
|
392
|
+
* or an await: it declares no outputs, or every directory the entry
|
|
393
|
+
* recorded and every file it holds are as the entry left them (the
|
|
394
|
+
* directory snapshot proves the set, the stamps the bytes). Undefined
|
|
395
|
+
* sends it down `restoreHit`'s full path. Nothing happens until the thunk
|
|
396
|
+
* runs, which replays the stored output: the scheduler starts a hit it
|
|
397
|
+
* settles in place first, so its output follows its start.
|
|
398
|
+
*/
|
|
399
|
+
export function currentHit(restore: RestoreHitArgs): (() => TaskOutcome) | undefined {
|
|
400
|
+
const { args, hash, hit } = restore
|
|
401
|
+
const { node } = args
|
|
402
|
+
const outputs = node.config.cache?.outputs.files ?? []
|
|
403
|
+
if ((node.config.cache?.outputs.workspaceFiles?.length ?? 0) > 0) return undefined
|
|
404
|
+
if (outputs.length > 0) {
|
|
405
|
+
const dirPrefixes = dirPrefixesOf(outputs)
|
|
406
|
+
if (dirPrefixes === null || args.cache.outputDirsCurrent === undefined) return undefined
|
|
407
|
+
const expected = hit.outputRows ?? args.cache.loadOutputFilesBatch([hash]).get(hash) ?? []
|
|
408
|
+
if (expected.length === 0) return undefined
|
|
409
|
+
// Loaded with the entry (batched by getMany, or with the lazy get).
|
|
410
|
+
const dirRows = hit.outputDirRows ?? []
|
|
411
|
+
// Rows are recorded for these same prefixes under this same key (the
|
|
412
|
+
// outputs are folded into it), so this cannot fail today (item 638).
|
|
413
|
+
// Kept: a row set that shrank would otherwise vouch for a tree it never
|
|
414
|
+
// saw, and the failure it would let through is a skipped restore.
|
|
415
|
+
if (!dirPrefixes.every((pre) => dirRows.some((r) => r.path === pre))) return undefined
|
|
416
|
+
if (!args.cache.outputDirsCurrent(node.projectDir, dirRows)) return undefined
|
|
417
|
+
// No workspace output is declared, so every row is the project's.
|
|
418
|
+
if (expected.some((e) => e.path.startsWith(WORKSPACE_OUTPUT_PREFIX))) return undefined
|
|
419
|
+
if (!args.cache.isOutputsCurrent(node.projectDir, expected)) return undefined
|
|
420
|
+
}
|
|
421
|
+
return () => hitOutcome(restore, false)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Replay the stored output and build the hit's outcome. */
|
|
425
|
+
function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
426
|
+
const { args, hash, hit, cacheOpStart, taskStartNs } = restore
|
|
427
|
+
const { node, log } = args
|
|
354
428
|
if (hit.stdout) {
|
|
355
429
|
// An entry saved before masking, or by another machine, can hold a
|
|
356
430
|
// secret-named variable's value (L-11). Asked only of a hit that prints.
|
|
@@ -359,10 +433,9 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
359
433
|
node.config.exec?.env?.secret,
|
|
360
434
|
)
|
|
361
435
|
// The stored log holds both streams in the order the run printed them.
|
|
362
|
-
for (const c of decodeOutputLog(hit.stdout)) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
else log.taskStdout(node, text)
|
|
436
|
+
for (const c of maskStreams(decodeOutputLog(hit.stdout), secrets ?? undefined)) {
|
|
437
|
+
if (c.err) log.taskStderr(node, c.text)
|
|
438
|
+
else log.taskStdout(node, c.text)
|
|
366
439
|
}
|
|
367
440
|
}
|
|
368
441
|
const status =
|
|
@@ -370,10 +443,10 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
370
443
|
// `restored` distinguishes "we just wrote files to disk" from
|
|
371
444
|
// "disk already matched the cached snapshot". Drives the
|
|
372
445
|
// "up-to-date" vs "local-cache" / "remote-cache" label in the
|
|
373
|
-
// framed block.
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
446
|
+
// framed block. A hit the remote answered is never up to date: up to
|
|
447
|
+
// date is fresh here, and a remote hit waited on the network and
|
|
448
|
+
// downloaded its entry, outputs or none (2026-10-24: "Up to date is
|
|
449
|
+
// a fresh. It should not require any cache").
|
|
377
450
|
return {
|
|
378
451
|
node,
|
|
379
452
|
status,
|
|
@@ -387,7 +460,7 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
387
460
|
...(hit.sizeBytes !== undefined ? { artifactBytes: hit.sizeBytes } : {}),
|
|
388
461
|
...(hit.fetchMs !== undefined ? { fetchMs: hit.fetchMs } : {}),
|
|
389
462
|
hash,
|
|
390
|
-
restored,
|
|
463
|
+
restored: restored || hit.source === 'remote',
|
|
391
464
|
wallclockStartNs: taskStartNs,
|
|
392
465
|
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
393
466
|
}
|
|
@@ -446,6 +519,31 @@ function inTheWay(found: readonly string[], held: readonly string[]): Set<string
|
|
|
446
519
|
}
|
|
447
520
|
|
|
448
521
|
/** The found outputs the entry does not hold: the stale ones, once it has restored. */
|
|
522
|
+
type Tree = { files: string[]; emptyDirs: string[] }
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* The empty directories found under the outputs (absolute) that the entry
|
|
526
|
+
* does not hold, as rels: one where it holds a file blocks that file's
|
|
527
|
+
* rename and goes first, the rest go after the restore, as a miss's clean
|
|
528
|
+
* takes them.
|
|
529
|
+
*/
|
|
530
|
+
function splitDirs(
|
|
531
|
+
root: string,
|
|
532
|
+
emptyDirs: readonly string[],
|
|
533
|
+
held: readonly string[],
|
|
534
|
+
): { blocking: string[]; stale: string[] } {
|
|
535
|
+
const heldKeys = new Set(held.map(fold))
|
|
536
|
+
const blocking: string[] = []
|
|
537
|
+
const stale: string[] = []
|
|
538
|
+
for (const d of emptyDirs) {
|
|
539
|
+
const rel = relPosix(root, d)
|
|
540
|
+
if (heldKeys.has(fold(`${rel}/`))) continue
|
|
541
|
+
if (heldKeys.has(fold(rel))) blocking.push(rel)
|
|
542
|
+
else stale.push(rel)
|
|
543
|
+
}
|
|
544
|
+
return { blocking, stale }
|
|
545
|
+
}
|
|
546
|
+
|
|
449
547
|
function notHeld(found: readonly string[], held: readonly string[]): string[] {
|
|
450
548
|
const keep = new Set(held.map(fold))
|
|
451
549
|
return found.filter((f) => !keep.has(fold(f)))
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// through here (enforced by tests/module-boundaries.test.ts).
|
|
3
3
|
|
|
4
4
|
export { run, planRun, nxProjectTarget, projectNamed } from './run.js'
|
|
5
|
+
export { CYCLE_INTERRUPT } from './run-stop.js'
|
|
5
6
|
export {
|
|
6
7
|
namesRemote,
|
|
7
8
|
openRemotes,
|
|
@@ -15,7 +16,6 @@ export type { AffectedReason } from './affected-tasks.js'
|
|
|
15
16
|
export {
|
|
16
17
|
discoverProjects,
|
|
17
18
|
gitOfDiscovery,
|
|
18
|
-
isDefaultBuild,
|
|
19
19
|
keepDiscoveryGraph,
|
|
20
20
|
loadProjects,
|
|
21
21
|
loadResolvedProjects,
|
|
@@ -42,6 +42,7 @@ export { exitSignal, shellQuote } from '../exec/index.js'
|
|
|
42
42
|
export { forwardedSignal } from './signals.js'
|
|
43
43
|
// `vx cache clean` waits for a run on the workspace before it evicts.
|
|
44
44
|
export { acquireRunLock } from './run-lock.js'
|
|
45
|
+
export { type LiveSnapshot, liveSocketPath } from './run-live.js'
|
|
45
46
|
export type { RunPlan, PlannedTask, PlanPrediction, CacheStatus } from './plan.js'
|
|
46
47
|
export {
|
|
47
48
|
cacheFacts,
|
|
@@ -145,8 +146,8 @@ export {
|
|
|
145
146
|
type TaskLog,
|
|
146
147
|
taskLog,
|
|
147
148
|
} from './run-failures.js'
|
|
148
|
-
export { type RunStep, runSteps } from './run-steps.js'
|
|
149
|
+
export { type RunStep, runSteps, runWarnings } from './run-steps.js'
|
|
149
150
|
export { resolveRunId, shortRunId } from './run-id.js'
|
|
150
151
|
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './doctor.js'
|
|
151
152
|
export { localFloor, type LocalFloor } from './local-floor.js'
|
|
152
|
-
export { sandboxRequestFor, type SandboxRequest } from './sandbox-request.js'
|
|
153
|
+
export { sandboxRequestFor, sweepPlaceholders, type SandboxRequest } from './sandbox-request.js'
|