@vzn/vx 0.0.644 → 0.0.646

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 (83) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +22 -48
  3. package/package.json +5 -5
  4. package/schemas/init.json +5 -0
  5. package/schemas/last.json +15 -0
  6. package/skills/vx/SKILL.md +1 -1
  7. package/src/bin.ts +1 -1
  8. package/src/cache/archive.ts +12 -76
  9. package/src/cache/cache.ts +72 -42
  10. package/src/cache/git-inputs.ts +55 -10
  11. package/src/cache/index.ts +5 -2
  12. package/src/cache/inputs.ts +61 -11
  13. package/src/cache/layer.ts +30 -8
  14. package/src/cache/layered-cache.ts +138 -44
  15. package/src/cache/output-index.ts +25 -11
  16. package/src/cache/plugin-cache.ts +9 -14
  17. package/src/cache/remote-lane.ts +60 -22
  18. package/src/cache/run-history.ts +13 -6
  19. package/src/cache/schema.ts +19 -2
  20. package/src/cache/scoped-cache.ts +3 -80
  21. package/src/cli/core-alias.ts +5 -2
  22. package/src/cli/help.ts +8 -10
  23. package/src/cli/index.ts +0 -12
  24. package/src/cli/init.ts +159 -25
  25. package/src/cli/last.ts +4 -2
  26. package/src/cli/plan-format.ts +6 -2
  27. package/src/cli/plugin-commands.ts +98 -6
  28. package/src/cli/plugin-templates.ts +2 -2
  29. package/src/cli/run.ts +2 -8
  30. package/src/cli/select.ts +5 -3
  31. package/src/cli/upgrade.ts +5 -6
  32. package/src/config.ts +2 -4
  33. package/src/exec/executor.ts +112 -9
  34. package/src/exec/index.ts +2 -0
  35. package/src/exec/proc-sample.ts +67 -5
  36. package/src/exec/runner.ts +4 -0
  37. package/src/exec/sandbox-runtime.ts +11 -4
  38. package/src/graph/scheduler.ts +59 -8
  39. package/src/history/history.ts +119 -7
  40. package/src/history/metrics.ts +53 -16
  41. package/src/index.ts +16 -1
  42. package/src/orchestrator/affected-tasks.ts +0 -18
  43. package/src/orchestrator/execute-args.ts +3 -0
  44. package/src/orchestrator/execute-task.ts +28 -7
  45. package/src/orchestrator/held-servers.ts +86 -0
  46. package/src/orchestrator/hit-restore.ts +11 -4
  47. package/src/orchestrator/index.ts +18 -1
  48. package/src/orchestrator/local-floor.ts +104 -0
  49. package/src/orchestrator/local-shortcircuit.ts +26 -7
  50. package/src/orchestrator/miss-save.ts +10 -4
  51. package/src/orchestrator/options.ts +0 -8
  52. package/src/orchestrator/persistent-task.ts +14 -0
  53. package/src/orchestrator/placement.ts +84 -9
  54. package/src/orchestrator/plan.ts +7 -1
  55. package/src/orchestrator/plugin-host.ts +22 -7
  56. package/src/orchestrator/plugin.ts +48 -7
  57. package/src/orchestrator/prepare.ts +14 -49
  58. package/src/orchestrator/run-context.ts +39 -1
  59. package/src/orchestrator/run-keys.ts +164 -0
  60. package/src/orchestrator/run-records.ts +6 -0
  61. package/src/orchestrator/run.ts +95 -6
  62. package/src/orchestrator/sandbox-request.ts +9 -3
  63. package/src/orchestrator/task-artifacts.ts +142 -0
  64. package/src/orchestrator/task-hash.ts +12 -4
  65. package/src/orchestrator/task-process.ts +16 -0
  66. package/src/orchestrator/telemetry-host.ts +2 -1
  67. package/src/orchestrator/telemetry.ts +60 -17
  68. package/src/report/forecast.ts +6 -27
  69. package/src/report/framed-output.ts +41 -5
  70. package/src/report/logger.ts +60 -20
  71. package/src/report/status-line.ts +38 -18
  72. package/src/report/summary.ts +75 -9
  73. package/src/settings.ts +8 -7
  74. package/src/util/colors.ts +16 -1
  75. package/src/util/index.ts +1 -0
  76. package/src/util/paths.ts +30 -0
  77. package/src/util/ui.ts +4 -1
  78. package/src/workspace/affected.ts +0 -39
  79. package/src/workspace/config-bytecode.ts +276 -0
  80. package/src/workspace/index.ts +0 -2
  81. package/src/workspace/migration.ts +4 -32
  82. package/src/workspace/project-loader.ts +92 -14
  83. package/src/cli/foreign-flags.ts +0 -727
@@ -18,11 +18,11 @@ const FILE_PREFIX_MTIME = -2
18
18
  import type { Database } from 'bun:sqlite'
19
19
  import { inHashes, lazyStatement } from './schema.js'
20
20
  import { modeMatches } from './umask.js'
21
- import { lstatSync, readlinkSync, statSync } from 'node:fs'
22
- import { lstat, readdir } from 'node:fs/promises'
21
+ import { lstatSync, readdirSync, readlinkSync, statSync } from 'node:fs'
23
22
  import path from 'node:path'
24
23
  import {
25
24
  OUTPUT_DIRS_CAP,
25
+ OUTPUT_DIRS_OVER_CAP,
26
26
  OUTPUT_DIRS_RACY_MS,
27
27
  type OutputDirRow,
28
28
  isIndexFull,
@@ -262,8 +262,8 @@ export class OutputIndex {
262
262
  * process may have written into them: `holds` is asked with the files
263
263
  * the walk saw, and a tree it refuses records nothing (item 1087).
264
264
  * Symlinked directories are not descended (the output walk refuses them
265
- * too). Over `OUTPUT_DIRS_CAP` directories, or on any error, the rows
266
- * are cleared and the next hit keeps the walk.
265
+ * too). Over `OUTPUT_DIRS_CAP` directories the one row left says so; on
266
+ * any error the rows are cleared. Either way the next hit keeps the walk.
267
267
  */
268
268
  async recordOutputDirs(
269
269
  hash: string,
@@ -273,11 +273,16 @@ export class OutputIndex {
273
273
  ): Promise<void> {
274
274
  const rows: Array<[string, number]> = []
275
275
  const files: string[] = []
276
- const walk = async (rel: string, isPrefix = false): Promise<boolean> => {
276
+ let overCap = false
277
+ // Synchronous, like the stats of `isOutputsCurrent`: an async lstat and
278
+ // readdir per directory are two thread-pool round trips, and a
279
+ // root-anchored glob's snapshot of solid's 47 directories took 4 ms of
280
+ // the run's end that way (2026-10-22).
281
+ const walk = (rel: string, isPrefix = false): boolean => {
277
282
  const abs = path.join(projectDir, rel)
278
283
  let st
279
284
  try {
280
- st = await lstat(abs)
285
+ st = lstatSync(abs)
281
286
  } catch {
282
287
  // A declared prefix the task never produced (`build/**` beside
283
288
  // `dist/**`, medusa's `.medusa/**`) is recorded ABSENT: the check
@@ -299,29 +304,38 @@ export class OutputIndex {
299
304
  return true
300
305
  }
301
306
  rows.push([rel, st.mtimeMs])
302
- if (rows.length > OUTPUT_DIRS_CAP) return false
307
+ if (rows.length > OUTPUT_DIRS_CAP) {
308
+ overCap = true
309
+ return false
310
+ }
303
311
  let entries
304
312
  try {
305
- entries = await readdir(abs, { withFileTypes: true })
313
+ entries = readdirSync(abs, { withFileTypes: true })
306
314
  } catch {
307
315
  return false
308
316
  }
309
317
  for (const e of entries) {
318
+ // The project itself is the prefix `''` of a root-anchored glob.
319
+ const child = rel === '' ? e.name : `${rel}/${e.name}`
310
320
  if (e.isDirectory() && !e.isSymbolicLink()) {
311
- if (!(await walk(`${rel}/${e.name}`))) return false
321
+ if (!walk(child)) return false
312
322
  } else {
313
- files.push(`${rel}/${e.name}`)
323
+ files.push(child)
314
324
  }
315
325
  }
316
326
  return true
317
327
  }
318
328
  let ok = true
319
329
  for (const prefix of prefixes) {
320
- if (!(await walk(prefix, true))) {
330
+ if (!walk(prefix, true)) {
321
331
  ok = false
322
332
  break
323
333
  }
324
334
  }
335
+ if (overCap) {
336
+ this.pendingDirs.set(hash, [[prefixes[0]!, OUTPUT_DIRS_OVER_CAP]])
337
+ return
338
+ }
325
339
  // All or nothing: a racy directory dropped alone would leave its
326
340
  // parent trusted while an addition inside it bumps only the dropped one.
327
341
  const now = Date.now()
@@ -8,7 +8,6 @@
8
8
 
9
9
  import type { Database } from 'bun:sqlite'
10
10
  import { xxh3hex } from '../util/index.js'
11
- import type { RemoteKey } from './layered-cache.js'
12
11
  import type { CachePolicy } from './policy.js'
13
12
  import type { ResolvedRemote } from './remote-lane.js'
14
13
  import { scopedKey } from './scoped-cache.js'
@@ -100,8 +99,6 @@ export function pluginCache(plugin: string, host: PluginCacheHost): PluginCache
100
99
  const { db, policy, scope } = host
101
100
  const readers = host.remotes.filter((r) => r.read)
102
101
  const writers = host.remotes.filter((r) => r.write)
103
- const at: RemoteKey = scope === undefined ? {} : { scope }
104
- const atOf = (key: string, name: string): RemoteKey => (key === entryKey(plugin, name) ? {} : at)
105
102
  const readKey = (name: string, scoped: boolean): string => {
106
103
  const key = entryKey(plugin, name)
107
104
  return scope === undefined || !scoped ? key : scopedKey(scope, key)
@@ -155,7 +152,7 @@ export function pluginCache(plugin: string, host: PluginCacheHost): PluginCache
155
152
  const key = readKey(name, scoped)
156
153
  for (const { remote } of readers) {
157
154
  try {
158
- const got = await remote.get(key, atOf(key, name))
155
+ const got = await remote.get(key)
159
156
  if (got === null) continue
160
157
  const bytes = await capped(got.body)
161
158
  if (bytes === null) continue
@@ -174,7 +171,7 @@ export function pluginCache(plugin: string, host: PluginCacheHost): PluginCache
174
171
  if (policy.localWrite) localPut(entryKey(plugin, name), bytes)
175
172
  for (const { remote } of writers) {
176
173
  try {
177
- await remote.put(key, new Blob([bytes]), { durationMs: 0, ...at })
174
+ await remote.put(key, new Blob([bytes]), { durationMs: 0 })
178
175
  } catch {
179
176
  // the other layers still keep it
180
177
  }
@@ -187,7 +184,7 @@ export function pluginCache(plugin: string, host: PluginCacheHost): PluginCache
187
184
  let answered = policy.localRead
188
185
  for (const { remote } of readers) {
189
186
  try {
190
- if (await remote.has(key, atOf(key, name))) return true
187
+ if (await remote.has(key)) return true
191
188
  answered = true
192
189
  } catch {
193
190
  // no answer from this remote
@@ -207,12 +204,10 @@ export function pluginCache(plugin: string, host: PluginCacheHost): PluginCache
207
204
  if (ask.length === 0) break
208
205
  if (remote.hasMany === undefined) return null
209
206
  try {
210
- const got: unknown = await remote.hasMany(
211
- ask,
212
- ask.map((k) => atOf(k, byKey.get(k)!)),
213
- )
214
- if (!(got instanceof Set)) return null
215
- for (const key of got) if (byKey.has(key as string)) found.add(byKey.get(key as string)!)
207
+ const got: unknown = await remote.hasMany(ask)
208
+ if (!(got instanceof Map)) return null
209
+ for (const key of got.keys())
210
+ if (byKey.has(key as string)) found.add(byKey.get(key as string)!)
216
211
  } catch {
217
212
  return null
218
213
  }
@@ -222,9 +217,9 @@ export function pluginCache(plugin: string, host: PluginCacheHost): PluginCache
222
217
 
223
218
  async getRun(name) {
224
219
  for (const { remote } of readers) {
225
- for (const [i, key] of runReadKeys(name, scope).entries()) {
220
+ for (const key of runReadKeys(name, scope)) {
226
221
  try {
227
- const got = await remote.get(key, i === 0 ? {} : at)
222
+ const got = await remote.get(key)
228
223
  if (got === null) continue
229
224
  const bytes = await capped(got.body, Infinity)
230
225
  if (bytes !== null) return bytes
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { machineParallelism } from '../util/index.js'
8
8
  import type { RemoteKeyTraffic, RemoteTraffic } from './cache.js'
9
- import type { RemoteCacheLayer } from './layered-cache.js'
9
+ import type { ArtifactInfo, RemoteCacheLayer } from './layered-cache.js'
10
10
 
11
11
  /** One remote, resolved: its wire and what the run's policy and its `access` let it do. */
12
12
  export interface ResolvedRemote {
@@ -48,6 +48,14 @@ function invalidRemoteResult(what: string): Error {
48
48
  )
49
49
  }
50
50
 
51
+ /** A plugin's `ArtifactInfo`, keeping only the fields that are finite, non-negative numbers; null when it is no object. */
52
+ function artifactInfo(value: unknown): ArtifactInfo | null {
53
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
54
+ const { size, durationMs } = value as Record<string, unknown>
55
+ const ok = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n) && n >= 0
56
+ return { ...(ok(size) ? { size } : {}), ...(ok(durationMs) ? { durationMs } : {}) }
57
+ }
58
+
51
59
  function isBody(value: unknown): value is Blob | Response {
52
60
  return value instanceof Blob || value instanceof Response
53
61
  }
@@ -106,14 +114,16 @@ export class RemoteLane {
106
114
  */
107
115
  private readonly absent = new Set<string>()
108
116
  /** What a `hasMany` said this remote holds, as `absent` records the rest. */
109
- private readonly held = new Set<string>()
117
+ private readonly held = new Map<string, ArtifactInfo>()
110
118
 
111
119
  /**
112
120
  * A remote the run writes but does not read (`--force`): which of the
113
121
  * run's up-front keys it already holds, from one `hasMany`. `found` is
114
122
  * null when the probe failed.
115
123
  */
116
- private presence: Promise<{ asked: Set<string>; found: Set<string> | null } | null> | undefined
124
+ private presence:
125
+ | Promise<{ asked: Set<string>; found: Map<string, ArtifactInfo> | null } | null>
126
+ | undefined
117
127
 
118
128
  private readonly uploadQueue: Array<() => Promise<void>> = []
119
129
  private activeUploads = 0
@@ -136,13 +146,11 @@ export class RemoteLane {
136
146
  constructor(
137
147
  resolved: ResolvedRemote,
138
148
  private readonly traffic: RemoteTraffic,
139
- /** The wire as this run asks it: a branch scope's view of `resolved.remote`. */
140
- remote: RemoteCacheLayer = resolved.remote,
141
149
  /** Background PUTs at once; excess uploads queue and drain FIFO. */
142
150
  private readonly uploadConcurrency: number = networkConcurrency(),
143
151
  ) {
144
152
  this.resolved = resolved
145
- this.remote = remote
153
+ this.remote = resolved.remote
146
154
  this.onError = resolved.onError
147
155
  this.endpoint = printableEndpoint(resolved.remote.endpoint)
148
156
  }
@@ -159,15 +167,28 @@ export class RemoteLane {
159
167
  * Existence probe; a failure is a miss. A key a `hasMany` this run
160
168
  * answered for is not asked again: `--dry` batches its keys first.
161
169
  */
162
- async has(hash: string): Promise<boolean> {
163
- if (this.held.has(hash)) return true
164
- if (this.absent.has(hash)) return false
170
+ async has(hash: string): Promise<ArtifactInfo | null> {
171
+ const held = this.held.get(hash)
172
+ if (held !== undefined) return held
173
+ if (this.absent.has(hash)) return null
174
+ let found: unknown
165
175
  try {
166
- return Boolean(await this.remote.has(hash))
176
+ found = await this.remote.has(hash)
167
177
  } catch (err) {
168
178
  this.report('probe', hash, err)
169
- return false
179
+ return null
170
180
  }
181
+ if (found === null || found === undefined) return null
182
+ const info = artifactInfo(found)
183
+ if (info === null)
184
+ this.report(
185
+ 'probe',
186
+ hash,
187
+ invalidRemoteResult(
188
+ `has() resolved ${describeValue(found)} (expected { size, durationMs } or null)`,
189
+ ),
190
+ )
191
+ return info
171
192
  }
172
193
 
173
194
  /**
@@ -175,7 +196,7 @@ export class RemoteLane {
175
196
  * rest as absent; `null` when this remote cannot batch or the probe
176
197
  * failed, which the caller reads as "no batch info".
177
198
  */
178
- async hasMany(hashes: readonly string[]): Promise<Set<string> | null> {
199
+ async hasMany(hashes: readonly string[]): Promise<Map<string, ArtifactInfo> | null> {
179
200
  if (this.remote.hasMany === undefined) return null
180
201
  let found: unknown
181
202
  try {
@@ -185,24 +206,37 @@ export class RemoteLane {
185
206
  return null
186
207
  }
187
208
  if (found === null || found === undefined) return null
188
- if (!(found instanceof Set)) {
209
+ if (!(found instanceof Map)) {
189
210
  this.report(
190
211
  'probe',
191
212
  batchOf(hashes),
192
213
  invalidRemoteResult(
193
- `hasMany() resolved ${describeValue(found)} (expected a Set of the hashes present, or null)`,
214
+ `hasMany() resolved ${describeValue(found)} (expected a Map of the hashes present, or null)`,
194
215
  ),
195
216
  )
196
217
  return null
197
218
  }
198
- const set = found as Set<string>
199
- for (const h of hashes) (set.has(h) ? this.held : this.absent).add(h)
200
- return set
219
+ const out = new Map<string, ArtifactInfo>()
220
+ for (const h of hashes) {
221
+ const info = found.has(h) ? (artifactInfo(found.get(h)) ?? {}) : undefined
222
+ if (info === undefined) this.absent.add(h)
223
+ else {
224
+ this.held.set(h, info)
225
+ out.set(h, info)
226
+ }
227
+ }
228
+ return out
201
229
  }
202
230
 
203
- /** The artifact's body, or null on a miss, a known absence, a failure or a malformed answer. */
204
- async get(hash: string): Promise<{ body: Blob | Response; durationMs?: number } | null> {
205
- if (this.absent.has(hash)) return null
231
+ /**
232
+ * The artifact's body, or null on a miss, a known absence (unless
233
+ * `fresh`: saved since the probe), a failure or a malformed answer.
234
+ */
235
+ async get(
236
+ hash: string,
237
+ fresh = false,
238
+ ): Promise<{ body: Blob | Response; durationMs?: number } | null> {
239
+ if (!fresh && this.absent.has(hash)) return null
206
240
  let result: unknown
207
241
  try {
208
242
  result = await this.remote.get(hash)
@@ -225,7 +259,10 @@ export class RemoteLane {
225
259
  )
226
260
  return null
227
261
  }
228
- return result as { body: Blob | Response; durationMs?: number }
262
+ // It holds the key: a save of it after this restore uploads nothing.
263
+ const got = result as { body: Blob | Response; durationMs?: number }
264
+ this.held.set(hash, got.durationMs === undefined ? {} : { durationMs: got.durationMs })
265
+ return got
229
266
  }
230
267
 
231
268
  /**
@@ -253,7 +290,7 @@ export class RemoteLane {
253
290
  const probed = await this.presence
254
291
  if (probed !== undefined && probed !== null && probed.found !== null && probed.asked.has(hash))
255
292
  return probed.found.has(hash)
256
- return this.has(hash)
293
+ return (await this.has(hash)) !== null
257
294
  }
258
295
 
259
296
  /** Queue a PUT in this remote's bounded pool; `body` is asked for when the job starts. */
@@ -270,6 +307,7 @@ export class RemoteLane {
270
307
  let release: (() => Promise<void>) | undefined
271
308
  try {
272
309
  if (this.stopped) return
310
+ if (this.held.has(hash)) return
273
311
  if (!this.read && (await this.holds(hash))) return
274
312
  if (this.stopped) return
275
313
  const start = performance.now()
@@ -13,11 +13,12 @@ const INSERT_RUNS = `
13
13
  started_at, ended_at,
14
14
  run_id, cpu_ms, peak_rss_bytes, wallclock_start_ns, wallclock_end_ns,
15
15
  cache_hit, attempts, cached,
16
- blocked_by, timed_out, sandbox_violations, not_ready, restored, remote_error
16
+ blocked_by, timed_out, sandbox_violations, not_ready, restored, remote_error,
17
+ peak_cores, avg_rss_bytes, placement, placement_cores, placement_memory, keyless_by
17
18
  )
18
19
  VALUES `
19
- /** One `runs` row's placeholders: the 23 columns `bindRun` fills. */
20
- const RUN_TUPLE = `(${Array(23).fill('?').join(', ')})`
20
+ /** One `runs` row's placeholders: the 29 columns `bindRun` fills. */
21
+ const RUN_TUPLE = `(${Array(29).fill('?').join(', ')})`
21
22
 
22
23
  /** An INSERT of `n` `runs` rows. */
23
24
  function insertRunsSql(n: number): string {
@@ -25,11 +26,11 @@ function insertRunsSql(n: number): string {
25
26
  }
26
27
 
27
28
  /**
28
- * Rows per INSERT: 40 × 23 variables stays under 999, the oldest default
29
+ * Rows per INSERT: 34 × 29 variables stays under 999, the oldest default
29
30
  * ceiling. A statement per row was 8.7 ms of 1,000 rows, 40 at a time 6.4
30
31
  * (2026-10-03).
31
32
  */
32
- const RUNS_PER_INSERT = 40
33
+ const RUNS_PER_INSERT = 34
33
34
 
34
35
  /** Rows per `task_durations` INSERT: 6 variables each, under 999. */
35
36
  const DURATIONS_PER_INSERT = 160
@@ -236,7 +237,7 @@ export class RunHistory {
236
237
  }
237
238
  }
238
239
 
239
- /** Bind a RunRecord to one `RUN_TUPLE`'s 23 positional parameters. */
240
+ /** Bind a RunRecord to one `RUN_TUPLE`'s 29 positional parameters. */
240
241
  function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBindings[] {
241
242
  return [
242
243
  // The ONE place the no-key sentinel is applied, so the column's
@@ -267,6 +268,12 @@ function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBi
267
268
  run.notReady ?? null,
268
269
  run.restored === undefined ? null : run.restored ? 1 : 0,
269
270
  run.remoteError ?? null,
271
+ run.peakCores ?? null,
272
+ run.avgRssBytes ?? null,
273
+ run.placement ?? null,
274
+ run.placementCores ?? null,
275
+ run.placementMemoryBytes ?? null,
276
+ run.keylessBy ?? null,
270
277
  ]
271
278
  }
272
279
 
@@ -101,7 +101,18 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
101
101
  restored INTEGER,
102
102
  -- v40: each remote read or write of the key that failed, as
103
103
  -- 'upload: <message>', '; '-joined; NULL when none did.
104
- remote_error TEXT
104
+ remote_error TEXT,
105
+ -- v43: the most cores one sampling interval burned and the mean
106
+ -- resident memory over the looks; NULL when nothing sampled.
107
+ peak_cores REAL,
108
+ avg_rss_bytes INTEGER,
109
+ -- v44: where an executor ran it (ExecuteResult.where) and that
110
+ -- machine's cores and memory; NULL = here.
111
+ placement TEXT,
112
+ placement_cores INTEGER,
113
+ placement_memory INTEGER,
114
+ -- v46: the task with no inputs its key folded (TaskOutcome.keylessBy).
115
+ keyless_by TEXT
105
116
  );
106
117
  -- Two whole-table indexes only, and both APPEND: every row of a run carries the
107
118
  -- same run_id and a started_at newer than everything before it, so
@@ -280,7 +291,13 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
280
291
  source TEXT NOT NULL,
281
292
  sha TEXT,
282
293
  duration_ms INTEGER NOT NULL,
283
- recorded_at INTEGER NOT NULL
294
+ recorded_at INTEGER NOT NULL,
295
+ -- v45: a 'remote' row's measured need (TaskResources); NULL on the
296
+ -- rest, whose need the run history holds.
297
+ cpu_parallelism REAL,
298
+ peak_rss_bytes INTEGER,
299
+ saturated_cores INTEGER,
300
+ saturated_memory_bytes INTEGER
284
301
  );
285
302
  CREATE INDEX IF NOT EXISTS ${store}.task_durations_task
286
303
  ON task_durations(project, task, source, id);
@@ -1,87 +1,10 @@
1
- // A branch's cache scope (owner, 2026-10-17): the local store never knows
2
- // one, so every branch shares one local pool under the task keys. Scopes
3
- // decide only which remote entry a task asks for: a task the branch changed
4
- // reads the scope's key, every other task the default key, and a scoped run
5
- // writes every remote under the scope's key. An artifact is packed under its
6
- // task key wherever it is stored, so a remote entry ingests under that key.
1
+ // A scope keys only what plugins store themselves (ctx.cache, run records):
2
+ // task keys reach every remote plain, and a remote decides which branch a
3
+ // read may see (VZN, 2026-10-20).
7
4
 
8
5
  import { xxh3hex } from '../util/index.js'
9
- import type { RemoteCacheLayer, RemoteKey } from './layered-cache.js'
10
6
 
11
7
  /** The key a scope stores `hash` under: task-key shaped, 16 hex, so every wire stores it unchanged. */
12
8
  export function scopedKey(scope: string, hash: string): string {
13
9
  return xxh3hex(`vx-cache-scope\0${scope}\0${hash}`)
14
10
  }
15
-
16
- /**
17
- * Which remote key each task asks for, in a run with a scope. The routes
18
- * come from the branch's diff, which only a remote call awaits: a run the
19
- * local cache answers never waits on git.
20
- */
21
- export class BranchScope {
22
- /** Finds the tasks that read the scope, at the first remote call; null: every task does. */
23
- private route: (() => Promise<ReadonlySet<string>>) | null = null
24
- private routes: Promise<ReadonlySet<string>> | null = null
25
- /** Task key → its task. */
26
- private readonly taskOf = new Map<string, string>()
27
-
28
- constructor(readonly scope: string) {}
29
-
30
- /** Only the tasks `ids` finds read the scope; set before any remote call. */
31
- routeTasks(ids: () => Promise<ReadonlySet<string>>): void {
32
- this.route = ids
33
- }
34
-
35
- noteKey(taskId: string, hash: string): void {
36
- this.taskOf.set(hash, taskId)
37
- }
38
-
39
- /** The remote key `hash` is read under, and the scope it is read in (none: the default key). */
40
- private async readKey(hash: string): Promise<{ key: string; at: RemoteKey }> {
41
- const id = this.taskOf.get(hash)
42
- if (this.route !== null) this.routes ??= this.route()
43
- const tasks = this.routes === null ? null : await this.routes
44
- return tasks !== null && id !== undefined && !tasks.has(id)
45
- ? { key: hash, at: {} }
46
- : { key: scopedKey(this.scope, hash), at: { scope: this.scope } }
47
- }
48
-
49
- /**
50
- * `inner` as this run asks it: each task key under its route's key; every
51
- * write under the scope's. Each call also names its scope, for a backend
52
- * that keeps one store per scope; no wire vx ships sends it.
53
- */
54
- remote(inner: RemoteCacheLayer): RemoteCacheLayer {
55
- const read = (h: string) => this.readKey(h)
56
- const at: RemoteKey = { scope: this.scope }
57
- const hasMany = inner.hasMany
58
- return {
59
- ...(inner.endpoint !== undefined ? { endpoint: inner.endpoint } : {}),
60
- ...(inner.open !== undefined ? { open: (ctx) => inner.open!(ctx) } : {}),
61
- has: async (h) => {
62
- const r = await read(h)
63
- return inner.has(r.key, r.at)
64
- },
65
- ...(hasMany !== undefined
66
- ? {
67
- hasMany: async (hashes) => {
68
- const reads = await Promise.all(hashes.map(read))
69
- const keys = reads.map((r) => r.key)
70
- const found = await hasMany.call(
71
- inner,
72
- keys,
73
- reads.map((r) => r.at),
74
- )
75
- if (!(found instanceof Set)) return found
76
- return new Set(hashes.filter((_, i) => found.has(keys[i]!)))
77
- },
78
- }
79
- : {}),
80
- get: async (h) => {
81
- const r = await read(h)
82
- return inner.get(r.key, r.at)
83
- },
84
- put: (h, body, meta) => inner.put(scopedKey(this.scope, h), body, { ...meta, ...at }),
85
- }
86
- }
87
- }
@@ -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: () => Promise<Record<string, unknown>>): void {
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', async () => ({ exports: await load(), loader: 'object' }))
25
+ build.module('@vzn/vx', () => ({ exports: load(), loader: 'object' }))
23
26
  },
24
27
  })
25
28
  }
package/src/cli/help.ts CHANGED
@@ -199,12 +199,6 @@ export function helpText(pluginCommands: readonly string[] = []): string {
199
199
  ' depends on none of them; a plain `vx run` never requires a plugin.',
200
200
  ' See the plugin guide: https://vznjs.github.io/vx/guides/plugins/',
201
201
  '',
202
- 'Turbo and Nx spellings (for run):',
203
- ' What turbo run / nx run-many / nx affected take works here, or is refused',
204
- ' naming the vx spelling: -t build,test, -p app, --exclude docs, --parallel 4,',
205
- ' and --base main, --dry-run, --skip-nx-cache. `vx build` names `vx run build`.',
206
- ' Every flag: https://vznjs.github.io/vx/cli/#turbo-and-nx-flags',
207
- '',
208
202
  'Argument forwarding (for run):',
209
203
  " Anything after `--` is forwarded (shell-quoted) to the task's exec",
210
204
  ' command. The forwarded args are folded into the cache key.',
@@ -264,10 +258,10 @@ export function helpText(pluginCommands: readonly string[] = []): string {
264
258
  ' --cache-dir <path> Read the cache a run with the same flag uses.',
265
259
  '',
266
260
  'Migration:',
267
- ' vx init With no package.json: write .vx/workspace.ts and an empty',
268
- ' root vx.config.ts. With one: runs @vzn/vx-migrate, which maps',
269
- ' turbo.json, an Nx graph or package.json scripts to one',
270
- ' vx.config.ts per package (asking native or keep in Turbo/Nx).',
261
+ ' vx init Install vx globally and as the root devDependency, then',
262
+ ' write .vx/workspace.ts and an empty root vx.config.ts. In a',
263
+ ' Turbo or Nx repo: runs @vzn/vx-migrate, which maps turbo.json',
264
+ ' or the Nx graph to vx (asking native or keep).',
271
265
  ' --dry Print the generated files instead of writing them.',
272
266
  ' --format json With --dry: the plan as one JSON document (schemas/init.json).',
273
267
  ' --force Overwrite existing vx.config.* files.',
@@ -356,6 +350,10 @@ export function flagHint(verb: string, arg: string): string {
356
350
  // `--json` is gh's and Nx's spelling; a verb with `--format` reads it
357
351
  // there, and no edit budget reaches `--format json` from `--json`.
358
352
  if (name === '--json' && flags.includes('--format')) return ' (did you mean --format json?)'
353
+ // `vx run` prints no JSON of its own, and edit distance reached `--force`
354
+ // from `--format`: a run with its cache reads turned off.
355
+ if ((name === '--format' || name === '--json') && flags.includes('--dry'))
356
+ return ' (the run as JSON is `vx last --format json` after it; the plan is --dry=json)'
359
357
  // A flag cut short at a word (`--dry` for prune's `--dry-run`, which
360
358
  // `vx run` spells `--dry`) is too many edits away for either budget.
361
359
  const cut = flags.filter((f) => f.startsWith(`${name}-`))
package/src/cli/index.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  import { VERSION } from '../version.js'
5
5
  import { checkSettingsEnv } from '../settings.js'
6
6
  import { CORE_VERBS, flagHint, printHelp, refusedWord, seeHelp } from './help.js'
7
- import { FOREIGN_VERBS } from './foreign-flags.js'
8
7
  import {
9
8
  errorDocument,
10
9
  hideEnvFromTasks,
@@ -99,11 +98,6 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
99
98
  // for the whole reference: it printed 145 lines and no hint.
100
99
  const resolved = await (await plugins()).resolvePluginCommand(verb)
101
100
  const unknown = resolved === null || 'declaredVerbs' in resolved
102
- const pointer = FOREIGN_VERBS[verb]
103
- if (unknown && pointer !== undefined) {
104
- sayNotice('info', 'vx', pointer)
105
- return 1
106
- }
107
101
  if (unknown && verb.startsWith('-')) {
108
102
  sayNotice(
109
103
  'error',
@@ -217,12 +211,6 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
217
211
  resolved !== null && 'loadError' in resolved
218
212
  ? `\n (plugin verbs could not be looked up: .vx/workspace failed to load: ${resolved.loadError})`
219
213
  : ''
220
- // Another tool's verb: the pointer, but only after the plugins had
221
- // their chance — a workspace that declares a plugin verb of that name keeps it.
222
- const moved = FOREIGN_VERBS[command]
223
- if (moved !== undefined) {
224
- return unknown(`${moved}${loadNote}\n`, undefined, 'info')
225
- }
226
214
  // A task typed where the verb goes (`turbo build`, `nx build app`),
227
215
  // `turbo dev` included: a repo's own `dev` task beats the no-service note.
228
216
  const declaredVerbs =