@vzn/vx 0.0.429 → 0.0.431

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.429",
3
+ "version": "0.0.431",
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.429",
25
- "@vzn/vx-linux-arm64": "0.0.429",
26
- "@vzn/vx-darwin-x64": "0.0.429",
27
- "@vzn/vx-darwin-arm64": "0.0.429"
24
+ "@vzn/vx-linux-x64": "0.0.431",
25
+ "@vzn/vx-linux-arm64": "0.0.431",
26
+ "@vzn/vx-darwin-x64": "0.0.431",
27
+ "@vzn/vx-darwin-arm64": "0.0.431"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -340,7 +340,8 @@ export function gitFiles(workspaceRoot: string): Set<string> | undefined {
340
340
  const files = new Set<string>()
341
341
  for (const p of new TextDecoder().decode(proc.stdout).split('\0')) {
342
342
  if (p.length === 0) continue
343
- let abs = path.join(workspaceRoot, p)
343
+ // An untracked nested repository is listed as `dir/`.
344
+ let abs = path.join(workspaceRoot, p.endsWith('/') ? p.slice(0, -1) : p)
344
345
  files.add(abs)
345
346
  for (abs = path.dirname(abs); abs !== workspaceRoot && !files.has(abs); abs = path.dirname(abs))
346
347
  files.add(abs)
@@ -111,8 +111,8 @@ export class ChangeJudge {
111
111
  // says which side of the arm it belongs to. A path already gone is a
112
112
  // change: a deletion has no date to read.
113
113
  // One exception: a path git did not list at the arm, and gone now, was
114
- // born and removed since (vim's `4913` write probe, a tool's lock file);
115
- // it started a cycle with nothing changed. The blind spot: one born in
114
+ // born and removed since (vim's `4913` write probe, a tool's lock file),
115
+ // unless a nested repository holds it (`inNestedRepo`); it started a cycle with nothing changed. The blind spot: one born in
116
116
  // the moment between a judgement and that cycle's keys, and gone by the
117
117
  // next judgement, was read and its deletion re-runs nothing. Git lists
118
118
  // no ignored path, so one of those is a deletion as before.
@@ -123,11 +123,21 @@ export class ChangeJudge {
123
123
  if (prev !== undefined) return prev === state
124
124
  if (state === ABSENT) {
125
125
  const listed = this.ctx.existedAtArm
126
- return !ignored && listed !== undefined && !listed.has(abs)
126
+ return !ignored && listed !== undefined && !listed.has(abs) && !this.inNestedRepo(abs)
127
127
  }
128
128
  return modifiedBefore(abs, this.ctx.armedAt)
129
129
  }
130
130
 
131
+ // Git lists a nested repository (a submodule, a vendored clone) as one
132
+ // entry, never the files in it, and keys read them: under one, a gone
133
+ // path the arm did not list may have existed, so it is a deletion.
134
+ private inNestedRepo(abs: string): boolean {
135
+ const root = this.ctx.workspaceRoot
136
+ for (let dir = path.dirname(abs); dir !== root && dir.startsWith(root); dir = path.dirname(dir))
137
+ if (fs.existsSync(path.join(dir, '.git'))) return true
138
+ return false
139
+ }
140
+
131
141
  private writtenDuringLastCycle(abs: string, openWhileHeld = false): boolean {
132
142
  // A server the last cycle left running is still that cycle's: its
133
143
  // writes land after the cycle ended, and with a closed window a dev
@@ -278,9 +278,15 @@ export function selectExecutor(
278
278
  for (const executor of executors) {
279
279
  if (task.pinnedLocal && executor.remote === true) continue
280
280
  if (executor.accepts === undefined) return executor
281
- let taken: boolean
281
+ let taken: unknown
282
282
  try {
283
283
  taken = executor.accepts(task)
284
+ // An `async accepts` answers a Promise: truthy, so the executor took
285
+ // every task, and a rejection went unheard (the shape of H-16's admit).
286
+ if (taken instanceof Promise) {
287
+ taken.catch(() => {})
288
+ throw new Error('returned a Promise; accepts is synchronous')
289
+ }
284
290
  } catch (err) {
285
291
  const m = err instanceof Error ? err.message : String(err)
286
292
  throw new UserError(`${label(executor)} failed in accepts for ${task.taskId}: ${m}`)
@@ -500,13 +500,20 @@ async function runOnBus(
500
500
  // it surfaced as a bare stack, or mid-run from the completion path
501
501
  // (item 1022).
502
502
  const tellDemand = (executor: TaskExecutor, remaining: ReadonlySet<string>): void => {
503
- try {
504
- executor.demand!(remaining)
505
- } catch (err) {
506
- demandOf.delete(executor)
503
+ const failed = (err: unknown): void => {
504
+ if (!demandOf.delete(executor)) return
507
505
  const m = err instanceof Error ? err.message : String(err)
508
506
  log.status(`[vx] ${executorLabel(executor)} failed in demand: ${m}; not asked again this run`)
509
507
  }
508
+ try {
509
+ // An `async demand` that rejects did so where no one listened: an
510
+ // unhandled rejection, a stack, the run killed. Its rejection is a
511
+ // throw like any other.
512
+ const ret: unknown = executor.demand!(remaining)
513
+ if (ret instanceof Promise) ret.catch(failed)
514
+ } catch (err) {
515
+ failed(err)
516
+ }
510
517
  }
511
518
  for (const [executor, remaining] of demandOf) tellDemand(executor, remaining)
512
519
  const narrowDemand =
@@ -388,9 +388,11 @@ export function createTelemetrySource(args: {
388
388
  // Never silently. The standing rule is that a never-fail path must still
389
389
  // WARN — telemetry that vanishes without a word is indistinguishable from
390
390
  // telemetry nobody configured.
391
- // Every hook call site skips a disabled sink first, so this runs at most
392
- // once per sink without a guard of its own (item 654).
391
+ // Every hook call site skips a disabled sink first (item 654), but an
392
+ // async hook's rejections land later, several records' worth of them
393
+ // after the first: those are said once too.
393
394
  const disable = (sink: TelemetrySink, hook: string, err: unknown): void => {
395
+ if (disabled.has(sink)) return
394
396
  disabled.add(sink)
395
397
  warn?.(
396
398
  `[vx] telemetry sink '${label(sink)}' threw in ${hook}; disabled for this run: ${err instanceof Error ? err.message : String(err)}`,
@@ -406,7 +408,10 @@ export function createTelemetrySource(args: {
406
408
  const kinds = sink.wants ?? DEFAULT_KINDS
407
409
  if (!kinds.includes(record.kind)) continue
408
410
  try {
409
- sink.onRecord(record)
411
+ // Typed `void`, yet an `async onRecord` rejects where no one
412
+ // listened: an unhandled rejection killed the run (C-101).
413
+ const ret: unknown = sink.onRecord(record)
414
+ if (ret instanceof Promise) ret.catch((err) => disable(sink, 'onRecord', err))
410
415
  } catch (err) {
411
416
  disable(sink, 'onRecord', err)
412
417
  }
@@ -483,7 +488,8 @@ export function createTelemetrySource(args: {
483
488
  for (const sink of sinks) {
484
489
  if (disabled.has(sink) || sink.onRunSummary === undefined) continue
485
490
  try {
486
- sink.onRunSummary(summary)
491
+ const ret: unknown = sink.onRunSummary(summary)
492
+ if (ret instanceof Promise) ret.catch((err) => disable(sink, 'onRunSummary', err))
487
493
  } catch (err) {
488
494
  disable(sink, 'onRunSummary', err)
489
495
  }
@@ -504,7 +504,10 @@ export async function loadProjectConfigs(
504
504
  ? []
505
505
  : [
506
506
  ...restoreBuiltins(builtins, overlapping),
507
- ...restoreEnv(env),
507
+ // Overlapping, the env is checked once at the round's end, as most
508
+ // built-ins are: reading all of it after every load was 15–25 µs a
509
+ // config (149 variables, compiled, 2026-10-03).
510
+ ...(overlapping ? [] : restoreEnv(env)),
508
511
  ...restoreCwd(cwd),
509
512
  // Alone in the round: nothing else reads the umask now. Beside
510
513
  // other loads the round reads it once they are done (see
@@ -591,7 +594,7 @@ export async function loadProjectConfigs(
591
594
  ...restoreUmask(umask),
592
595
  ]
593
596
  : []
594
- // Overlapping loads check most built-ins only here, so a change one
597
+ // Overlapping loads check most built-ins and the env only here, so a change one
595
598
  // config made may have broken another's load: it is refused first, and
596
599
  // nothing the round evaluated is stored.
597
600
  const changedInRound =