@vzn/vx 0.0.557 → 0.0.558
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.558",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.558",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.558",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.558",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.558"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/inputs.ts
CHANGED
|
@@ -133,6 +133,13 @@ export interface ResolveInputsArgs {
|
|
|
133
133
|
* only — global dedup so a root-level probe spawns once per run.
|
|
134
134
|
*/
|
|
135
135
|
workspaceRuntimeCache?: Map<string, Promise<string>>
|
|
136
|
+
/**
|
|
137
|
+
* Answer this task's probes apart from every other task's, in both memos
|
|
138
|
+
* above: the probes may read what its upstream wrote this run, so an
|
|
139
|
+
* answer another task took before that write is not this task's (X-34).
|
|
140
|
+
* The task id.
|
|
141
|
+
*/
|
|
142
|
+
runtimeScope?: string
|
|
136
143
|
/**
|
|
137
144
|
* Run-scoped memo for `cache.inputs.workspaceFiles`, keyed by the
|
|
138
145
|
* declaration and valid for one enumeration snapshot. A Turbo-mapped
|
|
@@ -207,14 +214,16 @@ export async function resolveInputs(args: ResolveInputsArgs): Promise<ResolvedIn
|
|
|
207
214
|
args.projectDir,
|
|
208
215
|
projectBinDirs(args.projectDir, args.workspaceRoot),
|
|
209
216
|
args.runtimeCache,
|
|
210
|
-
|
|
217
|
+
args.runtimeScope === undefined
|
|
218
|
+
? `${args.projectDir}\0`
|
|
219
|
+
: `${args.projectDir}\0${args.runtimeScope}\0`,
|
|
211
220
|
),
|
|
212
221
|
resolveRuntimeValues(
|
|
213
222
|
wsRuntimeDecl,
|
|
214
223
|
args.workspaceRoot,
|
|
215
224
|
[path.join(args.workspaceRoot, 'node_modules', '.bin')],
|
|
216
225
|
args.workspaceRuntimeCache,
|
|
217
|
-
''
|
|
226
|
+
args.runtimeScope === undefined ? '' : `\0${args.runtimeScope}\0`,
|
|
218
227
|
),
|
|
219
228
|
])
|
|
220
229
|
return {
|
|
@@ -167,6 +167,8 @@ export interface ExecuteArgs {
|
|
|
167
167
|
gitFilesCache?: GitFilesCache
|
|
168
168
|
/** Per-run memo for derived hashes (package.json bytes + task config). */
|
|
169
169
|
hashCache?: HashCache
|
|
170
|
+
/** The run's `probesAfterWrites`, handed to every key this task takes. */
|
|
171
|
+
probesAfterWrites?: ReadonlySet<string>
|
|
170
172
|
/**
|
|
171
173
|
* Up-front probe result from the local short-circuit classify, when
|
|
172
174
|
* this task was stable + cacheable + local-read. Reused here so there
|
|
@@ -715,6 +717,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
715
717
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
716
718
|
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
717
719
|
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
720
|
+
...(args.probesAfterWrites !== undefined
|
|
721
|
+
? { probesAfterWrites: args.probesAfterWrites }
|
|
722
|
+
: {}),
|
|
718
723
|
}))
|
|
719
724
|
|
|
720
725
|
// The local no-op half of `exec.remote: 'only'`: no remote executor took
|
|
@@ -814,6 +819,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
814
819
|
captureInto: captured,
|
|
815
820
|
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
816
821
|
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
822
|
+
...(args.probesAfterWrites !== undefined
|
|
823
|
+
? { probesAfterWrites: args.probesAfterWrites }
|
|
824
|
+
: {}),
|
|
817
825
|
})
|
|
818
826
|
: undefined
|
|
819
827
|
// A name only `exec.env.secret` makes secret is not one the name rule
|
|
@@ -50,7 +50,7 @@ export function keyedProjects(
|
|
|
50
50
|
continue
|
|
51
51
|
}
|
|
52
52
|
if (deps === undefined) {
|
|
53
|
-
frame[1] =
|
|
53
|
+
frame[1] = foldedDeps(node, nodeOf)
|
|
54
54
|
for (const { node: dep } of frame[1]) if (!below.has(dep.id)) stack.push([dep, undefined])
|
|
55
55
|
continue
|
|
56
56
|
}
|
|
@@ -75,7 +75,7 @@ export function keyedProjects(
|
|
|
75
75
|
* edges and lacked the dropped ones: the sandbox granted a sibling the key
|
|
76
76
|
* does not answer for.
|
|
77
77
|
*/
|
|
78
|
-
function
|
|
78
|
+
export function foldedDeps(node: TaskNode, nodeOf: (id: string) => TaskNode): FoldCandidate[] {
|
|
79
79
|
const candidates: FoldCandidate[] = []
|
|
80
80
|
for (const id of keyedDeps(node)) {
|
|
81
81
|
const dep = nodeOf(id)
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
} from './run-context.js'
|
|
87
87
|
import { startRemotePrefetch } from './remote-prefetch.js'
|
|
88
88
|
import { startLocalShortCircuit, type ShortCircuit } from './local-shortcircuit.js'
|
|
89
|
+
import { probesAfterWrites } from './stable-keys.js'
|
|
89
90
|
|
|
90
91
|
import { assembleRunRecords } from './run-records.js'
|
|
91
92
|
import { hasEnded, selectKeepAlive, shutdownPersistent } from './persistent.js'
|
|
@@ -927,6 +928,7 @@ async function runOnBus(
|
|
|
927
928
|
// when a sink exists.
|
|
928
929
|
const explainMiss =
|
|
929
930
|
telemetry === undefined ? undefined : createMissExplainer(prepared.localCache.dbHandle())
|
|
931
|
+
const lateProbes = probesAfterWrites(nodes, workspaceRoot)
|
|
930
932
|
const buildExecuteArgs = (node: TaskNode, upstream: TaskOutcome[], reuseProbe = true) => {
|
|
931
933
|
const probe = reuseProbe ? shortCircuit.preProbed.get(node.id) : undefined
|
|
932
934
|
const upfrontKey = shortCircuit.uncachedKeys.get(node.id)
|
|
@@ -961,6 +963,7 @@ async function runOnBus(
|
|
|
961
963
|
...(explainMiss !== undefined ? { explainMiss } : {}),
|
|
962
964
|
gitFilesCache,
|
|
963
965
|
hashCache,
|
|
966
|
+
probesAfterWrites: lateProbes,
|
|
964
967
|
...(probe !== undefined ? { preProbed: probe } : {}),
|
|
965
968
|
...(upfrontKey !== undefined ? { upfrontKey } : {}),
|
|
966
969
|
...(tainted ? { taintedUpstream: true } : {}),
|
|
@@ -988,6 +991,7 @@ async function runOnBus(
|
|
|
988
991
|
nestedDirsByProject,
|
|
989
992
|
gitFilesCache,
|
|
990
993
|
hashCache,
|
|
994
|
+
probesAfterWrites: lateProbes,
|
|
991
995
|
},
|
|
992
996
|
buildExecuteArgs,
|
|
993
997
|
})
|
|
@@ -15,6 +15,7 @@ import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
|
15
15
|
import type { CacheLayer, GitFilesCache } from '../cache/index.js'
|
|
16
16
|
import { isLiteralPattern, normalizeGlob, relPosix } from '../util/index.js'
|
|
17
17
|
import { commandWriteReach, mayWriteFingerprint, undeclaredWriteReach } from './sandbox-request.js'
|
|
18
|
+
import { foldedDeps } from './keyed-projects.js'
|
|
18
19
|
import { computeGroupKey, computeTaskHash, type HashCache } from './task-hash.js'
|
|
19
20
|
import { filterUpstreamHashes, keyUpstream } from './upstream.js'
|
|
20
21
|
|
|
@@ -53,6 +54,7 @@ export interface StableKey {
|
|
|
53
54
|
*/
|
|
54
55
|
export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<StableKey[]> {
|
|
55
56
|
const order = topoOrder(args.nodes)
|
|
57
|
+
const late = probesAfterWrites(args.nodes, args.workspaceRoot)
|
|
56
58
|
const keyById = new Map<string, string>()
|
|
57
59
|
const unstableById = new Set<string>()
|
|
58
60
|
// Transitive-upstream output producers, accumulated in topo order — the
|
|
@@ -203,6 +205,13 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
|
|
|
203
205
|
if (unfolded !== undefined) unfoldedById.set(id, unfolded)
|
|
204
206
|
if (wsRewriter) wsRewriters.add(id)
|
|
205
207
|
|
|
208
|
+
// Its probes answer only once its writers have run, so it has no key
|
|
209
|
+
// up front, and its dependants inherit that.
|
|
210
|
+
if (late.has(id)) {
|
|
211
|
+
unstableById.add(id)
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
if (isGroupTask(node)) {
|
|
207
216
|
// Groups have no exec/cache; they only fold upstream keys so
|
|
208
217
|
// dependents that filter inputs.tasks through the group still
|
|
@@ -271,6 +280,86 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
|
|
|
271
280
|
return stableKeys
|
|
272
281
|
}
|
|
273
282
|
|
|
283
|
+
const probesAfterWritesOf = new WeakMap<ReadonlyMap<string, TaskNode>, ReadonlySet<string>>()
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The cached tasks whose `cache.inputs.runtime` / `workspaceRuntime` probes
|
|
287
|
+
* may read what an upstream writes this run while the key folds nothing of
|
|
288
|
+
* that upstream (X-34). A probe is a shell command and may read anywhere,
|
|
289
|
+
* so no glob bounds it, and its answer taken before the writer ran is the
|
|
290
|
+
* previous run's bytes. Such a task is never stable (`deriveStableKeys`
|
|
291
|
+
* takes no key for it), and its probes are answered for it alone, after
|
|
292
|
+
* its upstream ran (`ComputeHashArgs.probesAfterWrites`).
|
|
293
|
+
*
|
|
294
|
+
* A writer is any upstream whose command may write (`commandWriteReach`)
|
|
295
|
+
* or that declares outputs. A key that folds the writer's covers what it
|
|
296
|
+
* writes, as for a rewriter in `deriveStableKeys`: an early answer then
|
|
297
|
+
* costs a spurious miss on the run after the bytes change, never a stale
|
|
298
|
+
* hit, and such a probe keeps the run's shared answer — one spawn per run,
|
|
299
|
+
* not per task. So only a writer the key does not fold, transitively
|
|
300
|
+
* (`tasks: []`, a filter that leaves it out, an order-only edge), makes
|
|
301
|
+
* the probe wait.
|
|
302
|
+
*
|
|
303
|
+
* Once per graph: run, admission and both up-front passes ask the same map.
|
|
304
|
+
*/
|
|
305
|
+
export function probesAfterWrites(
|
|
306
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
307
|
+
workspaceRoot: string,
|
|
308
|
+
): ReadonlySet<string> {
|
|
309
|
+
const memo = probesAfterWritesOf.get(nodes)
|
|
310
|
+
if (memo !== undefined) return memo
|
|
311
|
+
const late = new Set<string>()
|
|
312
|
+
probesAfterWritesOf.set(nodes, late)
|
|
313
|
+
const probes = (n: TaskNode): boolean =>
|
|
314
|
+
(n.config.cache?.inputs?.runtime?.length ?? 0) > 0 ||
|
|
315
|
+
(n.config.cache?.inputs?.workspaceRuntime?.length ?? 0) > 0
|
|
316
|
+
let anyProbe = false
|
|
317
|
+
for (const n of nodes.values()) {
|
|
318
|
+
if (probes(n)) {
|
|
319
|
+
anyProbe = true
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!anyProbe) return late
|
|
324
|
+
// A sandbox with no write grant still writes the declared outputs. A
|
|
325
|
+
// fingerprinted file is only writable where `commandWriteReach` is not
|
|
326
|
+
// 'none' already (`mayWriteFingerprint`).
|
|
327
|
+
const writers = new Set<string>()
|
|
328
|
+
for (const n of nodes.values()) {
|
|
329
|
+
if (
|
|
330
|
+
commandWriteReach(n, workspaceRoot) !== 'none' ||
|
|
331
|
+
(n.config.cache?.outputs.files.length ?? 0) > 0 ||
|
|
332
|
+
(n.config.cache?.outputs.workspaceFiles?.length ?? 0) > 0
|
|
333
|
+
)
|
|
334
|
+
writers.add(n.id)
|
|
335
|
+
}
|
|
336
|
+
const nodeOf = (id: string): TaskNode => nodes.get(id)!
|
|
337
|
+
// Any writer runs before the task; one its key does not fold does.
|
|
338
|
+
const writerBelow = new Set<string>()
|
|
339
|
+
const unfoldedBelow = new Set<string>()
|
|
340
|
+
for (const id of topoOrder(nodes)) {
|
|
341
|
+
const node = nodes.get(id)!
|
|
342
|
+
let writer = false
|
|
343
|
+
let unfolded = false
|
|
344
|
+
// No filter and no order-only edge: the key folds every dependency.
|
|
345
|
+
const foldsAll = node.orderOnly === undefined && node.config.cache?.inputs?.tasks === undefined
|
|
346
|
+
let folds: Set<string> | undefined
|
|
347
|
+
for (const dep of node.deps) {
|
|
348
|
+
if (!nodes.has(dep)) continue
|
|
349
|
+
const depWrites = writerBelow.has(dep) || writers.has(dep)
|
|
350
|
+
if (depWrites) writer = true
|
|
351
|
+
if (unfolded || !depWrites) continue
|
|
352
|
+
const folded =
|
|
353
|
+
foldsAll || (folds ??= new Set(foldedDeps(node, nodeOf).map((c) => c.node.id))).has(dep)
|
|
354
|
+
unfolded = folded ? unfoldedBelow.has(dep) : true
|
|
355
|
+
}
|
|
356
|
+
if (writer) writerBelow.add(id)
|
|
357
|
+
if (unfolded) unfoldedBelow.add(id)
|
|
358
|
+
if (unfolded && node.config.cache !== undefined && probes(node)) late.add(id)
|
|
359
|
+
}
|
|
360
|
+
return late
|
|
361
|
+
}
|
|
362
|
+
|
|
274
363
|
/**
|
|
275
364
|
* Does this task's key fold the dependency's? What `computeTaskHash` folds
|
|
276
365
|
* (`filterUpstreamHashes` over the same synthetic outcomes); a group folds
|
|
@@ -519,7 +608,7 @@ class ProjectSet implements ProjectNames {
|
|
|
519
608
|
}
|
|
520
609
|
}
|
|
521
610
|
|
|
522
|
-
function topoOrder(nodes:
|
|
611
|
+
function topoOrder(nodes: ReadonlyMap<string, TaskNode>): string[] {
|
|
523
612
|
const indegree = new Map<string, number>()
|
|
524
613
|
const dependents = new Map<string, string[]>()
|
|
525
614
|
for (const node of nodes.values()) {
|
|
@@ -88,6 +88,12 @@ export interface ComputeHashArgs {
|
|
|
88
88
|
* site, so the recorded set can't drift from the key.
|
|
89
89
|
*/
|
|
90
90
|
captureInto?: TaskInputComponent[]
|
|
91
|
+
/**
|
|
92
|
+
* The run's tasks whose runtime probes may read an upstream's writes
|
|
93
|
+
* (`probesAfterWrites`): such a task's probes are answered for it alone,
|
|
94
|
+
* after its upstream ran, never from the run's shared memo.
|
|
95
|
+
*/
|
|
96
|
+
probesAfterWrites?: ReadonlySet<string>
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/**
|
|
@@ -268,6 +274,7 @@ async function resolveKeyInput(args: ComputeHashArgs): Promise<CacheKeyInput> {
|
|
|
268
274
|
ownWorkspaceOutputs: cacheCfg?.outputs.workspaceFiles ?? [],
|
|
269
275
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
270
276
|
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
277
|
+
...(args.probesAfterWrites?.has(args.node.id) === true ? { runtimeScope: args.node.id } : {}),
|
|
271
278
|
...(args.hashCache !== undefined
|
|
272
279
|
? {
|
|
273
280
|
runtimeCache: args.hashCache.runtime,
|