Skip to content
GitHubRSS

src/orchestrator/metrics.ts — run-history queries

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.

listRuns(db, { limit?, project?, task?, runId? }): RunSummaryRow[]
getRun(db, runId): RunDetail | null // one invocation's task rows
<<<<<<< HEAD
listInvocations(db, { limit? }): InvocationDetail[]
=======
>>>>>>> 7a461de (docs(modules): correct module pages the source contradicts (J-18))
getInvocation(db, runId): InvocationDetail | null // the header row, tags parsed
explainCacheKey(db, taskId): CacheKeyExplanation // latest entry for a task
latestRunId(db, taskId): string | null // the run a caller without one means
whyDidThisRerun(db, runId, taskId): WhyDidThisRerun // this run vs the previous one
cacheKeyDiff(db, runId, taskId): CacheKeyDiff // which key components moved
diffKeyComponents(before, after): { entries, unchangedCount } // the join, no store

Every 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.

  • whyDidThisRerun compares a task’s row in runId with its immediately previous row: hashChanged when 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’s cache_policy read 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).
  • cacheKeyDiff is the moat: it resolves both runs to their task hashes and full-outer-joins the two entry_inputs fingerprint 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.
  • diffKeyComponents is that join with no store under it: two keys’ { kind, name, hash } sets in, the entries (ordered by kind, then name, in code units, not localeCompare, whose order follows the machine’s locale; the playground runs this join in a browser, item 707) and the unchanged count out. cacheKeyDiff calls it on the two entry_inputs sets, 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 rule vx why does (item 703; tests/playground-parity.unsafe.test.ts holds 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.

  • Open or close anything; no Cache lifecycle.
  • Evaluate configs or recompute a key — explainCacheKey says 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).

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.