@vzn/vx 0.0.645 → 0.0.648
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 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cache/run-history.ts
CHANGED
|
@@ -6,18 +6,45 @@
|
|
|
6
6
|
import type { Database, SQLQueryBindings } from 'bun:sqlite'
|
|
7
7
|
import { lazyStatement } from './schema.js'
|
|
8
8
|
import type { InvocationRecord, RunRecord } from './layer.js'
|
|
9
|
+
import { isMeasure } from '../util/index.js'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
/** The `runs` columns `bindRun` fills, in its order. */
|
|
12
|
+
const RUN_COLUMNS = [
|
|
13
|
+
'hash',
|
|
14
|
+
'project',
|
|
15
|
+
'task',
|
|
16
|
+
'status',
|
|
17
|
+
'exit_code',
|
|
18
|
+
'duration_ms',
|
|
19
|
+
'forward_args',
|
|
20
|
+
'started_at',
|
|
21
|
+
'ended_at',
|
|
22
|
+
'run_id',
|
|
23
|
+
'cpu_ms',
|
|
24
|
+
'peak_rss_bytes',
|
|
25
|
+
'wallclock_start_ns',
|
|
26
|
+
'wallclock_end_ns',
|
|
27
|
+
'cache_hit',
|
|
28
|
+
'attempts',
|
|
29
|
+
'cached',
|
|
30
|
+
'blocked_by',
|
|
31
|
+
'timed_out',
|
|
32
|
+
'sandbox_violations',
|
|
33
|
+
'not_ready',
|
|
34
|
+
'restored',
|
|
35
|
+
'remote_error',
|
|
36
|
+
'peak_cores',
|
|
37
|
+
'avg_rss_bytes',
|
|
38
|
+
'placement',
|
|
39
|
+
'placement_cores',
|
|
40
|
+
'placement_memory',
|
|
41
|
+
'keyless_by',
|
|
42
|
+
'def',
|
|
43
|
+
'cpu_share',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const INSERT_RUNS = `INSERT INTO runs(${RUN_COLUMNS.join(', ')}) VALUES `
|
|
47
|
+
const RUN_TUPLE = `(${RUN_COLUMNS.map(() => '?').join(', ')})`
|
|
21
48
|
|
|
22
49
|
/** An INSERT of `n` `runs` rows. */
|
|
23
50
|
function insertRunsSql(n: number): string {
|
|
@@ -25,14 +52,15 @@ function insertRunsSql(n: number): string {
|
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
/**
|
|
28
|
-
* Rows per INSERT:
|
|
29
|
-
* ceiling
|
|
30
|
-
*
|
|
55
|
+
* Rows per INSERT: as many as stay within 999 bound variables, the oldest
|
|
56
|
+
* default ceiling, read off the column count (33 rows were sized for 30
|
|
57
|
+
* columns and bound 1,023 at 31). A statement per row was 8.7 ms of 1,000
|
|
58
|
+
* rows, 40 at a time 6.4 (2026-10-03).
|
|
31
59
|
*/
|
|
32
|
-
const RUNS_PER_INSERT =
|
|
60
|
+
const RUNS_PER_INSERT = Math.floor(999 / RUN_COLUMNS.length)
|
|
33
61
|
|
|
34
|
-
/** Rows per `task_durations` INSERT:
|
|
35
|
-
const DURATIONS_PER_INSERT =
|
|
62
|
+
/** Rows per `task_durations` INSERT: 7 variables each, under 999. */
|
|
63
|
+
const DURATIONS_PER_INSERT = 140
|
|
36
64
|
|
|
37
65
|
/** Rows per `task_outcomes` INSERT: 6 variables each, under 999. */
|
|
38
66
|
const OUTCOMES_PER_INSERT = 160
|
|
@@ -62,6 +90,20 @@ const PASSED_HASHES = `
|
|
|
62
90
|
/** A local run's `task_durations.source`: its branch, `''` when it had none. */
|
|
63
91
|
export const branchSource = (branch: string | null): string => `branch:${branch ?? ''}`
|
|
64
92
|
|
|
93
|
+
/**
|
|
94
|
+
* How long a run would have taken alone: on a crowded worker
|
|
95
|
+
* (`cpuShare` < 1) each runnable thread waited for a core. A task that
|
|
96
|
+
* kept `n` cores busy lost its CPU's wait spread over them, under one
|
|
97
|
+
* core all of it. vx-dte#test (20 s CPU in 75 s) took 81-102 s beside
|
|
98
|
+
* test shards and the plan, reading those, said 118 s for a 92 s run
|
|
99
|
+
* (main fd5c72e, 2026-10-23).
|
|
100
|
+
*/
|
|
101
|
+
function aloneMs(r: Pick<RunRecord, 'durationMs' | 'cpuMs' | 'cpuShare'>): number {
|
|
102
|
+
const { durationMs: d, cpuMs: c, cpuShare: s } = r
|
|
103
|
+
if (c === undefined || s === undefined || !(s > 0 && s < 1) || !(d > 0)) return d
|
|
104
|
+
return Math.round(d - (c / s - c) / Math.max(1, c / (d * s)))
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
export class RunHistory {
|
|
66
108
|
private readonly insertInvocation: ReturnType<Database['prepare']>
|
|
67
109
|
|
|
@@ -105,16 +147,19 @@ export class RunHistory {
|
|
|
105
147
|
.immediate()
|
|
106
148
|
}
|
|
107
149
|
|
|
108
|
-
/** One `task_durations` row per executed success: what a forecast reads. */
|
|
150
|
+
/** One `task_durations` row per executed success, as if run alone: what a forecast reads. */
|
|
109
151
|
private insertDurations(runs: readonly RunRecord[], source: string, sha: string | null): void {
|
|
110
|
-
const done = runs.filter(
|
|
152
|
+
const done = runs.filter(
|
|
153
|
+
(r) => r.status === 'success' && r.cacheHit !== true && isMeasure(aloneMs(r)),
|
|
154
|
+
)
|
|
111
155
|
for (let i = 0; i < done.length; i += DURATIONS_PER_INSERT) {
|
|
112
156
|
const block = done.slice(i, i + DURATIONS_PER_INSERT)
|
|
113
157
|
const params: SQLQueryBindings[] = []
|
|
114
|
-
for (const r of block)
|
|
158
|
+
for (const r of block)
|
|
159
|
+
params.push(r.project, r.task, source, sha, aloneMs(r), r.endedAt, r.def ?? null)
|
|
115
160
|
this.db
|
|
116
161
|
.query(
|
|
117
|
-
`INSERT INTO task_durations(project, task, source, sha, duration_ms, recorded_at) VALUES ${Array(block.length).fill('(?, ?, ?, ?, ?, ?)').join(', ')}`,
|
|
162
|
+
`INSERT INTO task_durations(project, task, source, sha, duration_ms, recorded_at, def) VALUES ${Array(block.length).fill('(?, ?, ?, ?, ?, ?, ?)').join(', ')}`,
|
|
118
163
|
)
|
|
119
164
|
.run(...params)
|
|
120
165
|
}
|
|
@@ -170,7 +215,7 @@ export class RunHistory {
|
|
|
170
215
|
for (let i = 0; i < runs.length; i += RUNS_PER_INSERT) {
|
|
171
216
|
const block = runs.slice(i, i + RUNS_PER_INSERT)
|
|
172
217
|
const params: SQLQueryBindings[] = []
|
|
173
|
-
for (const r of block)
|
|
218
|
+
for (const r of block) for (const v of bindRun(r, digest)) params.push(v)
|
|
174
219
|
// `query` caches by text: one statement for every full block.
|
|
175
220
|
this.db.query(insertRunsSql(block.length)).run(...params)
|
|
176
221
|
}
|
|
@@ -233,10 +278,11 @@ export class RunHistory {
|
|
|
233
278
|
this.db.prepare('DELETE FROM task_durations WHERE recorded_at < ?').run(cutoff)
|
|
234
279
|
this.db.prepare('DELETE FROM task_outcomes WHERE recorded_at < ?').run(cutoff)
|
|
235
280
|
this.db.prepare('DELETE FROM plugin_blobs WHERE at < ?').run(cutoff)
|
|
281
|
+
this.db.prepare('DELETE FROM remote_hits WHERE seen_at < ?').run(cutoff)
|
|
236
282
|
}
|
|
237
283
|
}
|
|
238
284
|
|
|
239
|
-
/** Bind a RunRecord to one `RUN_TUPLE`'s
|
|
285
|
+
/** Bind a RunRecord to one `RUN_TUPLE`'s 29 positional parameters. */
|
|
240
286
|
function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBindings[] {
|
|
241
287
|
return [
|
|
242
288
|
// The ONE place the no-key sentinel is applied, so the column's
|
|
@@ -267,6 +313,14 @@ function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBi
|
|
|
267
313
|
run.notReady ?? null,
|
|
268
314
|
run.restored === undefined ? null : run.restored ? 1 : 0,
|
|
269
315
|
run.remoteError ?? null,
|
|
316
|
+
run.peakCores ?? null,
|
|
317
|
+
run.avgRssBytes ?? null,
|
|
318
|
+
run.placement ?? null,
|
|
319
|
+
run.placementCores ?? null,
|
|
320
|
+
run.placementMemoryBytes ?? null,
|
|
321
|
+
run.keylessBy ?? null,
|
|
322
|
+
run.def ?? null,
|
|
323
|
+
run.cpuShare ?? null,
|
|
270
324
|
]
|
|
271
325
|
}
|
|
272
326
|
|
package/src/cache/run-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What core keeps of a run that writes a remote (
|
|
1
|
+
// What core keeps of a run that writes a remote (2026-10-15): a failed
|
|
2
2
|
// task's log and the files its outputs left, an uncached task's log. Names
|
|
3
3
|
// come from the run id and task (`run/<id>/<task>/log`); each key is derived
|
|
4
4
|
// in a namespace no task key or plugin entry shares. Remote only: locally
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the cache is free). Plugins read them by name (`ctx.cache.getRun`).
|
|
7
7
|
|
|
8
8
|
import { xxh3hex } from '../util/index.js'
|
|
9
|
-
import type
|
|
9
|
+
import { sayRemoteFault, type ResolvedRemote } from './remote-lane.js'
|
|
10
10
|
import { scopedKey } from './scoped-cache.js'
|
|
11
11
|
import { tarPackInto, tarSize } from './tar-stream.js'
|
|
12
12
|
|
|
@@ -30,8 +30,8 @@ export function writesRunStore(remotes: readonly ResolvedRemote[]): boolean {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Store `bytes` under `name` in every remote the policy writes. An entry is
|
|
33
|
-
* written once; a remote's failure leaves the others theirs
|
|
34
|
-
* a run keeps here is a record, never its verdict.
|
|
33
|
+
* written once; a remote's failure is said and leaves the others theirs:
|
|
34
|
+
* what a run keeps here is a record, never its verdict.
|
|
35
35
|
*/
|
|
36
36
|
export async function putRunEntry(
|
|
37
37
|
remotes: readonly ResolvedRemote[],
|
|
@@ -41,16 +41,17 @@ export async function putRunEntry(
|
|
|
41
41
|
): Promise<boolean> {
|
|
42
42
|
const key = scope === undefined ? runKey(name) : scopedKey(scope, runKey(name))
|
|
43
43
|
let kept = false
|
|
44
|
-
for (const
|
|
45
|
-
if (!write) continue
|
|
44
|
+
for (const r of remotes) {
|
|
45
|
+
if (!r.write) continue
|
|
46
46
|
try {
|
|
47
|
-
await remote.put(key, new Blob([bytes]), {
|
|
47
|
+
const put = await r.remote.put(key, new Blob([bytes]), {
|
|
48
48
|
durationMs: 0,
|
|
49
49
|
...(scope === undefined ? {} : { scope }),
|
|
50
50
|
})
|
|
51
|
-
kept
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// `false`: the remote sent nothing, so nothing is kept there.
|
|
52
|
+
if (put !== false) kept = true
|
|
53
|
+
} catch (err) {
|
|
54
|
+
sayRemoteFault(r, err instanceof Error ? err : new Error(String(err)))
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
return kept
|
package/src/cache/schema.ts
CHANGED
|
@@ -24,7 +24,8 @@ export function lazyStatement(db: Database, sql: string): ReturnType<Database['p
|
|
|
24
24
|
* Create every table. `store` names the schema holding the ENTRY tables
|
|
25
25
|
* (`entries`, `entry_stdout`, `output_files`, `entry_inputs`, `store_meta`)
|
|
26
26
|
* the task timings (`task_durations`), the flaky record (`task_outcomes`),
|
|
27
|
-
* the blob sizes (`blob_sizes`)
|
|
27
|
+
* the blob sizes (`blob_sizes`), the plugins' blobs (`plugin_blobs`) and
|
|
28
|
+
* the remotes' hits (`remote_hits`):
|
|
28
29
|
* `main` when one `cache.db` holds everything (a `cacheDir`), `store` when
|
|
29
30
|
* they live in the shared store attached beside it (v32). The rest is the
|
|
30
31
|
* workspace's own: its history, its memos, and what its disk looked like
|
|
@@ -101,7 +102,24 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
101
102
|
restored INTEGER,
|
|
102
103
|
-- v40: each remote read or write of the key that failed, as
|
|
103
104
|
-- 'upload: <message>', '; '-joined; NULL when none did.
|
|
104
|
-
remote_error TEXT
|
|
105
|
+
remote_error TEXT,
|
|
106
|
+
-- v43: the most cores one sampling interval burned and the mean
|
|
107
|
+
-- resident memory over the looks; NULL when nothing sampled.
|
|
108
|
+
peak_cores REAL,
|
|
109
|
+
avg_rss_bytes INTEGER,
|
|
110
|
+
-- v44: where an executor ran it (ExecuteResult.where) and that
|
|
111
|
+
-- machine's cores and memory; NULL = here.
|
|
112
|
+
placement TEXT,
|
|
113
|
+
placement_cores INTEGER,
|
|
114
|
+
placement_memory INTEGER,
|
|
115
|
+
-- v46: the task with no inputs its key folded (TaskOutcome.keylessBy).
|
|
116
|
+
keyless_by TEXT,
|
|
117
|
+
-- v48: the task's definition (taskDefinition): a history reader
|
|
118
|
+
-- counts only the rows of the task as it is now.
|
|
119
|
+
def TEXT,
|
|
120
|
+
-- v49: the share of a core each runnable thread got where it ran
|
|
121
|
+
-- (ExecuteResult.machine.cpuShare); NULL = all it wanted.
|
|
122
|
+
cpu_share REAL
|
|
105
123
|
);
|
|
106
124
|
-- Two whole-table indexes only, and both APPEND: every row of a run carries the
|
|
107
125
|
-- same run_id and a started_at newer than everything before it, so
|
|
@@ -152,7 +170,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
152
170
|
-- worktree size git recorded (A-60). Fixed for its OID, so a warm
|
|
153
171
|
-- run asks git for none; swept with file_hashes by seen_at, the
|
|
154
172
|
-- time the row was written. v38: in the store, so a worktree reads
|
|
155
|
-
-- what another learned (
|
|
173
|
+
-- what another learned (2026-10-14).
|
|
156
174
|
CREATE TABLE IF NOT EXISTS ${store}.blob_sizes (
|
|
157
175
|
oid TEXT PRIMARY KEY,
|
|
158
176
|
size INTEGER NOT NULL,
|
|
@@ -269,7 +287,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
269
287
|
);
|
|
270
288
|
-- What a duration forecast reads (orchestrator/history.ts), one row per
|
|
271
289
|
-- executed success, shared by every worktree of the repo so a new one
|
|
272
|
-
-- forecasts on its first run (
|
|
290
|
+
-- forecasts on its first run (2026-10-14). Append-only: never
|
|
273
291
|
-- updated, pruned by age with the run history. \`source\` tags a local
|
|
274
292
|
-- run 'branch:<name>' ('branch:' detached or outside git); \`sha\` is its
|
|
275
293
|
-- commit. A forecast reads a task's newest rows of one source.
|
|
@@ -280,7 +298,15 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
280
298
|
source TEXT NOT NULL,
|
|
281
299
|
sha TEXT,
|
|
282
300
|
duration_ms INTEGER NOT NULL,
|
|
283
|
-
recorded_at INTEGER NOT NULL
|
|
301
|
+
recorded_at INTEGER NOT NULL,
|
|
302
|
+
-- v45: a 'remote' row's measured need (TaskResources); NULL on the
|
|
303
|
+
-- rest, whose need the run history holds.
|
|
304
|
+
cpu_parallelism REAL,
|
|
305
|
+
peak_rss_bytes INTEGER,
|
|
306
|
+
saturated_cores INTEGER,
|
|
307
|
+
saturated_memory_bytes INTEGER,
|
|
308
|
+
-- v48: the task's definition (taskDefinition), as on \`runs\`.
|
|
309
|
+
def TEXT
|
|
284
310
|
);
|
|
285
311
|
CREATE INDEX IF NOT EXISTS ${store}.task_durations_task
|
|
286
312
|
ON task_durations(project, task, source, id);
|
|
@@ -292,7 +318,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
292
318
|
-- v38: the flaky record (orchestrator/failure-mode.ts): each keyed,
|
|
293
319
|
-- cached, unblocked outcome that observed its key, shared by every
|
|
294
320
|
-- worktree so a key that passed in one and failed in another is flaky
|
|
295
|
-
-- in both (
|
|
321
|
+
-- in both (2026-10-14). Every executed success or failure, and
|
|
296
322
|
-- a key's first hit (run-history.ts): a later hit replays a pass the
|
|
297
323
|
-- record holds, so a warm run on keys that passed writes no row.
|
|
298
324
|
-- Append-only, ordered by id, pruned by age with the run history.
|
|
@@ -314,6 +340,16 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
314
340
|
bytes BLOB NOT NULL,
|
|
315
341
|
at INTEGER NOT NULL
|
|
316
342
|
);
|
|
343
|
+
-- What a remote's has-many said it holds (remote-hits.ts), shared by
|
|
344
|
+
-- every worktree: a key here is a hit until a has-many or a GET says it
|
|
345
|
+
-- is gone, and a key not here is a miss. Only hits are kept. Pruned by
|
|
346
|
+
-- \`seen_at\` with the run history.
|
|
347
|
+
CREATE TABLE IF NOT EXISTS ${store}.remote_hits (
|
|
348
|
+
hash TEXT PRIMARY KEY,
|
|
349
|
+
size INTEGER,
|
|
350
|
+
duration_ms INTEGER,
|
|
351
|
+
seen_at INTEGER NOT NULL
|
|
352
|
+
);
|
|
317
353
|
${artifactTables(store)}
|
|
318
354
|
`)
|
|
319
355
|
}
|
package/src/cache/tar-stream.ts
CHANGED
|
@@ -38,6 +38,8 @@ const BLOCK = 512
|
|
|
38
38
|
* numbers; a MiB is far past any writer's.
|
|
39
39
|
*/
|
|
40
40
|
const MAX_EXTENDED_HEADER = 1024 * 1024
|
|
41
|
+
/** The largest millisecond count a Date holds (ECMA-262 §21.4.1.1). */
|
|
42
|
+
const DATE_MAX_MS = 8.64e15
|
|
41
43
|
// `ignoreBOM`: a default decoder drops a leading U+FEFF, and a name or
|
|
42
44
|
// prefix field that began with one read back without it.
|
|
43
45
|
const decoder = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
@@ -232,6 +234,9 @@ class TarDecoder {
|
|
|
232
234
|
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156]!)
|
|
233
235
|
const size = this.pendingSize ?? octal(h, 124, 12)
|
|
234
236
|
const mtimeMs = octal(h, 136, 12) * 1000
|
|
237
|
+
// Only GNU base-256 reaches past it, and a restore's utimes threw on
|
|
238
|
+
// such a Date after the renames before it had landed.
|
|
239
|
+
if (mtimeMs > DATE_MAX_MS) throw new TarFormatError(`header mtime past what a Date holds`)
|
|
235
240
|
const padded = Math.ceil(size / BLOCK) * BLOCK
|
|
236
241
|
// POSIX `ustar\0` only: old GNU's `ustar ` keeps atime and ctime at
|
|
237
242
|
// 345, and `-G` fills them, so a prefix read there named every entry
|
package/src/cache/umask.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// The process umask, read when a restore starts: Bun reads it by setting 0
|
|
2
2
|
// and putting it back in one synchronous call (project-loader.ts
|
|
3
3
|
// `currentUmask`), so no spawn on this thread sees the 0, and a run
|
|
4
|
-
// evaluates no config in a worker.
|
|
5
|
-
// shim has no umask.
|
|
4
|
+
// evaluates no config in a worker.
|
|
6
5
|
const proc = globalThis.process as { umask?: () => number } | undefined
|
|
7
6
|
let umask = 0
|
|
8
7
|
|
package/src/cli/cache.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync } from 'node:fs'
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, rmSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { Cache, vxCacheEntries } from '../cache/index.js'
|
|
5
5
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
6
6
|
import {
|
|
7
7
|
errorCode,
|
|
8
|
+
isUserError,
|
|
8
9
|
nearest,
|
|
9
10
|
parseDuration,
|
|
10
11
|
parseSize,
|
|
@@ -57,11 +58,11 @@ const CACHE_ELSEWHERE: Record<string, string> = {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function cacheSubHint(sub: string): string {
|
|
60
|
-
|
|
61
|
-
if (
|
|
61
|
+
// Own keys only: `vx cache constructor` printed `Object`'s source.
|
|
62
|
+
if (Object.hasOwn(CACHE_ELSEWHERE, sub)) return ` — ${CACHE_ELSEWHERE[sub]}`
|
|
62
63
|
const best = nearest(sub, ['clean'])
|
|
63
64
|
// `clean` is the one subcommand: name it when nothing is near (M-58).
|
|
64
|
-
return best === undefined ? '.
|
|
65
|
+
return best === undefined ? '. Its one subcommand is `vx cache clean`' : `. Did you mean ${best}?`
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
@@ -78,6 +79,16 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
|
78
79
|
'VX_E_USAGE',
|
|
79
80
|
)
|
|
80
81
|
default:
|
|
82
|
+
// `vx cache --older-than 30d` dropped the verb, not misspelled it:
|
|
83
|
+
// name the clean command those flags make, when they parse as one.
|
|
84
|
+
if (sub.startsWith('-') && parsePruneArgs(args).error === undefined) {
|
|
85
|
+
return refuse(
|
|
86
|
+
'cache',
|
|
87
|
+
args,
|
|
88
|
+
`missing subcommand before ${sub}. Did you mean \`vx cache clean ${args.join(' ')}\`?${seeHelp('cache')}`,
|
|
89
|
+
'VX_E_USAGE',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
81
92
|
return refuse(
|
|
82
93
|
'cache',
|
|
83
94
|
args,
|
|
@@ -90,7 +101,7 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
|
90
101
|
interface PruneArgs {
|
|
91
102
|
olderThanMs?: number
|
|
92
103
|
maxBytes?: number
|
|
93
|
-
|
|
104
|
+
dry?: boolean
|
|
94
105
|
format?: 'pretty' | 'json'
|
|
95
106
|
/** `--cache-dir`: prune the cache a run with the same flag uses. */
|
|
96
107
|
cacheDir?: string
|
|
@@ -149,8 +160,8 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
out.maxBytes = bytes
|
|
152
|
-
} else if (a === '--dry
|
|
153
|
-
out.
|
|
163
|
+
} else if (a === '--dry') {
|
|
164
|
+
out.dry = true
|
|
154
165
|
} else if (a === '--global') {
|
|
155
166
|
out.global = true
|
|
156
167
|
} else if (a === '--format' || a?.startsWith('--format=')) {
|
|
@@ -205,7 +216,7 @@ async function cleanCmd(args: readonly string[]): Promise<number> {
|
|
|
205
216
|
else {
|
|
206
217
|
const ws = await loadCliWorkspace(root)
|
|
207
218
|
dir = ws.cacheDir
|
|
208
|
-
storeRoot = await ws.storeRoot
|
|
219
|
+
storeRoot = await ws.storeRoot()
|
|
209
220
|
}
|
|
210
221
|
if (parsed.olderThanMs === undefined && parsed.maxBytes === undefined) {
|
|
211
222
|
return clear(args, root, dir, storeRoot, parsed)
|
|
@@ -213,15 +224,17 @@ async function cleanCmd(args: readonly string[]): Promise<number> {
|
|
|
213
224
|
// A workspace that never ran has nothing to prune, and opening the cache
|
|
214
225
|
// to find that out made it: `.vx/cache` with a database and a
|
|
215
226
|
// `.gitignore`, where item 900 had held the dry run to making nothing
|
|
216
|
-
// (and a read-only checkout was refused as unwritable instead).
|
|
217
|
-
|
|
227
|
+
// (and a read-only checkout was refused as unwritable instead). So has a
|
|
228
|
+
// directory holding nothing vx wrote (`--cache-dir build`): opened, it got
|
|
229
|
+
// an index and a `*` .gitignore that hid the user's files from git.
|
|
230
|
+
const dry = parsed.dry === true
|
|
218
231
|
const json = parsed.format === 'json'
|
|
219
|
-
if (
|
|
232
|
+
if (vxCacheEntries(dir).length === 0 && (storeRoot === null || !existsSync(storeRoot))) {
|
|
220
233
|
printPruned({ evicted: 0, bytesFreed: 0, orphans: 0, orphanBytes: 0 }, dry, json)
|
|
221
234
|
return 0
|
|
222
235
|
}
|
|
223
236
|
const cache =
|
|
224
|
-
parsed.
|
|
237
|
+
parsed.dry === true
|
|
225
238
|
? new Cache(dir, undefined, undefined, undefined, 'preview', storeRoot)
|
|
226
239
|
: new Cache(dir, undefined, root, undefined, 'open', storeRoot)
|
|
227
240
|
// A prune deletes rows and artifacts; a cache this user cannot write is
|
|
@@ -229,19 +242,19 @@ async function cleanCmd(args: readonly string[]): Promise<number> {
|
|
|
229
242
|
// than dying in the first DELETE with SQLite's "readonly database" and a
|
|
230
243
|
// stack (an unprivileged user on a root-owned `.vx`, 2026-09-16). A dry
|
|
231
244
|
// run only reads, and reads a read-only cache fine.
|
|
232
|
-
if (!parsed.
|
|
245
|
+
if (!parsed.dry) cache.assertWritable()
|
|
233
246
|
// A prune beside a run on this workspace evicts what the run has just
|
|
234
247
|
// probed as hits, whose `accessed_at` bumps it has not flushed yet; the
|
|
235
248
|
// run then re-runs those tasks (upstream survey, nx#36688). So a prune
|
|
236
249
|
// takes the run's lock and waits its turn. A dry run deletes nothing.
|
|
237
|
-
const releaseRunLock = parsed.
|
|
250
|
+
const releaseRunLock = parsed.dry
|
|
238
251
|
? async (): Promise<void> => {}
|
|
239
252
|
: await acquireRunLock(root, { log: warnToStderr })
|
|
240
253
|
try {
|
|
241
|
-
const opts: { olderThanMs?: number; maxBytes?: number;
|
|
254
|
+
const opts: { olderThanMs?: number; maxBytes?: number; dry?: boolean } = {}
|
|
242
255
|
if (parsed.olderThanMs !== undefined) opts.olderThanMs = parsed.olderThanMs
|
|
243
256
|
if (parsed.maxBytes !== undefined) opts.maxBytes = parsed.maxBytes
|
|
244
|
-
if (parsed.
|
|
257
|
+
if (parsed.dry) opts.dry = true
|
|
245
258
|
printPruned(await cache.prune(opts), dry, json)
|
|
246
259
|
} finally {
|
|
247
260
|
cache.close()
|
|
@@ -258,7 +271,7 @@ function printPruned(
|
|
|
258
271
|
if (json) {
|
|
259
272
|
const { evicted, bytesFreed, orphans, orphanBytes } = result
|
|
260
273
|
process.stdout.write(
|
|
261
|
-
`${JSON.stringify({
|
|
274
|
+
`${JSON.stringify({ dry: dry, evicted, bytesFreed, orphans, orphanBytes })}\n`,
|
|
262
275
|
)
|
|
263
276
|
return
|
|
264
277
|
}
|
|
@@ -286,7 +299,7 @@ function freedStatus(dry: boolean, bytes: number, any: boolean): ui.Block {
|
|
|
286
299
|
|
|
287
300
|
/** `vx cache clean --format json` with no bound (schemas/cache.json `clear`). */
|
|
288
301
|
export interface ClearResult {
|
|
289
|
-
|
|
302
|
+
dry: boolean
|
|
290
303
|
/** Workspace-relative inside the workspace, else absolute. */
|
|
291
304
|
cleared: string[]
|
|
292
305
|
bytesFreed: number
|
|
@@ -308,17 +321,27 @@ async function clear(
|
|
|
308
321
|
storeRoot: string | null,
|
|
309
322
|
parsed: PruneArgs,
|
|
310
323
|
): Promise<number> {
|
|
311
|
-
// The store the index recorded
|
|
312
|
-
//
|
|
324
|
+
// The store the index recorded and today's (a changed HOME, an origin
|
|
325
|
+
// added) both go; a directory with no index is not a vx cache to delete.
|
|
313
326
|
const targets: string[] = []
|
|
314
327
|
if (existsSync(path.join(dir, 'cache.db'))) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
// An index SQLite cannot read is what its own refusal sends here to
|
|
329
|
+
// clear: today's store stands in for the one it recorded.
|
|
330
|
+
let store: string | undefined = storeRoot ?? undefined
|
|
331
|
+
try {
|
|
332
|
+
const cache = new Cache(dir, undefined, undefined, undefined, 'preview', storeRoot)
|
|
333
|
+
store = cache.storeDir
|
|
334
|
+
cache.close()
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (!isUserError(err)) throw err
|
|
337
|
+
}
|
|
318
338
|
targets.push(dir)
|
|
319
339
|
// The index is a file in the checkout (committed, or restored from a CI
|
|
320
340
|
// cache): the store it names is deleted only when it is one.
|
|
321
341
|
if (store !== undefined && isStore(store)) targets.push(store)
|
|
342
|
+
// Today's store too: the next run reads it, whichever store the index
|
|
343
|
+
// last recorded (a moved HOME, an origin added).
|
|
344
|
+
if (storeRoot !== null) targets.push(storeRoot)
|
|
322
345
|
} else if (storeRoot !== null) targets.push(storeRoot)
|
|
323
346
|
if (parsed.global === true) {
|
|
324
347
|
const vxHome = path.join(process.env['HOME'] || homedir(), '.vx')
|
|
@@ -338,12 +361,20 @@ async function clear(
|
|
|
338
361
|
'VX_E_CONFIG',
|
|
339
362
|
)
|
|
340
363
|
}
|
|
341
|
-
const dry = parsed.
|
|
364
|
+
const dry = parsed.dry === true
|
|
342
365
|
// `.vx/cache` and the stores are vx's own; any other directory is the
|
|
343
366
|
// user's, cleared entry by entry and removed only if that empties it.
|
|
344
|
-
|
|
367
|
+
// A `.vx/cache` linked to a volume (a CI cache mount) is emptied through
|
|
368
|
+
// the link: removing the link alone left the cache where it points.
|
|
369
|
+
const own = path.join(dotVx, 'cache')
|
|
370
|
+
const named = path.resolve(dir) === own ? undefined : path.resolve(dir)
|
|
371
|
+
const linked = named === undefined && existsSync(own) && lstatSync(own).isSymbolicLink()
|
|
345
372
|
const entries = (t: string): string[] =>
|
|
346
|
-
t === named
|
|
373
|
+
t === named
|
|
374
|
+
? vxCacheEntries(t).map((e) => path.join(t, e))
|
|
375
|
+
: linked && t === own
|
|
376
|
+
? readdirSync(t).map((e) => path.join(t, e))
|
|
377
|
+
: [t]
|
|
347
378
|
const bytes = doomed.reduce((n, t) => n + entries(t).reduce((m, e) => m + treeBytes(e), 0), 0)
|
|
348
379
|
if (!dry && doomed.length > 0) {
|
|
349
380
|
// A clear beside a run deletes what the run is restoring or saving; it
|
|
@@ -360,7 +391,7 @@ async function clear(
|
|
|
360
391
|
}
|
|
361
392
|
const shown = doomed.map((t) => (path.relative(root, t).startsWith('..') ? t : relPosix(root, t)))
|
|
362
393
|
if (parsed.format === 'json') {
|
|
363
|
-
const result: ClearResult = {
|
|
394
|
+
const result: ClearResult = { dry: dry, cleared: shown, bytesFreed: bytes }
|
|
364
395
|
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
365
396
|
} else
|
|
366
397
|
ui.print(
|
package/src/cli/completions.ts
CHANGED
|
@@ -5,14 +5,21 @@
|
|
|
5
5
|
// project names are not (a completion that evaluates configs on every
|
|
6
6
|
// Tab is the wrong price).
|
|
7
7
|
|
|
8
|
-
import { acceptedFlags, CORE_VERBS, flagHint, seeHelp } from './help.js'
|
|
8
|
+
import { acceptedFlags, CORE_VERBS, flagHint, refuse, seeHelp } from './help.js'
|
|
9
9
|
import { nearest, sayNotice } from '../util/index.js'
|
|
10
|
+
import { setting } from '../settings.js'
|
|
11
|
+
|
|
12
|
+
function enumValues(path: string): readonly string[] {
|
|
13
|
+
const kind = setting(path).kind
|
|
14
|
+
return kind.type === 'enum' ? kind.values : []
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
/** A flag whose value is one of a fixed set; the tests hold each to its parser. */
|
|
12
18
|
export const FLAG_VALUES: Readonly<Record<string, readonly string[]>> = {
|
|
13
19
|
'--format': ['pretty', 'json'],
|
|
14
20
|
'--output-logs': ['full', 'errors-only', 'hash-only', 'none'],
|
|
15
21
|
'--cache-download': ['all', 'toplevel', 'none'],
|
|
22
|
+
'--schedule': enumValues('run.schedule'),
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export type CompletionShell = 'bash' | 'zsh' | 'fish'
|
|
@@ -139,12 +146,12 @@ export async function completionsCmd(
|
|
|
139
146
|
if (args.length !== 1 || !(SHELLS as readonly string[]).includes(shell ?? '')) {
|
|
140
147
|
const flag = args.find((a) => a.startsWith('-'))
|
|
141
148
|
if (flag !== undefined) {
|
|
142
|
-
|
|
143
|
-
'
|
|
144
|
-
|
|
149
|
+
return refuse(
|
|
150
|
+
'completions',
|
|
151
|
+
args,
|
|
145
152
|
`unknown flag: ${flag}${flagHint('completions', flag)}${seeHelp('completions')}`,
|
|
153
|
+
'VX_E_USAGE',
|
|
146
154
|
)
|
|
147
|
-
return 1
|
|
148
155
|
}
|
|
149
156
|
const one = shell !== undefined && args.length === 1
|
|
150
157
|
// One edit: shell names are three or four letters, so two edits took
|
package/src/cli/core-alias.ts
CHANGED
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
* so a verb that never loads a plugin never loads the façade. The
|
|
15
15
|
* source form (`bun src/bin.ts`) resolves the specifier to the same file
|
|
16
16
|
* anyway; the alias makes that a guarantee rather than a layout fact.
|
|
17
|
+
* The loader is synchronous: the workspace config's bytecode bundle
|
|
18
|
+
* (workspace/config-bytecode.ts) is CommonJS, and `require` refuses a
|
|
19
|
+
* module whose loader is async.
|
|
17
20
|
*/
|
|
18
|
-
export function registerCoreAlias(load: () =>
|
|
21
|
+
export function registerCoreAlias(load: () => Record<string, unknown>): void {
|
|
19
22
|
Bun.plugin({
|
|
20
23
|
name: 'vx-core-alias',
|
|
21
24
|
setup(build) {
|
|
22
|
-
build.module('@vzn/vx',
|
|
25
|
+
build.module('@vzn/vx', () => ({ exports: load(), loader: 'object' }))
|
|
23
26
|
},
|
|
24
27
|
})
|
|
25
28
|
}
|