@vzn/vx 0.0.638 → 0.0.640
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 +1 -1
- package/docs/caching.md +152 -95
- package/docs/cli.md +225 -372
- package/docs/execution.md +22 -47
- package/docs/features.md +14 -16
- package/docs/patterns.md +6 -8
- package/docs/schema.md +268 -215
- package/docs/security.md +1 -1
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +10 -7
- package/src/cache/cache.ts +44 -11
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +8 -2
- package/src/cache/index.ts +5 -2
- package/src/cache/inputs.ts +154 -83
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +33 -18
- package/src/cache/layered-cache.ts +108 -315
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cache/plugin-cache.ts +2 -10
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +24 -8
- package/src/cache/run-store.ts +4 -4
- package/src/cache/schema.ts +6 -4
- package/src/cache/scoped-cache.ts +7 -0
- package/src/cli/completions.ts +2 -2
- package/src/cli/foreign-flags.ts +11 -23
- package/src/cli/help.ts +6 -31
- package/src/cli/info.ts +2 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +30 -4
- package/src/cli/plan-format.ts +5 -58
- package/src/cli/plugin-commands.ts +2 -2
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +23 -185
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +15 -6
- package/src/cli/upgrade.ts +215 -55
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +22 -6
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +103 -16
- package/src/cli/why.ts +9 -7
- package/src/config.ts +19 -21
- package/src/exec/executor.ts +5 -5
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +17 -2
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +45 -31
- package/src/graph/task-graph.ts +190 -58
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +1 -1
- package/src/{orchestrator → history}/history.ts +1 -1
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +7 -20
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +11 -29
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +50 -411
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +2 -73
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +1 -1
- 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 +5 -5
- package/src/orchestrator/plan.ts +19 -6
- package/src/orchestrator/plugin-host.ts +129 -69
- package/src/orchestrator/plugin.ts +18 -8
- package/src/orchestrator/prepare.ts +40 -34
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/remote-prefetch.ts +14 -1
- 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 +35 -3
- package/src/orchestrator/run-report.ts +151 -171
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +293 -604
- package/src/orchestrator/sandbox-fix.ts +1 -17
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +164 -27
- package/src/orchestrator/steps.ts +70 -0
- package/src/orchestrator/task-hash.ts +34 -25
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +4 -4
- package/src/orchestrator/telemetry.ts +14 -86
- package/src/orchestrator/upstream.ts +4 -181
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +40 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +158 -20
- package/src/{orchestrator → report}/status-line.ts +88 -7
- package/src/{orchestrator → report}/summary.ts +126 -35
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +12 -39
- package/src/util/colors.ts +44 -4
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +19 -4
- package/src/util/paths.ts +1 -1
- package/src/util/report.ts +13 -5
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +17 -6
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -280
- 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/docs/security.md
CHANGED
|
@@ -217,7 +217,7 @@ executor's error, telemetry,
|
|
|
217
217
|
`vx show` (L-26). Values in the run history are digests under a per-store
|
|
218
218
|
salt, and what follows `--` on the command line reaches telemetry as a
|
|
219
219
|
count, not a quote, a secret before it masked (L-44). A remote executor (`@vzn/vx-reapi`) receives a
|
|
220
|
-
task's `exec.env.define` values and the `
|
|
220
|
+
task's `exec.env.define` values and the `inputs.env` values its
|
|
221
221
|
local child gets, never `passThrough`; those values sit unmasked in the
|
|
222
222
|
action's Command, which the remote stores in its CAS, as Bazel's
|
|
223
223
|
`--action_env` does, so a secret a remote task needs is trusted to that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.640",
|
|
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.640",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.640",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.640",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.640"
|
|
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/show.json
CHANGED
|
@@ -90,24 +90,24 @@
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
|
+
"inputs": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"additionalProperties": false,
|
|
96
|
+
"required": ["files"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"files": { "$ref": "#/$defs/stringList" },
|
|
99
|
+
"workspaceFiles": { "$ref": "#/$defs/stringList" },
|
|
100
|
+
"env": { "$ref": "#/$defs/stringList" },
|
|
101
|
+
"tasks": { "$ref": "#/$defs/stringList" },
|
|
102
|
+
"runtime": { "$ref": "#/$defs/stringList" },
|
|
103
|
+
"workspaceRuntime": { "$ref": "#/$defs/stringList" }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
93
106
|
"cache": {
|
|
94
107
|
"type": "object",
|
|
95
108
|
"additionalProperties": false,
|
|
96
|
-
"required": ["
|
|
109
|
+
"required": ["outputs"],
|
|
97
110
|
"properties": {
|
|
98
|
-
"inputs": {
|
|
99
|
-
"type": "object",
|
|
100
|
-
"additionalProperties": false,
|
|
101
|
-
"required": ["files"],
|
|
102
|
-
"properties": {
|
|
103
|
-
"files": { "$ref": "#/$defs/stringList" },
|
|
104
|
-
"workspaceFiles": { "$ref": "#/$defs/stringList" },
|
|
105
|
-
"env": { "$ref": "#/$defs/stringList" },
|
|
106
|
-
"tasks": { "$ref": "#/$defs/stringList" },
|
|
107
|
-
"runtime": { "$ref": "#/$defs/stringList" },
|
|
108
|
-
"workspaceRuntime": { "$ref": "#/$defs/stringList" }
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
111
|
"outputs": {
|
|
112
112
|
"type": "object",
|
|
113
113
|
"additionalProperties": false,
|
package/skills/vx/SKILL.md
CHANGED
|
@@ -15,13 +15,13 @@ 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
|
|
@@ -90,9 +90,10 @@ vx info --format json # vx and Bun versions, projects, config errors, plugins,
|
|
|
90
90
|
- Never delete or edit the cache by hand; vx repairs it itself.
|
|
91
91
|
- A sandboxed task that fails on a denied path: grant that one path in
|
|
92
92
|
`exec.sandbox.allow`. Never remove `sandbox` or widen it further.
|
|
93
|
-
- A
|
|
94
|
-
does not re-run a task, the file is not one of its inputs. Fix
|
|
95
|
-
config, do not force.
|
|
93
|
+
- A task declares its inputs (`inputs.files`), cached or not; if a
|
|
94
|
+
change does not re-run a task, the file is not one of its inputs. Fix
|
|
95
|
+
the config, do not force. A task with no `inputs` has no key, so every
|
|
96
|
+
cached task behind it misses: give it `inputs`.
|
|
96
97
|
- Every JSON shape is a JSON Schema in `node_modules/@vzn/vx/schemas/`.
|
|
97
98
|
- Offline reference: `vx docs <words> --format json` searches the CLI, config
|
|
98
99
|
and cache pages that ship with vx; the sections holding every word print
|
|
@@ -108,3 +109,5 @@ vx info --format json # vx and Bun versions, projects, config errors, plugins,
|
|
|
108
109
|
`npx skills add vznjs/vx` installs this skill. To match the vx a workspace
|
|
109
110
|
has installed, copy it from there instead:
|
|
110
111
|
`cp node_modules/@vzn/vx/skills/vx/SKILL.md .claude/skills/vx/`.
|
|
112
|
+
`vx upgrade` inside a workspace moves the project's `@vzn/vx` and the
|
|
113
|
+
global vx that ran it to the same version.
|
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'
|
|
@@ -111,6 +112,7 @@ import { FileHashStore } from './file-hashes.js'
|
|
|
111
112
|
import { OutputIndex } from './output-index.js'
|
|
112
113
|
import { RunHistory } from './run-history.js'
|
|
113
114
|
import { CACHE_VERSION, foldKey } from './key-fold.js'
|
|
115
|
+
import { behindLink } from './linked-inputs.js'
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
118
|
* An artifact or temp file without an `entries` row is never reaped
|
|
@@ -309,17 +311,19 @@ function outOfFdsAtOpen(dbFile: string, err: unknown): UserError | undefined {
|
|
|
309
311
|
// v38: blob_sizes moved to the store, and the store's task_outcomes
|
|
310
312
|
// holds the flaky record, so worktrees share both. The cache KEY is
|
|
311
313
|
// unchanged.
|
|
312
|
-
|
|
314
|
+
// v39: invocations drops `tags` (`--tag` is gone). The cache KEY is
|
|
315
|
+
// unchanged.
|
|
316
|
+
// v40: runs.remote_error — each remote read or write of a task's key that
|
|
317
|
+
// failed, for `vx last`. The cache KEY is unchanged.
|
|
318
|
+
export const SCHEMA_VERSION = 'v40'
|
|
313
319
|
|
|
314
320
|
/**
|
|
315
321
|
* An entry's key components as its one `entry_inputs` row (v34): JSON
|
|
316
322
|
* `[kind, name, hash]` triples in (kind, name) code-unit order, the first
|
|
317
323
|
* of a repeated (kind, name) kept, as the per-component primary key did.
|
|
318
324
|
*/
|
|
319
|
-
function encodeEntryInputs(
|
|
320
|
-
|
|
321
|
-
): string {
|
|
322
|
-
const byId = new Map<string, Pick<TaskInputRow, 'kind' | 'name' | 'hash'>>()
|
|
325
|
+
function encodeEntryInputs(components: readonly TaskInputComponent[]): string {
|
|
326
|
+
const byId = new Map<string, TaskInputComponent>()
|
|
323
327
|
for (const c of components) {
|
|
324
328
|
const id = `${c.kind}\0${c.name}`
|
|
325
329
|
if (!byId.has(id)) byId.set(id, c)
|
|
@@ -788,6 +792,23 @@ export interface UploadTally {
|
|
|
788
792
|
failed: number
|
|
789
793
|
}
|
|
790
794
|
|
|
795
|
+
/** One task's traffic with the remotes this run, by its key. */
|
|
796
|
+
export interface RemoteKeyTraffic {
|
|
797
|
+
/** Its uploads queued or running. */
|
|
798
|
+
uploading: number
|
|
799
|
+
/** An upload of it landed on a remote. */
|
|
800
|
+
landed: boolean
|
|
801
|
+
/** Each distinct remote read or write of it that failed: `upload: <message>`. */
|
|
802
|
+
failures: string[]
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** The run's remote traffic per key: what the footer's remote part counts, task by task. */
|
|
806
|
+
export interface RemoteTraffic {
|
|
807
|
+
/** Uploads queued or running across every remote. */
|
|
808
|
+
inFlight: number
|
|
809
|
+
readonly keys: Map<string, RemoteKeyTraffic>
|
|
810
|
+
}
|
|
811
|
+
|
|
791
812
|
export class Cache implements CacheLayer {
|
|
792
813
|
/** This IS the local layer — there is nothing slower behind it. */
|
|
793
814
|
readonly hasRemote = false
|
|
@@ -797,6 +818,8 @@ export class Cache implements CacheLayer {
|
|
|
797
818
|
* so the run can report them without a method on the layer seam.
|
|
798
819
|
*/
|
|
799
820
|
readonly uploads: UploadTally = { count: 0, bytes: 0, ms: 0, failed: 0 }
|
|
821
|
+
/** The same remotes' traffic per key, filled by the same `LayeredCache`. */
|
|
822
|
+
readonly traffic: RemoteTraffic = { inFlight: 0, keys: new Map() }
|
|
800
823
|
|
|
801
824
|
private readonly db: Database
|
|
802
825
|
private readonly insertEntry: ReturnType<Database['prepare']>
|
|
@@ -1471,11 +1494,13 @@ export class Cache implements CacheLayer {
|
|
|
1471
1494
|
}
|
|
1472
1495
|
|
|
1473
1496
|
key(input: CacheKeyInput): Promise<string> {
|
|
1497
|
+
const hashFile = (f: string): Promise<string> => this.hashFile(f)
|
|
1474
1498
|
return foldKey(
|
|
1475
1499
|
input,
|
|
1476
|
-
|
|
1500
|
+
hashFile,
|
|
1477
1501
|
(f) => this.relFor(input.workspaceRoot, f),
|
|
1478
1502
|
(v) => this.digestValue(v),
|
|
1503
|
+
(link) => behindLink(link, hashFile),
|
|
1479
1504
|
)
|
|
1480
1505
|
}
|
|
1481
1506
|
|
|
@@ -1758,9 +1783,14 @@ export class Cache implements CacheLayer {
|
|
|
1758
1783
|
|
|
1759
1784
|
private async hasEntry(hash: string): Promise<'local' | 'remote' | null> {
|
|
1760
1785
|
if (!this.read) return null
|
|
1786
|
+
return this.storedHere(hash) ? 'local' : null
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** The index row and its artifact are both here, whatever the run may read. */
|
|
1790
|
+
private storedHere(hash: string): boolean {
|
|
1761
1791
|
const row = this.entryExists.get(hash) as { inline_at: number | null } | null
|
|
1762
|
-
if (row === null) return
|
|
1763
|
-
return row.inline_at !== null || existsSync(this.tarPath(hash))
|
|
1792
|
+
if (row === null) return false
|
|
1793
|
+
return row.inline_at !== null || existsSync(this.tarPath(hash))
|
|
1764
1794
|
}
|
|
1765
1795
|
|
|
1766
1796
|
// Local cache has no slower layer to warm from — prefetch is a no-op.
|
|
@@ -2054,8 +2084,6 @@ export class Cache implements CacheLayer {
|
|
|
2054
2084
|
entry: Omit<CacheEntry, 'hash' | 'storedAt' | 'outputFiles' | 'exitCode'>
|
|
2055
2085
|
projectDir: string
|
|
2056
2086
|
outputFiles: string[]
|
|
2057
|
-
/** See `CacheLayer.save`. */
|
|
2058
|
-
skipLocalWrite?: boolean
|
|
2059
2087
|
workspaceOutputFiles?: string[]
|
|
2060
2088
|
workspaceRoot?: string
|
|
2061
2089
|
inputComponents?: readonly TaskInputRow[]
|
|
@@ -2080,7 +2108,9 @@ export class Cache implements CacheLayer {
|
|
|
2080
2108
|
// still uploads to remote — it calls `packArtifact` itself to get
|
|
2081
2109
|
// the bytes, since there's no local artifact to read off disk.
|
|
2082
2110
|
if (!this.write) return
|
|
2083
|
-
|
|
2111
|
+
// A cache that reads saves only on a miss. One that does not (`--force`)
|
|
2112
|
+
// re-executes a key it may hold: that entry is kept as it is.
|
|
2113
|
+
if (!this.read && this.guard(() => this.storedHere(args.hash))) return
|
|
2084
2114
|
// The cache directory is the constructor's (`mkdirSync`, with a
|
|
2085
2115
|
// refusal recorded for `assertWritable`); re-creating it here and
|
|
2086
2116
|
// again in `writeArtifactAndIndex` cost every save two thread-pool
|
|
@@ -2631,6 +2661,9 @@ export class Cache implements CacheLayer {
|
|
|
2631
2661
|
recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
|
|
2632
2662
|
return this.guard(() => this.history.recordRunBundle(bundle))
|
|
2633
2663
|
}
|
|
2664
|
+
recordRemoteErrors(runId: string, errors: readonly (readonly [string, string])[]): void {
|
|
2665
|
+
return this.guard(() => this.history.recordRemoteErrors(runId, errors))
|
|
2666
|
+
}
|
|
2634
2667
|
stats(opts: CacheStatsOptions = {}): CacheStats {
|
|
2635
2668
|
return this.guard(() => this.statsOf(opts))
|
|
2636
2669
|
}
|
package/src/cache/file-hashes.ts
CHANGED
|
@@ -163,14 +163,10 @@ export class FileHashStore {
|
|
|
163
163
|
// a more useful error.
|
|
164
164
|
return await this.hashFileFromDisk(filePath)
|
|
165
165
|
}
|
|
166
|
-
// A symlink
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// cannot see. A link to a file inside the project tracks that file's
|
|
171
|
-
// content only when the file matches the task's globs too. No
|
|
172
|
-
// memo: readlink is one syscall, and the row would be keyed on the
|
|
173
|
-
// link's own stat, not its target's.
|
|
166
|
+
// A symlink's identity is git's: the blob of its TARGET STRING, its
|
|
167
|
+
// mode-120000 index OID, so a retarget moves it. What it points to
|
|
168
|
+
// folds after it (linked-inputs.ts). No memo: readlink is one syscall,
|
|
169
|
+
// and the row would be keyed on the link's own stat, not its target's.
|
|
174
170
|
if (st.isSymbolicLink()) {
|
|
175
171
|
return fileIdentity(
|
|
176
172
|
'120000',
|
|
@@ -257,12 +253,11 @@ export class FileHashStore {
|
|
|
257
253
|
for (const p of paths) {
|
|
258
254
|
if (stats.has(p) || out.has(p)) continue
|
|
259
255
|
try {
|
|
260
|
-
// lstat, not stat: `hashFile`
|
|
256
|
+
// lstat, not stat: `hashFile` identifies a symlink as git does —
|
|
261
257
|
// the blob of its TARGET STRING — and this form promises the same
|
|
262
258
|
// digest. Following the link here instead hashed the target's
|
|
263
259
|
// BYTES, so the same path identified differently depending on
|
|
264
|
-
// which entry point the caller used
|
|
265
|
-
// `git diff` and `--affected` cannot see.
|
|
260
|
+
// which entry point the caller used.
|
|
266
261
|
const st = lstatSync(p)
|
|
267
262
|
if (st.isSymbolicLink()) {
|
|
268
263
|
// No memo, for `hashFile`'s reason: the row would be keyed on
|
|
@@ -367,7 +362,7 @@ export class FileHashStore {
|
|
|
367
362
|
if (!isPermissionError(err)) throw err
|
|
368
363
|
throw new UserError(
|
|
369
364
|
`${filePath} is not readable by this user (${err.code}), and vx reads it to derive a ` +
|
|
370
|
-
'cache key. Make it readable, or, for a task input, take it out of
|
|
365
|
+
'cache key. Make it readable, or, for a task input, take it out of inputs.files.',
|
|
371
366
|
)
|
|
372
367
|
}
|
|
373
368
|
return this.hashBytes(bytes, filePath)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// How far the workspace's file system clock runs behind this process's. A
|
|
2
|
+
// racy window (`racyWindowMs`) compares a file's stamp, on the clock of the
|
|
3
|
+
// file system that holds it, with a moment vx read from `Date.now()`, as if
|
|
4
|
+
// they were one clock. A file system behind it (a WSL2 VM drifted from the
|
|
5
|
+
// Windows host that stamps `/mnt/c`, a Docker Desktop bind mount, an NFS
|
|
6
|
+
// server) dated an edit made while a command ran before the command, and the
|
|
7
|
+
// entry filed the edit's output under the key of the bytes before it. A
|
|
8
|
+
// clock ahead only makes a stamp look newer, which every window reads the
|
|
9
|
+
// safe way.
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A lag at or under this is the kernel's coarse tick, which stamps a write
|
|
16
|
+
* up to ~10 ms behind `Date.now()` on every box and which each window
|
|
17
|
+
* already holds; widening by it withheld saves after a fast upstream.
|
|
18
|
+
*/
|
|
19
|
+
const FS_CLOCK_TICK_MS = 20
|
|
20
|
+
|
|
21
|
+
let lagMs = 0
|
|
22
|
+
|
|
23
|
+
/** The lag every racy window is widened by: 0 unless measured past the tick. */
|
|
24
|
+
export function fsClockLag(): number {
|
|
25
|
+
return lagMs
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Widen every racy window by `ms`, when it is past the tick. */
|
|
29
|
+
export function setFsClockLag(ms: number): void {
|
|
30
|
+
lagMs = ms > FS_CLOCK_TICK_MS ? ms : 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What a stamp's own precision may hide (see `racyWindowMs`): a stamp with
|
|
35
|
+
* no sub-second part stands for any moment in its second, an even one for
|
|
36
|
+
* FAT's two.
|
|
37
|
+
*/
|
|
38
|
+
export function stampGrainMs(stampMs: number): number {
|
|
39
|
+
if (stampMs % 2000 === 0) return 2000
|
|
40
|
+
return stampMs % 1000 === 0 ? 1000 : 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Measure the lag from a file written and removed in `dir` (the run's cache
|
|
45
|
+
* dir: on the workspace's file system by default, and ignored by git and
|
|
46
|
+
* `vx watch`), and apply it. Once a run: one write, one stat, one unlink. A
|
|
47
|
+
* directory that cannot take the file measures no lag.
|
|
48
|
+
*/
|
|
49
|
+
export function measureFsClockLag(dir: string): void {
|
|
50
|
+
const stamp = path.join(dir, `.vx-clock-${process.pid}`)
|
|
51
|
+
let ms = 0
|
|
52
|
+
try {
|
|
53
|
+
mkdirSync(dir, { recursive: true })
|
|
54
|
+
writeFileSync(stamp, '')
|
|
55
|
+
const at = statSync(stamp).mtimeMs
|
|
56
|
+
ms = Date.now() - at - stampGrainMs(at)
|
|
57
|
+
unlinkSync(stamp)
|
|
58
|
+
} catch {
|
|
59
|
+
// unmeasured: no lag, as before
|
|
60
|
+
}
|
|
61
|
+
setFsClockLag(ms)
|
|
62
|
+
}
|
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
|
|
@@ -1256,7 +1262,7 @@ function stripPrefixFromSet(paths: Set<string>, prefix: string): Set<string> {
|
|
|
1256
1262
|
* directly — clean-tree input hashing costs zero reads/stats/SQLite.
|
|
1257
1263
|
*
|
|
1258
1264
|
* `workspaceWide`: set when any loaded task declares
|
|
1259
|
-
* `
|
|
1265
|
+
* `inputs.workspaceFiles`. Disables pathspec scoping (those
|
|
1260
1266
|
* globs must see every file from the root) and additionally stores a
|
|
1261
1267
|
* workspace-wide partition keyed by `workspaceRoot` (files + trusted
|
|
1262
1268
|
* OIDs), which `resolveWorkspaceFiles` consumes. When false, the
|
|
@@ -1833,7 +1839,7 @@ export async function applyGitEnumeration(
|
|
|
1833
1839
|
// workspace's git holds as ONE entry (a gitlink, `dir/` when untracked)
|
|
1834
1840
|
// — is listed through its own git above (`expandNestedRepos`), but under
|
|
1835
1841
|
// a pathspec naming a project inside it the workspace's git lists
|
|
1836
|
-
// nothing at all; so is a directory ignored outright. Storing the empty slice made the key never move: `
|
|
1842
|
+
// nothing at all; so is a directory ignored outright. Storing the empty slice made the key never move: `inputs
|
|
1837
1843
|
// matched no files`, then a stale hit under a green run once the source
|
|
1838
1844
|
// changed (2026-09-16). No partition instead: `resolveFiles` spawns
|
|
1839
1845
|
// `git ls-files` in the project's own directory, which a nested
|
package/src/cache/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
KEYED_RUNS_SQL,
|
|
22
22
|
parseCachePolicy,
|
|
23
23
|
type RunRecord,
|
|
24
|
+
type TaskInputComponent,
|
|
24
25
|
type TaskInputRow,
|
|
25
26
|
WORKSPACE_OUTPUT_PREFIX,
|
|
26
27
|
type OutputDirRow,
|
|
@@ -33,6 +34,8 @@ export {
|
|
|
33
34
|
vxCacheEntries,
|
|
34
35
|
} from './cache.js'
|
|
35
36
|
export { ABSENT_INPUT, absentOr, CACHE_VERSION } from './key-fold.js'
|
|
37
|
+
export { measureFsClockLag, setFsClockLag } from './fs-clock.js'
|
|
38
|
+
export { filesBehind } from './linked-inputs.js'
|
|
36
39
|
export {
|
|
37
40
|
addedInput,
|
|
38
41
|
type InputListing,
|
|
@@ -40,6 +43,7 @@ export {
|
|
|
40
43
|
committedFiles,
|
|
41
44
|
cleanWorkspaceOutputs,
|
|
42
45
|
declaresInput,
|
|
46
|
+
namedOutright,
|
|
43
47
|
workspaceFilesReachInto,
|
|
44
48
|
resolveInputs,
|
|
45
49
|
resolveOutputs,
|
|
@@ -69,14 +73,13 @@ export {
|
|
|
69
73
|
type BlobSizeMemo,
|
|
70
74
|
} from './git-inputs.js'
|
|
71
75
|
export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
|
|
76
|
+
export type { ResolvedRemote } from './remote-lane.js'
|
|
72
77
|
export { ScopedCache } from './scoped-cache.js'
|
|
73
78
|
export {
|
|
74
79
|
absentPluginCache,
|
|
75
80
|
pluginCache,
|
|
76
81
|
PLUGIN_ENTRY_MAX_BYTES,
|
|
77
82
|
type PluginCache,
|
|
78
|
-
type PluginCacheRemote,
|
|
79
83
|
} from './plugin-cache.js'
|
|
80
84
|
export { packRunOutputs, putRunEntry, runEntryName, writesRunStore } from './run-store.js'
|
|
81
|
-
export { ChainedCache } from './chained-cache.js'
|
|
82
85
|
export { branchSource } from './run-history.js'
|