@vzn/vx 0.0.651 → 0.0.653
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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
buildTaskGraph,
|
|
17
17
|
computeReverseDepCount,
|
|
18
18
|
declaredTask,
|
|
19
|
+
setStandIns,
|
|
20
|
+
standInsOf,
|
|
19
21
|
type TaskNode,
|
|
20
22
|
} from '../graph/index.js'
|
|
21
23
|
import { xxh3hex } from '../util/index.js'
|
|
@@ -25,7 +27,7 @@ import { codeIdentity } from './run-keys.js'
|
|
|
25
27
|
type BuildGraphOptions = Parameters<typeof buildTaskGraph>[0]
|
|
26
28
|
|
|
27
29
|
/** Moves every remembered graph: bump it when what a token stands for changes. */
|
|
28
|
-
const GRAPH_MEMO_VERSION = 'graph-memo-
|
|
30
|
+
const GRAPH_MEMO_VERSION = 'graph-memo-v2'
|
|
29
31
|
|
|
30
32
|
/** What names a run's configs: the lock they were read from and the projects discovered. */
|
|
31
33
|
export interface GraphMemo {
|
|
@@ -38,7 +40,9 @@ export interface GraphMemo {
|
|
|
38
40
|
* A node as the memo holds it: its project and task, requested, its deps
|
|
39
41
|
* and the tasks it adds outputs to as positions in the list (ids in full
|
|
40
42
|
* were 123 KB for 1,206 tasks, and parsing them back cost what the walk
|
|
41
|
-
* saved),
|
|
43
|
+
* saved), the globs that add to its own outputs, and its stand-ins
|
|
44
|
+
* (rebuilt without it, `inputs.tasks` dropped the edges past a default
|
|
45
|
+
* build on a package cycle on every warm run).
|
|
42
46
|
*/
|
|
43
47
|
type Row = [
|
|
44
48
|
project: string,
|
|
@@ -47,6 +51,7 @@ type Row = [
|
|
|
47
51
|
deps: number[],
|
|
48
52
|
adds?: number[] | null,
|
|
49
53
|
added?: string[] | null,
|
|
54
|
+
stands?: Array<[edge: number, past: number[]]>,
|
|
50
55
|
]
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -97,11 +102,23 @@ function rowsOf(nodes: Map<string, TaskNode>): Row[] {
|
|
|
97
102
|
const pos = (ids: readonly string[]): number[] => ids.map((d) => at.get(d)!)
|
|
98
103
|
return [...nodes.values()].map((n) => {
|
|
99
104
|
const row: Row = [n.projectName, n.taskName, n.requested ? 1 : 0, pos(n.deps)]
|
|
100
|
-
|
|
105
|
+
const standsFor = standInsOf(n)
|
|
106
|
+
if (
|
|
107
|
+
n.addsToOutputsOf !== undefined ||
|
|
108
|
+
n.outputsAddedToBy !== undefined ||
|
|
109
|
+
standsFor !== undefined
|
|
110
|
+
)
|
|
101
111
|
row.push(
|
|
102
112
|
n.addsToOutputsOf === undefined ? null : pos(n.addsToOutputsOf),
|
|
103
113
|
n.outputsAddedToBy ?? null,
|
|
104
114
|
)
|
|
115
|
+
if (standsFor !== undefined)
|
|
116
|
+
row.push(
|
|
117
|
+
Object.entries(standsFor).map(([edge, past]): [number, number[]] => [
|
|
118
|
+
at.get(edge)!,
|
|
119
|
+
pos(past),
|
|
120
|
+
]),
|
|
121
|
+
)
|
|
105
122
|
return row
|
|
106
123
|
})
|
|
107
124
|
}
|
|
@@ -134,11 +151,16 @@ function rebuilt(
|
|
|
134
151
|
const ids = (at: readonly number[]): string[] => at.map((i) => list[i]!.id)
|
|
135
152
|
const nodes = new Map<string, TaskNode>()
|
|
136
153
|
for (let i = 0; i < rows.length; i++) {
|
|
137
|
-
const [, , , deps, adds, added] = rows[i]!
|
|
154
|
+
const [, , , deps, adds, added, stands] = rows[i]!
|
|
138
155
|
const node = list[i]!
|
|
139
156
|
node.deps = ids(deps)
|
|
140
157
|
if (adds != null) node.addsToOutputsOf = ids(adds)
|
|
141
158
|
if (added != null) node.outputsAddedToBy = added
|
|
159
|
+
if (stands !== undefined) {
|
|
160
|
+
const standsFor: Record<string, string[]> = {}
|
|
161
|
+
for (const [edge, past] of stands) standsFor[list[edge]!.id] = ids(past)
|
|
162
|
+
setStandIns(node, standsFor)
|
|
163
|
+
}
|
|
142
164
|
nodes.set(node.id, node)
|
|
143
165
|
}
|
|
144
166
|
return nodes
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
// select over the graph the hooks leave.
|
|
4
4
|
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
setStandIns,
|
|
8
|
+
standInsOf,
|
|
9
|
+
type GraphRules,
|
|
10
|
+
type StandIns,
|
|
11
|
+
type TaskNode,
|
|
12
|
+
} from '../graph/index.js'
|
|
7
13
|
import { relPosix, UserError } from '../util/index.js'
|
|
8
14
|
import { type ProjectMeta, validateProjectConfig } from '../workspace/index.js'
|
|
9
15
|
import { applyGraphHooks } from './plugin-host.js'
|
|
@@ -31,9 +37,13 @@ export async function applyGraphStage(
|
|
|
31
37
|
const shown = (dir: string): string => relPosix(args.workspaceRoot, dir) || '.'
|
|
32
38
|
// The lock shares a task config two tasks declare alike: an edit in place
|
|
33
39
|
// works on this node's own copy.
|
|
34
|
-
if (plugins.some((p) => p.graph !== undefined))
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
if (!plugins.some((p) => p.graph !== undefined)) return
|
|
41
|
+
for (const n of nodes.values())
|
|
42
|
+
if (!isDefaultBuild(n.config)) n.config = structuredClone(n.config)
|
|
43
|
+
const standIns = new Map<string, StandIns>()
|
|
44
|
+
for (const n of nodes.values()) {
|
|
45
|
+
const s = standInsOf(n)
|
|
46
|
+
if (s !== undefined) standIns.set(n.id, s)
|
|
37
47
|
}
|
|
38
48
|
await applyGraphHooks(
|
|
39
49
|
plugins,
|
|
@@ -68,4 +78,17 @@ export async function applyGraphStage(
|
|
|
68
78
|
},
|
|
69
79
|
args.rules,
|
|
70
80
|
)
|
|
81
|
+
// A hook may rebuild a node, which drops what its edges stand in for, or
|
|
82
|
+
// remove an edge a record names: re-attach each by id, less what is gone.
|
|
83
|
+
for (const [id, before] of standIns) {
|
|
84
|
+
const n = nodes.get(id)
|
|
85
|
+
if (n === undefined) continue
|
|
86
|
+
const kept: Record<string, string[]> = {}
|
|
87
|
+
for (const [edge, past] of Object.entries(before)) {
|
|
88
|
+
if (!n.deps.includes(edge)) continue
|
|
89
|
+
const still = past.filter((p) => n.deps.includes(p))
|
|
90
|
+
if (still.length > 0) kept[edge] = still
|
|
91
|
+
}
|
|
92
|
+
setStandIns(n, kept)
|
|
93
|
+
}
|
|
71
94
|
}
|
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
// until the remote answers; when every dependant hit, the server never
|
|
4
4
|
// starts (2026-10-22), as it never starts behind local hits.
|
|
5
5
|
|
|
6
|
-
import type
|
|
6
|
+
import { idleServers, type TaskNode } from '../graph/index.js'
|
|
7
7
|
import type { CacheEntry, CacheLayer } from '../cache/index.js'
|
|
8
8
|
import type { ShortCircuit } from './local-shortcircuit.js'
|
|
9
9
|
import { getContext } from './remote-prefetch.js'
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The persistent tasks nobody asked for that would be idle if the remote
|
|
13
|
+
* held every key it may hold, each with the remote keys it waits on: those
|
|
14
|
+
* above it, through the groups and servers nobody asked for between, as
|
|
15
|
+
* `idleServers` walks local hits. Held only on its direct dependants, a
|
|
16
|
+
* server behind `env: { dependsOn: ['dev'] }` or under another held server
|
|
17
|
+
* booted on a remote-only warm run.
|
|
18
|
+
*/
|
|
12
19
|
export function serversToHold(
|
|
13
20
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
14
21
|
restoreTier: ReadonlySet<string>,
|
|
@@ -23,15 +30,28 @@ export function serversToHold(
|
|
|
23
30
|
if (list === undefined) dependents.set(d, [n.id])
|
|
24
31
|
else list.push(n.id)
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const idleNow = idleServers(nodes, dependents, restoreTier)
|
|
34
|
+
const between = new Set<string>()
|
|
35
|
+
const hoped = idleServers(
|
|
36
|
+
nodes,
|
|
37
|
+
dependents,
|
|
38
|
+
new Set([...restoreTier, ...remoteAsk.keys()]),
|
|
39
|
+
between,
|
|
40
|
+
)
|
|
41
|
+
for (const id of hoped) {
|
|
42
|
+
if (idleNow.has(id)) continue
|
|
43
|
+
const remote = new Set<string>()
|
|
44
|
+
const seen = new Set<string>()
|
|
45
|
+
const stack = [id]
|
|
46
|
+
while (stack.length > 0) {
|
|
47
|
+
for (const d of dependents.get(stack.pop()!) ?? []) {
|
|
48
|
+
if (seen.has(d)) continue
|
|
49
|
+
seen.add(d)
|
|
50
|
+
if (between.has(d)) stack.push(d)
|
|
51
|
+
else if (remoteAsk.has(d) && !restoreTier.has(d)) remote.add(d)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (remote.size > 0) out.set(id, [...remote])
|
|
35
55
|
}
|
|
36
56
|
return out
|
|
37
57
|
}
|
|
@@ -63,7 +83,9 @@ export function askRemote(
|
|
|
63
83
|
}
|
|
64
84
|
const out = new Map<string, Promise<readonly string[]>>()
|
|
65
85
|
for (const [server, ds] of held) {
|
|
66
|
-
|
|
86
|
+
// A set: counted against a list naming one twice, the hits never
|
|
87
|
+
// reached its length and the server waited forever.
|
|
88
|
+
const remote = new Set(ds.filter((d) => shortCircuit.remoteAsk.has(d)))
|
|
67
89
|
out.set(
|
|
68
90
|
server,
|
|
69
91
|
new Promise((resolve) => {
|
|
@@ -72,12 +94,12 @@ export function askRemote(
|
|
|
72
94
|
void ask(d).then((hit) => {
|
|
73
95
|
if (hit === null) return resolve([])
|
|
74
96
|
hits.set(d, hit)
|
|
75
|
-
if (hits.size < remote.
|
|
97
|
+
if (hits.size < remote.size) return
|
|
76
98
|
for (const [id, entry] of hits) {
|
|
77
99
|
shortCircuit.preProbed.set(id, { hash: shortCircuit.remoteAsk.get(id)!, hit: entry })
|
|
78
100
|
shortCircuit.restoreTier.add(id)
|
|
79
101
|
}
|
|
80
|
-
resolve(remote)
|
|
102
|
+
resolve([...remote])
|
|
81
103
|
})
|
|
82
104
|
}),
|
|
83
105
|
)
|
|
@@ -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,
|
|
@@ -25,6 +24,7 @@ import type { TaskOutcome } from '../graph/index.js'
|
|
|
25
24
|
import {
|
|
26
25
|
asTrees,
|
|
27
26
|
relPosix,
|
|
27
|
+
maskStreams,
|
|
28
28
|
secretMask,
|
|
29
29
|
span,
|
|
30
30
|
splitNegations,
|
|
@@ -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
|
|
36
|
+
import { decodeOutputLog } from './output-log.js'
|
|
37
37
|
|
|
38
38
|
export interface RestoreHitArgs {
|
|
39
39
|
args: ExecuteArgs
|
|
@@ -58,9 +58,10 @@ export interface RestoreHitArgs {
|
|
|
58
58
|
export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
59
59
|
const { args, hash } = given
|
|
60
60
|
const { node } = args
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
61
|
+
const outputNested = args.outputNestedDirs ?? args.nestedProjectDirs
|
|
62
|
+
const cfgOut = node.config.outputs
|
|
63
|
+
const outputs = cfgOut?.files ?? []
|
|
64
|
+
const wsOutputs = cfgOut?.workspaceFiles ?? []
|
|
64
65
|
const anyOutputs = outputs.length > 0 || wsOutputs.length > 0
|
|
65
66
|
// A layer that handed no rows: one SELECT, read by both paths below.
|
|
66
67
|
const restore =
|
|
@@ -95,6 +96,11 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
95
96
|
? node.outputsAddedToBy.flatMap((g) => asTrees([g])).map((g) => taskGlob(g))
|
|
96
97
|
: []
|
|
97
98
|
const isAddition = (rel: string): boolean => addedGlobs.some((g) => g.match(rel))
|
|
99
|
+
const ownInputs = {
|
|
100
|
+
projectDir: node.projectDir,
|
|
101
|
+
workspaceRoot: args.workspaceRoot,
|
|
102
|
+
declared: node.config.inputs,
|
|
103
|
+
}
|
|
98
104
|
|
|
99
105
|
// "Tree is already current" short-circuit — skip cleanOutputs
|
|
100
106
|
// + restoreOutputs when the on-disk state matches what this
|
|
@@ -131,14 +137,22 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
131
137
|
.filter((e) => e.path.startsWith(WORKSPACE_OUTPUT_PREFIX))
|
|
132
138
|
.map((e) => ({ ...e, path: e.path.slice(WORKSPACE_OUTPUT_PREFIX.length) }))
|
|
133
139
|
const endGlob = span('output glob')
|
|
140
|
+
// The FIFOs, sockets and devices listed only if the tree is stale: a
|
|
141
|
+
// listing reads every directory again, ~17% of a walk.
|
|
134
142
|
const projTree = await resolveOutputTree({
|
|
135
143
|
projectDir: node.projectDir,
|
|
136
144
|
outputs,
|
|
137
|
-
nestedProjectDirs:
|
|
145
|
+
nestedProjectDirs: outputNested,
|
|
146
|
+
inputs: ownInputs,
|
|
147
|
+
special: 'later',
|
|
148
|
+
clearing: true,
|
|
138
149
|
})
|
|
139
150
|
const wsTree = await resolveWorkspaceOutputTree({
|
|
140
151
|
workspaceRoot: args.workspaceRoot,
|
|
141
152
|
outputs: wsOutputs,
|
|
153
|
+
inputs: ownInputs,
|
|
154
|
+
special: 'later',
|
|
155
|
+
clearing: true,
|
|
142
156
|
})
|
|
143
157
|
endGlob()
|
|
144
158
|
const actualAbs = projTree.files
|
|
@@ -209,10 +223,7 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
209
223
|
// Held to the rows like the run-end snapshot (item 1087): a stray
|
|
210
224
|
// written since the walk listed its directory, more than a racy
|
|
211
225
|
// window before this, is not the entry's.
|
|
212
|
-
const holds = entryHolds(
|
|
213
|
-
node,
|
|
214
|
-
outputWalkTakes(node.projectDir, outputs, args.nestedProjectDirs),
|
|
215
|
-
)
|
|
226
|
+
const holds = entryHolds(node, outputWalkTakes(node.projectDir, outputs, outputNested))
|
|
216
227
|
await args.cache.recordOutputDirs?.(hash, node.projectDir, dirPrefixes, (files) =>
|
|
217
228
|
holds(files, expected),
|
|
218
229
|
)
|
|
@@ -232,11 +243,17 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
232
243
|
const actual = await resolveOutputTree({
|
|
233
244
|
projectDir: node.projectDir,
|
|
234
245
|
outputs,
|
|
235
|
-
nestedProjectDirs:
|
|
246
|
+
nestedProjectDirs: outputNested,
|
|
247
|
+
inputs: ownInputs,
|
|
248
|
+
special: 'later',
|
|
249
|
+
clearing: true,
|
|
236
250
|
})
|
|
237
251
|
const actualWs = await resolveWorkspaceOutputTree({
|
|
238
252
|
workspaceRoot: args.workspaceRoot,
|
|
239
253
|
outputs: wsOutputs,
|
|
254
|
+
inputs: ownInputs,
|
|
255
|
+
special: 'later',
|
|
256
|
+
clearing: true,
|
|
240
257
|
})
|
|
241
258
|
endGlob()
|
|
242
259
|
skipRestore =
|
|
@@ -249,12 +266,14 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
249
266
|
const cleanArgs = {
|
|
250
267
|
projectDir: node.projectDir,
|
|
251
268
|
outputs,
|
|
252
|
-
nestedProjectDirs:
|
|
269
|
+
nestedProjectDirs: outputNested,
|
|
270
|
+
inputs: ownInputs,
|
|
253
271
|
committed: committedFiles(args.gitFilesCache, node.projectDir),
|
|
254
272
|
}
|
|
255
273
|
const wsCleanArgs = {
|
|
256
274
|
workspaceRoot: args.workspaceRoot,
|
|
257
275
|
outputs: wsOutputs,
|
|
276
|
+
inputs: ownInputs,
|
|
258
277
|
committed: committedFiles(args.gitFilesCache, args.workspaceRoot),
|
|
259
278
|
}
|
|
260
279
|
const heldRels = hit.outputFiles.filter((p) => !p.startsWith(WORKSPACE_OUTPUT_PREFIX))
|
|
@@ -269,83 +288,105 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
269
288
|
// to run first: a reader polling a 64 MB output while ten hits restored
|
|
270
289
|
// met ENOENT on 153,336 of 204,411 reads (2026-10-16). An ADDITIVE task
|
|
271
290
|
// owns only its rows (item 588), and the restore rewrites exactly those.
|
|
291
|
+
const walkArgs = { special: 'later', clearing: true } as const
|
|
272
292
|
let foundRels: string[] = []
|
|
273
293
|
let foundWsRels: string[] = []
|
|
274
294
|
let staleRels: string[] = []
|
|
275
295
|
let staleWsRels: string[] = []
|
|
276
296
|
let staleDirs: string[] = []
|
|
277
297
|
let staleWsDirs: string[] = []
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
298
|
+
// The restore writes into the tree as a command would, and a write in a
|
|
299
|
+
// cycle no window covers is the user's to `vx watch` (`sawEvent`): with
|
|
300
|
+
// none, a hit's restore started one more cycle whenever the next
|
|
301
|
+
// command opened its window more than a racy window later (CI stalls).
|
|
302
|
+
const closeWindow =
|
|
303
|
+
args.commandWindows?.open(
|
|
304
|
+
node.id,
|
|
305
|
+
node.projectDir,
|
|
306
|
+
wsOutputs.length > 0 ? 'workspace' : 'project',
|
|
307
|
+
) ?? (() => {})
|
|
308
|
+
try {
|
|
309
|
+
if (!additive) {
|
|
310
|
+
sweepRestoreTemps(node.projectDir, outputs, cleanArgs.committed)
|
|
311
|
+
sweepRestoreTemps(args.workspaceRoot, wsOutputs, wsCleanArgs.committed)
|
|
312
|
+
if (outputs.length > 0) {
|
|
313
|
+
const tree = walked?.proj ?? (await resolveOutputTree({ ...walkArgs, ...cleanArgs }))
|
|
314
|
+
// A FIFO, socket or device is found too: one the entry does not hold
|
|
315
|
+
// is a stray like any file, and one in a held path's way goes first.
|
|
316
|
+
foundRels = [...tree.files, ...(tree.findSpecial?.() ?? [])].map((f) =>
|
|
317
|
+
relPosix(node.projectDir, f),
|
|
318
|
+
)
|
|
319
|
+
const blocking = inTheWay(foundRels, heldRels)
|
|
320
|
+
staleRels = notHeld(foundRels, heldRels).filter((f) => !blocking.has(f))
|
|
321
|
+
const dirs = splitDirs(node.projectDir, tree.emptyDirs, heldRels)
|
|
322
|
+
staleDirs = dirs.stale
|
|
323
|
+
if (blocking.size > 0 || dirs.blocking.length > 0) {
|
|
324
|
+
await cleanOutputPaths({
|
|
325
|
+
projectDir: node.projectDir,
|
|
326
|
+
rels: [...blocking],
|
|
327
|
+
outputs,
|
|
328
|
+
committed: cleanArgs.committed,
|
|
329
|
+
emptyDirs: dirs.blocking,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
296
332
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
333
|
+
if (wsOutputs.length > 0) {
|
|
334
|
+
const tree =
|
|
335
|
+
walked?.ws ?? (await resolveWorkspaceOutputTree({ ...walkArgs, ...wsCleanArgs }))
|
|
336
|
+
foundWsRels = [...tree.files, ...(tree.findSpecial?.() ?? [])].map((f) =>
|
|
337
|
+
relPosix(args.workspaceRoot, f),
|
|
338
|
+
)
|
|
339
|
+
const blocking = inTheWay(foundWsRels, heldWsRels)
|
|
340
|
+
staleWsRels = notHeld(foundWsRels, heldWsRels).filter((f) => !blocking.has(f))
|
|
341
|
+
const dirs = splitDirs(args.workspaceRoot, tree.emptyDirs, heldWsRels)
|
|
342
|
+
staleWsDirs = dirs.stale
|
|
343
|
+
if (blocking.size > 0 || dirs.blocking.length > 0) {
|
|
344
|
+
await cleanOutputPaths({
|
|
345
|
+
projectDir: args.workspaceRoot,
|
|
346
|
+
rels: [...blocking],
|
|
347
|
+
outputs: wsOutputs,
|
|
348
|
+
committed: wsCleanArgs.committed,
|
|
349
|
+
emptyDirs: dirs.blocking,
|
|
350
|
+
})
|
|
351
|
+
}
|
|
313
352
|
}
|
|
314
353
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
workspaceFiles: wsOutputs,
|
|
319
|
-
})
|
|
320
|
-
if (staleRels.length > 0 || staleDirs.length > 0) {
|
|
321
|
-
await cleanOutputPaths({
|
|
322
|
-
projectDir: node.projectDir,
|
|
323
|
-
rels: staleRels,
|
|
324
|
-
emptyDirs: staleDirs,
|
|
325
|
-
outputs,
|
|
326
|
-
committed: cleanArgs.committed,
|
|
327
|
-
// A stale file in an output directory the entry holds empty takes
|
|
328
|
-
// only itself: the directory stays, as the miss left it.
|
|
329
|
-
keep: new Set(
|
|
330
|
-
heldRels
|
|
331
|
-
.filter((r) => r.endsWith('/'))
|
|
332
|
-
.map((r) => path.join(node.projectDir, r.slice(0, -1))),
|
|
333
|
-
),
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
if (staleWsRels.length > 0 || staleWsDirs.length > 0) {
|
|
337
|
-
await cleanOutputPaths({
|
|
338
|
-
projectDir: args.workspaceRoot,
|
|
339
|
-
rels: staleWsRels,
|
|
340
|
-
emptyDirs: staleWsDirs,
|
|
341
|
-
outputs: wsOutputs,
|
|
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
|
-
),
|
|
354
|
+
await args.cache.restoreOutputs(hash, node.projectDir, args.workspaceRoot, {
|
|
355
|
+
files: outputs,
|
|
356
|
+
workspaceFiles: wsOutputs,
|
|
348
357
|
})
|
|
358
|
+
if (staleRels.length > 0 || staleDirs.length > 0) {
|
|
359
|
+
await cleanOutputPaths({
|
|
360
|
+
projectDir: node.projectDir,
|
|
361
|
+
rels: staleRels,
|
|
362
|
+
emptyDirs: staleDirs,
|
|
363
|
+
outputs,
|
|
364
|
+
committed: cleanArgs.committed,
|
|
365
|
+
// A stale file in an output directory the entry holds empty takes
|
|
366
|
+
// only itself: the directory stays, as the miss left it.
|
|
367
|
+
keep: new Set(
|
|
368
|
+
heldRels
|
|
369
|
+
.filter((r) => r.endsWith('/'))
|
|
370
|
+
.map((r) => path.join(node.projectDir, r.slice(0, -1))),
|
|
371
|
+
),
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
if (staleWsRels.length > 0 || staleWsDirs.length > 0) {
|
|
375
|
+
await cleanOutputPaths({
|
|
376
|
+
projectDir: args.workspaceRoot,
|
|
377
|
+
rels: staleWsRels,
|
|
378
|
+
emptyDirs: staleWsDirs,
|
|
379
|
+
outputs: wsOutputs,
|
|
380
|
+
committed: wsCleanArgs.committed,
|
|
381
|
+
keep: new Set(
|
|
382
|
+
heldWsRels
|
|
383
|
+
.filter((r) => r.endsWith('/'))
|
|
384
|
+
.map((r) => path.join(args.workspaceRoot, r.slice(0, -1))),
|
|
385
|
+
),
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
closeWindow()
|
|
349
390
|
}
|
|
350
391
|
// The tree now holds this entry's bytes: stamp them for the next hit
|
|
351
392
|
// (item 886).
|
|
@@ -363,7 +404,7 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
363
404
|
hash,
|
|
364
405
|
projectDir: node.projectDir,
|
|
365
406
|
prefixes: dirPrefixes,
|
|
366
|
-
holds: entryHolds(node, outputWalkTakes(node.projectDir, outputs,
|
|
407
|
+
holds: entryHolds(node, outputWalkTakes(node.projectDir, outputs, outputNested)),
|
|
367
408
|
})
|
|
368
409
|
}
|
|
369
410
|
// Restored outputs changed the project's tree — but on this
|
|
@@ -399,8 +440,8 @@ export async function restoreHit(given: RestoreHitArgs): Promise<TaskOutcome> {
|
|
|
399
440
|
export function currentHit(restore: RestoreHitArgs): (() => TaskOutcome) | undefined {
|
|
400
441
|
const { args, hash, hit } = restore
|
|
401
442
|
const { node } = args
|
|
402
|
-
const outputs = node.config.
|
|
403
|
-
if ((node.config.
|
|
443
|
+
const outputs = node.config.outputs?.files ?? []
|
|
444
|
+
if ((node.config.outputs?.workspaceFiles?.length ?? 0) > 0) return undefined
|
|
404
445
|
if (outputs.length > 0) {
|
|
405
446
|
const dirPrefixes = dirPrefixesOf(outputs)
|
|
406
447
|
if (dirPrefixes === null || args.cache.outputDirsCurrent === undefined) return undefined
|
|
@@ -425,7 +466,9 @@ export function currentHit(restore: RestoreHitArgs): (() => TaskOutcome) | undef
|
|
|
425
466
|
function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
426
467
|
const { args, hash, hit, cacheOpStart, taskStartNs } = restore
|
|
427
468
|
const { node, log } = args
|
|
428
|
-
|
|
469
|
+
// Read here, once: a local entry reads its log from the index on access.
|
|
470
|
+
const stored = hit.stdout
|
|
471
|
+
if (stored) {
|
|
429
472
|
// An entry saved before masking, or by another machine, can hold a
|
|
430
473
|
// secret-named variable's value (L-11). Asked only of a hit that prints.
|
|
431
474
|
const secrets = secretMask(
|
|
@@ -433,7 +476,7 @@ function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
|
433
476
|
node.config.exec?.env?.secret,
|
|
434
477
|
)
|
|
435
478
|
// The stored log holds both streams in the order the run printed them.
|
|
436
|
-
for (const c of maskStreams(decodeOutputLog(
|
|
479
|
+
for (const c of maskStreams(decodeOutputLog(stored), secrets ?? undefined)) {
|
|
437
480
|
if (c.err) log.taskStderr(node, c.text)
|
|
438
481
|
else log.taskStdout(node, c.text)
|
|
439
482
|
}
|
|
@@ -447,7 +490,9 @@ function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
|
447
490
|
// date is fresh here, and a remote hit waited on the network and
|
|
448
491
|
// downloaded its entry, outputs or none (2026-10-24: "Up to date is
|
|
449
492
|
// a fresh. It should not require any cache").
|
|
450
|
-
|
|
493
|
+
// Assigned, not spread: one outcome per hit, and five conditional
|
|
494
|
+
// spreads were ~3 ms of an 8,002-hit warm run (2026-11-07).
|
|
495
|
+
const outcome: TaskOutcome = {
|
|
451
496
|
node,
|
|
452
497
|
status,
|
|
453
498
|
exitCode: hit.exitCode,
|
|
@@ -455,18 +500,21 @@ function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
|
455
500
|
// `storedDurationMs`, which is what the hit skipped.
|
|
456
501
|
durationMs: Math.round(performance.now() - cacheOpStart),
|
|
457
502
|
storedDurationMs: hit.durationMs,
|
|
458
|
-
...(hit.cpuMs !== undefined ? { storedCpuMs: hit.cpuMs } : {}),
|
|
459
|
-
...(hit.peakRssBytes !== undefined ? { storedPeakRssBytes: hit.peakRssBytes } : {}),
|
|
460
|
-
...(hit.sizeBytes !== undefined ? { artifactBytes: hit.sizeBytes } : {}),
|
|
461
|
-
...(hit.outputRows !== undefined
|
|
462
|
-
? { outputBytes: hit.outputRows.reduce((n, r) => n + r.size, 0) }
|
|
463
|
-
: {}),
|
|
464
|
-
...(hit.fetchMs !== undefined ? { fetchMs: hit.fetchMs } : {}),
|
|
465
503
|
hash,
|
|
466
504
|
restored: restored || hit.source === 'remote',
|
|
467
505
|
wallclockStartNs: taskStartNs,
|
|
468
506
|
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
469
507
|
}
|
|
508
|
+
if (hit.cpuMs !== undefined) outcome.storedCpuMs = hit.cpuMs
|
|
509
|
+
if (hit.peakRssBytes !== undefined) outcome.storedPeakRssBytes = hit.peakRssBytes
|
|
510
|
+
if (hit.sizeBytes !== undefined) outcome.artifactBytes = hit.sizeBytes
|
|
511
|
+
if (hit.outputRows !== undefined) {
|
|
512
|
+
let bytes = 0
|
|
513
|
+
for (const r of hit.outputRows) bytes += r.size
|
|
514
|
+
outcome.outputBytes = bytes
|
|
515
|
+
}
|
|
516
|
+
if (hit.fetchMs !== undefined) outcome.fetchMs = hit.fetchMs
|
|
517
|
+
return outcome
|
|
470
518
|
}
|
|
471
519
|
|
|
472
520
|
/**
|
|
@@ -522,7 +570,7 @@ function inTheWay(found: readonly string[], held: readonly string[]): Set<string
|
|
|
522
570
|
}
|
|
523
571
|
|
|
524
572
|
/** The found outputs the entry does not hold: the stale ones, once it has restored. */
|
|
525
|
-
type Tree = { files: string[]; emptyDirs: string[] }
|
|
573
|
+
type Tree = { files: string[]; emptyDirs: string[]; findSpecial?: () => string[] }
|
|
526
574
|
|
|
527
575
|
/**
|
|
528
576
|
* The empty directories found under the outputs (absolute) that the entry
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Module contract for `orchestrator`. Cross-module imports must come
|
|
2
2
|
// through here (enforced by tests/module-boundaries.test.ts).
|
|
3
3
|
|
|
4
|
-
export { run, planRun, nxProjectTarget, projectNamed } from './run.js'
|
|
4
|
+
export { run, planRun, nxProjectTarget, projectAtDir, projectNamed } from './run.js'
|
|
5
5
|
export { CYCLE_INTERRUPT } from './run-stop.js'
|
|
6
6
|
export {
|
|
7
7
|
namesRemote,
|
|
8
8
|
openRemotes,
|
|
9
9
|
prepareRun,
|
|
10
|
+
unlockedDir,
|
|
10
11
|
workspaceCachePolicy,
|
|
11
12
|
type PreparedRun,
|
|
12
13
|
} from './prepare.js'
|
|
@@ -16,6 +17,7 @@ export type { AffectedReason } from './affected-tasks.js'
|
|
|
16
17
|
export {
|
|
17
18
|
discoverProjects,
|
|
18
19
|
gitOfDiscovery,
|
|
20
|
+
ignoredConfigRefusal,
|
|
19
21
|
keepDiscoveryGraph,
|
|
20
22
|
loadProjects,
|
|
21
23
|
loadResolvedProjects,
|
|
@@ -27,6 +29,7 @@ export {
|
|
|
27
29
|
} from './projects.js'
|
|
28
30
|
export { computeTaskHash, createHashCache, type HashCache } from './task-hash.js'
|
|
29
31
|
export type { HeldPersistent, RunOptions, RunSummary } from './options.js'
|
|
32
|
+
export { commandWindows, type CommandWindows } from './command-windows.js'
|
|
30
33
|
// Re-surface the cache policy contract (defined in the cache module) so
|
|
31
34
|
// embedders constructing RunOptions.cache and the package façade can
|
|
32
35
|
// reach it without importing the cache module directly.
|