@vzn/vx 0.0.649 → 0.0.651
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 +5 -20
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/src/bin.ts +9 -31
- package/src/cache/cache.ts +225 -51
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/fold-memo.ts +8 -1
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/index.ts +12 -0
- package/src/cache/inputs.ts +3 -0
- package/src/cache/layer.ts +11 -0
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +120 -0
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-hits.ts +9 -1
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +11 -1
- package/src/cache/run-notes.ts +123 -0
- package/src/cache/schema.ts +54 -1
- package/src/cli/cache.ts +26 -16
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +12 -23
- package/src/cli/index.ts +4 -9
- package/src/cli/info.ts +15 -7
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +224 -223
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +10 -3
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +38 -42
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +2 -1
- package/src/cli/watch.ts +83 -44
- package/src/cli/why.ts +126 -92
- package/src/cli/workspace-config.ts +2 -1
- package/src/exec/executor.ts +2 -1
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-runtime.ts +11 -2
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +12 -2
- package/src/graph/task-graph.ts +27 -1
- package/src/handoff.ts +10 -7
- package/src/history/history.ts +39 -2
- package/src/history/index.ts +3 -0
- package/src/history/metrics.ts +9 -1
- package/src/index.ts +12 -3
- package/src/orchestrator/doctor.ts +3 -3
- package/src/orchestrator/execute-task.ts +16 -2
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +3 -0
- package/src/orchestrator/index.ts +3 -1
- package/src/orchestrator/live-usage.ts +98 -0
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +176 -102
- package/src/orchestrator/miss-save.ts +31 -3
- package/src/orchestrator/persistent-task.ts +3 -1
- package/src/orchestrator/plan.ts +5 -1
- package/src/orchestrator/plugin-host.ts +61 -0
- package/src/orchestrator/plugin.ts +49 -4
- package/src/orchestrator/prepare.ts +18 -2
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +35 -114
- package/src/orchestrator/run-records.ts +10 -0
- package/src/orchestrator/run.ts +89 -26
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-hash.ts +17 -3
- package/src/report/forecast.ts +12 -33
- package/src/report/framed-output.ts +388 -161
- package/src/report/index.ts +8 -2
- package/src/report/logger.ts +100 -36
- package/src/report/status-line.ts +81 -365
- package/src/report/summary.ts +104 -131
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +65 -10
- package/src/util/edit-distance.ts +6 -1
- package/src/util/hidden-prompt.ts +8 -2
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +20 -2
- package/src/util/secret-mask.ts +39 -3
- package/src/util/secrets.ts +3 -0
- package/src/util/settle.ts +31 -0
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/config-schema.ts +4 -2
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +28 -0
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +4 -3
- package/src/workspace/workspace.ts +3 -0
- package/src/orchestrator/run-steps.ts +0 -93
- package/src/util/colors.ts +0 -130
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
UserError,
|
|
21
21
|
} from '../util/index.js'
|
|
22
22
|
import {
|
|
23
|
+
indexDb,
|
|
23
24
|
Cache,
|
|
24
25
|
type CacheLayer,
|
|
25
26
|
applyCacheFlags,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
gitPathspecs,
|
|
39
40
|
MAX_SCOPED_PATHSPECS,
|
|
40
41
|
startGitEnumeration,
|
|
42
|
+
checkBlobSizes,
|
|
41
43
|
lazyGitEnumeration,
|
|
42
44
|
measureFsClockLag,
|
|
43
45
|
DEFAULT_STORE,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
lockDigest,
|
|
52
54
|
PROJECT_CONFIG_FILENAMES,
|
|
53
55
|
readClaimedFiles,
|
|
56
|
+
readDependenciesLock,
|
|
54
57
|
resolveCacheDir,
|
|
55
58
|
resolveStoreRoot,
|
|
56
59
|
type ProjectEntry,
|
|
@@ -75,6 +78,7 @@ import {
|
|
|
75
78
|
teardownPlugins,
|
|
76
79
|
type ScheduleRun,
|
|
77
80
|
} from './plugin-host.js'
|
|
81
|
+
import { lockedFiles } from './locked-digests.js'
|
|
78
82
|
import {
|
|
79
83
|
discoverProjects,
|
|
80
84
|
gitOfDiscovery,
|
|
@@ -381,6 +385,16 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
381
385
|
const fingerprintsAt = Date.now()
|
|
382
386
|
const claimed = [...fingerprintClaims(plugins).keys()]
|
|
383
387
|
const claimedBytes = await readClaimedFiles(workspaceRoot, claimed, reads)
|
|
388
|
+
// What `vx lock` recorded of them: a run keys from it (locked-digests.ts).
|
|
389
|
+
const dependenciesLock = claimed.length > 0 ? readDependenciesLock(workspaceRoot, reads) : null
|
|
390
|
+
dependenciesLock?.catch(() => {})
|
|
391
|
+
// A whole-tree run's blob-size check starts with the cache it reads its
|
|
392
|
+
// verdict from, beside the config load and graph build: after them, a cold
|
|
393
|
+
// 100-package run waited 40-50 ms on it before its first task (2026-11-03).
|
|
394
|
+
if (git.started !== undefined) {
|
|
395
|
+
localCache.holdBlobSizes()
|
|
396
|
+
git.started.then((e) => checkBlobSizes(e, localCache)).catch(() => {})
|
|
397
|
+
}
|
|
384
398
|
mark('open cache')
|
|
385
399
|
|
|
386
400
|
// Configs load FROM .vx/projects.lock.json with no evaluation and no check of
|
|
@@ -775,6 +789,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
775
789
|
// 1,000 projects when it ran before the spawn.
|
|
776
790
|
const settleIgnored = askIgnoredOutputs(gatedOutputs(nodes, workspaceRoot), workspaceRoot)
|
|
777
791
|
const enumeration = await enumerating
|
|
792
|
+
localCache.holdBlobSizes()
|
|
778
793
|
await applyGitEnumeration(
|
|
779
794
|
enumeration,
|
|
780
795
|
workspaceRoot,
|
|
@@ -799,7 +814,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
799
814
|
}
|
|
800
815
|
}
|
|
801
816
|
if (hasHook(plugins, 'key')) {
|
|
802
|
-
|
|
817
|
+
const locked = lockedFiles(workspaceRoot, claimed, claimedBytes, await dependenciesLock)
|
|
818
|
+
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: warnOf(log), locked })
|
|
803
819
|
}
|
|
804
820
|
refuseFiltersThroughGroups(nodes)
|
|
805
821
|
const exclude = options.excludeDependencies
|
|
@@ -834,7 +850,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
834
850
|
let own = pluginCaches.get(plugin)
|
|
835
851
|
if (own === undefined) {
|
|
836
852
|
own = pluginCache(plugin, {
|
|
837
|
-
db: localCache
|
|
853
|
+
db: indexDb(localCache),
|
|
838
854
|
policy,
|
|
839
855
|
remotes,
|
|
840
856
|
scope: cacheScope,
|
|
@@ -370,8 +370,11 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
370
370
|
}
|
|
371
371
|
continue
|
|
372
372
|
}
|
|
373
|
-
const
|
|
373
|
+
const frozen: ProjectConfig =
|
|
374
374
|
meta.configPath === null ? { tasks: {} } : (loaded[next++] as ProjectConfig)
|
|
375
|
+
// The lock shares a task two projects declare alike: an edit in place
|
|
376
|
+
// works on this project's own copy.
|
|
377
|
+
const config = projectStage ? structuredClone(frozen) : frozen
|
|
375
378
|
if (projectStage) {
|
|
376
379
|
// The `project` stage edits the validated object in place; core
|
|
377
380
|
// re-validates after EACH plugin so a plugin can only produce what
|
|
@@ -30,7 +30,7 @@ import type { RunContextRecord } from './telemetry.js'
|
|
|
30
30
|
export interface GitContext {
|
|
31
31
|
/** `git rev-parse HEAD`, or null outside a repo / on failure. */
|
|
32
32
|
commitSha: string | null
|
|
33
|
-
/** `
|
|
33
|
+
/** The branch HEAD names (`refs/heads/` stripped), with or without a commit; null when detached. */
|
|
34
34
|
branch: string | null
|
|
35
35
|
/** True if `git status --porcelain` was non-empty; null on failure. */
|
|
36
36
|
dirty: boolean | null
|
|
@@ -61,9 +61,11 @@ export interface HostContext {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* One short `git` spawn per run (not per task), behind try/catch.
|
|
64
|
-
* `git rev-parse HEAD --
|
|
65
|
-
* and the
|
|
66
|
-
* two separate `rev-parse` calls.
|
|
64
|
+
* `git rev-parse HEAD --symbolic-full-name HEAD` returns the commit on
|
|
65
|
+
* line 1 and the ref on line 2 in a SINGLE invocation — half the spawns
|
|
66
|
+
* of two separate `rev-parse` calls. The full name, not `--abbrev-ref`:
|
|
67
|
+
* a tag named like the branch made that answer `heads/main`
|
|
68
|
+
* (vercel/turborepo#13882). `dirty` is NOT probed here: the run's
|
|
67
69
|
* `GitFilesCache` populate already ran `git status --porcelain` for
|
|
68
70
|
* input enumeration, so the orchestrator passes that aggregate in
|
|
69
71
|
* (`dirty`) rather than paying for a second status spawn. Pass
|
|
@@ -95,7 +97,7 @@ export function captureGitContext(
|
|
|
95
97
|
workspaceRoot,
|
|
96
98
|
'rev-parse',
|
|
97
99
|
'HEAD',
|
|
98
|
-
'--
|
|
100
|
+
'--symbolic-full-name',
|
|
99
101
|
'HEAD',
|
|
100
102
|
],
|
|
101
103
|
env: gitEnv(),
|
|
@@ -107,15 +109,19 @@ export function captureGitContext(
|
|
|
107
109
|
const sha = lines[0]?.trim()
|
|
108
110
|
const br = lines[1]?.trim()
|
|
109
111
|
if (sha) commitSha = sha
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
+
// A detached HEAD answers the literal string `HEAD`, never `refs/heads/…`.
|
|
113
|
+
// Recording that verbatim INVENTS a branch: every detached
|
|
112
114
|
// run shares a pseudo-branch "HEAD", which is how `actions/checkout`
|
|
113
115
|
// leaves a pull_request build. The trust boundary (`branch ===
|
|
114
116
|
// defaultBranch`) means it is at least never mistaken for trunk, but
|
|
115
117
|
// every PR collapses into one scope — so one PR's timings feed another's
|
|
116
118
|
// baseline, cross-branch regression detection counts them as a single
|
|
117
119
|
// branch, and a consumer grouping runs by branch reads "HEAD".
|
|
118
|
-
if (br
|
|
120
|
+
if (br?.startsWith('refs/heads/')) branch = br.slice('refs/heads/'.length)
|
|
121
|
+
} else {
|
|
122
|
+
// No commit yet: `rev-parse HEAD` fails, yet HEAD already names the
|
|
123
|
+
// branch the first commit lands on (`git branch --show-current`).
|
|
124
|
+
branch = unbornBranch(workspaceRoot)
|
|
119
125
|
}
|
|
120
126
|
} catch {
|
|
121
127
|
// git unavailable / non-repo: both fields stay null.
|
|
@@ -129,6 +135,18 @@ export function captureGitContext(
|
|
|
129
135
|
return { commitSha, branch, dirty }
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
/** The branch HEAD names, asked of git only after `rev-parse HEAD` failed. */
|
|
139
|
+
function unbornBranch(workspaceRoot: string): string | null {
|
|
140
|
+
const proc = Bun.spawnSync({
|
|
141
|
+
cmd: [executablePath('git'), '-C', workspaceRoot, 'symbolic-ref', '--quiet', 'HEAD'],
|
|
142
|
+
env: gitEnv(),
|
|
143
|
+
stdout: 'pipe',
|
|
144
|
+
stderr: 'ignore',
|
|
145
|
+
})
|
|
146
|
+
const ref = proc.exitCode === 0 ? new TextDecoder().decode(proc.stdout).trim() : ''
|
|
147
|
+
return ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : null
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
/**
|
|
133
151
|
* `HEAD`'s commit and branch from the `.git` files, or null when anything
|
|
134
152
|
* about the layout is unfamiliar (the caller then spawns git). Handles a
|
|
@@ -138,7 +156,7 @@ export function captureGitContext(
|
|
|
138
156
|
*/
|
|
139
157
|
function readHeadDirect(
|
|
140
158
|
workspaceRoot: string,
|
|
141
|
-
): { commitSha: string; branch: string | null } | null {
|
|
159
|
+
): { commitSha: string | null; branch: string | null } | null {
|
|
142
160
|
try {
|
|
143
161
|
const dirs = refDirsOf(workspaceRoot)
|
|
144
162
|
if (dirs === null) return null
|
|
@@ -166,7 +184,8 @@ function readHeadDirect(
|
|
|
166
184
|
}
|
|
167
185
|
}
|
|
168
186
|
}
|
|
169
|
-
|
|
187
|
+
// Neither a loose nor a packed ref: the branch has no commit yet.
|
|
188
|
+
return { commitSha: null, branch }
|
|
170
189
|
} catch {
|
|
171
190
|
return null
|
|
172
191
|
}
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
// A failed run's task output and the files it names, so an agent reads why
|
|
2
|
-
// a task failed without the terminal (`vx last --format json`).
|
|
3
|
-
//
|
|
4
|
-
// shared store under it) stays as it was.
|
|
2
|
+
// a task failed without the terminal (`vx last --format json`). Rows of
|
|
3
|
+
// cache.db's `failures` (cache/run-notes.ts), the newest 50 failed runs kept.
|
|
5
4
|
|
|
6
5
|
import type { Database } from 'bun:sqlite'
|
|
7
|
-
import {
|
|
8
|
-
import path from 'node:path'
|
|
6
|
+
import { indexDb, type Cache, keepRunNotes, keptFailures } from '../cache/index.js'
|
|
9
7
|
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
10
8
|
import { decodeOutputLog } from './output-log.js'
|
|
11
9
|
import { PASS_STATUSES } from '../history/index.js'
|
|
12
|
-
import {
|
|
13
|
-
import { fileLocations, fileMemo, type OutputLocation } from '../
|
|
14
|
-
|
|
15
|
-
/** Failed runs whose output is kept; run ids are UUIDv7, so names sort by age. */
|
|
16
|
-
const KEPT = 50
|
|
10
|
+
import { maskedLine, splitTaskId } from '../util/index.js'
|
|
11
|
+
import { fileLocations, fileMemo, type OutputLocation } from '../ui/index.js'
|
|
17
12
|
|
|
18
13
|
/** A failed task of a run and what it said. */
|
|
19
14
|
export interface TaskFailure {
|
|
@@ -36,17 +31,19 @@ export interface RunFailures {
|
|
|
36
31
|
tasks: TaskFailure[]
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Keep the run's failures, when any outcome carries output. A refused write
|
|
36
|
+
* (a full disk) costs only this output: `warn` says so and the run's
|
|
37
|
+
* verdict stands.
|
|
38
|
+
*/
|
|
39
|
+
export function writeRunFailures(
|
|
40
|
+
cache: Cache,
|
|
41
|
+
runId: string,
|
|
42
|
+
outcomes: readonly TaskOutcome[],
|
|
43
|
+
warn: (message: string) => void,
|
|
44
|
+
): void {
|
|
48
45
|
const isFile = fileMemo()
|
|
49
|
-
const
|
|
46
|
+
const failures = []
|
|
50
47
|
for (const o of outcomes) {
|
|
51
48
|
// A task failed by a moved input may have printed nothing: the input
|
|
52
49
|
// is still why, and an agent reads it here.
|
|
@@ -60,76 +57,30 @@ function failuresOf(outcomes: readonly TaskOutcome[]): StoredFailure[] {
|
|
|
60
57
|
.map((c) => c.text)
|
|
61
58
|
.join(''),
|
|
62
59
|
)
|
|
63
|
-
|
|
60
|
+
failures.push({
|
|
64
61
|
taskId: o.node.id,
|
|
65
|
-
project: o.node.projectName,
|
|
66
|
-
task: o.node.taskName,
|
|
67
|
-
exitCode: o.exitCode,
|
|
68
|
-
...(o.timedOut === true ? { timedOut: true as const } : {}),
|
|
69
62
|
...(o.inputMoved !== undefined ? { inputMoved: o.inputMoved } : {}),
|
|
70
63
|
output,
|
|
71
64
|
locations: fileLocations(output, o.node.projectDir, isFile),
|
|
72
65
|
...(o.sandboxFix !== undefined ? { sandboxFix: o.sandboxFix } : {}),
|
|
73
66
|
})
|
|
74
67
|
}
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Write the run's failures, when any outcome carries output, and drop all
|
|
80
|
-
* but the newest `KEPT` files. Atomic: a reader sees the whole file or none.
|
|
81
|
-
* A refused write (a full disk, a read-only cache dir) costs only this
|
|
82
|
-
* output: `warn` says so and the run's verdict stands.
|
|
83
|
-
*/
|
|
84
|
-
export function writeRunFailures(
|
|
85
|
-
cacheDir: string,
|
|
86
|
-
runId: string,
|
|
87
|
-
outcomes: readonly TaskOutcome[],
|
|
88
|
-
warn: (message: string) => void,
|
|
89
|
-
): void {
|
|
90
|
-
const tasks = failuresOf(outcomes)
|
|
91
|
-
if (tasks.length === 0) return
|
|
68
|
+
if (failures.length === 0) return
|
|
92
69
|
try {
|
|
93
|
-
|
|
70
|
+
keepRunNotes(indexDb(cache), runId, { failures })
|
|
94
71
|
} catch (err) {
|
|
95
|
-
|
|
96
|
-
warn(`[vx] failed tasks' output not kept: ${
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
73
|
+
warn(`[vx] failed tasks' output not kept: ${message} — the verdict above stands`)
|
|
97
74
|
}
|
|
98
75
|
}
|
|
99
76
|
|
|
100
|
-
function keep(cacheDir: string, runId: string, tasks: TaskFailure[]): void {
|
|
101
|
-
const dir = dirOf(cacheDir)
|
|
102
|
-
mkdirSync(dir, { recursive: true })
|
|
103
|
-
const file = path.join(dir, `${runId}.json`)
|
|
104
|
-
const tmp = `${file}.${process.pid}.tmp`
|
|
105
|
-
writeFileSync(tmp, JSON.stringify({ runId, tasks }))
|
|
106
|
-
renameSync(tmp, file)
|
|
107
|
-
trimKept(dir, KEPT)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Keep the newest `kept` runs' files in `dir`, and the temp files of runs
|
|
112
|
-
* no older: a writer that crashed between its write and its rename left
|
|
113
|
-
* `<runId>.json.<pid>.tmp`, which the trim of `.json` names never took. A
|
|
114
|
-
* live writer's temp is of a run newer than every kept one.
|
|
115
|
-
*/
|
|
116
|
-
export function trimKept(dir: string, kept: number): void {
|
|
117
|
-
const all = readdirSync(dir)
|
|
118
|
-
const names = all.filter((n) => n.endsWith('.json')).sort()
|
|
119
|
-
for (const n of names.slice(0, -kept)) rmSync(path.join(dir, n), { force: true })
|
|
120
|
-
if (names.length < kept) return
|
|
121
|
-
const oldest = names[names.length - kept]!
|
|
122
|
-
for (const n of all)
|
|
123
|
-
if (n.endsWith('.tmp') && n.slice(0, n.lastIndexOf('.json.') + 5) < oldest)
|
|
124
|
-
rmSync(path.join(dir, n), { force: true })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
77
|
/**
|
|
128
78
|
* The failed tasks of `runId`, or of the latest failed run when omitted;
|
|
129
|
-
* null when there is no such run. A failed task
|
|
130
|
-
*
|
|
79
|
+
* null when there is no such run. A failed task with no kept row (a run
|
|
80
|
+
* past the newest 50, a write the disk refused) reads `output: ''`,
|
|
81
|
+
* `locations: []`.
|
|
131
82
|
*/
|
|
132
|
-
export function runFailures(
|
|
83
|
+
export function runFailures(db: Database, runId?: string): RunFailures | null {
|
|
133
84
|
const id =
|
|
134
85
|
runId ??
|
|
135
86
|
(
|
|
@@ -138,7 +89,7 @@ export function runFailures(cacheDir: string, db: Database, runId?: string): Run
|
|
|
138
89
|
.get() as { run_id: string } | null
|
|
139
90
|
)?.run_id
|
|
140
91
|
if (id === undefined) return null
|
|
141
|
-
const kept =
|
|
92
|
+
const kept = keptFailures(db, id)
|
|
142
93
|
const rows = db
|
|
143
94
|
.query(
|
|
144
95
|
`SELECT r.project, r.task, r.exit_code AS exitCode, r.timed_out AS timedOut,
|
|
@@ -165,41 +116,16 @@ export function runFailures(cacheDir: string, db: Database, runId?: string): Run
|
|
|
165
116
|
...(r.timedOut === 1 ? { timedOut: true as const } : {}),
|
|
166
117
|
...(k?.inputMoved !== undefined ? { inputMoved: k.inputMoved } : {}),
|
|
167
118
|
output: k?.output ?? '',
|
|
168
|
-
locations: k?.locations ?? [],
|
|
169
|
-
...(k?.sandboxFix !== undefined
|
|
119
|
+
locations: (k?.locations ?? []) as OutputLocation[],
|
|
120
|
+
...(k?.sandboxFix !== undefined
|
|
121
|
+
? { sandboxFix: k.sandboxFix as NonNullable<TaskOutcome['sandboxFix']> }
|
|
122
|
+
: {}),
|
|
170
123
|
...(r.fixedIn !== null ? { fixedIn: r.fixedIn } : {}),
|
|
171
124
|
}
|
|
172
125
|
}),
|
|
173
126
|
}
|
|
174
127
|
}
|
|
175
128
|
|
|
176
|
-
/** What `runId`'s failures file holds, by task id; empty when the run kept none. */
|
|
177
|
-
function storedFailures(cacheDir: string, runId: string): Map<string, StoredFailure> {
|
|
178
|
-
const kept = new Map<string, StoredFailure>()
|
|
179
|
-
// A run id the caller typed names a file in failures/ only.
|
|
180
|
-
if (path.basename(runId) !== runId || runId === '..') return kept
|
|
181
|
-
let text: string
|
|
182
|
-
try {
|
|
183
|
-
text = readFileSync(path.join(dirOf(cacheDir), `${runId}.json`), 'utf8')
|
|
184
|
-
} catch (err) {
|
|
185
|
-
// No file: the run kept no output.
|
|
186
|
-
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
|
|
187
|
-
return kept
|
|
188
|
-
}
|
|
189
|
-
// A file the disk left torn (a rename that landed before its data, on
|
|
190
|
-
// power loss) is output the run did not keep: it threw on every `vx last`
|
|
191
|
-
// until 50 newer runs rotated it out.
|
|
192
|
-
let stored: unknown
|
|
193
|
-
try {
|
|
194
|
-
stored = JSON.parse(text)
|
|
195
|
-
} catch {
|
|
196
|
-
return kept
|
|
197
|
-
}
|
|
198
|
-
const tasks = (stored as { tasks?: unknown } | null)?.tasks
|
|
199
|
-
if (Array.isArray(tasks)) for (const t of tasks as StoredFailure[]) kept.set(t.taskId, t)
|
|
200
|
-
return kept
|
|
201
|
-
}
|
|
202
|
-
|
|
203
129
|
/** One task's output in a recorded run, as `vx last --log` and getTaskLog read it. */
|
|
204
130
|
export interface TaskLog {
|
|
205
131
|
runId: string
|
|
@@ -219,12 +145,7 @@ export interface TaskLog {
|
|
|
219
145
|
* `taskId`'s output in `runId`, or in the latest run that recorded the
|
|
220
146
|
* task when omitted; null when no such run recorded it.
|
|
221
147
|
*/
|
|
222
|
-
export function taskLog(
|
|
223
|
-
cacheDir: string,
|
|
224
|
-
db: Database,
|
|
225
|
-
taskId: string,
|
|
226
|
-
runId?: string,
|
|
227
|
-
): TaskLog | null {
|
|
148
|
+
export function taskLog(db: Database, taskId: string, runId?: string): TaskLog | null {
|
|
228
149
|
const [project, task] = splitTaskId(taskId)
|
|
229
150
|
if (project === '' || task === '') return null
|
|
230
151
|
const row = db
|
|
@@ -241,9 +162,9 @@ export function taskLog(
|
|
|
241
162
|
if (row === null) return null
|
|
242
163
|
const base = { runId: row.runId, taskId, status: row.status }
|
|
243
164
|
if (row.status === 'failed') {
|
|
244
|
-
// Only the
|
|
245
|
-
// if the task printed nothing, when the run is past the newest 50.
|
|
246
|
-
const kept =
|
|
165
|
+
// Only the kept row: runFailures reads `output: ''` for a task it lacks,
|
|
166
|
+
// as if the task printed nothing, when the run is past the newest 50.
|
|
167
|
+
const kept = keptFailures(db, row.runId).get(taskId)
|
|
247
168
|
if (kept !== undefined) return { ...base, source: 'failure', output: kept.output }
|
|
248
169
|
} else if (row.hash !== '') {
|
|
249
170
|
const stored = db.query('SELECT stdout FROM entry_stdout WHERE hash = ?').get(row.hash) as {
|
|
@@ -38,6 +38,8 @@ export interface RunRecordsInput {
|
|
|
38
38
|
host: HostContext
|
|
39
39
|
/** Build the telemetry mirror; a run with no sink allocates none. */
|
|
40
40
|
withTelemetry: boolean
|
|
41
|
+
/** Each task's count of the tasks and groups downstream of it (the scheduler's): the rows' `blocks`. */
|
|
42
|
+
blocks?: ReadonlyMap<string, number>
|
|
41
43
|
/** The run's remote failures by key, so far (`Cache.traffic`). */
|
|
42
44
|
remoteTraffic?: ReadonlyMap<string, { failures: readonly string[] }>
|
|
43
45
|
}
|
|
@@ -124,6 +126,14 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
124
126
|
if (o.whereCores !== undefined) r.placementCores = o.whereCores
|
|
125
127
|
if (o.whereMemoryBytes !== undefined) r.placementMemoryBytes = o.whereMemoryBytes
|
|
126
128
|
if (o.cpuShare !== undefined) r.cpuShare = o.cpuShare
|
|
129
|
+
if (o.queuedMs !== undefined) r.queuedMs = o.queuedMs
|
|
130
|
+
if (o.outputBytes !== undefined) r.outputBytes = o.outputBytes
|
|
131
|
+
const blocks = input.blocks?.get(o.node.id)
|
|
132
|
+
if (blocks !== undefined) r.blocks = blocks
|
|
133
|
+
if (o.flaky !== undefined) {
|
|
134
|
+
r.flakyPasses = o.flaky.passes
|
|
135
|
+
r.flakyFailures = o.flaky.failures
|
|
136
|
+
}
|
|
127
137
|
if (o.wallclockStartNs !== undefined) r.wallclockStartNs = o.wallclockStartNs
|
|
128
138
|
if (o.wallclockEndNs !== undefined) r.wallclockEndNs = o.wallclockEndNs
|
|
129
139
|
if (hit) r.restored = o.restored === true
|