@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.
- package/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cache/schema.ts
CHANGED
|
@@ -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,
|
|
4
|
-
//
|
|
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
|
-
|
|
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
|
+
}
|