src/orchestrator/metrics.ts — run-history queries
Purpose
Section titled “Purpose”Pure functions over the run-history tables (runs, invocations,
entries, entry_inputs) — one canonical home for every aggregate,
so vx last, vx why, and any out-of-process reader (@vzn/vx-mcp’s
explainCacheKey / whyDidThisRerun tools) ask the same questions
the same way. The schema itself is owned by src/cache/schema.ts; this
module only reads it.
Public surface
Section titled “Public surface”listRuns(db, { limit?, project?, task?, runId? }): RunSummaryRow[]getRun(db, runId): RunDetail | null // one invocation's task rows<<<<<<< HEADlistInvocations(db, { limit? }): InvocationDetail[]=======>>>>>>> 7a461de (docs(modules): correct module pages the source contradicts (J-18))getInvocation(db, runId): InvocationDetail | null // the header row, tags parsedexplainCacheKey(db, taskId): CacheKeyExplanation // latest entry for a tasklatestRunId(db, taskId): string | null // the run a caller without one meanswhyDidThisRerun(db, runId, taskId): WhyDidThisRerun // this run vs the previous onecacheKeyDiff(db, runId, taskId): CacheKeyDiff // which key components moveddiffKeyComponents(before, after): { entries, unchangedCount } // the join, no storeEvery function but diffKeyComponents takes an open bun:sqlite
Database (the caller owns the Cache lifecycle — cache.dbHandle()),
returns JSON-safe shapes (bigint spans as decimal strings, like
WireEvent.timeUnixNano), and never throws on a missing row:
found: false, null, or an empty list, with a note that says which
case it is.
The two explanations
Section titled “The two explanations”whyDidThisReruncompares a task’s row inrunIdwith its immediately previous row:hashChangedwhen the keys differ; when they do not, the note says whether the run was served from cache, recorded no cache outcome at all, or re-executed on the same key — and why, from what the index holds: the previous run on the key failed (saving nothing), the invocation’scache_policyread no cache, or the key’s entry was created by this run (none was there when it ran). Only when none applies does it name--no-cache/--force(item 1009).cacheKeyDiffis the moat: it resolves both runs to their task hashes and full-outer-joins the twoentry_inputsfingerprint sets over(kind, name)—changed/added/removed, unchanged ones counted — with no config re-evaluation and no re-hash. Values are digests, never the material (an env value can be a secret); STATUS § Next 8(g) records why a plugin part’s raw value is not stored.diffKeyComponentsis that join with no store under it: two keys’{ kind, name, hash }sets in, the entries (ordered by kind, then name, in code units, notlocaleCompare, whose order follows the machine’s locale; the playground runs this join in a browser, item 707) and the unchanged count out.cacheKeyDiffcalls it on the twoentry_inputssets, and the site’s playground calls the bundled copy on the components its key fold captured (captureInto), so the page names what moved a key by the rulevx whydoes (item 703;tests/playground-parity.unsafe.test.tsholds the two to one answer).
Keyed-run filtering (KEYED_RUNS_SQL, the cache module’s) is
imported, not restated, so a rule written once cannot drift between
readers.
What it does NOT do
Section titled “What it does NOT do”- Open or close anything; no
Cachelifecycle. - Evaluate configs or recompute a key —
explainCacheKeysays so in its note: it returns persisted entry metadata. - Write rows.
recordRunBundle(cache/run-history.ts) is the writer.
tests/metrics.test.ts (every query, the three unchanged-key notes,
the diff’s four verdicts and its order by kind and by name, degraded
rows whose fingerprints were pruned);
tests/run-record-completeness.test.ts (a run writes what these read);
tests/status-vocabulary.test.ts (no hand-typed status lists).
Replacing this module
Section titled “Replacing this module”A reader over another store implements the same eight signatures (the
join, diffKeyComponents, needs no store and is reused as it is); the
CLI verbs and the MCP tools format, they do not query.