@vzn/vx 0.0.599 → 0.0.601
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.601",
|
|
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.601",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.601",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.601",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.601"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/exec/runner.ts
CHANGED
|
@@ -867,8 +867,8 @@ export function runPersistent(opts: PersistentOptions): PersistentSpawn {
|
|
|
867
867
|
// Wire up readers. We deliberately don't await them — they run for
|
|
868
868
|
// the child's lifetime. The `ready` promise resolves out-of-band.
|
|
869
869
|
const [stdout, stderr] = pipes.streams(child)
|
|
870
|
-
|
|
871
|
-
pipes.close(),
|
|
870
|
+
const readers = Promise.all([consumeChunks(stdout, false), consumeChunks(stderr, true)]).finally(
|
|
871
|
+
() => pipes.close(),
|
|
872
872
|
)
|
|
873
873
|
|
|
874
874
|
opts.liveChildren?.add(child)
|
|
@@ -915,10 +915,19 @@ export function runPersistent(opts: PersistentOptions): PersistentSpawn {
|
|
|
915
915
|
|
|
916
916
|
// If the child exits BEFORE ready fires, that's a failure to start
|
|
917
917
|
// — reject the ready promise so the caller can surface it.
|
|
918
|
-
void child.exited.then((code) => {
|
|
918
|
+
void child.exited.then(async (code) => {
|
|
919
919
|
opts.liveChildren?.delete(child)
|
|
920
920
|
releaseServerGroup(child)
|
|
921
921
|
if (readyTimer !== undefined) clearTimeout(readyTimer)
|
|
922
|
+
// The exit can land before the readers took the marker it printed on
|
|
923
|
+
// the way out: `echo READY; exit` read as never ready under load. The
|
|
924
|
+
// drain is bounded, as a task's is, for a grandchild holding the pipe.
|
|
925
|
+
if (readyAt === undefined && gaveUpAt === undefined) {
|
|
926
|
+
await Promise.race([
|
|
927
|
+
readers,
|
|
928
|
+
new Promise((resolve) => setTimeout(resolve, POST_EXIT_DRAIN_MS).unref?.()),
|
|
929
|
+
])
|
|
930
|
+
}
|
|
922
931
|
if (readyAt === undefined && gaveUpAt === undefined) {
|
|
923
932
|
rejectReady(
|
|
924
933
|
new PersistentReadyError(
|
|
@@ -1554,6 +1554,12 @@ function literalReadPaths(
|
|
|
1554
1554
|
* `/**` is stripped before the compile). A task's read grant whose only
|
|
1555
1555
|
* brackets are escaped names one path too, and Linux binds it whole, so
|
|
1556
1556
|
* it gets the same subtree: `out/\[id\]` read `out/[id]` and none of it.
|
|
1557
|
+
*
|
|
1558
|
+
* A `*` or `?` meant as itself (escaped in a grant, or in a baseline's
|
|
1559
|
+
* name) has no spelling: SRT's rewrite of each runs inside a class too,
|
|
1560
|
+
* and a backslash compiles as a literal one, so `a\*.txt` granted
|
|
1561
|
+
* `a\bc.txt` and never `a*.txt`. Left out and said once, as on Linux
|
|
1562
|
+
* (`bindableReads`).
|
|
1557
1563
|
*/
|
|
1558
1564
|
export function seatbeltBrackets(
|
|
1559
1565
|
config: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2],
|
|
@@ -1563,7 +1569,9 @@ export function seatbeltBrackets(
|
|
|
1563
1569
|
if (fs === undefined) return config
|
|
1564
1570
|
const literal = (p: string): string => p.replaceAll('\\[', '[[]').replaceAll('\\]', ']')
|
|
1565
1571
|
const bracketed = new Set(names.filter((n) => /[[\]]/.test(n)))
|
|
1572
|
+
const baseline = new Set(names)
|
|
1566
1573
|
const read = (p: string): string[] => {
|
|
1574
|
+
if (!seatbeltSpells(p, 'read', baseline.has(p))) return []
|
|
1567
1575
|
const name = bracketed.has(p) ? p : /\\[[\]]/.test(p) ? namedPath(p) : undefined
|
|
1568
1576
|
if (name === undefined) return [literal(p)]
|
|
1569
1577
|
const at = name.replaceAll('[', '[[]')
|
|
@@ -1574,7 +1582,7 @@ export function seatbeltBrackets(
|
|
|
1574
1582
|
filesystem: {
|
|
1575
1583
|
...fs,
|
|
1576
1584
|
denyRead: fs.denyRead.map((p) => p.replaceAll('[', '[[]')),
|
|
1577
|
-
allowWrite: fs.allowWrite.map(literal),
|
|
1585
|
+
allowWrite: fs.allowWrite.filter((p) => seatbeltSpells(p, 'write', false)).map(literal),
|
|
1578
1586
|
...(fs.allowRead !== undefined
|
|
1579
1587
|
? { allowRead: [...new Set(fs.allowRead.flatMap(read))] }
|
|
1580
1588
|
: {}),
|
|
@@ -1582,6 +1590,28 @@ export function seatbeltBrackets(
|
|
|
1582
1590
|
}
|
|
1583
1591
|
}
|
|
1584
1592
|
|
|
1593
|
+
/** A `*` or `?` behind an odd run of backslashes: the grant means the character. */
|
|
1594
|
+
const ESCAPED_WILDCARD = /(?:^|[^\\])(?:\\\\)*\\[*?]/
|
|
1595
|
+
|
|
1596
|
+
const warnedUnspellable = new Set<string>()
|
|
1597
|
+
|
|
1598
|
+
/** Whether seatbelt's SRT can take `grant` as written; said once when not. */
|
|
1599
|
+
function seatbeltSpells(grant: string, kind: 'read' | 'write', name: boolean): boolean {
|
|
1600
|
+
const m = name ? /[*?]/.exec(grant) : ESCAPED_WILDCARD.exec(grant)
|
|
1601
|
+
if (m === null) return true
|
|
1602
|
+
const at = name ? m.index : m.index + m[0].length - 2
|
|
1603
|
+
if (!warnedUnspellable.has(`${kind} ${grant}`)) {
|
|
1604
|
+
warnedUnspellable.add(`${kind} ${grant}`)
|
|
1605
|
+
process.stderr.write(
|
|
1606
|
+
`[vx] sandbox: the ${kind} grant ${grant} names a literal ${grant[at + (name ? 0 : 1)]}, ` +
|
|
1607
|
+
`which the macOS sandbox reads as a pattern and cannot spell as a name, so it is not ` +
|
|
1608
|
+
`granted. Rename it, or grant the directory above it: ` +
|
|
1609
|
+
`${grant.slice(0, grant.lastIndexOf(path.sep, at) + 1)}\n`,
|
|
1610
|
+
)
|
|
1611
|
+
}
|
|
1612
|
+
return false
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1585
1615
|
/** SRT's wrap, with the socket lift and the git-config grant this task asked for, or none (L-6, B-41). */
|
|
1586
1616
|
function wrapForTask(
|
|
1587
1617
|
SandboxManager: SrtModule['SandboxManager'],
|
|
@@ -22,10 +22,13 @@ export type FailureMode = 'stable' | 'flaky-recoverable' | 'flaky-fatal'
|
|
|
22
22
|
/**
|
|
23
23
|
* Per-key outcome projection: every keyed (project, task, hash) under
|
|
24
24
|
* `source` (`runs`, or a relation with its `id`, `project`, `task`, `hash`,
|
|
25
|
-
* `status`, `cache_hit` columns) with how often it failed, how often it
|
|
25
|
+
* `status`, `cache_hit`, `cached` columns) with how often it failed, how often it
|
|
26
26
|
* passed — an executed success or a cache hit, which replays a success —
|
|
27
|
-
* and how often it failed AFTER its first pass (`relapses`).
|
|
28
|
-
*
|
|
27
|
+
* and how often it failed AFTER its first pass (`relapses`). A task with
|
|
28
|
+
* no cache block keys on its config alone and runs every time, so its key
|
|
29
|
+
* says nothing about its inputs: no reader counts it, as no run's row
|
|
30
|
+
* calls it flaky. Every flakiness reader is a filter over this one
|
|
31
|
+
* projection.
|
|
29
32
|
*/
|
|
30
33
|
function keyOutcomesSql(source: string, where = ''): string {
|
|
31
34
|
return `SELECT project, task, hash, SUM(failed) AS failures, SUM(passed) AS passes,
|
|
@@ -39,7 +42,7 @@ function keyOutcomesSql(source: string, where = ''): string {
|
|
|
39
42
|
CASE WHEN status = 'success' OR status IN ${HIT_STATUSES} OR cache_hit = 1
|
|
40
43
|
THEN 1 ELSE 0 END AS passed
|
|
41
44
|
FROM ${source}
|
|
42
|
-
WHERE ${KEYED_RUNS_SQL}${where}))
|
|
45
|
+
WHERE ${KEYED_RUNS_SQL} AND cached IS NOT 0${where}))
|
|
43
46
|
GROUP BY project, task, hash`
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -14,6 +14,8 @@ import { isCacheHit, taskTelemetryOf, type TaskTelemetry } from './telemetry.js'
|
|
|
14
14
|
|
|
15
15
|
export interface RunRecordsInput {
|
|
16
16
|
outcomes: readonly TaskOutcome[]
|
|
17
|
+
/** Tasks that ran behind a failed dependency (`--continue=always`). */
|
|
18
|
+
tainted?: ReadonlySet<string>
|
|
17
19
|
runId: string
|
|
18
20
|
/** Wall-clock ms at run start; every per-task ns offset is anchored to it. */
|
|
19
21
|
startedAtMs: number
|
|
@@ -66,7 +68,11 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
66
68
|
if (o.status === 'aborted') continue
|
|
67
69
|
if (input.withTelemetry) telemetryTasks.push(taskTelemetryOf(o))
|
|
68
70
|
runs.push({
|
|
69
|
-
|
|
71
|
+
// A failure behind a failed dependency is recorded keyless (`''`, which
|
|
72
|
+
// the key readers skip): the flaky list asks a key whether it failed.
|
|
73
|
+
...(o.hash !== undefined && !(o.status === 'failed' && input.tainted?.has(o.node.id) === true)
|
|
74
|
+
? { hash: o.hash }
|
|
75
|
+
: {}),
|
|
70
76
|
project: o.node.projectName,
|
|
71
77
|
task: o.node.taskName,
|
|
72
78
|
status: o.status,
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -939,6 +939,7 @@ async function runOnBus(
|
|
|
939
939
|
// dispatch, and the tracker carries it to what is built on it.
|
|
940
940
|
const taintSeeds = new Set(excluded.seeds)
|
|
941
941
|
const taint = taintTracker(options.continueMode === 'always', taintSeeds, nodes)
|
|
942
|
+
const taintedRan = new Set<string>()
|
|
942
943
|
const dependedOn = new Set<string>()
|
|
943
944
|
for (const n of nodes.values()) for (const d of n.deps) dependedOn.add(d)
|
|
944
945
|
|
|
@@ -959,6 +960,7 @@ async function runOnBus(
|
|
|
959
960
|
)
|
|
960
961
|
taintSeeds.add(node.id)
|
|
961
962
|
const tainted = taint.judge(node, upstream)
|
|
963
|
+
if (tainted) taintedRan.add(node.id)
|
|
962
964
|
return {
|
|
963
965
|
node,
|
|
964
966
|
upstream,
|
|
@@ -1027,7 +1029,7 @@ async function runOnBus(
|
|
|
1027
1029
|
const historyDb = prepared.localCache.dbHandle()
|
|
1028
1030
|
const cacheOff = cachesNothing(policy)
|
|
1029
1031
|
const judgeFlaky = (o: TaskOutcome): void => {
|
|
1030
|
-
const candidates = flakyCandidates([o])
|
|
1032
|
+
const candidates = flakyCandidates([o], taintedRan)
|
|
1031
1033
|
if (candidates.length === 0) return
|
|
1032
1034
|
try {
|
|
1033
1035
|
const found = detectFlaky(historyDb, candidates)[0]
|
|
@@ -1212,6 +1214,7 @@ async function runOnBus(
|
|
|
1212
1214
|
const recordsOf = (final: TaskOutcome[], runOk: boolean) =>
|
|
1213
1215
|
assembleRunRecords({
|
|
1214
1216
|
outcomes: final,
|
|
1217
|
+
tainted: taintedRan,
|
|
1215
1218
|
runId,
|
|
1216
1219
|
startedAtMs: endedAtMsAtStart,
|
|
1217
1220
|
endedAtMs,
|
|
@@ -1759,7 +1762,10 @@ export function projectNamed(
|
|
|
1759
1762
|
}
|
|
1760
1763
|
|
|
1761
1764
|
/** The executed, keyed outcomes of a run — what flakiness is judged on. */
|
|
1762
|
-
function flakyCandidates(
|
|
1765
|
+
function flakyCandidates(
|
|
1766
|
+
outcomes: readonly TaskOutcome[],
|
|
1767
|
+
tainted: ReadonlySet<string>,
|
|
1768
|
+
): FlakyCandidate[] {
|
|
1763
1769
|
const out: FlakyCandidate[] = []
|
|
1764
1770
|
for (const o of outcomes) {
|
|
1765
1771
|
if (o.status !== 'success' && o.status !== 'failed') continue
|
|
@@ -1768,6 +1774,9 @@ function flakyCandidates(outcomes: readonly TaskOutcome[]): FlakyCandidate[] {
|
|
|
1768
1774
|
// and runs every time, so one bad network day would read as a flake for
|
|
1769
1775
|
// thirty days. Groups do no work.
|
|
1770
1776
|
if (o.hash === undefined || o.node.config.cache === undefined || isGroupTask(o.node)) continue
|
|
1777
|
+
// Behind a failed dependency the failure is the dependency's: the key it
|
|
1778
|
+
// had passed on read as a flake for thirty days.
|
|
1779
|
+
if (o.status === 'failed' && tainted.has(o.node.id)) continue
|
|
1771
1780
|
out.push({
|
|
1772
1781
|
project: o.node.projectName,
|
|
1773
1782
|
task: o.node.taskName,
|