@vzn/vx 0.0.637 → 0.0.639
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/docs/caching.md +84 -28
- package/docs/cli.md +273 -408
- package/docs/execution.md +19 -44
- package/docs/features.md +13 -15
- package/docs/patterns.md +5 -7
- package/docs/schema.md +68 -61
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +19 -6
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +60 -11
- package/src/cache/git-inputs.ts +6 -0
- package/src/cache/index.ts +10 -2
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +32 -12
- package/src/cache/layered-cache.ts +112 -315
- package/src/cache/plugin-cache.ts +240 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +92 -8
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +44 -13
- package/src/cache/scoped-cache.ts +22 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +11 -7
- package/src/cli/foreign-flags.ts +10 -22
- package/src/cli/help.ts +22 -38
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +8 -6
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +20 -10
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +10 -61
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +31 -191
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +72 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +132 -39
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +73 -34
- package/src/cli/why.ts +14 -5
- package/src/cli/workspace-config.ts +2 -2
- package/src/config.ts +0 -6
- package/src/exec/executor.ts +1 -1
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/index.ts +9 -1
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +12 -16
- package/src/graph/task-graph.ts +119 -21
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +20 -11
- package/src/{orchestrator → history}/history.ts +61 -5
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +14 -23
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +21 -29
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +47 -429
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +3 -74
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +3 -3
- package/src/orchestrator/plan.ts +18 -5
- package/src/orchestrator/plugin-host.ts +77 -47
- package/src/orchestrator/plugin.ts +86 -14
- package/src/orchestrator/prepare.ts +74 -31
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +23 -6
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +36 -4
- package/src/orchestrator/run-report.ts +81 -179
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +313 -555
- package/src/orchestrator/sandbox-fix.ts +0 -16
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +107 -3
- package/src/orchestrator/steps.ts +50 -0
- package/src/orchestrator/task-hash.ts +3 -16
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +1 -1
- package/src/orchestrator/telemetry.ts +6 -83
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +39 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +166 -21
- package/src/{orchestrator → report}/status-line.ts +84 -7
- package/src/{orchestrator → report}/summary.ts +179 -63
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +13 -23
- package/src/{orchestrator → util}/colors.ts +49 -4
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +25 -3
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +39 -0
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +197 -0
- package/src/workspace/affected.ts +20 -23
- package/src/workspace/dependency-rules.ts +12 -4
- package/src/workspace/migration.ts +87 -91
- package/src/workspace/workspace.ts +5 -4
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -276
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.639",
|
|
4
4
|
"description": "Task orchestration you own: correct, sandboxed, extensible, with a content-addressed build cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.639",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.639",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.639",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.639"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/schemas/last.json
CHANGED
|
@@ -107,8 +107,7 @@
|
|
|
107
107
|
"host",
|
|
108
108
|
"os",
|
|
109
109
|
"arch",
|
|
110
|
-
"vxVersion"
|
|
111
|
-
"tags"
|
|
110
|
+
"vxVersion"
|
|
112
111
|
],
|
|
113
112
|
"properties": {
|
|
114
113
|
"runId": { "type": "string" },
|
|
@@ -146,8 +145,7 @@
|
|
|
146
145
|
"host": { "type": ["string", "null"] },
|
|
147
146
|
"os": { "type": ["string", "null"] },
|
|
148
147
|
"arch": { "type": ["string", "null"] },
|
|
149
|
-
"vxVersion": { "type": "string" }
|
|
150
|
-
"tags": { "type": "object", "additionalProperties": { "type": "string" } }
|
|
148
|
+
"vxVersion": { "type": "string" }
|
|
151
149
|
}
|
|
152
150
|
},
|
|
153
151
|
"task": {
|
|
@@ -174,6 +172,7 @@
|
|
|
174
172
|
"timedOut",
|
|
175
173
|
"sandboxViolations",
|
|
176
174
|
"notReady",
|
|
175
|
+
"remoteError",
|
|
177
176
|
"forwarded"
|
|
178
177
|
],
|
|
179
178
|
"properties": {
|
|
@@ -209,6 +208,10 @@
|
|
|
209
208
|
"timedOut": { "type": ["boolean", "null"] },
|
|
210
209
|
"sandboxViolations": { "type": ["integer", "null"] },
|
|
211
210
|
"notReady": { "enum": ["timeout", "exited", "spawn", null] },
|
|
211
|
+
"remoteError": {
|
|
212
|
+
"type": ["string", "null"],
|
|
213
|
+
"description": "each failed remote read or write of the key, `; `-joined"
|
|
214
|
+
},
|
|
212
215
|
"forwarded": { "type": "boolean" },
|
|
213
216
|
"output": {
|
|
214
217
|
"type": "string",
|
package/schemas/plan.json
CHANGED
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"required": ["kind"],
|
|
46
46
|
"properties": {
|
|
47
47
|
"kind": {
|
|
48
|
-
"enum": ["input", "project", "package", "named", "selected"],
|
|
49
|
-
"description": "input: a changed file is a declared input; project: its project changed and the task reads it whole; package: a ^ edge passes a changed package; named: asked as pkg#task; selected: another filter selected its project"
|
|
48
|
+
"enum": ["input", "project", "workspace", "package", "named", "selected"],
|
|
49
|
+
"description": "input: a changed file is a declared input; project: its project changed and the task reads it whole; workspace: a workspace-wide file (the workspace config, a root file a plugin claims) changed; package: a ^ edge passes a changed package; named: asked as pkg#task; selected: another filter selected its project"
|
|
50
50
|
},
|
|
51
51
|
"file": { "type": "string", "description": "the changed file, workspace-relative" },
|
|
52
52
|
"project": { "type": "string", "description": "the changed package (kind package)" },
|
package/skills/vx/SKILL.md
CHANGED
|
@@ -15,22 +15,33 @@ dependencies.
|
|
|
15
15
|
vx run build --all # a task in every project that declares it
|
|
16
16
|
vx run test --affected=origin/main # only what the diff since origin/main reaches
|
|
17
17
|
vx run app#build # one project's task (and what it depends on)
|
|
18
|
-
vx
|
|
18
|
+
vx last --format json # the last run's result as JSON
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
The exit code is the verdict: `0` every task passed or hit the cache, `1` a
|
|
22
|
-
task failed or was skipped, or the command was refused.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
task failed or was skipped, or the command was refused. `vx last --format
|
|
23
|
+
json` prints the run as JSON afterwards, a row per task. A refusal under
|
|
24
|
+
`--format json` (or `--dry=json`) is one line on stdout,
|
|
25
25
|
`{"ok":false,"error":{"code","message","docs"}}`: branch on `code`;
|
|
26
26
|
`docs` (or `vx docs <code>` offline) says what to do.
|
|
27
27
|
Without a TTY a missing task name never prompts: vx exits 1 and lists the
|
|
28
28
|
tasks it found.
|
|
29
29
|
|
|
30
|
+
## After editing a config
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
vx lock # after any edit to a vx.config.ts or .vx/workspace.ts
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A run reads every config from `.vx/projects.lock.json`, never the files:
|
|
37
|
+
a task you add, or an edit to one, does nothing until `vx lock`. Commit
|
|
38
|
+
the lock with the config.
|
|
39
|
+
|
|
30
40
|
## Look before running
|
|
31
41
|
|
|
32
42
|
```sh
|
|
33
|
-
vx show --format json # projects
|
|
43
|
+
vx show --format json # projects and their tasks
|
|
44
|
+
vx show app --format json # one project's tasks as a run reads them
|
|
34
45
|
vx run test --affected=origin/main --dry=json # what would run, cache hit or miss, and why
|
|
35
46
|
```
|
|
36
47
|
|
|
@@ -94,6 +105,8 @@ vx info --format json # vx and Bun versions, projects, config errors, plugins,
|
|
|
94
105
|
|
|
95
106
|
## Install
|
|
96
107
|
|
|
97
|
-
`npx skills add vznjs/vx
|
|
108
|
+
`npx skills add vznjs/vx` installs this skill. To match the vx a workspace
|
|
98
109
|
has installed, copy it from there instead:
|
|
99
110
|
`cp node_modules/@vzn/vx/skills/vx/SKILL.md .claude/skills/vx/`.
|
|
111
|
+
`vx upgrade` inside a workspace moves the project's `@vzn/vx` and the
|
|
112
|
+
global vx that ran it to the same version.
|
package/src/bin.ts
CHANGED
|
@@ -46,7 +46,7 @@ async function main(): Promise<void> {
|
|
|
46
46
|
process.exitCode = code
|
|
47
47
|
} catch (err) {
|
|
48
48
|
const {
|
|
49
|
-
|
|
49
|
+
detectColors,
|
|
50
50
|
errorCode,
|
|
51
51
|
errorDocument,
|
|
52
52
|
fsRefusalHint,
|
|
@@ -55,6 +55,8 @@ async function main(): Promise<void> {
|
|
|
55
55
|
isUserError,
|
|
56
56
|
maskedLine,
|
|
57
57
|
OUT_OF_FDS_HINT,
|
|
58
|
+
refusalNotice,
|
|
59
|
+
ui,
|
|
58
60
|
wantsJson,
|
|
59
61
|
} = await import('./util/index.js')
|
|
60
62
|
// A plugin's failure quotes what it was told (a remote's reply, a
|
|
@@ -66,12 +68,16 @@ async function main(): Promise<void> {
|
|
|
66
68
|
// on. Everything else gets the full stack so internal bugs are
|
|
67
69
|
// debuggable.
|
|
68
70
|
if (isUserError(err)) {
|
|
69
|
-
// A message that
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
71
|
+
// A message that names its verb (`vx why: …`) is that verb's; any
|
|
72
|
+
// other is the verb typed. A config refusal shows its line.
|
|
73
|
+
const named = /^(vx [a-z][\w-]*): /.exec(err.message)
|
|
74
|
+
const typed = /^[a-z][\w-]*$/.test(argv[0] ?? '') ? `vx ${argv[0]}` : 'vx'
|
|
75
|
+
const block = refusalNotice(
|
|
76
|
+
named?.[1] ?? typed,
|
|
77
|
+
named === null ? err.message : err.message.slice(named[0].length),
|
|
78
|
+
err.parts,
|
|
79
|
+
)
|
|
80
|
+
process.stderr.write(maskedLine(ui.render([block], detectColors(process.stderr))))
|
|
75
81
|
} else if (isOutOfFds(err)) {
|
|
76
82
|
say(`vx: ${err.message} — ${OUT_OF_FDS_HINT}\n`)
|
|
77
83
|
} else if (isFsRefusal(err)) {
|
package/src/cache/cache.ts
CHANGED
|
@@ -96,6 +96,7 @@ import {
|
|
|
96
96
|
type PruneResult,
|
|
97
97
|
type RunRecord,
|
|
98
98
|
type SaveArgs,
|
|
99
|
+
type TaskInputComponent,
|
|
99
100
|
type TaskInputRow,
|
|
100
101
|
WORKSPACE_OUTPUT_PREFIX,
|
|
101
102
|
} from './layer.js'
|
|
@@ -303,17 +304,25 @@ function outOfFdsAtOpen(dbFile: string, err: unknown): UserError | undefined {
|
|
|
303
304
|
// unchanged.
|
|
304
305
|
// v36: the store's task_durations: each executed success's time, tagged
|
|
305
306
|
// by branch, read by the forecast. The cache KEY is unchanged.
|
|
306
|
-
|
|
307
|
+
// v37: the store's plugin_blobs (a plugin's `ctx.cache`) and one
|
|
308
|
+
// 'remote' task_durations row per (sha, task). The cache KEY is
|
|
309
|
+
// unchanged.
|
|
310
|
+
// v38: blob_sizes moved to the store, and the store's task_outcomes
|
|
311
|
+
// holds the flaky record, so worktrees share both. The cache KEY is
|
|
312
|
+
// unchanged.
|
|
313
|
+
// v39: invocations drops `tags` (`--tag` is gone). The cache KEY is
|
|
314
|
+
// unchanged.
|
|
315
|
+
// v40: runs.remote_error — each remote read or write of a task's key that
|
|
316
|
+
// failed, for `vx last`. The cache KEY is unchanged.
|
|
317
|
+
export const SCHEMA_VERSION = 'v40'
|
|
307
318
|
|
|
308
319
|
/**
|
|
309
320
|
* An entry's key components as its one `entry_inputs` row (v34): JSON
|
|
310
321
|
* `[kind, name, hash]` triples in (kind, name) code-unit order, the first
|
|
311
322
|
* of a repeated (kind, name) kept, as the per-component primary key did.
|
|
312
323
|
*/
|
|
313
|
-
function encodeEntryInputs(
|
|
314
|
-
|
|
315
|
-
): string {
|
|
316
|
-
const byId = new Map<string, Pick<TaskInputRow, 'kind' | 'name' | 'hash'>>()
|
|
324
|
+
function encodeEntryInputs(components: readonly TaskInputComponent[]): string {
|
|
325
|
+
const byId = new Map<string, TaskInputComponent>()
|
|
317
326
|
for (const c of components) {
|
|
318
327
|
const id = `${c.kind}\0${c.name}`
|
|
319
328
|
if (!byId.has(id)) byId.set(id, c)
|
|
@@ -333,6 +342,9 @@ const STORE_TABLES = [
|
|
|
333
342
|
'output_files',
|
|
334
343
|
'entry_stdout',
|
|
335
344
|
'task_durations',
|
|
345
|
+
'task_outcomes',
|
|
346
|
+
'blob_sizes',
|
|
347
|
+
'plugin_blobs',
|
|
336
348
|
'store_meta',
|
|
337
349
|
'entries',
|
|
338
350
|
]
|
|
@@ -779,6 +791,23 @@ export interface UploadTally {
|
|
|
779
791
|
failed: number
|
|
780
792
|
}
|
|
781
793
|
|
|
794
|
+
/** One task's traffic with the remotes this run, by its key. */
|
|
795
|
+
export interface RemoteKeyTraffic {
|
|
796
|
+
/** Its uploads queued or running. */
|
|
797
|
+
uploading: number
|
|
798
|
+
/** An upload of it landed on a remote. */
|
|
799
|
+
landed: boolean
|
|
800
|
+
/** Each distinct remote read or write of it that failed: `upload: <message>`. */
|
|
801
|
+
failures: string[]
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** The run's remote traffic per key: what the footer's remote part counts, task by task. */
|
|
805
|
+
export interface RemoteTraffic {
|
|
806
|
+
/** Uploads queued or running across every remote. */
|
|
807
|
+
inFlight: number
|
|
808
|
+
readonly keys: Map<string, RemoteKeyTraffic>
|
|
809
|
+
}
|
|
810
|
+
|
|
782
811
|
export class Cache implements CacheLayer {
|
|
783
812
|
/** This IS the local layer — there is nothing slower behind it. */
|
|
784
813
|
readonly hasRemote = false
|
|
@@ -788,6 +817,8 @@ export class Cache implements CacheLayer {
|
|
|
788
817
|
* so the run can report them without a method on the layer seam.
|
|
789
818
|
*/
|
|
790
819
|
readonly uploads: UploadTally = { count: 0, bytes: 0, ms: 0, failed: 0 }
|
|
820
|
+
/** The same remotes' traffic per key, filled by the same `LayeredCache`. */
|
|
821
|
+
readonly traffic: RemoteTraffic = { inFlight: 0, keys: new Map() }
|
|
791
822
|
|
|
792
823
|
private readonly db: Database
|
|
793
824
|
private readonly insertEntry: ReturnType<Database['prepare']>
|
|
@@ -1030,7 +1061,7 @@ export class Cache implements CacheLayer {
|
|
|
1030
1061
|
// keeps it so).
|
|
1031
1062
|
if (found !== undefined) {
|
|
1032
1063
|
this.db.exec(
|
|
1033
|
-
'DROP TABLE IF EXISTS main.entries; DROP TABLE IF EXISTS main.runs; DROP TABLE IF EXISTS main.file_hashes; DROP TABLE IF EXISTS main.blob_sizes; DROP TABLE IF EXISTS main.blob_verdicts; DROP TABLE IF EXISTS main.output_files; DROP TABLE IF EXISTS main.output_stamps; DROP TABLE IF EXISTS main.invocations; DROP TABLE IF EXISTS main.run_task_inputs; DROP TABLE IF EXISTS main.entry_inputs; DROP TABLE IF EXISTS main.config_evals; DROP TABLE IF EXISTS main.config_closures; DROP TABLE IF EXISTS main.output_dirs; DROP TABLE IF EXISTS main.entry_stdout; DROP TABLE IF EXISTS main.task_durations; DROP TABLE IF EXISTS main.store_meta;',
|
|
1064
|
+
'DROP TABLE IF EXISTS main.entries; DROP TABLE IF EXISTS main.runs; DROP TABLE IF EXISTS main.file_hashes; DROP TABLE IF EXISTS main.blob_sizes; DROP TABLE IF EXISTS main.blob_verdicts; DROP TABLE IF EXISTS main.output_files; DROP TABLE IF EXISTS main.output_stamps; DROP TABLE IF EXISTS main.invocations; DROP TABLE IF EXISTS main.run_task_inputs; DROP TABLE IF EXISTS main.entry_inputs; DROP TABLE IF EXISTS main.config_evals; DROP TABLE IF EXISTS main.config_closures; DROP TABLE IF EXISTS main.output_dirs; DROP TABLE IF EXISTS main.entry_stdout; DROP TABLE IF EXISTS main.task_durations; DROP TABLE IF EXISTS main.plugin_blobs; DROP TABLE IF EXISTS main.task_outcomes; DROP TABLE IF EXISTS main.store_meta;',
|
|
1034
1065
|
)
|
|
1035
1066
|
}
|
|
1036
1067
|
// The tables land under the same write lock as the stamp: made
|
|
@@ -1737,11 +1768,26 @@ export class Cache implements CacheLayer {
|
|
|
1737
1768
|
return this.guard(() => this.hasEntry(hash))
|
|
1738
1769
|
}
|
|
1739
1770
|
|
|
1771
|
+
/**
|
|
1772
|
+
* The subset of `hashes` this store holds, `has` for each: the prefetch
|
|
1773
|
+
* pass asks the remote only about the rest (M, 2026-10-15).
|
|
1774
|
+
*/
|
|
1775
|
+
async localHasMany(hashes: readonly string[]): Promise<Set<string>> {
|
|
1776
|
+
const out = new Set<string>()
|
|
1777
|
+
for (const hash of hashes) if ((await this.has(hash)) === 'local') out.add(hash)
|
|
1778
|
+
return out
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1740
1781
|
private async hasEntry(hash: string): Promise<'local' | 'remote' | null> {
|
|
1741
1782
|
if (!this.read) return null
|
|
1783
|
+
return this.storedHere(hash) ? 'local' : null
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/** The index row and its artifact are both here, whatever the run may read. */
|
|
1787
|
+
private storedHere(hash: string): boolean {
|
|
1742
1788
|
const row = this.entryExists.get(hash) as { inline_at: number | null } | null
|
|
1743
|
-
if (row === null) return
|
|
1744
|
-
return row.inline_at !== null || existsSync(this.tarPath(hash))
|
|
1789
|
+
if (row === null) return false
|
|
1790
|
+
return row.inline_at !== null || existsSync(this.tarPath(hash))
|
|
1745
1791
|
}
|
|
1746
1792
|
|
|
1747
1793
|
// Local cache has no slower layer to warm from — prefetch is a no-op.
|
|
@@ -2035,8 +2081,6 @@ export class Cache implements CacheLayer {
|
|
|
2035
2081
|
entry: Omit<CacheEntry, 'hash' | 'storedAt' | 'outputFiles' | 'exitCode'>
|
|
2036
2082
|
projectDir: string
|
|
2037
2083
|
outputFiles: string[]
|
|
2038
|
-
/** See `CacheLayer.save`. */
|
|
2039
|
-
skipLocalWrite?: boolean
|
|
2040
2084
|
workspaceOutputFiles?: string[]
|
|
2041
2085
|
workspaceRoot?: string
|
|
2042
2086
|
inputComponents?: readonly TaskInputRow[]
|
|
@@ -2061,7 +2105,9 @@ export class Cache implements CacheLayer {
|
|
|
2061
2105
|
// still uploads to remote — it calls `packArtifact` itself to get
|
|
2062
2106
|
// the bytes, since there's no local artifact to read off disk.
|
|
2063
2107
|
if (!this.write) return
|
|
2064
|
-
|
|
2108
|
+
// A cache that reads saves only on a miss. One that does not (`--force`)
|
|
2109
|
+
// re-executes a key it may hold: that entry is kept as it is.
|
|
2110
|
+
if (!this.read && this.guard(() => this.storedHere(args.hash))) return
|
|
2065
2111
|
// The cache directory is the constructor's (`mkdirSync`, with a
|
|
2066
2112
|
// refusal recorded for `assertWritable`); re-creating it here and
|
|
2067
2113
|
// again in `writeArtifactAndIndex` cost every save two thread-pool
|
|
@@ -2612,6 +2658,9 @@ export class Cache implements CacheLayer {
|
|
|
2612
2658
|
recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
|
|
2613
2659
|
return this.guard(() => this.history.recordRunBundle(bundle))
|
|
2614
2660
|
}
|
|
2661
|
+
recordRemoteErrors(runId: string, errors: readonly (readonly [string, string])[]): void {
|
|
2662
|
+
return this.guard(() => this.history.recordRemoteErrors(runId, errors))
|
|
2663
|
+
}
|
|
2615
2664
|
stats(opts: CacheStatsOptions = {}): CacheStats {
|
|
2616
2665
|
return this.guard(() => this.statsOf(opts))
|
|
2617
2666
|
}
|
package/src/cache/git-inputs.ts
CHANGED
|
@@ -289,6 +289,12 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
289
289
|
* a cache no enumeration filled; its OIDs are then re-checked by content.
|
|
290
290
|
*/
|
|
291
291
|
enumeratedAtMs: number | undefined
|
|
292
|
+
/**
|
|
293
|
+
* The declared outputs (root-relative globs) git ignores whole, asked
|
|
294
|
+
* once per run beside the enumeration: no key reads them, so a reader
|
|
295
|
+
* beside their producer keys up front (`dependsOnSiblingOutputs`).
|
|
296
|
+
*/
|
|
297
|
+
ignoredOutputs: ReadonlySet<string> = new Set()
|
|
292
298
|
|
|
293
299
|
get undecodableNames(): ReadonlySet<string> {
|
|
294
300
|
return this.undecodable
|
package/src/cache/index.ts
CHANGED
|
@@ -15,11 +15,13 @@ export {
|
|
|
15
15
|
type CachePolicy,
|
|
16
16
|
cachesNothing,
|
|
17
17
|
EXECUTED_RUNS_SQL,
|
|
18
|
+
formatCachePolicy,
|
|
18
19
|
FULL_CACHE_POLICY,
|
|
19
20
|
type InvocationRecord,
|
|
20
21
|
KEYED_RUNS_SQL,
|
|
21
22
|
parseCachePolicy,
|
|
22
23
|
type RunRecord,
|
|
24
|
+
type TaskInputComponent,
|
|
23
25
|
type TaskInputRow,
|
|
24
26
|
WORKSPACE_OUTPUT_PREFIX,
|
|
25
27
|
type OutputDirRow,
|
|
@@ -47,7 +49,6 @@ export {
|
|
|
47
49
|
type ProjectFilesCache,
|
|
48
50
|
type WorkspaceFilesCache,
|
|
49
51
|
cleanOutputPaths,
|
|
50
|
-
anyUntouched,
|
|
51
52
|
ownOutputsSince,
|
|
52
53
|
ownWorkspaceOutputsSince,
|
|
53
54
|
stampOutputs,
|
|
@@ -69,6 +70,13 @@ export {
|
|
|
69
70
|
type BlobSizeMemo,
|
|
70
71
|
} from './git-inputs.js'
|
|
71
72
|
export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
|
|
73
|
+
export type { ResolvedRemote } from './remote-lane.js'
|
|
72
74
|
export { ScopedCache } from './scoped-cache.js'
|
|
73
|
-
export {
|
|
75
|
+
export {
|
|
76
|
+
absentPluginCache,
|
|
77
|
+
pluginCache,
|
|
78
|
+
PLUGIN_ENTRY_MAX_BYTES,
|
|
79
|
+
type PluginCache,
|
|
80
|
+
} from './plugin-cache.js'
|
|
81
|
+
export { packRunOutputs, putRunEntry, runEntryName, writesRunStore } from './run-store.js'
|
|
74
82
|
export { branchSource } from './run-history.js'
|
package/src/cache/inputs.ts
CHANGED
|
@@ -81,7 +81,6 @@ const ALWAYS_IGNORE = [
|
|
|
81
81
|
'**/.git/**',
|
|
82
82
|
// vx's machine-local state; the workspace config beside it is a source.
|
|
83
83
|
'**/.vx/cache/**',
|
|
84
|
-
'**/*.tsbuildinfo',
|
|
85
84
|
'**/.vx/projects.lock.json',
|
|
86
85
|
// `vx lock`'s review copy of a project's resolved config: the config is
|
|
87
86
|
// keyed already, and a re-lock must not move every `**` reader's key.
|
|
@@ -1018,29 +1017,6 @@ function changedSince(
|
|
|
1018
1017
|
return kept < before.size ? undefined : own
|
|
1019
1018
|
}
|
|
1020
1019
|
|
|
1021
|
-
/**
|
|
1022
|
-
* Is any file `before` stamped still there, unchanged? Under
|
|
1023
|
-
* `cache.outputs.clean: false` such a file is one the run found and never
|
|
1024
|
-
* wrote, so no entry may claim it as the run's output.
|
|
1025
|
-
*/
|
|
1026
|
-
export function anyUntouched(before: ReadonlyMap<string, OutputStamp>): boolean {
|
|
1027
|
-
for (const [f, was] of before) {
|
|
1028
|
-
try {
|
|
1029
|
-
const st = lstatSync(f)
|
|
1030
|
-
if (
|
|
1031
|
-
st.size === was.size &&
|
|
1032
|
-
st.mtimeMs === was.mtimeMs &&
|
|
1033
|
-
st.ino === was.ino &&
|
|
1034
|
-
st.ctimeMs === was.ctimeMs
|
|
1035
|
-
)
|
|
1036
|
-
return true
|
|
1037
|
-
} catch {
|
|
1038
|
-
// Removed by the run: nothing of it is left to claim.
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return false
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
1020
|
/**
|
|
1045
1021
|
* Up to this many paths, a clean removes synchronously: the threadpool round
|
|
1046
1022
|
* trip per `rm`/`rmdir` cost more than the unlink itself, 0.30 ms against
|
package/src/cache/layer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The cache layer CONTRACT and its records — what every cache implementation
|
|
2
|
-
// honours (`Cache`, `LayeredCache`,
|
|
2
|
+
// honours (`Cache`, `LayeredCache`, a plugin's remote layer) and
|
|
3
3
|
// the shapes that cross it: the key input, entries, run-history rows, output
|
|
4
4
|
// fingerprints, stats, prune options. No implementation lives here; the one
|
|
5
5
|
// reference to `Cache` below is a type (the local handle a layer may wrap).
|
|
@@ -208,6 +208,8 @@ export interface RunRecord {
|
|
|
208
208
|
timedOut?: true
|
|
209
209
|
sandboxViolations?: number
|
|
210
210
|
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
211
|
+
/** Each remote read or write of its key that failed, `upload: <message>`, `; `-joined (v40). */
|
|
212
|
+
remoteError?: string
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
/**
|
|
@@ -221,7 +223,7 @@ export interface InvocationRecord {
|
|
|
221
223
|
command: string
|
|
222
224
|
/** JSON-serialized `string[]` of the requested task names. */
|
|
223
225
|
requestedTasks: string
|
|
224
|
-
/**
|
|
226
|
+
/** The `--cache-policy` spec, e.g. `'local:rw,remote:r'` (a flag omitted = off). */
|
|
225
227
|
cachePolicy: string
|
|
226
228
|
concurrency: number
|
|
227
229
|
flow: 'focused' | 'broad' | null
|
|
@@ -247,8 +249,18 @@ export interface InvocationRecord {
|
|
|
247
249
|
os: string | null
|
|
248
250
|
arch: string | null
|
|
249
251
|
vxVersion: string
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* One cache-key component as `key()` folded it, the `captureInto` row.
|
|
256
|
+
* Persisted on a miss (`TaskInputRow`) so a later run can diff its inputs
|
|
257
|
+
* against this one (the Tier-3 "why did this re-run?" moat); a hit saves
|
|
258
|
+
* nothing, so the warm path is free.
|
|
259
|
+
*/
|
|
260
|
+
export interface TaskInputComponent {
|
|
261
|
+
kind: string
|
|
262
|
+
name: string
|
|
263
|
+
hash: string
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
/**
|
|
@@ -544,12 +556,25 @@ export interface CacheLayer {
|
|
|
544
556
|
* batch omits it and the prefetch pass falls back to per-hash GETs.
|
|
545
557
|
*/
|
|
546
558
|
remoteHasMany?(hashes: readonly string[]): Promise<Set<string> | null>
|
|
559
|
+
/**
|
|
560
|
+
* The subset of `hashes` the local store already holds. The prefetch
|
|
561
|
+
* pass probes the remote only for the rest, so a run the local cache
|
|
562
|
+
* answers makes no remote call. Absent reads as "none held".
|
|
563
|
+
*/
|
|
564
|
+
localHasMany?(hashes: readonly string[]): Promise<Set<string>>
|
|
547
565
|
/**
|
|
548
566
|
* Record that the remote has NO artifact for each of `hashes` (from a
|
|
549
567
|
* batch probe), so a later `get`/`prefetch` short-circuits to a miss with
|
|
550
568
|
* no round-trip. Only meaningful with `hasRemote`.
|
|
551
569
|
*/
|
|
552
570
|
markRemoteAbsent?(hashes: Iterable<string>): void
|
|
571
|
+
/**
|
|
572
|
+
* The run's up-front keys, for each remote the run writes but does not
|
|
573
|
+
* read (`--force`): one batch probe in the background, and an upload of
|
|
574
|
+
* a key the remote already holds is skipped. Only meaningful with
|
|
575
|
+
* `hasRemote`.
|
|
576
|
+
*/
|
|
577
|
+
probeBeforeUpload?(hashes: Promise<readonly string[]>): void
|
|
553
578
|
/**
|
|
554
579
|
* Resolve once every background write-through upload settles. The
|
|
555
580
|
* orchestrator awaits it before `close()` — an upload reading layer state
|
|
@@ -557,6 +582,8 @@ export interface CacheLayer {
|
|
|
557
582
|
* uploads synchronously (or not at all) omits it.
|
|
558
583
|
*/
|
|
559
584
|
drainUploads?(): Promise<void>
|
|
585
|
+
/** The run was stopped: start no more uploads, and let `drainUploads` resolve now. */
|
|
586
|
+
stopUploads?(): void
|
|
560
587
|
key(input: CacheKeyInput): Promise<string>
|
|
561
588
|
get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null>
|
|
562
589
|
/**
|
|
@@ -574,7 +601,7 @@ export interface CacheLayer {
|
|
|
574
601
|
* artifact read, no remote download, no local ingest, no accessed_at
|
|
575
602
|
* bump (on the remote side the layer decides the call: an HTTP HEAD, or
|
|
576
603
|
* REAPI's `getActionResult` + `findMissingBlobs`). Planning
|
|
577
|
-
* (`--dry`
|
|
604
|
+
* (`--dry`) predicts hits with this instead of `get` so a
|
|
578
605
|
* dry run can't pull N artifacts over the network. Remote errors
|
|
579
606
|
* degrade to `null` — an existence probe never fails anything.
|
|
580
607
|
*/
|
|
@@ -664,13 +691,6 @@ export interface CacheLayer {
|
|
|
664
691
|
entry: Omit<CacheEntry, 'hash' | 'storedAt' | 'outputFiles' | 'exitCode'>
|
|
665
692
|
projectDir: string
|
|
666
693
|
outputFiles: string[]
|
|
667
|
-
/**
|
|
668
|
-
* Set only by ChainedCache when an EARLIER layer in the chain already
|
|
669
|
-
* wrote this artifact to the same local handle: the local pack + write
|
|
670
|
-
* is skipped and only the layer's remote side acts. A layer with no
|
|
671
|
-
* remote side treats it as a full no-op.
|
|
672
|
-
*/
|
|
673
|
-
skipLocalWrite?: boolean
|
|
674
694
|
/**
|
|
675
695
|
* Resolved `outputs.workspaceFiles` (absolute paths) + the root
|
|
676
696
|
* they're relative to. Packed under `workspace-outputs/<rel>`.
|