@vzn/vx 0.0.636 → 0.0.638

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.
Files changed (151) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +246 -213
  3. package/docs/cli.md +791 -705
  4. package/docs/execution.md +68 -76
  5. package/docs/features.md +47 -42
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +392 -205
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +4 -4
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +28 -13
  19. package/src/bin.ts +30 -9
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +124 -53
  22. package/src/cache/chained-cache.ts +4 -0
  23. package/src/cache/git-inputs.ts +277 -63
  24. package/src/cache/index.ts +15 -1
  25. package/src/cache/inputs.ts +102 -36
  26. package/src/cache/key-fold.ts +8 -7
  27. package/src/cache/layer.ts +15 -18
  28. package/src/cache/layered-cache.ts +20 -102
  29. package/src/cache/plugin-cache.ts +248 -0
  30. package/src/cache/policy.ts +32 -27
  31. package/src/cache/run-history.ts +104 -14
  32. package/src/cache/run-store.ts +77 -0
  33. package/src/cache/schema.ts +58 -31
  34. package/src/cache/scoped-cache.ts +237 -0
  35. package/src/cli/cache.ts +173 -34
  36. package/src/cli/completions.ts +11 -7
  37. package/src/cli/core-alias.ts +1 -1
  38. package/src/cli/foreign-flags.ts +13 -12
  39. package/src/cli/help.ts +110 -70
  40. package/src/cli/index.ts +67 -24
  41. package/src/cli/info.ts +43 -27
  42. package/src/cli/init.ts +115 -407
  43. package/src/cli/last.ts +157 -9
  44. package/src/cli/lock.ts +356 -122
  45. package/src/cli/plan-format.ts +7 -5
  46. package/src/cli/plugin-commands.ts +81 -14
  47. package/src/cli/plugin-templates.ts +11 -11
  48. package/src/cli/run.ts +116 -160
  49. package/src/cli/select.ts +50 -47
  50. package/src/cli/show.ts +83 -59
  51. package/src/cli/task-verb.ts +6 -13
  52. package/src/cli/upgrade.ts +415 -149
  53. package/src/cli/watch-cycle.ts +9 -2
  54. package/src/cli/watch-filter.ts +39 -61
  55. package/src/cli/watch-fs.ts +29 -16
  56. package/src/cli/watch-judge.ts +18 -4
  57. package/src/cli/watch-set.ts +16 -42
  58. package/src/cli/watch.ts +227 -230
  59. package/src/cli/why.ts +38 -18
  60. package/src/cli/workspace-config.ts +33 -70
  61. package/src/config.ts +169 -70
  62. package/src/exec/env.ts +1 -31
  63. package/src/exec/executor.ts +1 -5
  64. package/src/exec/kill-tree.ts +2 -1
  65. package/src/exec/sandbox-runtime.ts +12 -1
  66. package/src/exec/sandbox-violations.ts +9 -1
  67. package/src/graph/dependency-spec.ts +1 -1
  68. package/src/graph/index.ts +1 -0
  69. package/src/graph/priorities.ts +1 -1
  70. package/src/graph/scheduler.ts +27 -7
  71. package/src/graph/task-graph.ts +76 -24
  72. package/src/handoff.ts +169 -0
  73. package/src/index.ts +35 -58
  74. package/src/orchestrator/affected-tasks.ts +15 -8
  75. package/src/orchestrator/command-windows.ts +44 -0
  76. package/src/orchestrator/deferred-outputs.ts +12 -10
  77. package/src/orchestrator/doctor.ts +98 -110
  78. package/src/orchestrator/download-policy.ts +3 -3
  79. package/src/orchestrator/events.ts +19 -3
  80. package/src/orchestrator/excluded-keys.ts +0 -2
  81. package/src/orchestrator/execute-task.ts +147 -69
  82. package/src/orchestrator/failure-mode.ts +19 -10
  83. package/src/orchestrator/fingerprint-watch.ts +18 -15
  84. package/src/orchestrator/forecast.ts +10 -2
  85. package/src/orchestrator/framed-output.ts +9 -5
  86. package/src/orchestrator/graph-stage.ts +5 -5
  87. package/src/orchestrator/history.ts +126 -30
  88. package/src/orchestrator/hit-restore.ts +15 -3
  89. package/src/orchestrator/index.ts +20 -5
  90. package/src/orchestrator/local-shortcircuit.ts +2 -1
  91. package/src/orchestrator/lockfile-claim.ts +79 -20
  92. package/src/orchestrator/logger.ts +80 -41
  93. package/src/orchestrator/metrics.ts +78 -9
  94. package/src/orchestrator/miss-save.ts +3 -0
  95. package/src/orchestrator/options.ts +42 -29
  96. package/src/orchestrator/output-host.ts +72 -0
  97. package/src/orchestrator/plan.ts +6 -8
  98. package/src/orchestrator/plugin-host.ts +100 -66
  99. package/src/orchestrator/plugin.ts +173 -67
  100. package/src/orchestrator/prepare.ts +163 -78
  101. package/src/orchestrator/projects.ts +105 -99
  102. package/src/orchestrator/remote-prefetch.ts +11 -6
  103. package/src/orchestrator/run-context.ts +25 -6
  104. package/src/orchestrator/run-failures.ts +17 -8
  105. package/src/orchestrator/run-lock.ts +1 -1
  106. package/src/orchestrator/run-records.ts +1 -1
  107. package/src/orchestrator/run-report.ts +9 -9
  108. package/src/orchestrator/run-settings.ts +48 -0
  109. package/src/orchestrator/run.ts +244 -119
  110. package/src/orchestrator/sandbox-fix.ts +39 -20
  111. package/src/orchestrator/sandbox-request.ts +84 -13
  112. package/src/orchestrator/signals.ts +1 -1
  113. package/src/orchestrator/stable-keys.ts +13 -11
  114. package/src/orchestrator/status-line.ts +49 -7
  115. package/src/orchestrator/summary.ts +115 -106
  116. package/src/orchestrator/task-hash.ts +12 -83
  117. package/src/orchestrator/telemetry.ts +4 -4
  118. package/src/settings.ts +428 -0
  119. package/src/{orchestrator → util}/colors.ts +5 -0
  120. package/src/util/config-frame.ts +31 -2
  121. package/src/util/errors.ts +33 -1
  122. package/src/util/essential-env.ts +31 -0
  123. package/src/util/hide-env.ts +107 -0
  124. package/src/util/index.ts +16 -3
  125. package/src/util/paths.ts +10 -0
  126. package/src/util/refusal-notice.ts +111 -0
  127. package/src/util/report.ts +31 -0
  128. package/src/util/settle.ts +49 -30
  129. package/src/util/size.ts +23 -4
  130. package/src/util/timing.ts +3 -2
  131. package/src/util/ui.ts +186 -0
  132. package/src/util/ulid.ts +1 -1
  133. package/src/util/verbs.ts +0 -15
  134. package/src/workspace/affected.ts +84 -232
  135. package/src/workspace/config-eval.ts +15 -20
  136. package/src/workspace/config-imports.ts +78 -3
  137. package/src/workspace/config-schema.ts +209 -268
  138. package/src/workspace/dependency-rules.ts +65 -0
  139. package/src/workspace/filter.ts +2 -2
  140. package/src/workspace/fingerprint.ts +11 -121
  141. package/src/workspace/index.ts +20 -27
  142. package/src/workspace/load-reads.ts +4 -4
  143. package/src/workspace/lockfile.ts +96 -140
  144. package/src/workspace/migration.ts +307 -215
  145. package/src/workspace/package-graph.ts +30 -216
  146. package/src/workspace/project-loader.ts +80 -285
  147. package/src/workspace/repo-id.ts +2 -1
  148. package/src/workspace/workspace.ts +363 -831
  149. package/src/cache/config-evals.ts +0 -130
  150. package/src/workspace/config-cache.ts +0 -661
  151. package/src/workspace/migrate-scripts.ts +0 -1121
@@ -1,7 +1,7 @@
1
1
  // The index's tables, one concern: what a cache.db holds. `Cache` opens the
2
2
  // file, checks `SCHEMA_VERSION` and creates these; every statement against
3
- // them lives in the store that owns the table (file-hashes, config-evals,
4
- // output-index, history) or in cache.ts for `entries`, `entry_inputs` and
3
+ // them lives in the store that owns the table (file-hashes, output-index,
4
+ // history) or in cache.ts for `entries`, `entry_inputs` and
5
5
  // `artifacts`.
6
6
 
7
7
  import type { Database, SQLQueryBindings } from 'bun:sqlite'
@@ -22,7 +22,9 @@ export function lazyStatement(db: Database, sql: string): ReturnType<Database['p
22
22
 
23
23
  /**
24
24
  * Create every table. `store` names the schema holding the ENTRY tables
25
- * (`entries`, `entry_stdout`, `output_files`, `entry_inputs`, `store_meta`):
25
+ * (`entries`, `entry_stdout`, `output_files`, `entry_inputs`, `store_meta`)
26
+ * the task timings (`task_durations`), the flaky record (`task_outcomes`),
27
+ * the blob sizes (`blob_sizes`) and the plugins' blobs (`plugin_blobs`):
26
28
  * `main` when one `cache.db` holds everything (a `cacheDir`), `store` when
27
29
  * they live in the shared store attached beside it (v32). The rest is the
28
30
  * workspace's own: its history, its memos, and what its disk looked like
@@ -30,17 +32,6 @@ export function lazyStatement(db: Database, sql: string): ReturnType<Database['p
30
32
  * a statement; SQLite finds it in `main` or in `store`, whichever has it.
31
33
  */
32
34
  export function createTables(db: Database, store: 'main' | 'store' = 'main'): void {
33
- // Cached config evaluations (workspace/config-cache.ts): the validated
34
- // config as JSON, keyed by everything the evaluation could observe. A
35
- // separate exec so the artifact schema stays byte-identical.
36
- db.exec(`
37
- CREATE TABLE IF NOT EXISTS main.config_evals (
38
- key TEXT PRIMARY KEY,
39
- json TEXT NOT NULL,
40
- created_at INTEGER NOT NULL
41
- );
42
- `)
43
-
44
35
  db.exec(`
45
36
  -- The queryable index: command, exit_code, duration, size and
46
37
  -- timestamps; stdout in entry_stdout below.
@@ -124,17 +115,14 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
124
115
  -- runs_ended (2026-09-09) served only the retention DELETE, which
125
116
  -- prunes on started_at now (a row ends after it starts, so the
126
117
  -- 30-day window moves by at most one task's duration).
118
+ -- runs_failed (v38) served the flakiness probe, which reads the
119
+ -- store's task_outcomes now.
127
120
  DROP INDEX IF EXISTS main.runs_hash;
128
121
  DROP INDEX IF EXISTS main.runs_project;
129
122
  DROP INDEX IF EXISTS main.runs_ended;
123
+ DROP INDEX IF EXISTS main.runs_failed;
130
124
  CREATE INDEX IF NOT EXISTS main.runs_started_at ON runs(started_at);
131
125
  CREATE INDEX IF NOT EXISTS main.runs_run_id ON runs(run_id);
132
- -- The one keyed index, PARTIAL over failed rows: a green run's
133
- -- inserts only evaluate its predicate, so the append-only cost above
134
- -- holds, and the flakiness probe after a miss (failure-mode.ts,
135
- -- "did this key ever fail?") reads a handful of leaves instead of
136
- -- scanning the table (2026-09-10: 10–95 ms at 170k rows without it).
137
- CREATE INDEX IF NOT EXISTS main.runs_failed ON runs(hash) WHERE status = 'failed';
138
126
  -- Per-file (mtime, size, content_hash) cache. Lets Cache.key()
139
127
  -- skip the content-hash on inputs whose stat hasn't changed
140
128
  -- since the last run. Pure performance optimization; the stored
@@ -152,8 +140,9 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
152
140
  -- The size of each index blob the enumeration checked against the
153
141
  -- worktree size git recorded (A-60). Fixed for its OID, so a warm
154
142
  -- run asks git for none; swept with file_hashes by seen_at, the
155
- -- time the row was written.
156
- CREATE TABLE IF NOT EXISTS main.blob_sizes (
143
+ -- time the row was written. v38: in the store, so a worktree reads
144
+ -- what another learned (owner, 2026-10-14).
145
+ CREATE TABLE IF NOT EXISTS ${store}.blob_sizes (
157
146
  oid TEXT PRIMARY KEY,
158
147
  size INTEGER NOT NULL,
159
148
  seen_at INTEGER NOT NULL
@@ -197,15 +186,6 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
197
186
  ctime_ms INTEGER NOT NULL,
198
187
  PRIMARY KEY (entry_hash, path)
199
188
  );
200
- -- Each config's ORDERED import closure (the config first), so a warm
201
- -- load keys it by stat-hashing the list (the file_hashes memo) instead
202
- -- of reading and scanning every file. Machine-local; pruned with
203
- -- config_evals (2026-09-03).
204
- CREATE TABLE IF NOT EXISTS main.config_closures (
205
- config_path TEXT PRIMARY KEY,
206
- files_json TEXT NOT NULL,
207
- created_at INTEGER NOT NULL
208
- );
209
189
  -- Every directory under a whole-subtree output glob, with its mtime
210
190
  -- as of the last save/restore on THIS machine (2026-09-03). On a warm
211
191
  -- hit, unchanged mtimes prove the output SET is unchanged, replacing
@@ -277,6 +257,53 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
277
257
  key TEXT PRIMARY KEY,
278
258
  value TEXT NOT NULL
279
259
  );
260
+ -- What a duration forecast reads (orchestrator/history.ts), one row per
261
+ -- executed success, shared by every worktree of the repo so a new one
262
+ -- forecasts on its first run (owner, 2026-10-14). Append-only: never
263
+ -- updated, pruned by age with the run history. \`source\` tags a local
264
+ -- run 'branch:<name>' ('branch:' detached or outside git); \`sha\` is its
265
+ -- commit. A forecast reads a task's newest rows of one source.
266
+ CREATE TABLE IF NOT EXISTS ${store}.task_durations (
267
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
268
+ project TEXT NOT NULL,
269
+ task TEXT NOT NULL,
270
+ source TEXT NOT NULL,
271
+ sha TEXT,
272
+ duration_ms INTEGER NOT NULL,
273
+ recorded_at INTEGER NOT NULL
274
+ );
275
+ CREATE INDEX IF NOT EXISTS ${store}.task_durations_task
276
+ ON task_durations(project, task, source, id);
277
+ -- v37: a 'remote' row is a default-branch commit's timing a plugin
278
+ -- imported (\`sha\` that commit): one per task, so a repeat import is a
279
+ -- no-op under INSERT OR IGNORE.
280
+ CREATE UNIQUE INDEX IF NOT EXISTS ${store}.task_durations_remote
281
+ ON task_durations(sha, project, task) WHERE source = 'remote';
282
+ -- v38: the flaky record (orchestrator/failure-mode.ts): each keyed,
283
+ -- cached, unblocked outcome that observed its key, shared by every
284
+ -- worktree so a key that passed in one and failed in another is flaky
285
+ -- in both (owner, 2026-10-14). Every executed success or failure, and
286
+ -- a key's first hit (run-history.ts): a later hit replays a pass the
287
+ -- record holds, so a warm run on keys that passed writes no row.
288
+ -- Append-only, ordered by id, pruned by age with the run history.
289
+ CREATE TABLE IF NOT EXISTS ${store}.task_outcomes (
290
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
291
+ project TEXT NOT NULL,
292
+ task TEXT NOT NULL,
293
+ hash TEXT NOT NULL,
294
+ status TEXT NOT NULL,
295
+ attempts INTEGER,
296
+ recorded_at INTEGER NOT NULL
297
+ );
298
+ CREATE INDEX IF NOT EXISTS ${store}.task_outcomes_hash ON task_outcomes(hash, status);
299
+ -- v37: what a plugin keeps through \`ctx.cache\` (plugin-cache.ts), by
300
+ -- its namespaced key. Written once, never updated; pruned by age with
301
+ -- the run history.
302
+ CREATE TABLE IF NOT EXISTS ${store}.plugin_blobs (
303
+ key TEXT PRIMARY KEY,
304
+ bytes BLOB NOT NULL,
305
+ at INTEGER NOT NULL
306
+ );
280
307
  ${artifactTables(store)}
281
308
  `)
282
309
  }
@@ -0,0 +1,237 @@
1
+ // The run's `cache.scope` over every layer at once (owner, 2026-10-14): a
2
+ // scoped run reads the default key, then the scope's, and writes only the
3
+ // scope's, in the local store and every remote alike. Below this wrapper no
4
+ // layer knows a scope exists: the scope's key is a key like any other.
5
+
6
+ import { xxh3hex } from '../util/index.js'
7
+ import type {
8
+ CacheEntry,
9
+ CacheGetContext,
10
+ CacheLayer,
11
+ IngestMeta,
12
+ InvocationRecord,
13
+ OutputDirRow,
14
+ RunRecord,
15
+ SaveArgs,
16
+ } from './layer.js'
17
+
18
+ /** The key a scope stores `hash` under: task-key shaped, 16 hex, so every wire stores it unchanged. */
19
+ export function scopedKey(scope: string, hash: string): string {
20
+ return xxh3hex(`vx-cache-scope\0${scope}\0${hash}`)
21
+ }
22
+
23
+ /**
24
+ * A layer as a scoped run sees it. Callers speak task keys; each answer is
25
+ * remembered under the key that served it (the default key on a default hit,
26
+ * the scope's on a scope hit or a save), so the restore, the output index and
27
+ * the run's history all reach the entry that answered. An artifact records
28
+ * the key it was packed under and ingest refuses another, so a derived-key
29
+ * collision degrades to a miss. A default artifact a remote ingest refuses is
30
+ * a miss there, and the scope's key is asked next (Security F6).
31
+ */
32
+ export class ScopedCache implements CacheLayer {
33
+ readonly local?: CacheLayer['local']
34
+ readonly hasRemote?: boolean
35
+ readonly drainUploads?: () => Promise<void>
36
+ readonly remoteHasMany?: (hashes: readonly string[]) => Promise<Set<string> | null>
37
+ readonly getMany?: (
38
+ hashes: readonly string[],
39
+ ctx?: (hash: string) => CacheGetContext,
40
+ ) => Promise<Map<string, CacheEntry>>
41
+ /** Task key → the key that answers for it this run. */
42
+ private readonly served = new Map<string, string>()
43
+
44
+ constructor(
45
+ private readonly inner: CacheLayer,
46
+ private readonly scope: string,
47
+ ) {
48
+ if (inner.local !== undefined) this.local = inner.local
49
+ if (inner.hasRemote !== undefined) this.hasRemote = inner.hasRemote
50
+ if (inner.drainUploads !== undefined) this.drainUploads = () => inner.drainUploads!()
51
+ if (inner.remoteHasMany !== undefined) this.remoteHasMany = (h) => this.scopedHasMany(h)
52
+ if (inner.getMany !== undefined) this.getMany = (h, ctx) => this.scopedGetMany(h, ctx)
53
+ }
54
+
55
+ private own(hash: string): string {
56
+ return scopedKey(this.scope, hash)
57
+ }
58
+
59
+ private keyOf(hash: string): string {
60
+ return this.served.get(hash) ?? hash
61
+ }
62
+
63
+ /** A batch keyed by served keys, keyed back by task key. */
64
+ private batch<T>(
65
+ hashes: readonly string[],
66
+ read: (keys: string[]) => Map<string, T>,
67
+ ): Map<string, T> {
68
+ const keys = hashes.map((h) => this.keyOf(h))
69
+ const found = read(keys)
70
+ const out = new Map<string, T>()
71
+ for (let i = 0; i < hashes.length; i++) {
72
+ const v = found.get(keys[i]!)
73
+ if (v !== undefined) out.set(hashes[i]!, v)
74
+ }
75
+ return out
76
+ }
77
+
78
+ /** A task key the store holds under the default key or the scope's. */
79
+ async localHasMany(hashes: readonly string[]): Promise<Set<string>> {
80
+ const held = await this.inner.localHasMany?.([...hashes, ...hashes.map((h) => this.own(h))])
81
+ if (held === undefined) return new Set()
82
+ return new Set(hashes.filter((h) => held.has(h) || held.has(this.own(h))))
83
+ }
84
+
85
+ private async scopedHasMany(hashes: readonly string[]): Promise<Set<string> | null> {
86
+ const scoped = hashes.map((h) => this.own(h))
87
+ const [plain, own] = await Promise.all([
88
+ this.inner.remoteHasMany!(hashes),
89
+ this.inner.remoteHasMany!(scoped),
90
+ ])
91
+ if (plain == null || own == null) return null
92
+ const found = new Set(plain)
93
+ const scopeOnly: string[] = []
94
+ for (let i = 0; i < hashes.length; i++) {
95
+ if (plain.has(hashes[i]!) || !own.has(scoped[i]!)) continue
96
+ found.add(hashes[i]!)
97
+ scopeOnly.push(hashes[i]!)
98
+ }
99
+ // Their default key is known absent: its GET is skipped.
100
+ this.inner.markRemoteAbsent?.(scopeOnly)
101
+ return found
102
+ }
103
+
104
+ markRemoteAbsent(hashes: Iterable<string>): void {
105
+ const all: string[] = []
106
+ for (const h of hashes) all.push(h, this.own(h))
107
+ this.inner.markRemoteAbsent?.(all)
108
+ }
109
+
110
+ key(input: Parameters<CacheLayer['key']>[0]): Promise<string> {
111
+ return this.inner.key(input)
112
+ }
113
+
114
+ async get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> {
115
+ // The scope's key first when only it is local: the default key's
116
+ // remote GET would cost a round-trip for the same inputs.
117
+ const own = this.own(hash)
118
+ const keys = (await this.inner.localHasMany?.([hash, own]))?.has(own)
119
+ ? [own, hash]
120
+ : [hash, own]
121
+ for (const key of keys) {
122
+ const entry = await this.inner.get(key, ctx)
123
+ if (entry === null) continue
124
+ this.served.set(hash, key)
125
+ return key === hash ? entry : { ...entry, hash }
126
+ }
127
+ return null
128
+ }
129
+
130
+ private async scopedGetMany(
131
+ hashes: readonly string[],
132
+ ctx?: (hash: string) => CacheGetContext,
133
+ ): Promise<Map<string, CacheEntry>> {
134
+ const out = await this.inner.getMany!(hashes, ctx)
135
+ for (const h of out.keys()) this.served.set(h, h)
136
+ const rest = hashes.filter((h) => !out.has(h))
137
+ if (rest.length === 0) return out
138
+ const byOwn = new Map(rest.map((h) => [this.own(h), h]))
139
+ const scoped = await this.inner.getMany!(
140
+ [...byOwn.keys()],
141
+ ctx === undefined ? undefined : (key) => ctx(byOwn.get(key)!),
142
+ )
143
+ for (const [key, entry] of scoped) {
144
+ const hash = byOwn.get(key)!
145
+ this.served.set(hash, key)
146
+ out.set(hash, { ...entry, hash })
147
+ }
148
+ return out
149
+ }
150
+
151
+ async has(hash: string): Promise<'local' | 'remote' | null> {
152
+ return (await this.inner.has(hash)) ?? (await this.inner.has(this.own(hash)))
153
+ }
154
+
155
+ async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> {
156
+ return (await this.inner.prefetch(hash, ctx)) || this.inner.prefetch(this.own(hash), ctx)
157
+ }
158
+
159
+ loadOutputFilesBatch(hashes: readonly string[]): ReturnType<CacheLayer['loadOutputFilesBatch']> {
160
+ return this.batch(hashes, (keys) => this.inner.loadOutputFilesBatch(keys))
161
+ }
162
+
163
+ isOutputsCurrent(
164
+ ...args: Parameters<CacheLayer['isOutputsCurrent']>
165
+ ): ReturnType<CacheLayer['isOutputsCurrent']> {
166
+ return this.inner.isOutputsCurrent(...args)
167
+ }
168
+
169
+ async recordOutputDirs(
170
+ hash: string,
171
+ projectDir: string,
172
+ prefixes: readonly string[],
173
+ holds?: (files: readonly string[]) => boolean,
174
+ ): Promise<void> {
175
+ await this.inner.recordOutputDirs?.(this.keyOf(hash), projectDir, prefixes, holds)
176
+ }
177
+
178
+ recordOutputStamps(hash: string, projectDir: string, workspaceRoot: string): void {
179
+ this.inner.recordOutputStamps?.(this.keyOf(hash), projectDir, workspaceRoot)
180
+ }
181
+
182
+ loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> {
183
+ return this.batch(hashes, (keys) => this.inner.loadOutputDirsBatch?.(keys) ?? new Map())
184
+ }
185
+
186
+ async outputDirsCurrent(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> {
187
+ return (await this.inner.outputDirsCurrent?.(projectDir, rows)) ?? false
188
+ }
189
+
190
+ restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> {
191
+ return this.inner.restoreOutputs(this.keyOf(hash), projectDir, workspaceRoot)
192
+ }
193
+
194
+ save(args: SaveArgs): Promise<void> {
195
+ const key = this.own(args.hash)
196
+ this.served.set(args.hash, key)
197
+ return this.inner.save({ ...args, hash: key })
198
+ }
199
+
200
+ ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> {
201
+ const key = this.own(hash)
202
+ this.served.set(hash, key)
203
+ return this.inner.ingest(key, body, meta)
204
+ }
205
+
206
+ /** History names the key that answered, so `vx why` reads that entry's components. */
207
+ recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
208
+ this.inner.recordRunBundle({
209
+ ...bundle,
210
+ runs: bundle.runs.map((r) => (r.hash === undefined ? r : { ...r, hash: this.keyOf(r.hash) })),
211
+ })
212
+ }
213
+
214
+ stats(...args: Parameters<CacheLayer['stats']>): ReturnType<CacheLayer['stats']> {
215
+ return this.inner.stats(...args)
216
+ }
217
+
218
+ hashFile(filePath: string): Promise<string> {
219
+ return this.inner.hashFile(filePath)
220
+ }
221
+
222
+ outputsPath(hash: string): string {
223
+ return this.inner.outputsPath(this.keyOf(hash))
224
+ }
225
+
226
+ artifactSize(hash: string): number | undefined {
227
+ return this.inner.artifactSize?.(this.keyOf(hash))
228
+ }
229
+
230
+ prune(...args: Parameters<CacheLayer['prune']>): ReturnType<CacheLayer['prune']> {
231
+ return this.inner.prune(...args)
232
+ }
233
+
234
+ close(): void {
235
+ this.inner.close()
236
+ }
237
+ }