@vzn/vx 0.0.21 → 0.0.23
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/README.md +21 -18
- package/package.json +6 -6
- package/src/bin.ts +52 -3
- package/src/cache/archive.ts +170 -21
- package/src/cache/cache.ts +667 -288
- package/src/cache/chained-cache.ts +14 -2
- package/src/cache/config-evals.ts +29 -6
- package/src/cache/file-hashes.ts +63 -35
- package/src/cache/git-inputs.ts +398 -69
- package/src/cache/index.ts +8 -1
- package/src/cache/inputs.ts +403 -102
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +81 -11
- package/src/cache/layered-cache.ts +199 -32
- package/src/cache/output-index.ts +109 -10
- package/src/cache/run-history.ts +8 -3
- package/src/cache/zstd.ts +53 -30
- package/src/cli/cache.ts +51 -21
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +35 -9
- package/src/cli/info.ts +49 -7
- package/src/cli/init.ts +17 -5
- package/src/cli/last.ts +96 -21
- package/src/cli/lock.ts +34 -17
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/plugin-commands.ts +20 -5
- package/src/cli/run.ts +55 -12
- package/src/cli/select.ts +23 -13
- package/src/cli/show.ts +9 -4
- package/src/cli/upgrade.ts +134 -14
- package/src/cli/watch.ts +622 -102
- package/src/cli/why.ts +8 -16
- package/src/cli/workspace-config.ts +21 -9
- package/src/config.ts +46 -9
- package/src/exec/executor.ts +51 -5
- package/src/exec/index.ts +10 -0
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/runner.ts +327 -82
- package/src/exec/sandbox-binds.ts +34 -5
- package/src/exec/sandbox-paths.ts +22 -2
- package/src/exec/sandbox-runtime.ts +627 -102
- package/src/exec/sandbox-violations.ts +36 -23
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +12 -2
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +182 -134
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +16 -2
- package/src/orchestrator/admission.ts +41 -17
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +22 -7
- package/src/orchestrator/doctor.ts +122 -14
- package/src/orchestrator/events.ts +71 -2
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +331 -37
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +81 -14
- package/src/orchestrator/history.ts +6 -7
- package/src/orchestrator/hit-restore.ts +82 -18
- package/src/orchestrator/index.ts +7 -1
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +152 -32
- package/src/orchestrator/logger.ts +116 -20
- package/src/orchestrator/metrics.ts +156 -60
- package/src/orchestrator/miss-save.ts +106 -43
- package/src/orchestrator/options.ts +30 -4
- package/src/orchestrator/persistent.ts +41 -21
- package/src/orchestrator/placement.ts +55 -25
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +17 -23
- package/src/orchestrator/plugin.ts +31 -8
- package/src/orchestrator/prepare.ts +99 -13
- package/src/orchestrator/projects.ts +19 -8
- package/src/orchestrator/remote-prefetch.ts +21 -1
- package/src/orchestrator/run-artifacts.ts +9 -0
- package/src/orchestrator/run-context.ts +19 -4
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +6 -23
- package/src/orchestrator/run-report.ts +4 -2
- package/src/orchestrator/run.ts +264 -57
- package/src/orchestrator/sandbox-request.ts +388 -62
- package/src/orchestrator/save-lane.ts +4 -7
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +86 -30
- package/src/orchestrator/stable-keys.ts +202 -10
- package/src/orchestrator/status-line.ts +33 -1
- package/src/orchestrator/summary.ts +49 -2
- package/src/orchestrator/task-hash.ts +87 -7
- package/src/orchestrator/task-log-buffer.ts +7 -7
- package/src/orchestrator/telemetry-host.ts +16 -7
- package/src/orchestrator/telemetry.ts +69 -28
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +32 -3
- package/src/util/paths.ts +147 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +4 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +272 -64
- package/src/workspace/config-cache.ts +262 -32
- package/src/workspace/config-eval.ts +49 -12
- package/src/workspace/config-imports.ts +126 -5
- package/src/workspace/config-schema.ts +237 -41
- package/src/workspace/filter.ts +33 -18
- package/src/workspace/fingerprint.ts +29 -11
- package/src/workspace/index.ts +7 -0
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +33 -11
- package/src/workspace/migrate-scripts.ts +60 -7
- package/src/workspace/migration.ts +40 -1
- package/src/workspace/package-graph.ts +154 -16
- package/src/workspace/project-loader.ts +227 -77
- package/src/workspace/workspace.ts +173 -23
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The cache key, derived: `foldKey` is the whole of what `Cache.key` computes,
|
|
2
|
+
// with the two things it read off the class (the file hasher and the
|
|
3
|
+
// workspace-relative path) passed in. Nothing here touches a store, so the
|
|
4
|
+
// fold runs wherever xxh3 does: the CLI, a remote layer, and the site's
|
|
5
|
+
// playground, which bundles this file to derive the keys the CLI would
|
|
6
|
+
// (design/playground-spike-2026-09.md, P1; item 691).
|
|
7
|
+
|
|
8
|
+
import { xxh3, xxh3hex } from '../util/index.js'
|
|
9
|
+
import type { CacheKeyInput } from './layer.js'
|
|
10
|
+
|
|
11
|
+
// v17: artifact carries only logs + outputs (stdout + outputs/<rel>).
|
|
12
|
+
// Local and remote layers transport the SAME tar.zst bytes — no
|
|
13
|
+
// separate stage/meta.json/tar.gz dance for remote, no
|
|
14
|
+
// `cache-archive.ts`. stderr is no longer cached: we only cache
|
|
15
|
+
// successful runs and stderr is rarely meaningful on success.
|
|
16
|
+
// v19: '^task' dependsOn expansion switched from transitive-deps to
|
|
17
|
+
// nearest-holder frontier — upstream-hash sets shrink, so keys change.
|
|
18
|
+
// v20: input-file content hashes switched from xxh3 to git blob OIDs
|
|
19
|
+
// (Turbo parity). Clean tracked files take their OID straight from
|
|
20
|
+
// the index (harvested by the bulk `git ls-files -s`); dirty /
|
|
21
|
+
// untracked files get the identical OID computed in-process. Every
|
|
22
|
+
// file's hash bytes change → bump. SCHEMA_VERSION moves with it:
|
|
23
|
+
// pre-v20 `file_hashes.content_hash` rows hold xxh3 digests that
|
|
24
|
+
// must not leak into the OID domain via the mtime+size memo.
|
|
25
|
+
// v22: reverted the v21 output-fold "early cutoff". Downstream keys
|
|
26
|
+
// fold the upstream's INPUT key (its task hash) again — pure-input
|
|
27
|
+
// transitive hashing (Turbo/Nx model). No output content participates
|
|
28
|
+
// in any cache key. SCHEMA v21 drops the now-unused outputs_hash
|
|
29
|
+
// column. Early cutoff removed (an upstream that re-emits identical
|
|
30
|
+
// output still re-runs dependents) — rare, not worth the cascade.
|
|
31
|
+
// v23: fold cache.inputs.runtime / workspaceRuntime command output into
|
|
32
|
+
// the key (two namespaced sections after env-values). Command strings
|
|
33
|
+
// live in the resolved config (frozen by `vx lock`); the OUTPUT is
|
|
34
|
+
// resolved live every run, so it stays correct under --frozen. No
|
|
35
|
+
// SCHEMA bump — only Cache.key derivation changed.
|
|
36
|
+
// v24: vx-lock.json added to the always-ignored input set (cache/
|
|
37
|
+
// inputs.ts ALWAYS_IGNORE). A task whose globs matched the root
|
|
38
|
+
// lockfile (broad `**/*` on the root project) drops it from the hashed
|
|
39
|
+
// file set, so those keys change; tasks that never matched it are
|
|
40
|
+
// byte-identical. No SCHEMA bump — only the input file set changed.
|
|
41
|
+
//
|
|
42
|
+
// CACHE_VERSION stays v24 through the Tier-3 schema roll (SCHEMA v22).
|
|
43
|
+
// Tier 3 persists the very components already fed to `Cache.key()` —
|
|
44
|
+
// it adds nothing to, reorders nothing in, and reweights nothing
|
|
45
|
+
// inside the key fold (the new `captureInto` sink is a pure
|
|
46
|
+
// side-channel). A task's hash is byte-identical before and after, so
|
|
47
|
+
// existing artifacts stay valid and there is no cache-version bump.
|
|
48
|
+
// v25: the ARTIFACT BYTES are wrong in every existing entry, while the
|
|
49
|
+
// key that addresses them is unchanged — the one situation a version
|
|
50
|
+
// bump exists for. Two defects: (a) `packArtifact` staged outputs with
|
|
51
|
+
// `Bun.write`, which does not carry the source mode, so every artifact
|
|
52
|
+
// records 0644 and a cache hit strips the executable bit off any binary
|
|
53
|
+
// or generated script; (b) it packed `--format=ustar`, which splits a
|
|
54
|
+
// name over 100 bytes into prefix+name, and the reader read only `name`
|
|
55
|
+
// — so those entries lost their `outputs/` prefix and were silently
|
|
56
|
+
// dropped from both the restore and the output_files index. Neither is
|
|
57
|
+
// self-healing: the stored bytes/headers are already wrong, so without a
|
|
58
|
+
// new namespace the fixed code would keep replaying them forever.
|
|
59
|
+
// v26: same situation, different producer. A task whose child was killed by
|
|
60
|
+
// a shutdown signal reported `aborted`, but `aborted` did not propagate to
|
|
61
|
+
// dependents — so a dependent ran against PARTIAL outputs, succeeded, and
|
|
62
|
+
// cached what it built. The fold takes the upstream's INPUT key, which a
|
|
63
|
+
// signal does not change, so that entry sits under exactly the key a healthy
|
|
64
|
+
// run derives and replays forever as a green hit. Fixing the propagation
|
|
65
|
+
// stops new poison but cannot reach entries already written, and a
|
|
66
|
+
// `LayeredCache` uploads them, so the reach is a whole team's shared cache
|
|
67
|
+
// rather than one developer's disk. Pre-alpha, so one cold rebuild is the
|
|
68
|
+
// cheap side of that trade.
|
|
69
|
+
// v28: the same shape again (item 667). An output glob over a bracket route
|
|
70
|
+
// directory (`app/[id]/page.js`) was read as a character class, so its
|
|
71
|
+
// entries saved nothing (or the class's namesakes) under a key the fix
|
|
72
|
+
// leaves unchanged: the glob text is what folds. Read literally, the first
|
|
73
|
+
// hit on such an entry cleaned the route and restored nothing, green.
|
|
74
|
+
// v29: every key moves (item 682). The seed-chained fold carried only 32
|
|
75
|
+
// bits of state (Bun's xxHash3 reads the low half of its seed), so two
|
|
76
|
+
// input sets could share a key; `xxh3` now feeds the seed forward. The
|
|
77
|
+
// old keys were wrong, not their bytes, so this is self-healing; the bump
|
|
78
|
+
// is for the first run's notice, since every entry misses once.
|
|
79
|
+
// v30: stored bytes wrong under an unchanged key (item 720). npm and Yarn
|
|
80
|
+
// link every workspace package at the root, the task's own included, and
|
|
81
|
+
// the sandbox granted that link's target whole: a sandboxed task read an
|
|
82
|
+
// undeclared file of its own project unreported and saved what it built.
|
|
83
|
+
// The fix withholds the self-link, but the key never saw the file, so an
|
|
84
|
+
// entry saved before it hits forever when only that file changes.
|
|
85
|
+
// v31: the same shape for a sibling (item 726). A cached sandboxed task was
|
|
86
|
+
// granted every linked workspace package, so it read a sibling its key
|
|
87
|
+
// never folded and saved what it built. The fix withholds that link unless
|
|
88
|
+
// the key answers for the package, but an entry saved before it still hits
|
|
89
|
+
// after the sibling changes (probed: the old output replayed).
|
|
90
|
+
export const CACHE_VERSION = 'vx-cache-v35'
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fold one task's key inputs into its 16-hex cache key. `hashFile` answers
|
|
94
|
+
* for an input file the caller's `fileHashes` map does not cover (the git
|
|
95
|
+
* blob OID of its worktree bytes); `relOf` renders an input file relative to
|
|
96
|
+
* `input.workspaceRoot`, POSIX-separated.
|
|
97
|
+
*/
|
|
98
|
+
export async function foldKey(
|
|
99
|
+
input: CacheKeyInput,
|
|
100
|
+
hashFile: (file: string) => Promise<string>,
|
|
101
|
+
relOf: (file: string) => string,
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
// Seed-chained xxHash3: each step folds one field into the
|
|
104
|
+
// running digest via `xxh3(part, prevDigest)`. Equivalent to the
|
|
105
|
+
// old CryptoHasher.update() pattern, no intermediate buffer.
|
|
106
|
+
// Field-order matters; each line is prefixed with its label so
|
|
107
|
+
// adjacent fields can't collide via concat.
|
|
108
|
+
// Optional capture sink — see CacheKeyInput.captureInto. Pure
|
|
109
|
+
// side-channel; every push below mirrors the fold immediately
|
|
110
|
+
// above/beside it so the recorded set can't drift from the key.
|
|
111
|
+
const cap = input.captureInto
|
|
112
|
+
let h = xxh3(CACHE_VERSION)
|
|
113
|
+
h = xxh3(`task:${input.taskId}`, h)
|
|
114
|
+
h = xxh3(`workspace:${input.workspaceFingerprint}`, h)
|
|
115
|
+
h = xxh3(`pkg:${input.projectPackageJsonHash}`, h)
|
|
116
|
+
h = xxh3(`config:${input.taskConfigHash}`, h)
|
|
117
|
+
if (cap) {
|
|
118
|
+
cap.push({ kind: 'workspace', name: 'fingerprint', hash: input.workspaceFingerprint })
|
|
119
|
+
cap.push({ kind: 'package', name: 'package.json', hash: input.projectPackageJsonHash })
|
|
120
|
+
cap.push({ kind: 'config', name: 'config', hash: input.taskConfigHash })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// NOTE on `cap` (entry_inputs capture): the `hash` field stores a
|
|
124
|
+
// DIGEST of each component, never the raw value. For value-bearing kinds
|
|
125
|
+
// (env, runtime, ws-runtime, forward) the payload is a secret or
|
|
126
|
+
// sensitive string (API keys via cache.inputs.env, runtime-command
|
|
127
|
+
// output, args after `--`), so we push `xxh3hex(v)` — the diff consumer
|
|
128
|
+
// only needs to know whether a component CHANGED, which a digest
|
|
129
|
+
// preserves losslessly. cache.db must never hold plaintext secrets at
|
|
130
|
+
// rest. The cache KEY (`h`) folds the plaintext separately below and is
|
|
131
|
+
// unaffected.
|
|
132
|
+
const forwarded = input.forwardArgs ?? []
|
|
133
|
+
h = xxh3(`forward-args:${forwarded.length}`, h)
|
|
134
|
+
for (const a of forwarded) h = xxh3(a, h)
|
|
135
|
+
if (cap && forwarded.length > 0) {
|
|
136
|
+
cap.push({ kind: 'forward', name: 'argv', hash: xxh3hex(JSON.stringify(forwarded)) })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
h = xxh3(`env-values:${input.envValues.length}`, h)
|
|
140
|
+
// \0 delimiter, not `=`: names and values may themselves contain
|
|
141
|
+
// `=`, and `A` + `B=C` must never fold the same bytes as `A=B` + `C`.
|
|
142
|
+
// An unset name folds its bare name: a name holds no \0 (the schema
|
|
143
|
+
// refuses one), so that is no `name\0value` for any value, the empty
|
|
144
|
+
// one included, and a set value's bytes stay what they always were.
|
|
145
|
+
for (const [n, v] of input.envValues) h = xxh3(v === undefined ? n : `${n}\0${v}`, h)
|
|
146
|
+
if (cap)
|
|
147
|
+
for (const [n, v] of input.envValues)
|
|
148
|
+
cap.push({ kind: 'env', name: n, hash: v === undefined ? 'unset' : xxh3hex(v) })
|
|
149
|
+
|
|
150
|
+
const runtimeValues = input.runtimeValues ?? []
|
|
151
|
+
h = xxh3(`runtime-values:${runtimeValues.length}`, h)
|
|
152
|
+
for (const [c, o] of runtimeValues) h = xxh3(`${c}\0${o}`, h)
|
|
153
|
+
if (cap)
|
|
154
|
+
for (const [c, o] of runtimeValues) cap.push({ kind: 'runtime', name: c, hash: xxh3hex(o) })
|
|
155
|
+
|
|
156
|
+
const wsRuntimeValues = input.workspaceRuntimeValues ?? []
|
|
157
|
+
h = xxh3(`ws-runtime-values:${wsRuntimeValues.length}`, h)
|
|
158
|
+
for (const [c, o] of wsRuntimeValues) h = xxh3(`${c}\0${o}`, h)
|
|
159
|
+
if (cap)
|
|
160
|
+
for (const [c, o] of wsRuntimeValues)
|
|
161
|
+
cap.push({ kind: 'ws-runtime', name: c, hash: xxh3hex(o) })
|
|
162
|
+
|
|
163
|
+
const upstream = [...input.upstreamHashes].sort()
|
|
164
|
+
h = xxh3(`upstream:${upstream.length}`, h)
|
|
165
|
+
for (const u of upstream) h = xxh3(u, h)
|
|
166
|
+
if (cap) {
|
|
167
|
+
for (const u of upstream) {
|
|
168
|
+
cap.push({ kind: 'upstream', name: input.upstreamIds?.get(u) ?? u, hash: u })
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pluginParts = input.pluginParts ?? []
|
|
173
|
+
if (pluginParts.length > 0) {
|
|
174
|
+
h = xxh3(`plugin:${pluginParts.length}`, h)
|
|
175
|
+
for (const [n, v] of pluginParts) h = xxh3(`${n}\0${v}`, h)
|
|
176
|
+
if (cap) {
|
|
177
|
+
for (const [n, v] of pluginParts) cap.push({ kind: 'plugin', name: n, hash: xxh3hex(v) })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// `resolveFiles` already returns its paths sorted, and a copy-and-sort
|
|
182
|
+
// of three thousand strings per task is not free: 7.4 ms of a 44-task
|
|
183
|
+
// run here (2026-09-20). A linear order check costs one comparison per
|
|
184
|
+
// file instead of n log n, and a caller that hands over an unsorted set
|
|
185
|
+
// — a plugin's own input list — still gets one. `>` on strings compares
|
|
186
|
+
// UTF-16 code units, which is what a comparator-less `sort` does.
|
|
187
|
+
let sortedInputs: readonly string[] = input.inputFiles
|
|
188
|
+
for (let i = 1; i < sortedInputs.length; i++) {
|
|
189
|
+
if (sortedInputs[i - 1]! > sortedInputs[i]!) {
|
|
190
|
+
sortedInputs = [...input.inputFiles].sort()
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
h = xxh3(`inputs:${sortedInputs.length}`, h)
|
|
195
|
+
// Per-file hash source, in preference order: the caller-supplied
|
|
196
|
+
// index-OID map (clean tracked files — zero I/O), then hashFile's
|
|
197
|
+
// stat memo (no read), then a full in-process blob-OID computation.
|
|
198
|
+
// All three describe the WORKTREE bytes, which is what the task
|
|
199
|
+
// reads — the index-OID map is only populated for paths whose
|
|
200
|
+
// worktree form matches their index form, because a clean filter
|
|
201
|
+
// (`text`/`eol`/`ident`/`filter`/`working-tree-encoding`) makes the index blob a DIFFERENT sequence
|
|
202
|
+
// of bytes and folding it would let two distinct worktree contents
|
|
203
|
+
// share a key. The fold order is locked to `sortedInputs` so
|
|
204
|
+
// results are stable across runs.
|
|
205
|
+
// The common warm shape is that the caller's OID map covers every file,
|
|
206
|
+
// and then there is nothing to await: building three thousand promises
|
|
207
|
+
// per task to resolve values already in hand cost 8.4 ms of that same
|
|
208
|
+
// run. One pass fills the array, and the first gap falls back to the
|
|
209
|
+
// awaited form for the whole list.
|
|
210
|
+
let fileHashes: readonly string[] | undefined
|
|
211
|
+
const provided = input.fileHashes
|
|
212
|
+
if (provided !== undefined) {
|
|
213
|
+
const out: string[] = []
|
|
214
|
+
for (const file of sortedInputs) {
|
|
215
|
+
const oid = provided.get(file)
|
|
216
|
+
if (oid === undefined) break
|
|
217
|
+
out.push(oid)
|
|
218
|
+
}
|
|
219
|
+
if (out.length === sortedInputs.length) fileHashes = out
|
|
220
|
+
}
|
|
221
|
+
fileHashes ??= await Promise.all(sortedInputs.map((f) => provided?.get(f) ?? hashFile(f)))
|
|
222
|
+
for (let i = 0; i < sortedInputs.length; i++) {
|
|
223
|
+
const file = sortedInputs[i]!
|
|
224
|
+
const rel = relOf(file)
|
|
225
|
+
const oid = fileHashes[i]!
|
|
226
|
+
h = xxh3(`${rel}\0${oid}`, h)
|
|
227
|
+
// The OID is already awaited — file capture is zero extra I/O.
|
|
228
|
+
if (cap) cap.push({ kind: 'file', name: rel, hash: oid })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return h.toString(16).padStart(16, '0')
|
|
232
|
+
}
|
package/src/cache/layer.ts
CHANGED
|
@@ -17,9 +17,10 @@ export interface CacheKeyInput {
|
|
|
17
17
|
taskConfigHash: string
|
|
18
18
|
/**
|
|
19
19
|
* Runtime values of declared cache-input env names (from parent at hash
|
|
20
|
-
* time). Independent of `exec.env`;
|
|
20
|
+
* time), `undefined` for a name that is unset. Independent of `exec.env`;
|
|
21
|
+
* lives here for cache identity.
|
|
21
22
|
*/
|
|
22
|
-
envValues: Array<[name: string, value: string]>
|
|
23
|
+
envValues: Array<[name: string, value: string | undefined]>
|
|
23
24
|
/**
|
|
24
25
|
* Resolved `cache.inputs.runtime` commands as [command, output] pairs
|
|
25
26
|
* (output = trimmed stdout+stderr, resolved live at hash time). Folded
|
|
@@ -199,6 +200,16 @@ export interface RunRecord {
|
|
|
199
200
|
* decision. Absent on rows written before the column existed.
|
|
200
201
|
*/
|
|
201
202
|
cached?: boolean
|
|
203
|
+
/**
|
|
204
|
+
* Why the task failed or was skipped, as the run's own footer said it
|
|
205
|
+
* (v27 columns; NULL on older rows and where the reason does not apply):
|
|
206
|
+
* a skip's root blocker, vx's own timeout, the sandbox's violation count,
|
|
207
|
+
* and why a persistent task never became ready.
|
|
208
|
+
*/
|
|
209
|
+
blockedBy?: string
|
|
210
|
+
timedOut?: true
|
|
211
|
+
sandboxViolations?: number
|
|
212
|
+
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
/**
|
|
@@ -296,6 +307,17 @@ export interface PruneResult {
|
|
|
296
307
|
orphanBytes: number
|
|
297
308
|
}
|
|
298
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Artifact + `output_files` namespace prefix for workspace-root-
|
|
312
|
+
* anchored outputs (`cache.outputs.workspaceFiles`). Project outputs
|
|
313
|
+
* keep their bare project-relative `path` rows; workspace rows store
|
|
314
|
+
* the full `workspace-outputs/<rel-to-root>` tar entry name as the
|
|
315
|
+
* discriminator — least-invasive row format, no schema change. A
|
|
316
|
+
* project output dir literally named `workspace-outputs/` would
|
|
317
|
+
* collide with the namespace; the name is reserved.
|
|
318
|
+
*/
|
|
319
|
+
export const WORKSPACE_OUTPUT_PREFIX = 'workspace-outputs/'
|
|
320
|
+
|
|
299
321
|
/**
|
|
300
322
|
* Per-output-file fingerprint, scoped by the cache entry that
|
|
301
323
|
* produced it. Batch-loaded once at the top of a run via
|
|
@@ -314,6 +336,14 @@ export interface OutputFileRow {
|
|
|
314
336
|
size: number
|
|
315
337
|
mode: number
|
|
316
338
|
mtimeMs: number
|
|
339
|
+
/**
|
|
340
|
+
* The inode and ctime THIS machine saw the file carry after the save or
|
|
341
|
+
* restore that left it equal to the entry (`recordOutputStamps`).
|
|
342
|
+
* Absent until then, and from any ingest: a row without them is never
|
|
343
|
+
* current.
|
|
344
|
+
*/
|
|
345
|
+
ino?: number
|
|
346
|
+
ctimeMs?: number
|
|
317
347
|
}
|
|
318
348
|
|
|
319
349
|
/** One directory under a whole-subtree output glob, as it stood after the last save or restore on THIS machine. */
|
|
@@ -347,12 +377,6 @@ export const OUTPUT_DIRS_RACY_MS = 50
|
|
|
347
377
|
/** The stat memo's twin of `OUTPUT_DIRS_RACY_MS`: a file changed within this window of the stat is not memoised. */
|
|
348
378
|
export const FILE_HASH_RACY_MS = 50
|
|
349
379
|
|
|
350
|
-
/**
|
|
351
|
-
* The shape every cache implementation honors. `Cache` (the local v10
|
|
352
|
-
* implementation) and `LayeredCache` both `implements` this so the
|
|
353
|
-
* orchestrator's `executeTask` can take either without a discriminated
|
|
354
|
-
* union and we get a compile-time guarantee the surfaces stay congruent.
|
|
355
|
-
*/
|
|
356
380
|
/**
|
|
357
381
|
* Context passed to `get()`. Optional, but required when the lookup
|
|
358
382
|
* may resolve through the remote layer — the local SQL row inserted on
|
|
@@ -363,6 +387,20 @@ export const FILE_HASH_RACY_MS = 50
|
|
|
363
387
|
export interface CacheGetContext {
|
|
364
388
|
taskId: string
|
|
365
389
|
command: string
|
|
390
|
+
/**
|
|
391
|
+
* The task's declared `cache.outputs`. A remote artifact is ingested
|
|
392
|
+
* only when every file it carries is one of them: bytes off the network
|
|
393
|
+
* named `outputs/src/in.txt` or `workspace-outputs/.git/hooks/…` wrote
|
|
394
|
+
* an input and a git hook on a green `cache-hit-remote` (item 942).
|
|
395
|
+
* Absent, the names are not checked.
|
|
396
|
+
*/
|
|
397
|
+
outputs?: DeclaredOutputs
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** A task's declared outputs, as its config spells them. */
|
|
401
|
+
export interface DeclaredOutputs {
|
|
402
|
+
files: readonly string[]
|
|
403
|
+
workspaceFiles: readonly string[]
|
|
366
404
|
}
|
|
367
405
|
|
|
368
406
|
/** Metadata supplied at ingest time — values the artifact does not carry. */
|
|
@@ -379,6 +417,8 @@ export interface IngestMeta {
|
|
|
379
417
|
* miss/save, so a cache hit never reaches here.
|
|
380
418
|
*/
|
|
381
419
|
inputComponents?: readonly TaskInputRow[]
|
|
420
|
+
/** See `CacheGetContext.outputs`: the names the artifact may carry. */
|
|
421
|
+
outputs?: DeclaredOutputs
|
|
382
422
|
}
|
|
383
423
|
|
|
384
424
|
/**
|
|
@@ -399,6 +439,28 @@ export class CorruptArtifactError extends Error {
|
|
|
399
439
|
}
|
|
400
440
|
}
|
|
401
441
|
|
|
442
|
+
/**
|
|
443
|
+
* `restoreOutputs` found no artifact where the probe found one: it was
|
|
444
|
+
* removed in between — a `vx cache prune` in another shell, or the
|
|
445
|
+
* `cacheRetention` of another workspace sharing the cache directory. The
|
|
446
|
+
* bytes are not wrong, they are gone, so the entry is a MISS and the task
|
|
447
|
+
* runs (execute-task.ts); nothing about the cache needs throwing away.
|
|
448
|
+
*/
|
|
449
|
+
export class ArtifactVanishedError extends Error {
|
|
450
|
+
constructor(public readonly hash: string) {
|
|
451
|
+
super(
|
|
452
|
+
`its cache artifact ${hash} vanished before the restore (a prune or another run's retention)`,
|
|
453
|
+
)
|
|
454
|
+
this.name = 'ArtifactVanishedError'
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The shape every cache implementation honors. `Cache` (the local v10
|
|
460
|
+
* implementation) and `LayeredCache` both `implements` this so the
|
|
461
|
+
* orchestrator's `executeTask` can take either without a discriminated
|
|
462
|
+
* union and we get a compile-time guarantee the surfaces stay congruent.
|
|
463
|
+
*/
|
|
402
464
|
export interface CacheLayer {
|
|
403
465
|
/**
|
|
404
466
|
* The local handle this layer wraps, when it wraps one (`LayeredCache`).
|
|
@@ -509,6 +571,14 @@ export interface CacheLayer {
|
|
|
509
571
|
* state, like the output rows: a remote ingest records none.
|
|
510
572
|
*/
|
|
511
573
|
recordOutputDirs?(hash: string, projectDir: string, prefixes: readonly string[]): Promise<void>
|
|
574
|
+
/**
|
|
575
|
+
* Stamp `hash`'s output rows with each file's inode and ctime once a
|
|
576
|
+
* save or restore has left the tree equal to the entry (optional — a
|
|
577
|
+
* layer without it has rows `isOutputsCurrent` never trusts, so every
|
|
578
|
+
* hit restores). Machine-local; skips a file that no longer matches its
|
|
579
|
+
* row.
|
|
580
|
+
*/
|
|
581
|
+
recordOutputStamps?(hash: string, projectDir: string, workspaceRoot: string): void
|
|
512
582
|
loadOutputDirsBatch?(hashes: readonly string[]): Map<string, OutputDirRow[]>
|
|
513
583
|
outputDirsCurrent?(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean>
|
|
514
584
|
/**
|
|
@@ -557,13 +627,13 @@ export interface CacheLayer {
|
|
|
557
627
|
inputComponents?: readonly TaskInputRow[]
|
|
558
628
|
}): Promise<void>
|
|
559
629
|
/**
|
|
560
|
-
* Adopt an artifact produced elsewhere — the remote-hit path.
|
|
561
|
-
* the compressed
|
|
630
|
+
* Adopt an artifact produced elsewhere — the remote-hit path. Streams
|
|
631
|
+
* the compressed body to `<cacheDir>/<hash>.tar.zst`, parses the
|
|
562
632
|
* tar headers to populate the `output_files` rows, and inserts the
|
|
563
633
|
* `entries` row using the caller-supplied `meta`. After this returns,
|
|
564
634
|
* the next `get(hash)` resolves locally.
|
|
565
635
|
*/
|
|
566
|
-
ingest(hash: string,
|
|
636
|
+
ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void>
|
|
567
637
|
/**
|
|
568
638
|
* The ONE run-history write: a whole `vx run` atomically, the per-task
|
|
569
639
|
* `runs` rows and the one `invocations` header row — in a SINGLE
|