@vzn/vx 0.0.66 → 0.0.68
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.68",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.68",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.68",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.68",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.68"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch.ts
CHANGED
|
@@ -292,7 +292,13 @@ export function pollWatcher(
|
|
|
292
292
|
}
|
|
293
293
|
if (!e.isFile()) continue
|
|
294
294
|
try {
|
|
295
|
-
|
|
295
|
+
// The later of the two clocks, as `modifiedBefore` reads them: a
|
|
296
|
+
// replacement that carries the old file's mtime (`cp -p`, `rsync
|
|
297
|
+
// -a`, `mv` of a file stamped the same) moved nothing under mtime
|
|
298
|
+
// alone, and the poller never ran it where the native watcher did.
|
|
299
|
+
// A rename or a write moves ctime, and no process can set it.
|
|
300
|
+
const st = fs.statSync(path.join(abs, e.name))
|
|
301
|
+
current.set(childRel, Math.max(st.mtimeMs, st.ctimeMs))
|
|
296
302
|
} catch {
|
|
297
303
|
// raced with a delete; the next scan settles it
|
|
298
304
|
}
|
package/src/exec/executor.ts
CHANGED
|
@@ -190,7 +190,8 @@ export interface TaskExecutor {
|
|
|
190
190
|
/**
|
|
191
191
|
* How many tasks this executor runs at once. Its tasks then occupy a pool
|
|
192
192
|
* of this size instead of the local worker slots, so a remote pool is not
|
|
193
|
-
* throttled by the local CPU count.
|
|
193
|
+
* throttled by the local CPU count. A positive integer; anything else
|
|
194
|
+
* is refused when the executor is resolved. Absent = the local pool.
|
|
194
195
|
*/
|
|
195
196
|
readonly capacity?: number
|
|
196
197
|
/** Per-task opt-out at placement time. Absent = accepts every task it is offered. */
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -76,6 +76,13 @@ export interface TaskOutcome {
|
|
|
76
76
|
* outcomes only; never folded into any key.
|
|
77
77
|
*/
|
|
78
78
|
groupUpstream?: readonly TaskOutcome[]
|
|
79
|
+
/**
|
|
80
|
+
* This task RAN over inputs its key no longer describes: one moved while
|
|
81
|
+
* it ran, the workspace fingerprint moved, or an upstream carried this
|
|
82
|
+
* mark. Its save was withheld, and its outputs are not what `hash` names,
|
|
83
|
+
* so a dependant that folds `hash` saves nothing either (A-12).
|
|
84
|
+
*/
|
|
85
|
+
unkeyed?: true
|
|
79
86
|
/**
|
|
80
87
|
* On a `skipped` outcome: the task at the ROOT of what blocked it — the
|
|
81
88
|
* failed (or aborted) upstream, followed through any chain of skips
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
type TaskInputComponent,
|
|
63
63
|
} from './task-hash.js'
|
|
64
64
|
import { getContext } from './remote-prefetch.js'
|
|
65
|
+
import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
|
|
65
66
|
|
|
66
67
|
export interface ExecuteArgs {
|
|
67
68
|
node: TaskNode
|
|
@@ -412,8 +413,27 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
412
413
|
const willRead = !remoteOnly && cfgCacheable && (policy.localRead || policy.remoteRead)
|
|
413
414
|
const willWrite = !remoteOnly && cfgCacheable && (policy.localWrite || policy.remoteWrite)
|
|
414
415
|
// `willWrite` governs output hygiene (clean before exec); the save itself
|
|
415
|
-
// is also withheld from a task downstream of a failure (see `taintedUpstream`)
|
|
416
|
-
|
|
416
|
+
// is also withheld from a task downstream of a failure (see `taintedUpstream`),
|
|
417
|
+
// and from one downstream of a task that ran over inputs its key no longer
|
|
418
|
+
// describes: this key folds that upstream's key, while the bytes on disk
|
|
419
|
+
// are not the ones that key names. With `a.txt` edited while `gen` ran,
|
|
420
|
+
// `gen` withheld its save, `use` saved over the edit under a key folding
|
|
421
|
+
// `gen`'s, and once `a.txt` was put back `use` hit the edit's output (A-12).
|
|
422
|
+
// Only an upstream whose key this one folds: one read by content
|
|
423
|
+
// (`tasks: []` and a file input) is judged by its bytes. `upstream` holds
|
|
424
|
+
// a hole where a dependency did not run (a skipped one, a restore-tier
|
|
425
|
+
// task's unfinished deps).
|
|
426
|
+
const present = upstream.filter((u): u is TaskOutcome => u !== undefined)
|
|
427
|
+
const folded = new Set(
|
|
428
|
+
filterUpstreamHashes(present, cacheCfg?.inputs?.tasks, node.projectName, node.id).map(
|
|
429
|
+
([id]) => id,
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
const unkeyedUpstream = expandGroupUpstream(present.filter((u) => folded.has(u.node.id))).find(
|
|
433
|
+
(u) => u.unkeyed === true,
|
|
434
|
+
)
|
|
435
|
+
let unkeyed = unkeyedUpstream !== undefined
|
|
436
|
+
const willSave = willWrite && args.taintedUpstream !== true && !unkeyed
|
|
417
437
|
|
|
418
438
|
// Retain only what is read back. `cache.save` below is the single consumer
|
|
419
439
|
// of `result.stdout`, and it runs only when this task will WRITE an entry;
|
|
@@ -968,7 +988,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
968
988
|
* withholds it too: the key folded the old one.
|
|
969
989
|
*/
|
|
970
990
|
async function keyStillTrue(): Promise<boolean> {
|
|
971
|
-
if (fingerprintMoved())
|
|
991
|
+
if (fingerprintMoved()) {
|
|
992
|
+
unkeyed = true
|
|
993
|
+
return false
|
|
994
|
+
}
|
|
972
995
|
const endCheck = span('miss: recheck inputs')
|
|
973
996
|
const moved = await movedSinceKey()
|
|
974
997
|
endCheck()
|
|
@@ -979,9 +1002,17 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
979
1002
|
`but is not saved under a key that no longer describes it`,
|
|
980
1003
|
)
|
|
981
1004
|
forgetUndeclaredWrites(args, wsOutputs.length > 0 ? 'workspace' : 'project')
|
|
1005
|
+
unkeyed = true
|
|
982
1006
|
return false
|
|
983
1007
|
}
|
|
984
1008
|
|
|
1009
|
+
if (unkeyedUpstream !== undefined && willWrite && effectiveExitCode === 0) {
|
|
1010
|
+
log.status(
|
|
1011
|
+
`[vx] ${node.id}: ran over ${unkeyedUpstream.node.id}'s outputs, which its key no longer ` +
|
|
1012
|
+
`describes — the result stands, but is not saved`,
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
985
1016
|
const finalViolations = violations
|
|
986
1017
|
|
|
987
1018
|
return {
|
|
@@ -991,6 +1022,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
991
1022
|
durationMs: spentMs,
|
|
992
1023
|
hash,
|
|
993
1024
|
...(attempt > 1 ? { attempts: attempt } : {}),
|
|
1025
|
+
...(unkeyed ? { unkeyed: true as const } : {}),
|
|
994
1026
|
...(result.timedOut === true && effectiveExitCode !== 0 ? { timedOut: true as const } : {}),
|
|
995
1027
|
...(result.cpuMs !== undefined ? { cpuMs: result.cpuMs } : {}),
|
|
996
1028
|
...(result.peakRssBytes !== undefined ? { peakRssBytes: result.peakRssBytes } : {}),
|
|
@@ -419,6 +419,13 @@ export async function resolveExecutors(
|
|
|
419
419
|
if (typeof executor.name !== 'string' || executor.name.length === 0) {
|
|
420
420
|
throw new UserError(`plugin '${plugin.name}' returned an executor with no name`)
|
|
421
421
|
}
|
|
422
|
+
// The pool's room is `active < capacity`: 0 or NaN parks its tasks for good.
|
|
423
|
+
const capacity: unknown = executor.capacity
|
|
424
|
+
if (capacity !== undefined && !(Number.isInteger(capacity) && (capacity as number) > 0)) {
|
|
425
|
+
throw new UserError(
|
|
426
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with capacity ${typeof capacity === 'number' ? String(capacity) : JSON.stringify(capacity)}: it must be a positive integer`,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
422
429
|
executorPlugin.set(executor, plugin.name)
|
|
423
430
|
executors.push(executor)
|
|
424
431
|
}
|