@vzn/vx 0.0.651 → 0.0.652
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 +2 -3
- package/package.json +5 -5
- package/schemas/show.json +14 -20
- package/src/bin.ts +12 -0
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +19 -5
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/inputs.ts +74 -14
- package/src/cache/layer.ts +6 -6
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/run-history.ts +46 -17
- package/src/cache/schema.ts +4 -1
- package/src/cli/cache.ts +2 -2
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +24 -0
- package/src/cli/info.ts +1 -1
- package/src/cli/last.ts +34 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/show.ts +7 -10
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +11 -8
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +40 -1
- package/src/exec/index.ts +1 -0
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +314 -29
- package/src/exec/task-proxy.ts +93 -0
- package/src/graph/definition.ts +16 -3
- package/src/graph/scheduler.ts +3 -1
- package/src/graph/task-graph.ts +24 -23
- package/src/history/failure-mode.ts +24 -1
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +5 -5
- package/src/index.ts +4 -2
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +51 -18
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/miss-save.ts +27 -8
- package/src/orchestrator/persistent-task.ts +2 -0
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +2 -2
- package/src/orchestrator/plugin-host.ts +6 -2
- package/src/orchestrator/plugin.ts +6 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-failures.ts +61 -12
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +86 -67
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +92 -19
- package/src/orchestrator/sandbox-request.ts +50 -7
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +4 -4
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +3 -3
- package/src/report/framed-output.ts +16 -7
- package/src/report/logger.ts +1 -1
- package/src/report/plain-output.ts +2 -0
- package/src/report/summary.ts +5 -5
- package/src/settings.ts +8 -8
- package/src/util/config-frame.ts +128 -6
- package/src/util/paths.ts +1 -1
- package/src/util/settle.ts +2 -1
- package/src/util/tail.ts +4 -1
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +66 -61
- package/src/workspace/filter.ts +8 -1
- package/src/workspace/migration.ts +12 -6
package/src/cache/run-history.ts
CHANGED
|
@@ -48,21 +48,22 @@ const RUN_COLUMNS = [
|
|
|
48
48
|
'flaky_failures',
|
|
49
49
|
]
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
/**
|
|
52
|
+
* An INSERT of `n` `runs` rows over the columns `kept` names (indexes into
|
|
53
|
+
* `RUN_COLUMNS`); a column it leaves out is NULL, as no `runs` column has
|
|
54
|
+
* a default.
|
|
55
|
+
*/
|
|
56
|
+
function insertRunsSql(kept: readonly number[], n: number): string {
|
|
57
|
+
const tuple = `(${kept.map(() => '?').join(', ')})`
|
|
58
|
+
return `INSERT INTO runs(${kept.map((i) => RUN_COLUMNS[i]).join(', ')}) VALUES ${Array(n).fill(tuple).join(', ')}`
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* rows, 40 at a time 6.4 (2026-10-03).
|
|
62
|
+
* Bound variables per INSERT: within 999, the oldest default ceiling. A
|
|
63
|
+
* statement per row was 8.7 ms of 1,000 rows, 40 at a time 6.4
|
|
64
|
+
* (2026-10-03).
|
|
64
65
|
*/
|
|
65
|
-
const
|
|
66
|
+
const VARIABLES_PER_INSERT = 999
|
|
66
67
|
|
|
67
68
|
/** Rows per `task_durations` INSERT: 7 variables each, under 999. */
|
|
68
69
|
const DURATIONS_PER_INSERT = 140
|
|
@@ -152,6 +153,21 @@ export class RunHistory {
|
|
|
152
153
|
.immediate()
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Hits written while the run goes on (history-stream.ts): their rows and
|
|
158
|
+
* the flaky record's first hit of a key. A hit times nothing, so it
|
|
159
|
+
* leaves no `task_durations` row; IMMEDIATE, as `recordRunBundle` says.
|
|
160
|
+
*/
|
|
161
|
+
recordHits(runs: readonly RunRecord[]): void {
|
|
162
|
+
if (runs.length === 0) return
|
|
163
|
+
this.db
|
|
164
|
+
.transaction(() => {
|
|
165
|
+
this.insertRuns(runs)
|
|
166
|
+
this.insertOutcomes(runs)
|
|
167
|
+
})
|
|
168
|
+
.immediate()
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
/** One `task_durations` row per executed success, as if run alone: what a forecast reads. */
|
|
156
172
|
private insertDurations(runs: readonly RunRecord[], source: string, sha: string | null): void {
|
|
157
173
|
const done = runs.filter(
|
|
@@ -209,7 +225,13 @@ export class RunHistory {
|
|
|
209
225
|
}
|
|
210
226
|
}
|
|
211
227
|
|
|
212
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* `runs` as few INSERTs; each distinct forward-args list digested once.
|
|
230
|
+
* Only the columns some row fills are bound: a hit leaves half of them
|
|
231
|
+
* NULL, and binding 18 of 36 took the 8,002 rows of a warm run from
|
|
232
|
+
* 31.7 to 24.0 ms alone, the whole record stage 86.8 to 83.3 ms (median
|
|
233
|
+
* of 15 interleaved runs, 2026-11-06).
|
|
234
|
+
*/
|
|
213
235
|
private insertRuns(runs: readonly RunRecord[]): void {
|
|
214
236
|
const digests = new Map<string, string>()
|
|
215
237
|
const digest = (value: string): string => {
|
|
@@ -217,12 +239,19 @@ export class RunHistory {
|
|
|
217
239
|
if (d === undefined) digests.set(value, (d = this.digestValue(value)))
|
|
218
240
|
return d
|
|
219
241
|
}
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
const bound = runs.map((r) => bindRun(r, digest))
|
|
243
|
+
const filled = RUN_COLUMNS.map(() => false)
|
|
244
|
+
for (const row of bound)
|
|
245
|
+
for (let c = 0; c < row.length; c++) if (row[c] !== null) filled[c] = true
|
|
246
|
+
const kept: number[] = []
|
|
247
|
+
for (let c = 0; c < filled.length; c++) if (filled[c]) kept.push(c)
|
|
248
|
+
const perInsert = Math.floor(VARIABLES_PER_INSERT / kept.length)
|
|
249
|
+
for (let i = 0; i < bound.length; i += perInsert) {
|
|
250
|
+
const block = bound.slice(i, i + perInsert)
|
|
222
251
|
const params: SQLQueryBindings[] = []
|
|
223
|
-
for (const
|
|
252
|
+
for (const row of block) for (const c of kept) params.push(row[c] as SQLQueryBindings)
|
|
224
253
|
// `query` caches by text: one statement for every full block.
|
|
225
|
-
this.db.query(insertRunsSql(block.length)).run(...params)
|
|
254
|
+
this.db.query(insertRunsSql(kept, block.length)).run(...params)
|
|
226
255
|
}
|
|
227
256
|
}
|
|
228
257
|
|
|
@@ -287,7 +316,7 @@ export class RunHistory {
|
|
|
287
316
|
}
|
|
288
317
|
}
|
|
289
318
|
|
|
290
|
-
/**
|
|
319
|
+
/** A RunRecord as one value per `RUN_COLUMNS` entry, in its order. */
|
|
291
320
|
function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBindings[] {
|
|
292
321
|
return [
|
|
293
322
|
// The ONE place the no-key sentinel is applied, so the column's
|
package/src/cache/schema.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Database, SQLQueryBindings } from 'bun:sqlite'
|
|
8
8
|
|
|
9
|
+
/** SQLite's file-control opcode (sqlite3.h). */
|
|
10
|
+
export const SQLITE_FCNTL_PERSIST_WAL = 10
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* A statement prepared on its first `run` or `get`. A warm run executes few
|
|
11
14
|
* of the index's statements, and preparing every one at the open cost each
|
|
@@ -84,7 +87,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
84
87
|
-- v23: attempts a retried task took (>1); NULL for a once-run task.
|
|
85
88
|
-- The direct within-run flaky signal.
|
|
86
89
|
attempts INTEGER,
|
|
87
|
-
-- v25: 1 when the task declared
|
|
90
|
+
-- v25: 1 when the task declared cache: true, 0 when it runs every
|
|
88
91
|
-- time by design; NULL on rows older than the column.
|
|
89
92
|
cached INTEGER,
|
|
90
93
|
-- v27: why a task failed or was skipped, as the run's own footer
|
package/src/cli/cache.ts
CHANGED
|
@@ -105,7 +105,7 @@ interface PruneArgs {
|
|
|
105
105
|
format?: 'pretty' | 'json'
|
|
106
106
|
/** `--cache-dir`: prune the cache a run with the same flag uses. */
|
|
107
107
|
cacheDir?: string
|
|
108
|
-
/** `--global`: clear every
|
|
108
|
+
/** `--global`: clear every workspace's store under `~/.vx`. */
|
|
109
109
|
global?: boolean
|
|
110
110
|
error?: string
|
|
111
111
|
}
|
|
@@ -181,7 +181,7 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
181
181
|
}
|
|
182
182
|
if (out.global === true && (out.olderThanMs !== undefined || out.maxBytes !== undefined)) {
|
|
183
183
|
return {
|
|
184
|
-
error: `--global clears every
|
|
184
|
+
error: `--global clears every workspace's store and takes no --older-than or --max-size${seeHelp('cache')}`,
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
if (out.global === true && out.cacheDir !== undefined) {
|
package/src/cli/help.ts
CHANGED
|
@@ -204,9 +204,9 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
204
204
|
'Cache management:',
|
|
205
205
|
' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
206
206
|
' vx cache clean --max-size 1G Keep total cache under 1 GB (LRU eviction).',
|
|
207
|
-
" vx cache clean Clear .vx/cache and this
|
|
207
|
+
" vx cache clean Clear .vx/cache and this workspace's store",
|
|
208
208
|
' ~/.vx/<id>/cache.',
|
|
209
|
-
" --global Clear every
|
|
209
|
+
" --global Clear every workspace's store under ~/.vx.",
|
|
210
210
|
' --dry Say what the policy would reap; delete nothing.',
|
|
211
211
|
' --format <fmt> pretty (default) | json.',
|
|
212
212
|
' --cache-dir <path> The cache a run with the same flag uses (why,',
|
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { checkSettingsEnv } from '../settings.js'
|
|
|
6
6
|
import { CORE_VERBS, flagHint, printHelp, refuse, refusedWord, seeHelp } from './help.js'
|
|
7
7
|
import {
|
|
8
8
|
errorDocument,
|
|
9
|
+
hasControl,
|
|
9
10
|
hideEnvFromTasks,
|
|
10
11
|
isFsRefusal,
|
|
11
12
|
isOutOfFds,
|
|
@@ -30,6 +31,7 @@ const plugins = () => import('./plugin-commands.js')
|
|
|
30
31
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
31
32
|
refuseLossyArgs(argv)
|
|
32
33
|
const [command, ...rest] = argv
|
|
34
|
+
if (command !== undefined) refuseControlArgs(command, rest)
|
|
33
35
|
await hideEnvFromTasks()
|
|
34
36
|
|
|
35
37
|
// `vx <verb> --help` is the universal reflex, and every verb answered
|
|
@@ -69,6 +71,28 @@ function refuseLossyArgs(argv: readonly string[]): void {
|
|
|
69
71
|
)
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* No core verb's argument holds a control character, and a refusal echoing
|
|
76
|
+
* one raw broke its headline at a newline. A forwarded one (after `--`) is
|
|
77
|
+
* the task's, a `--sql` query may span lines, and a plugin verb owns its own.
|
|
78
|
+
*/
|
|
79
|
+
function refuseControlArgs(command: string, rest: readonly string[]): void {
|
|
80
|
+
if (!(CORE_VERBS as readonly string[]).includes(command)) return
|
|
81
|
+
for (let i = 0; i < rest.length; i++) {
|
|
82
|
+
const a = rest[i]!
|
|
83
|
+
if (a === '--') return
|
|
84
|
+
if (a === '--sql') {
|
|
85
|
+
i++
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
if (a.startsWith('--sql=') || !hasControl(a)) continue
|
|
89
|
+
throw new UserError(
|
|
90
|
+
`argument ${i + 2} (${JSON.stringify(a)}) holds a control character, which no \`vx ${command}\` argument takes`,
|
|
91
|
+
'VX_E_USAGE',
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
/**
|
|
73
97
|
* A crash from a hand-broken lock entry is that entry's fault: runs validate
|
|
74
98
|
* no entry, so it surfaced as a raw TypeError (2026-10-14). The lock is
|
package/src/cli/info.ts
CHANGED
|
@@ -97,7 +97,7 @@ function infoReport(f: InfoFacts): ui.Block {
|
|
|
97
97
|
'cache store',
|
|
98
98
|
f.cacheStore === null
|
|
99
99
|
? 'none: the cache dir holds the entries'
|
|
100
|
-
: `${tildePath(f.cacheStore)} (${formatBytes(f.cacheStoreBytes ?? 0)}, shared by this
|
|
100
|
+
: `${tildePath(f.cacheStore)} (${formatBytes(f.cacheStoreBytes ?? 0)}, shared by this workspace's checkouts)`,
|
|
101
101
|
],
|
|
102
102
|
['cache versions', `keys ${f.cacheVersion} · index schema ${f.schemaVersion}`],
|
|
103
103
|
['cache entries', describeEntries(f)],
|
package/src/cli/last.ts
CHANGED
|
@@ -433,6 +433,15 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
433
433
|
return 0
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
/** `text` as JSON, or undefined where it is not. */
|
|
437
|
+
function parsed(text: string): unknown {
|
|
438
|
+
try {
|
|
439
|
+
return JSON.parse(text)
|
|
440
|
+
} catch {
|
|
441
|
+
return undefined
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
436
445
|
/** How long a run has to answer before `--live` gives up on it. */
|
|
437
446
|
const LIVE_ANSWER_MS = 5_000
|
|
438
447
|
|
|
@@ -457,18 +466,36 @@ async function printLive(
|
|
|
457
466
|
}
|
|
458
467
|
let buffered = ''
|
|
459
468
|
let snapshot: LiveSnapshot | undefined
|
|
469
|
+
/** The run answered a snapshot this vx cannot read: another vx version's. */
|
|
470
|
+
let unreadable = false
|
|
460
471
|
let settle!: (answered: boolean) => void
|
|
461
472
|
const done = new Promise<boolean>((resolve) => (settle = resolve))
|
|
462
473
|
const line = (text: string): void => {
|
|
474
|
+
if (unreadable) return
|
|
463
475
|
if (snapshot === undefined) {
|
|
464
|
-
|
|
476
|
+
const s = parsed(text) as Partial<LiveSnapshot> | undefined
|
|
477
|
+
const l = s?.live
|
|
478
|
+
if (
|
|
479
|
+
typeof l?.runId !== 'string' ||
|
|
480
|
+
typeof l.command !== 'string' ||
|
|
481
|
+
typeof l.startedAt !== 'number' ||
|
|
482
|
+
typeof l.elapsedMs !== 'number' ||
|
|
483
|
+
!Array.isArray(s!.tasks) ||
|
|
484
|
+
!Array.isArray(s!.warnings)
|
|
485
|
+
) {
|
|
486
|
+
unreadable = true
|
|
487
|
+
return settle(false)
|
|
488
|
+
}
|
|
489
|
+
snapshot = s as LiveSnapshot
|
|
465
490
|
if (format === 'json') ui.data('out', `${text}\n`)
|
|
466
491
|
else ui.print('out', ...formatLive(snapshot))
|
|
467
492
|
return
|
|
468
493
|
}
|
|
469
494
|
if (format === 'json') ui.data('out', `${text}\n`)
|
|
470
495
|
else {
|
|
471
|
-
const
|
|
496
|
+
const e = parsed(text)
|
|
497
|
+
const event =
|
|
498
|
+
typeof e === 'object' && e !== null ? formatLiveEvent(e as WireEvent) : undefined
|
|
472
499
|
if (event !== undefined) ui.print('out', event)
|
|
473
500
|
}
|
|
474
501
|
}
|
|
@@ -503,6 +530,11 @@ async function printLive(
|
|
|
503
530
|
: setTimeout(() => settle(snapshot !== undefined), LIVE_ANSWER_MS)
|
|
504
531
|
const answered = await done
|
|
505
532
|
clearTimeout(timer)
|
|
533
|
+
if (unreadable)
|
|
534
|
+
throw refusal({
|
|
535
|
+
headline: 'the run in progress answered in a form this vx does not read',
|
|
536
|
+
fix: 'ask with the vx that started the run',
|
|
537
|
+
})
|
|
506
538
|
if (!answered) none()
|
|
507
539
|
return 0
|
|
508
540
|
}
|