@vzn/vx 0.0.600 → 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.600",
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.600",
25
- "@vzn/vx-linux-arm64": "0.0.600",
26
- "@vzn/vx-darwin-x64": "0.0.600",
27
- "@vzn/vx-darwin-arm64": "0.0.600"
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(
@@ -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,