@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.
Files changed (78) hide show
  1. package/README.md +2 -3
  2. package/package.json +5 -5
  3. package/schemas/show.json +14 -20
  4. package/src/bin.ts +12 -0
  5. package/src/cache/archive.ts +1 -1
  6. package/src/cache/cache.ts +19 -5
  7. package/src/cache/history-stream.ts +134 -0
  8. package/src/cache/history-writer.ts +76 -0
  9. package/src/cache/index.ts +2 -0
  10. package/src/cache/inputs.ts +74 -14
  11. package/src/cache/layer.ts +6 -6
  12. package/src/cache/lockfile-memo.ts +2 -1
  13. package/src/cache/run-history.ts +46 -17
  14. package/src/cache/schema.ts +4 -1
  15. package/src/cli/cache.ts +2 -2
  16. package/src/cli/help.ts +2 -2
  17. package/src/cli/index.ts +24 -0
  18. package/src/cli/info.ts +1 -1
  19. package/src/cli/last.ts +34 -2
  20. package/src/cli/plugin-templates.ts +36 -45
  21. package/src/cli/show.ts +7 -10
  22. package/src/cli/watch-judge.ts +1 -1
  23. package/src/cli/watch-set.ts +4 -4
  24. package/src/cli/watch.ts +11 -8
  25. package/src/config.ts +47 -30
  26. package/src/exec/executor.ts +40 -1
  27. package/src/exec/index.ts +1 -0
  28. package/src/exec/sandbox-paths.ts +3 -1
  29. package/src/exec/sandbox-runtime.ts +314 -29
  30. package/src/exec/task-proxy.ts +93 -0
  31. package/src/graph/definition.ts +16 -3
  32. package/src/graph/scheduler.ts +3 -1
  33. package/src/graph/task-graph.ts +24 -23
  34. package/src/history/failure-mode.ts +24 -1
  35. package/src/history/index.ts +1 -0
  36. package/src/history/metrics.ts +5 -5
  37. package/src/index.ts +4 -2
  38. package/src/orchestrator/admission.ts +1 -1
  39. package/src/orchestrator/command-windows.ts +1 -1
  40. package/src/orchestrator/deferred-outputs.ts +1 -1
  41. package/src/orchestrator/download-policy.ts +5 -7
  42. package/src/orchestrator/excluded-keys.ts +1 -1
  43. package/src/orchestrator/execute-task.ts +51 -18
  44. package/src/orchestrator/hit-restore.ts +5 -6
  45. package/src/orchestrator/keyed-projects.ts +28 -0
  46. package/src/orchestrator/local-shortcircuit.ts +2 -2
  47. package/src/orchestrator/miss-save.ts +27 -8
  48. package/src/orchestrator/persistent-task.ts +2 -0
  49. package/src/orchestrator/placement.ts +1 -1
  50. package/src/orchestrator/plan.ts +2 -2
  51. package/src/orchestrator/plugin-host.ts +6 -2
  52. package/src/orchestrator/plugin.ts +6 -0
  53. package/src/orchestrator/remote-prefetch.ts +1 -1
  54. package/src/orchestrator/run-failures.ts +61 -12
  55. package/src/orchestrator/run-keys.ts +1 -1
  56. package/src/orchestrator/run-records.ts +86 -67
  57. package/src/orchestrator/run-report.ts +1 -1
  58. package/src/orchestrator/run.ts +92 -19
  59. package/src/orchestrator/sandbox-request.ts +50 -7
  60. package/src/orchestrator/shell-verdict.ts +9 -0
  61. package/src/orchestrator/stable-keys.ts +17 -18
  62. package/src/orchestrator/task-artifacts.ts +1 -1
  63. package/src/orchestrator/task-hash.ts +4 -4
  64. package/src/orchestrator/telemetry.ts +5 -1
  65. package/src/report/events.ts +3 -3
  66. package/src/report/framed-output.ts +16 -7
  67. package/src/report/logger.ts +1 -1
  68. package/src/report/plain-output.ts +2 -0
  69. package/src/report/summary.ts +5 -5
  70. package/src/settings.ts +8 -8
  71. package/src/util/config-frame.ts +128 -6
  72. package/src/util/paths.ts +1 -1
  73. package/src/util/settle.ts +2 -1
  74. package/src/util/tail.ts +4 -1
  75. package/src/workspace/bracket-literal.ts +2 -2
  76. package/src/workspace/config-schema.ts +66 -61
  77. package/src/workspace/filter.ts +8 -1
  78. package/src/workspace/migration.ts +12 -6
@@ -48,21 +48,22 @@ const RUN_COLUMNS = [
48
48
  'flaky_failures',
49
49
  ]
50
50
 
51
- const INSERT_RUNS = `INSERT INTO runs(${RUN_COLUMNS.join(', ')}) VALUES `
52
- const RUN_TUPLE = `(${RUN_COLUMNS.map(() => '?').join(', ')})`
53
-
54
- /** An INSERT of `n` `runs` rows. */
55
- function insertRunsSql(n: number): string {
56
- return INSERT_RUNS + Array(n).fill(RUN_TUPLE).join(', ')
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
- * Rows per INSERT: as many as stay within 999 bound variables, the oldest
61
- * default ceiling, read off the column count (33 rows were sized for 30
62
- * columns and bound 1,023 at 31). A statement per row was 8.7 ms of 1,000
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 RUNS_PER_INSERT = Math.floor(999 / RUN_COLUMNS.length)
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
- /** `runs` as few INSERTs; each distinct forward-args list digested once. */
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
- for (let i = 0; i < runs.length; i += RUNS_PER_INSERT) {
221
- const block = runs.slice(i, i + RUNS_PER_INSERT)
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 r of block) for (const v of bindRun(r, digest)) params.push(v)
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
- /** Bind a RunRecord to one `RUN_TUPLE`'s positional parameters, in `RUN_COLUMNS` order. */
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
@@ -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 a cache block, 0 when it runs every
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 repository's store under `~/.vx`. */
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 repository's store and takes no --older-than or --max-size${seeHelp('cache')}`,
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 repo's store",
207
+ " vx cache clean Clear .vx/cache and this workspace's store",
208
208
  ' ~/.vx/<id>/cache.',
209
- " --global Clear every repo's store under ~/.vx.",
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 repo's checkouts)`,
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
- snapshot = JSON.parse(text) as LiveSnapshot
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 event = formatLiveEvent(JSON.parse(text) as WireEvent)
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
  }