The shared store (2026-10-06)
Status: IMPLEMENTED (2026-10-06) as written, schema v32.
Owner ask (2026-10-05): “global cache in home folder like Nx in order to share cache across workspaces”.
What changed
Section titled “What changed”A workspace’s cache splits in two:
- The store — the entries and their artifacts:
entries,entry_stdout,output_files,entry_inputs,store_metainstore.db, and<hash>.tar.zstbeside it. Content-addressed. Default:~/.vx/<id>/cacheon every platform, one per repository, as Nx 23 keeps~/.nx/<id>/cache(2026-10-06: “do exactly like nx”; the first cut used~/.cache/vx, one for every repository). - The workspace index —
<root>/.vx/cache/cache.db: run history, the file-hash and blob memos, config evaluations, and what this checkout’s disk looked like after a save or restore (output_stamps,output_dirs). It attachesstore.dbasstore; SQLite resolves an unqualified table name inmainfirst, then instore, so no statement names a schema.
A cache dir the workspace names (cacheDir, --cache-dir,
VX_CACHE_DIR) holds both, in one cache.db, as before: naming one is
how a workspace opts out of sharing.
The repository id, as Nx 23.2
Section titled “The repository id, as Nx 23.2”Nx 23.2 shares by default at ~/.nx/<id>/{cache,databases}
(utils/cache-directory.js, utils/git-utils.js); repo-id.ts ports
its rules, without the Nx Cloud id:
- id: 16 hex of sha256(sha256(
<identity>#<workspace path in the repo>)). - identity: the remote,
origin, thenupstream,base, the first; four url shapes (scpgit@,https://user@,https://,ssh://user@host[:port]) tohost/owner/repo, lower case. Read from the common dir’sconfig; aninclude, aurl "…"rewrite or a value with#,;,\or a quote is left togit remote -v. - no remote: the sorted-first root commit; a shallow clone or a repository with no commit has no id and shares nothing (the workspace holds everything, as with a named cache dir).
- the walk to
.gitstops at one not ours (owner,HEAD,objects), reads files withO_NOFOLLOW, and~/.vx,~/.vx/<id>and itscacheare made 0700 one level at a time; a level of ours open to other users is chmodded 0700 (2026-10-06), and one not ours, or one the chmod fails on, sends the store to the workspace, said once.
Entries are content-addressed, so one store for every repository would be as correct; the split is housekeeping (delete a repository’s cache as one directory, no lock shared with an unrelated repository’s run).
Why split, not move
Section titled “Why split, not move”Moving the whole cache.db to the home directory would share what is
one workspace’s alone:
vx last,vx why,vx-historyand the MCP tools readrunsandinvocations: one shared table answers with another repository’s runs (about 25 statements would each need a workspace filter).output_filescarried the inode and ctime THIS checkout saw after a restore. Two worktrees of one repo share every key, so each overwrote the other’s stamps and every switch between them restored (the row “two worktrees on one entry each stay up to date” fails withoutput_stampsmoved into the store).- Plugin files keyed by name alone (
vx-migrate’s mapping, the Nx graph snapshot) would be read across repositories.
The keys were already workspace-relative (relFor), and the run lock is
keyed by the workspace, not the cache dir, so the entries share as is.
Decisions
Section titled “Decisions”- The artifact is the record; the index its inventory
(2026-10-06). Every key is seeded with
CACHE_VERSION, bumped when hashing or the artifact layout changes, so an artifact one vx wrote is never a hit for another and the store needs no versioned directory.store.dbrecords its own schema (store_meta.schema); a vx of anotherSCHEMA_VERSIONdrops its tables and keeps the artifacts. A lookup that finds no row but finds<hash>.tar.zstindexes it from its bytes (Cache.adopt: key and names checked as a remote’s are) and hits. Two vx versions on one repository each reset the other’s inventory; the artifacts survive both. - A home that cannot hold the store does not fail the run. The
store goes to
<cacheDir>instead, said once per fallback. - An index that held its entries itself loses them when it next
opens with a store (they would shadow the store’s): its history
stays, its entries miss once,
vx cache prunereaps their artifacts. - Stamps and directory snapshots carry no foreign key: the entry may
be another database’s.
pruneand a re-save delete what they orphan; a lookup starts at the entry, so a leftover is never read. - One salt per store.
entry_inputsdigests are compared with another workspace’s run, so the salt moved fromschema_metatostore_meta. - Trust. Every workspace of the user writes the store, as two
workspaces on one
--cache-diralways could. A sandboxed task cannot write it unless a grant names it; an unsandboxed one can write anywhere, as before.
v38: shared memos (2026-10-14 20:59)
Section titled “v38: shared memos (2026-10-14 20:59)”blob_sizes (fixed for its OID) and the flaky record, task_outcomes,
moved to the store: a worktree reads the blob sizes another learned, and
a key that passed in one worktree and failed in another is flaky in
both. task_outcomes keeps every executed outcome that names its key
and a key’s first hit (a later hit replays a pass on record), so a warm
run writes no row: recording 1,000 hits costs one index probe, ~1 ms.
blob_verdicts stays: it is keyed by this checkout’s index file.
Measured
Section titled “Measured”Warm vx run build --all, 500 generated packages, 15 interleaved
rounds, min / median: main 195 / 214 ms, shared store 183 / 202 ms,
named cache dir 188 / 213 ms. The attached store costs nothing
measurable.