@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.
- package/LICENSE +1 -1
- package/README.md +22 -48
- package/package.json +5 -5
- package/schemas/init.json +5 -0
- package/schemas/last.json +15 -0
- package/skills/vx/SKILL.md +1 -1
- package/src/bin.ts +1 -1
- package/src/cache/archive.ts +12 -76
- package/src/cache/cache.ts +72 -42
- package/src/cache/git-inputs.ts +55 -10
- package/src/cache/index.ts +5 -2
- package/src/cache/inputs.ts +61 -11
- package/src/cache/layer.ts +30 -8
- package/src/cache/layered-cache.ts +138 -44
- package/src/cache/output-index.ts +25 -11
- package/src/cache/plugin-cache.ts +9 -14
- package/src/cache/remote-lane.ts +60 -22
- package/src/cache/run-history.ts +13 -6
- package/src/cache/schema.ts +19 -2
- package/src/cache/scoped-cache.ts +3 -80
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/help.ts +8 -10
- package/src/cli/index.ts +0 -12
- package/src/cli/init.ts +159 -25
- package/src/cli/last.ts +4 -2
- package/src/cli/plan-format.ts +6 -2
- package/src/cli/plugin-commands.ts +98 -6
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +2 -8
- package/src/cli/select.ts +5 -3
- package/src/cli/upgrade.ts +5 -6
- package/src/config.ts +2 -4
- package/src/exec/executor.ts +112 -9
- package/src/exec/index.ts +2 -0
- package/src/exec/proc-sample.ts +67 -5
- package/src/exec/runner.ts +4 -0
- package/src/exec/sandbox-runtime.ts +11 -4
- package/src/graph/scheduler.ts +59 -8
- package/src/history/history.ts +119 -7
- package/src/history/metrics.ts +53 -16
- package/src/index.ts +16 -1
- package/src/orchestrator/affected-tasks.ts +0 -18
- package/src/orchestrator/execute-args.ts +3 -0
- package/src/orchestrator/execute-task.ts +28 -7
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +11 -4
- package/src/orchestrator/index.ts +18 -1
- package/src/orchestrator/local-floor.ts +104 -0
- package/src/orchestrator/local-shortcircuit.ts +26 -7
- package/src/orchestrator/miss-save.ts +10 -4
- package/src/orchestrator/options.ts +0 -8
- package/src/orchestrator/persistent-task.ts +14 -0
- package/src/orchestrator/placement.ts +84 -9
- package/src/orchestrator/plan.ts +7 -1
- package/src/orchestrator/plugin-host.ts +22 -7
- package/src/orchestrator/plugin.ts +48 -7
- package/src/orchestrator/prepare.ts +14 -49
- package/src/orchestrator/run-context.ts +39 -1
- package/src/orchestrator/run-keys.ts +164 -0
- package/src/orchestrator/run-records.ts +6 -0
- package/src/orchestrator/run.ts +95 -6
- package/src/orchestrator/sandbox-request.ts +9 -3
- package/src/orchestrator/task-artifacts.ts +142 -0
- package/src/orchestrator/task-hash.ts +12 -4
- package/src/orchestrator/task-process.ts +16 -0
- package/src/orchestrator/telemetry-host.ts +2 -1
- package/src/orchestrator/telemetry.ts +60 -17
- package/src/report/forecast.ts +6 -27
- package/src/report/framed-output.ts +41 -5
- package/src/report/logger.ts +60 -20
- package/src/report/status-line.ts +38 -18
- package/src/report/summary.ts +75 -9
- package/src/settings.ts +8 -7
- package/src/util/colors.ts +16 -1
- package/src/util/index.ts +1 -0
- package/src/util/paths.ts +30 -0
- package/src/util/ui.ts +4 -1
- package/src/workspace/affected.ts +0 -39
- package/src/workspace/config-bytecode.ts +276 -0
- package/src/workspace/index.ts +0 -2
- package/src/workspace/migration.ts +4 -32
- package/src/workspace/project-loader.ts +92 -14
- 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
|
|
266
|
-
* are cleared
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
307
|
+
if (rows.length > OUTPUT_DIRS_CAP) {
|
|
308
|
+
overCap = true
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
303
311
|
let entries
|
|
304
312
|
try {
|
|
305
|
-
entries =
|
|
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 (!
|
|
321
|
+
if (!walk(child)) return false
|
|
312
322
|
} else {
|
|
313
|
-
files.push(
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
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
|
|
220
|
+
for (const key of runReadKeys(name, scope)) {
|
|
226
221
|
try {
|
|
227
|
-
const got = await remote.get(key
|
|
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
|
package/src/cache/remote-lane.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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<
|
|
163
|
-
|
|
164
|
-
if (
|
|
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
|
-
|
|
176
|
+
found = await this.remote.has(hash)
|
|
167
177
|
} catch (err) {
|
|
168
178
|
this.report('probe', hash, err)
|
|
169
|
-
return
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
199
|
-
for (const h of hashes)
|
|
200
|
-
|
|
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
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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()
|
package/src/cache/run-history.ts
CHANGED
|
@@ -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
|
|
20
|
-
const RUN_TUPLE = `(${Array(
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
package/src/cache/schema.ts
CHANGED
|
@@ -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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
}
|
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
|
}
|
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
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
'
|
|
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 =
|