@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.599",
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.599",
25
- "@vzn/vx-linux-arm64": "0.0.599",
26
- "@vzn/vx-darwin-x64": "0.0.599",
27
- "@vzn/vx-darwin-arm64": "0.0.599"
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"
@@ -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
- void Promise.all([consumeChunks(stdout, false), consumeChunks(stderr, true)]).finally(() =>
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`). Every
28
- * flakiness reader is a filter over this one projection.
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
- ...(o.hash !== undefined ? { hash: o.hash } : {}),
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,
@@ -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(outcomes: readonly TaskOutcome[]): FlakyCandidate[] {
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,