Skip to content
vxvx
GitHubBlueskydev.toRSS
Releases

Runs follow the lock, workers end on time

A run reads every project from the lock, turbo() and nx() mappings too. Distributed task execution ends at the critical path on less CPU and says what it held. The sandbox refuses a keyed file it would hide.

vx 0.0.654: Runs follow the lock, workers end on time

A run now reads every project, edge and mapped task from the lock, and nothing else.

Distributed task execution ends when the critical path does, holds less CPU, and says how much it held.

A sandboxed task can no longer pass while blind to a file its key holds.

This release is breaking. Read the breaking changes first.

In this release

vx lock froze each vx.config.ts, but a plugin’s project and discover stages still ran on every run.

With turbo() or nx(), a run read turbo.json, the manifests and git HEAD again, so what ran could differ from what the lock held.

Now vx lock runs both stages and records what they return: each project, its edges, and every task a plugin maps.

A run calls no project or discover hook and reads only the lock. After you change a file a plugin reads, run vx lock; vx lock --check names what moved.

Terminal window
$ vx lock --check
✗ vx lock --check packages/app: the `project` stage maps it differently from the lock here — a file the plugin reads, or the environment, changed; run 'vx lock'
tasks.build.inputs.files[1]: "package.json" → "tsconfig.json"
tasks.build.inputs.files[2]: (none) → "package.json"

--affected selects a project whose locked config, name or edges changed.

See vx lock.

A run with known task times could still end past its critical path, and a big worker stayed up for one small task while most of its cores sat idle.

Now a run ends by its critical path plus one warm-up. When a big worker’s big work is over, its last task moves to a smaller worker that joins just then.

Workers needed at once start as fewer, bigger ones, and a task goes only on a size with room for its cores and memory.

The footer weighs a run’s cost. ideal adds the least CPU a run ending then holds; total adds the CPU the workers held, idle or not, warm-up included.

Terminal window
$ VX_DTE_RUN=$(uuidgen) vx run build --all
workers linux.small#1 stopped 3 done · warm-up 0.1s
cpu peak 0.0 · used avg 0.0 cores · machine avg 0.1 · booked avg 0.3
cpu utilization machine 5% · tasks 0% · worker 2%
ideal 3.2s: each task as long as it took, placed perfectly, holding 0.3 core-s at least
best 1 × linux.small (2c/8G linux-x64): 3.2s, the fewest of its sizes
any 1 × (2c/8G linux-x64): 3.2s, of any cores, the least core time
total cpu utilization machine 5% · tasks 0% · worker 2% · held 6.4 core-s

When a run ends late, a drift line says why, along the chain that ended it: workers joining, waiting for room, sending inputs, and time after the last task.

See Distributed task execution.

A sandboxed task that keyed tsconfig.base.json at the workspace root, with no read grant for it, ran blind to it.

A tool that stats a config before it opens it left no violation, so the task passed and its wrong output was cached.

Now vx checks the key’s files outside the project against the sandbox’s grants before the command starts. A file no grant reaches fails the task, unrun, with the grant to add.

Terminal window
$ vx run build --filter packages/lib
┌─ packages/lib#build > failed (exit 1, 1 sandbox violation)
$ mkdir -p dist && cat ../../tsconfig.base.json > dist/tsconfig.json
├─ SANDBOX ───────────────────────────────────────────────────
vx: hidden by the sandbox, held by the key: /home/user/acme/tsconfig.base.json
vx: the sandbox hides a file this task's key holds, so it was not run: /home/user/acme/tsconfig.base.json. Grant it, e.g. `allow: { read: ['../../tsconfig.base.json'] }`.
fix: exec.sandbox.allow: { read: ['../../tsconfig.base.json'] }
└─ packages/lib#build ── (0ms) failed (exit 1, 1 sandbox violation)

See Sandboxing.

  • vx lock refuses a glob no file can match: a backwards class ([z-a]), surrounding whitespace, a control character, or braces expanding past 1024.
  • When only literal outputs matched, vx names the wildcard globs that matched nothing: a later hit restores only the literal files.
  • A run that removes a committed file under its outputs saves nothing, since no hit could replay the removal.
  • A damaged cache.db page or a FIFO in an output tree degrades to a miss; a long Ctrl-Z no longer lifts a task’s timeout.
  • vx last --live --follow exits 1 when its run is lost or killed.
  • Discovery no longer follows a symlinked directory.
  • A .vx/dependencies.lock.json written by a newer vx is refused.
  • dte() machine names may hold a dot again (c5.large).
  • turbo() keys package.json past negated inputs; nx() reads ?? outputs and refuses kebab-case or alias output options it cannot map.
  • Hardening closed stale cache hits through linked outputs, NFD and NFC sibling projects, .gitattributes files and lockfile plugins.
Terminal window
vx upgrade

Run vx lock after upgrading. Every cached task misses once.

  • .vx/projects.lock.json → version 6, holding the project and discover stages. A plugin’s change to projects, edges or mapped tasks reaches runs only through vx lock.
  • Cache format → vx-cache-v55: the local cache starts empty once.

See also Upgrading.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.