@vzn/vx 0.0.638 → 0.0.640
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 +1 -1
- package/docs/caching.md +152 -95
- package/docs/cli.md +225 -372
- package/docs/execution.md +22 -47
- package/docs/features.md +14 -16
- package/docs/patterns.md +6 -8
- package/docs/schema.md +268 -215
- package/docs/security.md +1 -1
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +10 -7
- package/src/cache/cache.ts +44 -11
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +8 -2
- package/src/cache/index.ts +5 -2
- package/src/cache/inputs.ts +154 -83
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +33 -18
- package/src/cache/layered-cache.ts +108 -315
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cache/plugin-cache.ts +2 -10
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +24 -8
- package/src/cache/run-store.ts +4 -4
- package/src/cache/schema.ts +6 -4
- package/src/cache/scoped-cache.ts +7 -0
- package/src/cli/completions.ts +2 -2
- package/src/cli/foreign-flags.ts +11 -23
- package/src/cli/help.ts +6 -31
- package/src/cli/info.ts +2 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +30 -4
- package/src/cli/plan-format.ts +5 -58
- package/src/cli/plugin-commands.ts +2 -2
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +23 -185
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +15 -6
- package/src/cli/upgrade.ts +215 -55
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +22 -6
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +103 -16
- package/src/cli/why.ts +9 -7
- package/src/config.ts +19 -21
- package/src/exec/executor.ts +5 -5
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +17 -2
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +45 -31
- package/src/graph/task-graph.ts +190 -58
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +1 -1
- package/src/{orchestrator → history}/history.ts +1 -1
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +7 -20
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +11 -29
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +50 -411
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +2 -73
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +1 -1
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +5 -5
- package/src/orchestrator/plan.ts +19 -6
- package/src/orchestrator/plugin-host.ts +129 -69
- package/src/orchestrator/plugin.ts +18 -8
- package/src/orchestrator/prepare.ts +40 -34
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/remote-prefetch.ts +14 -1
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +35 -3
- package/src/orchestrator/run-report.ts +151 -171
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +293 -604
- package/src/orchestrator/sandbox-fix.ts +1 -17
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +164 -27
- package/src/orchestrator/steps.ts +70 -0
- package/src/orchestrator/task-hash.ts +34 -25
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +4 -4
- package/src/orchestrator/telemetry.ts +14 -86
- package/src/orchestrator/upstream.ts +4 -181
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +40 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +158 -20
- package/src/{orchestrator → report}/status-line.ts +88 -7
- package/src/{orchestrator → report}/summary.ts +126 -35
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +12 -39
- package/src/util/colors.ts +44 -4
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +19 -4
- package/src/util/paths.ts +1 -1
- package/src/util/report.ts +13 -5
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +17 -6
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -280
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
package/src/orchestrator/run.ts
CHANGED
|
@@ -12,10 +12,6 @@ import {
|
|
|
12
12
|
type CachePolicy,
|
|
13
13
|
cachesNothing,
|
|
14
14
|
formatCachePolicy,
|
|
15
|
-
packRunOutputs,
|
|
16
|
-
resolveOutputs,
|
|
17
|
-
resolveWorkspaceOutputs,
|
|
18
|
-
runEntryName,
|
|
19
15
|
stopRuntimeProbes,
|
|
20
16
|
} from '../cache/index.js'
|
|
21
17
|
import { VERSION } from '../version.js'
|
|
@@ -40,7 +36,6 @@ import {
|
|
|
40
36
|
import {
|
|
41
37
|
mark,
|
|
42
38
|
stageTimes,
|
|
43
|
-
killGraceMs,
|
|
44
39
|
MAX_TIMEOUT_MS,
|
|
45
40
|
printTimings,
|
|
46
41
|
ulid,
|
|
@@ -49,60 +44,65 @@ import {
|
|
|
49
44
|
nearest,
|
|
50
45
|
UserError,
|
|
51
46
|
machineParallelism,
|
|
52
|
-
teardownTimeoutMs,
|
|
53
47
|
secretMask,
|
|
54
48
|
isFsRefusal,
|
|
55
49
|
isUserError,
|
|
56
50
|
detectColors,
|
|
57
|
-
relPosix,
|
|
58
51
|
type ColorSupport,
|
|
59
52
|
} from '../util/index.js'
|
|
60
53
|
import { keyedProjects } from './keyed-projects.js'
|
|
61
54
|
import { isDefaultBuild } from './projects.js'
|
|
62
55
|
import { commandWriteReach, prepareSandbox } from './sandbox-request.js'
|
|
63
|
-
import type
|
|
56
|
+
import { recordOutputDirSnapshots, type OutputDirSnapshot, type SaveFacts } from './miss-save.js'
|
|
64
57
|
import { admitTasks, taintTracker } from './admission.js'
|
|
65
58
|
import { unkeyedGroupOutcome, type ExecuteArgs } from './execute-task.js'
|
|
59
|
+
import { executeArgsOf, type RunScope } from './execute-args.js'
|
|
66
60
|
import { excludedTaint } from './excluded-keys.js'
|
|
67
61
|
import { keyUpstream } from './upstream.js'
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
import {
|
|
63
|
+
busLogger,
|
|
64
|
+
createEventBus,
|
|
65
|
+
createForecast,
|
|
66
|
+
defaultLogger,
|
|
67
|
+
type DefaultLogger,
|
|
68
|
+
type EventBus,
|
|
69
|
+
formatOutcomeTable,
|
|
70
|
+
formatPersistentList,
|
|
71
|
+
formatRunSummary,
|
|
72
|
+
type Logger,
|
|
73
|
+
plainOutput,
|
|
74
|
+
resolveOutputView,
|
|
75
|
+
terminalSubscriber,
|
|
76
|
+
warnOf,
|
|
77
|
+
} from '../report/index.js'
|
|
78
|
+
import { installPlugins, PluginSetupError, type RunReport } from './plugin.js'
|
|
79
|
+
import { keepsFailures, reportOf, RunKeeper } from './run-report.js'
|
|
80
|
+
import { buildAdmission, executorDemand, resolveExecutors, teardownPlugins } from './plugin-host.js'
|
|
81
|
+
import { createSteps } from './steps.js'
|
|
82
|
+
import { writeRunSteps } from './run-steps.js'
|
|
72
83
|
import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
73
84
|
import { claimOutput, guardedOutput } from './output-host.js'
|
|
74
|
-
import { assembleRunSummary
|
|
85
|
+
import { assembleRunSummary } from './telemetry.js'
|
|
86
|
+
import { isPassStatus } from '../graph/index.js'
|
|
75
87
|
import type { RunContextRecord } from './telemetry.js'
|
|
76
|
-
import { defaultLogger, type DefaultLogger, resolveOutputView, type Logger } from './logger.js'
|
|
77
|
-
import { plainOutput } from './plain-output.js'
|
|
78
|
-
import { formatPersistentList } from './framed-output.js'
|
|
79
|
-
import { createForecast } from './forecast.js'
|
|
80
|
-
import { LocalHistoryProvider } from './history.js'
|
|
81
88
|
import { plan, type RunPlan } from './plan.js'
|
|
82
89
|
import type { AffectedReason } from './affected-tasks.js'
|
|
83
90
|
import { prepareRun, type PreparedRun } from './prepare.js'
|
|
84
91
|
import { acquireRunLock } from './run-lock.js'
|
|
85
|
-
import {
|
|
86
|
-
|
|
87
|
-
type StopSignal,
|
|
88
|
-
forwardSignals,
|
|
89
|
-
SIGNAL_SHUTDOWN_GRACE_MS,
|
|
90
|
-
terminateChildren,
|
|
91
|
-
} from './signals.js'
|
|
92
|
+
import { terminateChildren } from './signals.js'
|
|
93
|
+
import { runStop } from './run-stop.js'
|
|
92
94
|
import {
|
|
93
95
|
hasPooledExecutor,
|
|
94
96
|
placeTasks,
|
|
95
97
|
planExecutorOf,
|
|
96
98
|
poolOfPlacement,
|
|
97
99
|
terminalHolders,
|
|
98
|
-
UNPLACED_EXECUTOR,
|
|
99
100
|
locallyPlaced,
|
|
100
101
|
} from './placement.js'
|
|
101
102
|
import {
|
|
102
|
-
|
|
103
|
+
telemetryRunContext,
|
|
103
104
|
captureGitContext,
|
|
104
105
|
captureHostContext,
|
|
105
|
-
captureWorkspaceIdentity,
|
|
106
106
|
detectCi,
|
|
107
107
|
} from './run-context.js'
|
|
108
108
|
import { startRemotePrefetch } from './remote-prefetch.js'
|
|
@@ -114,26 +114,25 @@ import {
|
|
|
114
114
|
import { deriveStableKeys, probesAfterWrites } from './stable-keys.js'
|
|
115
115
|
|
|
116
116
|
import { writeRunFailures } from './run-failures.js'
|
|
117
|
-
import { assembleRunRecords } from './run-records.js'
|
|
117
|
+
import { assembleRunRecords, recordLateRemoteErrors } from './run-records.js'
|
|
118
118
|
import {
|
|
119
119
|
hasEnded,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
disposeServers,
|
|
121
|
+
failServer,
|
|
122
|
+
holdServers,
|
|
123
|
+
sessionEnd,
|
|
123
124
|
takeHeldServers,
|
|
124
125
|
} from './persistent.js'
|
|
125
|
-
import {
|
|
126
|
-
runSummaryJson,
|
|
127
|
-
writeRunProfile,
|
|
128
|
-
writeRunSummary,
|
|
129
|
-
type RunSummaryJson,
|
|
130
|
-
} from './run-artifacts.js'
|
|
126
|
+
import { writeRunProfile } from './run-artifacts.js'
|
|
131
127
|
import { createSaveLane } from './save-lane.js'
|
|
132
128
|
import { commandWindows } from './command-windows.js'
|
|
133
|
-
import {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
import {
|
|
130
|
+
createMissExplainer,
|
|
131
|
+
detectFlaky,
|
|
132
|
+
type FlakyCandidate,
|
|
133
|
+
LocalHistoryProvider,
|
|
134
|
+
} from '../history/index.js'
|
|
135
|
+
import type { RunOptions, RunSummary } from './options.js'
|
|
137
136
|
import { withRunSettings } from './run-settings.js'
|
|
138
137
|
import { resolveSetting } from '../settings.js'
|
|
139
138
|
|
|
@@ -146,21 +145,6 @@ const emptyShortCircuit = (): ShortCircuit => ({
|
|
|
146
145
|
groupKeys: new Map(),
|
|
147
146
|
})
|
|
148
147
|
|
|
149
|
-
/** The files `node`'s declared outputs match on disk now, workspace-relative and sorted. */
|
|
150
|
-
async function outputsLeft(
|
|
151
|
-
node: TaskNode,
|
|
152
|
-
workspaceRoot: string,
|
|
153
|
-
nestedProjectDirs: string[],
|
|
154
|
-
): Promise<string[]> {
|
|
155
|
-
const declared = node.config.cache?.outputs
|
|
156
|
-
if (declared === undefined) return []
|
|
157
|
-
const [own, ws] = await Promise.all([
|
|
158
|
-
resolveOutputs({ projectDir: node.projectDir, outputs: declared.files, nestedProjectDirs }),
|
|
159
|
-
resolveWorkspaceOutputs({ workspaceRoot, outputs: declared.workspaceFiles ?? [] }),
|
|
160
|
-
])
|
|
161
|
-
return [...new Set([...own, ...ws].map((p) => relPosix(workspaceRoot, p)))].sort()
|
|
162
|
-
}
|
|
163
|
-
|
|
164
148
|
/**
|
|
165
149
|
* The policy that will actually govern the run. The remote axes are inert
|
|
166
150
|
* without a remote layer to serve them: a `remote:w` policy over a bare
|
|
@@ -210,17 +194,6 @@ function maskInvocation(command: string): string {
|
|
|
210
194
|
return secretMask([process.env])?.mask(command) ?? command
|
|
211
195
|
}
|
|
212
196
|
|
|
213
|
-
/**
|
|
214
|
-
* A run's `--tag`s, stored on its history row and handed to telemetry
|
|
215
|
-
* (`vx.tag.<key>`): a tag that carries a secret is masked as the
|
|
216
|
-
* invocation line that spells it is (L-38).
|
|
217
|
-
*/
|
|
218
|
-
function maskTags(tags: Readonly<Record<string, string>>): Record<string, string> {
|
|
219
|
-
const mask = secretMask([process.env])
|
|
220
|
-
if (mask === null) return { ...tags }
|
|
221
|
-
return Object.fromEntries(Object.entries(tags).map(([k, v]) => [mask.mask(k), mask.mask(v)]))
|
|
222
|
-
}
|
|
223
|
-
|
|
224
197
|
/**
|
|
225
198
|
* The command line a run hands to its telemetry sinks, with what follows
|
|
226
199
|
* `--` counted, not quoted. Those are the task's arguments,
|
|
@@ -295,15 +268,11 @@ function refuseRunOptions(options: RunOptions): void {
|
|
|
295
268
|
refuse('concurrency', concurrency, 'a positive integer')
|
|
296
269
|
if (retries !== undefined && !(Number.isInteger(retries) && retries >= 0))
|
|
297
270
|
refuse('retries', retries, 'a non-negative integer')
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
] as const) {
|
|
304
|
-
if (ms !== undefined && !(Number.isInteger(ms) && ms > 0 && ms <= MAX_TIMEOUT_MS))
|
|
305
|
-
refuse(name, ms, `a positive integer of ms, at most ${MAX_TIMEOUT_MS}`)
|
|
306
|
-
}
|
|
271
|
+
if (
|
|
272
|
+
timeout !== undefined &&
|
|
273
|
+
!(Number.isInteger(timeout) && timeout > 0 && timeout <= MAX_TIMEOUT_MS)
|
|
274
|
+
)
|
|
275
|
+
refuse('timeout', timeout, `a positive integer of ms, at most ${MAX_TIMEOUT_MS}`)
|
|
307
276
|
}
|
|
308
277
|
|
|
309
278
|
export async function run(options: RunOptions): Promise<RunSummary> {
|
|
@@ -324,17 +293,12 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
324
293
|
// See docs/design/event-stream-2026-06.md.
|
|
325
294
|
const terminal =
|
|
326
295
|
options.log === undefined
|
|
327
|
-
? defaultLogger(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
options.
|
|
331
|
-
{
|
|
332
|
-
|
|
333
|
-
...(options.tty === true ? { tty: true } : {}),
|
|
334
|
-
...(options.forwardArgs !== undefined ? { forwardArgs: options.forwardArgs } : {}),
|
|
335
|
-
...(options.footer === false ? { footer: false } : {}),
|
|
336
|
-
},
|
|
337
|
-
)
|
|
296
|
+
? defaultLogger(colors, resolveOutputView(options), process.stdout, {
|
|
297
|
+
coalesce: true,
|
|
298
|
+
...(options.tty === true ? { tty: true } : {}),
|
|
299
|
+
...(options.forwardArgs !== undefined ? { forwardArgs: options.forwardArgs } : {}),
|
|
300
|
+
...(options.footer === false ? { footer: false } : {}),
|
|
301
|
+
})
|
|
338
302
|
: null
|
|
339
303
|
const sink = options.log ?? terminal!
|
|
340
304
|
// An injected bus already has surfaces subscribed; we add the terminal
|
|
@@ -364,7 +328,10 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
364
328
|
}
|
|
365
329
|
let held = false
|
|
366
330
|
try {
|
|
367
|
-
|
|
331
|
+
// vx's own steps are timed only where something renders them.
|
|
332
|
+
const showsSteps =
|
|
333
|
+
terminal !== null ? terminal.showsSteps : options.log?.stepStart !== undefined
|
|
334
|
+
const summary = await runOnBus(options, bus, colors, terminal, showsSteps, takeOutput)
|
|
368
335
|
if (summary.persistent === undefined) return summary
|
|
369
336
|
// Servers handed back still running still write: `vx watch dev` printed
|
|
370
337
|
// none of its server's log while it idled, the renderer gone with this
|
|
@@ -388,9 +355,12 @@ async function runOnBus(
|
|
|
388
355
|
bus: EventBus,
|
|
389
356
|
colors: ColorSupport,
|
|
390
357
|
terminal: DefaultLogger | null,
|
|
358
|
+
showsSteps: boolean,
|
|
391
359
|
takeOutput?: (claimed: { logger: Logger; owner: string }) => void,
|
|
392
360
|
): Promise<RunSummary> {
|
|
393
361
|
const log = busLogger(bus)
|
|
362
|
+
const steps = createSteps(log, showsSteps)
|
|
363
|
+
const { step, stepSync } = steps
|
|
394
364
|
// A server's output goes through a cell of its own: a later watch cycle
|
|
395
365
|
// that keeps the server points it at that cycle's bus (`RunOptions.keep`).
|
|
396
366
|
const serverOuts = new Map<string, { bus: EventBus }>()
|
|
@@ -404,7 +374,7 @@ async function runOnBus(
|
|
|
404
374
|
}
|
|
405
375
|
}
|
|
406
376
|
|
|
407
|
-
const prepared = await prepareRun(options, log)
|
|
377
|
+
const prepared = await step('vx:load', () => prepareRun(options, log))
|
|
408
378
|
options = withRunSettings(options, prepared.workspaceConfig)
|
|
409
379
|
mark('plugin stages')
|
|
410
380
|
// Every exit from here tears the plugins down: their capability
|
|
@@ -418,7 +388,7 @@ async function runOnBus(
|
|
|
418
388
|
if (tornDown) return
|
|
419
389
|
tornDown = true
|
|
420
390
|
const plugins = prepared.plugins.filter((p) => p !== except)
|
|
421
|
-
await teardownPlugins(plugins, (
|
|
391
|
+
await teardownPlugins(plugins, warnOf(log), report, step)
|
|
422
392
|
}
|
|
423
393
|
// A task whose command re-enters `vx run` in the workspace running it
|
|
424
394
|
// is refused. When the inner run reaches this task again it forks a run
|
|
@@ -482,17 +452,36 @@ async function runOnBus(
|
|
|
482
452
|
const runId = ulid()
|
|
483
453
|
const head = captureGitContext(prepared.workspaceRoot)
|
|
484
454
|
const ciContext = detectCi(process.env)
|
|
485
|
-
const
|
|
455
|
+
const keepRun =
|
|
456
|
+
prepared.keepRunEntry === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: {
|
|
459
|
+
failures: keepsFailures(
|
|
460
|
+
resolveSetting('cache.upload.failures', prepared.workspaceConfig).value,
|
|
461
|
+
ciContext.ci,
|
|
462
|
+
),
|
|
463
|
+
}
|
|
464
|
+
const keeper =
|
|
465
|
+
prepared.keepRunEntry === undefined || keepRun === undefined
|
|
466
|
+
? undefined
|
|
467
|
+
: new RunKeeper({
|
|
468
|
+
runId,
|
|
469
|
+
workspaceRoot: prepared.workspaceRoot,
|
|
470
|
+
nestedDirsByProject: prepared.nestedDirsByProject,
|
|
471
|
+
keep: prepared.keepRunEntry,
|
|
472
|
+
failures: keepRun.failures,
|
|
473
|
+
})
|
|
486
474
|
try {
|
|
487
475
|
disposePlugins = await installPlugins({
|
|
488
476
|
plugins: prepared.plugins,
|
|
489
477
|
bus,
|
|
490
478
|
workspaceRoot: prepared.workspaceRoot,
|
|
491
479
|
cacheDir: prepared.cacheDir,
|
|
492
|
-
warn: (
|
|
480
|
+
warn: warnOf(log),
|
|
493
481
|
cache: prepared.cacheFor,
|
|
494
482
|
ci: ciContext.ci,
|
|
495
483
|
branch: prepared.branch(),
|
|
484
|
+
step,
|
|
496
485
|
})
|
|
497
486
|
} catch (err) {
|
|
498
487
|
disposePlugins?.()
|
|
@@ -545,7 +534,7 @@ async function runOnBus(
|
|
|
545
534
|
resolveExecutors(prepared.plugins, {
|
|
546
535
|
workspaceRoot: prepared.workspaceRoot,
|
|
547
536
|
cacheDir: prepared.cacheDir,
|
|
548
|
-
warn: (
|
|
537
|
+
warn: warnOf(log),
|
|
549
538
|
concurrency,
|
|
550
539
|
}),
|
|
551
540
|
)
|
|
@@ -569,51 +558,11 @@ async function runOnBus(
|
|
|
569
558
|
: nodes.get(id)?.config.cache === undefined
|
|
570
559
|
? 'no remote executor accepted it — the task declares no `cache` block, so there is no input set to ship'
|
|
571
560
|
: 'no remote executor accepted it'
|
|
572
|
-
log
|
|
561
|
+
warnOf(log)(`[vx] ${id}: exec.remote is 'only' and ${why} — nothing ran`)
|
|
573
562
|
}
|
|
574
563
|
}
|
|
575
|
-
// Demand: what each executor still has left to run.
|
|
576
|
-
|
|
577
|
-
// guess from `capacity` alone and hold everything until teardown. Built
|
|
578
|
-
// ONLY for executors that asked — with none declared there is no map, no
|
|
579
|
-
// set, and no call in the completion path.
|
|
580
|
-
const demandOf = new Map<TaskExecutor, Set<string>>()
|
|
581
|
-
for (const [id, executor] of placements.executors) {
|
|
582
|
-
if (executor.demand === undefined) continue
|
|
583
|
-
let remaining = demandOf.get(executor)
|
|
584
|
-
if (remaining === undefined) demandOf.set(executor, (remaining = new Set()))
|
|
585
|
-
remaining.add(id)
|
|
586
|
-
}
|
|
587
|
-
// The SAME set is handed back every time, narrowed in place: a consumer
|
|
588
|
-
// reading it later sees the truth rather than a stale snapshot. A hint,
|
|
589
|
-
// as `admit` is: one that throws is named once and asked no more, where
|
|
590
|
-
// it surfaced as a bare stack, or mid-run from the completion path
|
|
591
|
-
// (item 1022).
|
|
592
|
-
const tellDemand = (executor: TaskExecutor, remaining: ReadonlySet<string>): void => {
|
|
593
|
-
const failed = (err: unknown): void => {
|
|
594
|
-
if (!demandOf.delete(executor)) return
|
|
595
|
-
const m = err instanceof Error ? err.message : String(err)
|
|
596
|
-
log.status(`[vx] ${executorLabel(executor)} failed in demand: ${m}; not asked again this run`)
|
|
597
|
-
}
|
|
598
|
-
try {
|
|
599
|
-
// An `async demand` that rejects did so where no one listened: an
|
|
600
|
-
// unhandled rejection, a stack, the run killed. Its rejection is a
|
|
601
|
-
// throw like any other.
|
|
602
|
-
const ret: unknown = executor.demand!(remaining)
|
|
603
|
-
if (ret instanceof Promise) ret.catch(failed)
|
|
604
|
-
} catch (err) {
|
|
605
|
-
failed(err)
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
for (const [executor, remaining] of demandOf) tellDemand(executor, remaining)
|
|
609
|
-
const narrowDemand =
|
|
610
|
-
demandOf.size === 0
|
|
611
|
-
? (): void => {}
|
|
612
|
-
: (id: string): void => {
|
|
613
|
-
for (const [executor, remaining] of demandOf) {
|
|
614
|
-
if (remaining.delete(id)) tellDemand(executor, remaining)
|
|
615
|
-
}
|
|
616
|
-
}
|
|
564
|
+
// Demand: what each executor still has left to run (`executorDemand`).
|
|
565
|
+
const narrowDemand = executorDemand(placements.executors, warnOf(log))
|
|
617
566
|
|
|
618
567
|
// `cache.download` (default `all`) decides ONCE per task, here, whether a
|
|
619
568
|
// remote execution's outputs come home.
|
|
@@ -640,88 +589,20 @@ async function runOnBus(
|
|
|
640
589
|
| boolean
|
|
641
590
|
| undefined
|
|
642
591
|
|
|
643
|
-
|
|
644
|
-
// - `liveChildren`: in-flight children. The runner adds/removes
|
|
645
|
-
// each child around its spawn (persistent children stay until
|
|
646
|
-
// they exit).
|
|
647
|
-
// - `persistentRegistry`: ready persistent tasks (dev servers,
|
|
648
|
-
// watchers). executeTask spawns them but does NOT await their
|
|
649
|
-
// exit; ownership moves here so the orchestrator can SIGTERM
|
|
650
|
-
// them once the rest of the graph finishes.
|
|
651
|
-
//
|
|
652
|
-
// A SIGINT/SIGTERM mid-run forwards that signal to everything in both
|
|
653
|
-
// (`signals.ts`); the handlers are removed in the finally below so
|
|
654
|
-
// repeated run() calls (test suites) never stack listeners.
|
|
655
|
-
const liveChildren = new Set<ReturnType<typeof Bun.spawn>>()
|
|
656
|
-
// A server that ends on its own while the graph still runs is said at
|
|
657
|
-
// once: its dependants were failing against it, and the end of the run
|
|
658
|
-
// was the first word of it (C-69). Not once the graph is done (the end
|
|
659
|
-
// of the run and the keep-alive wait say it) nor under a stop.
|
|
660
|
-
let graphDone = false
|
|
661
|
-
const persistentRegistry = new (class extends Map<string, ReturnType<typeof Bun.spawn>> {
|
|
662
|
-
override set(id: string, child: ReturnType<typeof Bun.spawn>): this {
|
|
663
|
-
super.set(id, child)
|
|
664
|
-
void child.exited.then((code) => {
|
|
665
|
-
if (code !== 0 && !graphDone && !stopRun.signal.aborted)
|
|
666
|
-
log.status(`vx: ${id} exited with code ${code} while the run went on`)
|
|
667
|
-
})
|
|
668
|
-
return this
|
|
669
|
-
}
|
|
670
|
-
})()
|
|
671
|
-
// One stop for the run: an embedder's `RunOptions.signal` and a process
|
|
672
|
-
// signal both abort it. The scheduler stops dispatching (it reads the
|
|
673
|
-
// signal), the children are torn down, and run() returns through its own
|
|
674
|
-
// end-of-run path. Detached in the finally below.
|
|
675
|
-
const stopRun = new AbortController()
|
|
676
|
-
const forwardAbort = (): void => stopRun.abort(options.signal?.reason)
|
|
677
|
-
if (options.signal?.aborted === true) forwardAbort()
|
|
678
|
-
else options.signal?.addEventListener('abort', forwardAbort, { once: true })
|
|
679
|
-
// A run a process signal stopped records no history: until item 849 the
|
|
680
|
-
// handler exited before any outcome landed, and a Ctrl-C'd run then read
|
|
681
|
-
// in `vx last` as FAILED with 0 tasks (item 854). An embedder's abort is
|
|
682
|
-
// its own call and keeps its record.
|
|
683
|
-
let stoppedBy: StopSignal | undefined
|
|
684
|
-
let leftRun = (): void => {}
|
|
685
|
-
const runLeft = new Promise<void>((resolve) => {
|
|
686
|
-
leftRun = resolve
|
|
687
|
-
})
|
|
688
|
-
const signals = forwardSignals({
|
|
689
|
-
enabled: options.handleSignals ?? true,
|
|
592
|
+
const stop = runStop({
|
|
690
593
|
log,
|
|
691
594
|
cache,
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
stop: (signal) => {
|
|
695
|
-
stoppedBy = signal
|
|
696
|
-
stopRun.abort(signal)
|
|
697
|
-
},
|
|
698
|
-
done: runLeft,
|
|
595
|
+
external: options.signal,
|
|
596
|
+
handleSignals: options.handleSignals ?? true,
|
|
699
597
|
handsTerminal: holders.size > 0,
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
598
|
+
teardowns: prepared.plugins.filter((p) => p.teardown).length,
|
|
599
|
+
onStop: () => {
|
|
600
|
+
stopRuntimeProbes(hashCache.runtime, hashCache.workspaceRuntime)
|
|
601
|
+
cache.stopUploads?.()
|
|
602
|
+
keeper?.stop()
|
|
603
|
+
},
|
|
706
604
|
})
|
|
707
|
-
|
|
708
|
-
// once while a child it backgrounded ignores it and dies only to the
|
|
709
|
-
// SIGKILL after the grace, and a run that returned first left that child
|
|
710
|
-
// running past vx's exit (turborepo#14043's class).
|
|
711
|
-
let aborting: Promise<void> | undefined
|
|
712
|
-
// The servers that had ended when the stop landed: those crashed. One that
|
|
713
|
-
// ends after it is the stop's own kill, and was named a crash (item 1061).
|
|
714
|
-
let endedBeforeStop: ReadonlySet<ReturnType<typeof Bun.spawn>> | undefined
|
|
715
|
-
const onAbort = (): void => {
|
|
716
|
-
stopRuntimeProbes(hashCache.runtime, hashCache.workspaceRuntime)
|
|
717
|
-
endedBeforeStop = new Set([...persistentRegistry.values()].filter(hasEnded))
|
|
718
|
-
aborting = terminateChildren(
|
|
719
|
-
() => [...liveChildren, ...persistentRegistry.values()],
|
|
720
|
-
forwardedSignal(stopRun.signal.reason),
|
|
721
|
-
)
|
|
722
|
-
}
|
|
723
|
-
if (stopRun.signal.aborted) onAbort()
|
|
724
|
-
else stopRun.signal.addEventListener('abort', onAbort, { once: true })
|
|
605
|
+
const { liveChildren, persistentRegistry } = stop
|
|
725
606
|
// The cache handle must be released on EVERY exit path, not just the
|
|
726
607
|
// happy one: `close()` is also where the run's deferred `accessed_at`
|
|
727
608
|
// bumps are flushed, so a throw between opening the cache and the
|
|
@@ -748,10 +629,12 @@ async function runOnBus(
|
|
|
748
629
|
// Taken after the early exits above (nothing they do touches a tree) and
|
|
749
630
|
// before the schedule: from here on tasks clean, restore and write.
|
|
750
631
|
const releaseRunLock = await abandoning(() =>
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
632
|
+
step('vx:run-lock', () =>
|
|
633
|
+
acquireRunLock(prepared.workspaceRoot, {
|
|
634
|
+
log: warnOf(log),
|
|
635
|
+
signal: stop.signal,
|
|
636
|
+
}),
|
|
637
|
+
),
|
|
755
638
|
)
|
|
756
639
|
try {
|
|
757
640
|
const runStartHrTimeNs = process.hrtime.bigint()
|
|
@@ -764,7 +647,7 @@ async function runOnBus(
|
|
|
764
647
|
// happened.
|
|
765
648
|
const policy: CachePolicy = effectiveCachePolicy(prepared.cachePolicy, prepared.hasRemoteLayer)
|
|
766
649
|
if (options.remoteRequested === true && !prepared.hasRemoteLayer) {
|
|
767
|
-
log
|
|
650
|
+
warnOf(log)(
|
|
768
651
|
'vx: --cache-policy named the remote cache, but no cache plugin in .vx/workspace.* supplies one — the remote axes are off for this run',
|
|
769
652
|
)
|
|
770
653
|
}
|
|
@@ -807,51 +690,29 @@ async function runOnBus(
|
|
|
807
690
|
// but declines still pays — its answer is only knowable by asking.
|
|
808
691
|
const hasTelemetryPlugin = prepared.plugins.some((p) => p.telemetry !== undefined)
|
|
809
692
|
if (hasTelemetryPlugin || options.telemetrySinks !== undefined) {
|
|
810
|
-
|
|
811
|
-
// consumer can exist — a plain run never reaches here. The remote URL
|
|
812
|
-
// is the enumeration's `git var -l`: a spawn here cost ~4 ms.
|
|
813
|
-
const wsIdentity = captureWorkspaceIdentity(
|
|
814
|
-
workspaceRoot,
|
|
815
|
-
gitFilesCache.configValue('remote.origin.url'),
|
|
816
|
-
)
|
|
817
|
-
runContextRecord = {
|
|
693
|
+
runContextRecord = telemetryRunContext({
|
|
818
694
|
runId,
|
|
819
|
-
vxVersion: VERSION,
|
|
820
695
|
// An embedder's `command` is redacted as the argv is: it passed a
|
|
821
696
|
// token after `--` to every sink verbatim (C-67).
|
|
822
|
-
// Masked as the stored line is:
|
|
697
|
+
// Masked as the stored line is: a flag's secret value sits before
|
|
823
698
|
// the `--` the count covers (L-44).
|
|
824
699
|
command: maskInvocation(
|
|
825
700
|
invocationCommand(options.command?.split(' ') ?? process.argv.slice(1)),
|
|
826
701
|
),
|
|
827
|
-
requestedTasks:
|
|
702
|
+
requestedTasks: options.tasks,
|
|
828
703
|
cachePolicy: formatCachePolicy(policy),
|
|
829
704
|
concurrency,
|
|
830
705
|
flow: options.flow ?? null,
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
...(ciContext.runUrl !== undefined ? { ciRunUrl: ciContext.runUrl } : {}),
|
|
838
|
-
...(ciContext.change !== undefined ? { ciChange: ciContext.change } : {}),
|
|
839
|
-
...(ciContext.pipeline !== undefined ? { ciPipeline: ciContext.pipeline } : {}),
|
|
840
|
-
...(ciContext.job !== undefined ? { ciJob: ciContext.job } : {}),
|
|
841
|
-
...(ciContext.attempt !== undefined ? { ciAttempt: ciContext.attempt } : {}),
|
|
842
|
-
host: hostContext.host,
|
|
843
|
-
os: hostContext.os,
|
|
844
|
-
arch: hostContext.arch,
|
|
845
|
-
workspaceId: wsIdentity.id,
|
|
846
|
-
workspaceName: wsIdentity.name,
|
|
847
|
-
...(wsIdentity.repository !== undefined ? { repository: wsIdentity.repository } : {}),
|
|
848
|
-
...(wsIdentity.path !== undefined ? { workspacePath: wsIdentity.path } : {}),
|
|
849
|
-
tags: maskTags(options.tags ?? {}),
|
|
850
|
-
}
|
|
706
|
+
git: gitContext,
|
|
707
|
+
ci: ciContext,
|
|
708
|
+
host: hostContext,
|
|
709
|
+
workspaceRoot,
|
|
710
|
+
originUrl: gitFilesCache.configValue('remote.origin.url'),
|
|
711
|
+
})
|
|
851
712
|
telemetry = await subscribeTelemetry(
|
|
852
713
|
prepared.plugins,
|
|
853
714
|
bus,
|
|
854
|
-
{ workspaceRoot, cacheDir, warn: (
|
|
715
|
+
{ workspaceRoot, cacheDir, warn: warnOf(log) },
|
|
855
716
|
runContextRecord,
|
|
856
717
|
options.telemetrySinks,
|
|
857
718
|
nodes,
|
|
@@ -884,7 +745,7 @@ async function runOnBus(
|
|
|
884
745
|
// each pack holds an artifact's bytes); a failed save is a miss next
|
|
885
746
|
// time, said once — the task's work ran.
|
|
886
747
|
const saveLane = createSaveLane(2 * concurrency, (err) =>
|
|
887
|
-
log
|
|
748
|
+
warnOf(log)(`[vx] cache save failed: ${err instanceof Error ? err.message : String(err)}`),
|
|
888
749
|
)
|
|
889
750
|
const deferredSaves = new Map<string, Promise<SaveFacts>>()
|
|
890
751
|
const windows = commandWindows()
|
|
@@ -900,9 +761,10 @@ async function runOnBus(
|
|
|
900
761
|
// for display. Marks `node.surfaced`; never touches `requested`.
|
|
901
762
|
markSurfacedDeps(nodes)
|
|
902
763
|
|
|
903
|
-
// Header counts: unique projects
|
|
904
|
-
// dependsOn-pulled deps, not just the user-requested
|
|
905
|
-
//
|
|
764
|
+
// Header counts: unique projects with a real (non-group) task in the
|
|
765
|
+
// graph (including dependsOn-pulled deps, not just the user-requested
|
|
766
|
+
// set; a `^build` placeholder in a project with no build is not one),
|
|
767
|
+
// and the total number of real task executions. Mirrors the
|
|
906
768
|
// count the end-of-run summary reports under "total". The
|
|
907
769
|
// requested count drives the focused logger's live-vs-buffered
|
|
908
770
|
// decision, so surfaced nodes count toward it too — they display
|
|
@@ -911,8 +773,8 @@ async function runOnBus(
|
|
|
911
773
|
let taskCount = 0
|
|
912
774
|
let requestedCount = 0
|
|
913
775
|
for (const node of nodes.values()) {
|
|
914
|
-
packagesInScope.add(node.projectName)
|
|
915
776
|
if (!isGroupTask(node)) {
|
|
777
|
+
packagesInScope.add(node.projectName)
|
|
916
778
|
taskCount++
|
|
917
779
|
if (node.requested || node.surfaced === true) requestedCount++
|
|
918
780
|
}
|
|
@@ -920,7 +782,7 @@ async function runOnBus(
|
|
|
920
782
|
// Admission over the worker count is a plugin's (`admit`): built once
|
|
921
783
|
// into the predicate the scheduler asks at every local dispatch. No
|
|
922
784
|
// plugin answers → undefined → the count-only path, byte for byte.
|
|
923
|
-
const admit = buildAdmission(prepared.plugins, nodes, concurrency, (
|
|
785
|
+
const admit = buildAdmission(prepared.plugins, nodes, concurrency, warnOf(log))
|
|
924
786
|
|
|
925
787
|
// Run context for the footer. The top-of-run header is gone — the
|
|
926
788
|
// banner now lives in the summary, where the eye lands at the end.
|
|
@@ -932,7 +794,7 @@ async function runOnBus(
|
|
|
932
794
|
local: policy.localRead || policy.localWrite,
|
|
933
795
|
localWrite: policy.localWrite,
|
|
934
796
|
remote: remoteCacheEnabled && (policy.remoteRead || policy.remoteWrite),
|
|
935
|
-
|
|
797
|
+
traffic: prepared.localCache.traffic,
|
|
936
798
|
notDownloaded: () => deferredOutputs.pending().length,
|
|
937
799
|
},
|
|
938
800
|
concurrency,
|
|
@@ -941,14 +803,12 @@ async function runOnBus(
|
|
|
941
803
|
|
|
942
804
|
// Asked only when a plugin declares `output`: a plain run pays nothing.
|
|
943
805
|
if (takeOutput !== undefined && prepared.plugins.some((p) => p.output !== undefined)) {
|
|
944
|
-
const json = options.json === true
|
|
945
806
|
const claimed = await claimOutput(prepared.plugins, {
|
|
946
807
|
workspaceRoot,
|
|
947
808
|
cacheDir,
|
|
948
|
-
warn: (
|
|
949
|
-
isTTY:
|
|
809
|
+
warn: warnOf(log),
|
|
810
|
+
isTTY: process.stdout.isTTY === true,
|
|
950
811
|
ci: ciContext.ci,
|
|
951
|
-
json,
|
|
952
812
|
})
|
|
953
813
|
if (claimed !== undefined) takeOutput(claimed)
|
|
954
814
|
}
|
|
@@ -984,18 +844,21 @@ async function runOnBus(
|
|
|
984
844
|
// unchanged (no upfront key pass, no local probing).
|
|
985
845
|
let prefetchDone: Promise<void> = Promise.resolve()
|
|
986
846
|
if (prepared.hasRemoteLayer) {
|
|
987
|
-
prefetchDone =
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
847
|
+
prefetchDone = steps.background('vx:prefetch', () =>
|
|
848
|
+
startRemotePrefetch({
|
|
849
|
+
nodes,
|
|
850
|
+
cache,
|
|
851
|
+
workspaceRoot,
|
|
852
|
+
claimedFiles: fingerprintWatch.names,
|
|
853
|
+
forwardArgs: options.forwardArgs,
|
|
854
|
+
nestedDirsByProject,
|
|
855
|
+
gitFilesCache,
|
|
856
|
+
hashCache,
|
|
857
|
+
concurrency,
|
|
858
|
+
remoteRead: policy.remoteRead,
|
|
859
|
+
remoteWrite: policy.remoteWrite,
|
|
860
|
+
}),
|
|
861
|
+
)
|
|
999
862
|
}
|
|
1000
863
|
|
|
1001
864
|
// Local cache short-circuit (default-on). Up-front classify: derive
|
|
@@ -1016,17 +879,19 @@ async function runOnBus(
|
|
|
1016
879
|
prepared.localCache.holdsNothing()
|
|
1017
880
|
if (coldLocal) prewarm()
|
|
1018
881
|
else if (shouldShortCircuit(nodes, policy, cache)) {
|
|
1019
|
-
shortCircuit = await
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
882
|
+
shortCircuit = await step('vx:keys', () =>
|
|
883
|
+
startLocalShortCircuit({
|
|
884
|
+
nodes,
|
|
885
|
+
cache,
|
|
886
|
+
workspaceRoot,
|
|
887
|
+
claimedFiles: fingerprintWatch.names,
|
|
888
|
+
forwardArgs: options.forwardArgs,
|
|
889
|
+
nestedDirsByProject,
|
|
890
|
+
gitFilesCache,
|
|
891
|
+
hashCache,
|
|
892
|
+
concurrency,
|
|
893
|
+
}),
|
|
894
|
+
)
|
|
1030
895
|
// A confirmed miss runs too.
|
|
1031
896
|
if (sandboxed.some((n) => shortCircuit.preProbed.get(n.id)?.hit === null)) prewarm()
|
|
1032
897
|
}
|
|
@@ -1037,7 +902,7 @@ async function runOnBus(
|
|
|
1037
902
|
// admission.ts.
|
|
1038
903
|
const excluded = excludedTaint(nodes)
|
|
1039
904
|
if (excluded.unsaved > 0) {
|
|
1040
|
-
log
|
|
905
|
+
warnOf(log)(
|
|
1041
906
|
`[vx] --exclude-dependencies: ${excluded.unsaved} cached task(s) build on a skipped dependency; what they build is not saved`,
|
|
1042
907
|
)
|
|
1043
908
|
}
|
|
@@ -1066,19 +931,50 @@ async function runOnBus(
|
|
|
1066
931
|
// The scheduler's fail-fast stop, as the retry loop of a task already
|
|
1067
932
|
// in flight sees it.
|
|
1068
933
|
const failFast = new AbortController()
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
934
|
+
const scope: RunScope = {
|
|
935
|
+
workspaceRoot,
|
|
936
|
+
cache,
|
|
937
|
+
cachePolicy: policy,
|
|
938
|
+
forwardArgs: options.forwardArgs,
|
|
939
|
+
deferred: deferredOutputs,
|
|
940
|
+
cacheDir,
|
|
941
|
+
runStartHrTimeNs,
|
|
942
|
+
persistentRegistry,
|
|
943
|
+
liveChildren,
|
|
944
|
+
gitFilesCache,
|
|
945
|
+
hashCache,
|
|
946
|
+
probesAfterWrites: lateProbes,
|
|
947
|
+
fingerprintWatch,
|
|
948
|
+
keyedProjects: keyed,
|
|
949
|
+
outputDirSnapshots,
|
|
950
|
+
deferSave: saveLane.defer,
|
|
951
|
+
deferredSaves,
|
|
952
|
+
commandWindows: windows,
|
|
953
|
+
stopSignal: stop.signal,
|
|
954
|
+
failFast: failFast.signal,
|
|
955
|
+
inputStability,
|
|
956
|
+
}
|
|
957
|
+
if (options.retries !== undefined) scope.retries = options.retries
|
|
958
|
+
if (taskTimeoutDefault !== undefined) scope.timeout = taskTimeoutDefault
|
|
959
|
+
if (cleanOutputs !== undefined) scope.cleanOutputs = cleanOutputs
|
|
960
|
+
if (telemetry?.track !== undefined) scope.track = telemetry.track
|
|
961
|
+
if (explainMiss !== undefined) scope.explainMiss = explainMiss
|
|
962
|
+
if (keepRun !== undefined) scope.keepRun = keepRun
|
|
963
|
+
if (sandboxArmer !== null) scope.armSandbox = () => sandboxArmer.arm()
|
|
964
|
+
const argsOf = executeArgsOf(scope, {
|
|
965
|
+
log: (node) => (node.config.exec?.persistent !== undefined ? serverLog(node.id) : log),
|
|
966
|
+
placements,
|
|
967
|
+
nestedDirsByProject,
|
|
968
|
+
downloadModeOf: download.modeOf,
|
|
969
|
+
shortCircuit,
|
|
970
|
+
dependedOn,
|
|
971
|
+
holders,
|
|
972
|
+
})
|
|
1074
973
|
const buildExecuteArgs = (
|
|
1075
974
|
node: TaskNode,
|
|
1076
975
|
upstream: TaskOutcome[],
|
|
1077
976
|
reuseProbe = true,
|
|
1078
977
|
): ExecuteArgs => {
|
|
1079
|
-
const probe = reuseProbe ? shortCircuit.preProbed.get(node.id) : undefined
|
|
1080
|
-
const upfrontKey = shortCircuit.uncachedKeys.get(node.id)
|
|
1081
|
-
const upfrontGroupKey = shortCircuit.groupKeys.get(node.id)
|
|
1082
978
|
let deadServer: string | undefined
|
|
1083
979
|
if (options.continueMode === 'always') {
|
|
1084
980
|
for (const d of node.deps) {
|
|
@@ -1089,52 +985,7 @@ async function runOnBus(
|
|
|
1089
985
|
}
|
|
1090
986
|
const tainted = taint.judge(node, upstream)
|
|
1091
987
|
if (tainted) taintedRan.set(node.id, deadServer ?? taint.cause(node, upstream))
|
|
1092
|
-
|
|
1093
|
-
node,
|
|
1094
|
-
upstream,
|
|
1095
|
-
workspaceRoot,
|
|
1096
|
-
cache,
|
|
1097
|
-
cachePolicy: policy,
|
|
1098
|
-
forwardArgs: options.forwardArgs,
|
|
1099
|
-
log: node.config.exec?.persistent !== undefined ? serverLog(node.id) : log,
|
|
1100
|
-
executor: placements.executors.get(node.id) ?? UNPLACED_EXECUTOR,
|
|
1101
|
-
deferred: deferredOutputs,
|
|
1102
|
-
nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
|
|
1103
|
-
cacheDir,
|
|
1104
|
-
runStartHrTimeNs,
|
|
1105
|
-
persistentRegistry,
|
|
1106
|
-
liveChildren,
|
|
1107
|
-
gitFilesCache,
|
|
1108
|
-
hashCache,
|
|
1109
|
-
probesAfterWrites: lateProbes,
|
|
1110
|
-
fingerprintWatch,
|
|
1111
|
-
keyedProjects: keyed,
|
|
1112
|
-
outputDirSnapshots,
|
|
1113
|
-
deferSave: saveLane.defer,
|
|
1114
|
-
deferredSaves,
|
|
1115
|
-
commandWindows: windows,
|
|
1116
|
-
stopSignal: stopRun.signal,
|
|
1117
|
-
failFast: failFast.signal,
|
|
1118
|
-
}
|
|
1119
|
-
if (options.retries !== undefined) a.retries = options.retries
|
|
1120
|
-
if (taskTimeoutDefault !== undefined) a.timeout = taskTimeoutDefault
|
|
1121
|
-
if (options.readyNotice !== undefined) a.readyNotice = options.readyNotice
|
|
1122
|
-
a.inputStability = inputStability
|
|
1123
|
-
if (cleanOutputs !== undefined) a.cleanOutputs = cleanOutputs
|
|
1124
|
-
if (download.modeOf.get(node.id) === 'deferred') a.download = 'deferred'
|
|
1125
|
-
if (placements.remoteOnlyNoop.has(node.id)) a.remoteOnlyNoop = true
|
|
1126
|
-
if (placements.remoteOnly.has(node.id)) a.remoteOnly = true
|
|
1127
|
-
if (track !== undefined) a.track = track
|
|
1128
|
-
if (explainMiss !== undefined) a.explainMiss = explainMiss
|
|
1129
|
-
if (probe !== undefined) a.preProbed = probe
|
|
1130
|
-
if (upfrontKey !== undefined) a.upfrontKey = upfrontKey
|
|
1131
|
-
if (upfrontGroupKey !== undefined) a.upfrontGroupKey = upfrontGroupKey
|
|
1132
|
-
if (tainted) a.taintedUpstream = true
|
|
1133
|
-
if (keepRunLog) a.keepRunLog = true
|
|
1134
|
-
if (!dependedOn.has(node.id)) a.noDependants = true
|
|
1135
|
-
if (holders.has(node.id)) a.terminal = true
|
|
1136
|
-
if (armSandbox !== undefined) a.armSandbox = armSandbox
|
|
1137
|
-
return a
|
|
988
|
+
return argsOf(node, upstream, reuseProbe, tainted)
|
|
1138
989
|
}
|
|
1139
990
|
|
|
1140
991
|
const executeWithDedup = admitTasks({
|
|
@@ -1156,17 +1007,27 @@ async function runOnBus(
|
|
|
1156
1007
|
mark('classify + probe')
|
|
1157
1008
|
// A run whose every task is a confirmed local hit dispatches only
|
|
1158
1009
|
// restores, which no weight orders: the `schedule` stage is not asked.
|
|
1010
|
+
// A plugin's later weights (`ctx.refine`) re-rank the ready queues
|
|
1011
|
+
// while the graph runs; any that land before it starts, at its start.
|
|
1012
|
+
let reprioritize: ((weights: ReadonlyMap<string, number>) => void) | undefined
|
|
1013
|
+
let refinedEarly: ReadonlyMap<string, number> | undefined
|
|
1014
|
+
let graphOver = false
|
|
1159
1015
|
const priorities = [...nodes.values()].every(
|
|
1160
1016
|
(n) => n.config.exec?.command === undefined || shortCircuit.restoreTier.has(n.id),
|
|
1161
1017
|
)
|
|
1162
1018
|
? new Map<string, number>()
|
|
1163
|
-
: await prepared.priorities(
|
|
1019
|
+
: await prepared.priorities({
|
|
1020
|
+
step,
|
|
1021
|
+
refined: (merged) => {
|
|
1022
|
+
if (reprioritize !== undefined) reprioritize(merged)
|
|
1023
|
+
else if (!graphOver) refinedEarly = merged
|
|
1024
|
+
},
|
|
1025
|
+
})
|
|
1164
1026
|
mark('schedule')
|
|
1165
1027
|
// Judged as each task finishes, against the history BEFORE this run's
|
|
1166
1028
|
// rows land, so the row that prints next carries the verdict: nothing
|
|
1167
1029
|
// prints below the footer. A hit or a skip asks nothing; a green miss
|
|
1168
1030
|
// is one probe of the failed-row index.
|
|
1169
|
-
const flaky: FlakyFinding[] = []
|
|
1170
1031
|
const historyDb = prepared.localCache.dbHandle()
|
|
1171
1032
|
const cacheOff = cachesNothing(policy)
|
|
1172
1033
|
const judgeFlaky = (o: TaskOutcome): void => {
|
|
@@ -1175,7 +1036,6 @@ async function runOnBus(
|
|
|
1175
1036
|
try {
|
|
1176
1037
|
const found = detectFlaky(historyDb, candidates)[0]
|
|
1177
1038
|
if (found === undefined) return
|
|
1178
|
-
flaky.push(found)
|
|
1179
1039
|
o.flaky = { passes: found.passes, failures: found.failures }
|
|
1180
1040
|
} catch {
|
|
1181
1041
|
// History is observability: an unreadable one judges nothing.
|
|
@@ -1211,7 +1071,7 @@ async function runOnBus(
|
|
|
1211
1071
|
...(options.continueMode !== undefined ? { continueMode: options.continueMode } : {}),
|
|
1212
1072
|
serverDied,
|
|
1213
1073
|
onFailFast: () => failFast.abort(),
|
|
1214
|
-
signal:
|
|
1074
|
+
signal: stop.signal,
|
|
1215
1075
|
onStart: (node) => {
|
|
1216
1076
|
log.taskStart?.(node)
|
|
1217
1077
|
},
|
|
@@ -1219,6 +1079,7 @@ async function runOnBus(
|
|
|
1219
1079
|
if (cacheOff) o.cacheOff = true
|
|
1220
1080
|
taint.settled(o)
|
|
1221
1081
|
judgeFlaky(o)
|
|
1082
|
+
keeper?.taskDone(o)
|
|
1222
1083
|
log.taskComplete(o.node, o)
|
|
1223
1084
|
narrowDemand(o.node.id)
|
|
1224
1085
|
},
|
|
@@ -1242,7 +1103,7 @@ async function runOnBus(
|
|
|
1242
1103
|
...(options.continueMode !== 'always' && taintSeeds.size === 0
|
|
1243
1104
|
? {
|
|
1244
1105
|
settleNow: (node: TaskNode, upstream: TaskOutcome[]) =>
|
|
1245
|
-
isGroupTask(node) && node.config.
|
|
1106
|
+
isGroupTask(node) && node.config.inputs === undefined
|
|
1246
1107
|
? unkeyedGroupOutcome(
|
|
1247
1108
|
node,
|
|
1248
1109
|
keyUpstream(node, upstream),
|
|
@@ -1255,6 +1116,10 @@ async function runOnBus(
|
|
|
1255
1116
|
// A `schedule` plugin's weights; the scheduler keeps its structural
|
|
1256
1117
|
// baseline as the tie-break. Empty map → baseline only.
|
|
1257
1118
|
...(priorities.size > 0 ? { priorities } : {}),
|
|
1119
|
+
reprioritize: (apply) => {
|
|
1120
|
+
if (refinedEarly !== undefined) apply(refinedEarly)
|
|
1121
|
+
reprioritize = apply
|
|
1122
|
+
},
|
|
1258
1123
|
// The baseline under them; `most-work` when the workspace names none.
|
|
1259
1124
|
...(options.schedule !== undefined ? { strategy: options.schedule } : {}),
|
|
1260
1125
|
// Local short-circuit: confirmed stable local hits the scheduler
|
|
@@ -1262,7 +1127,9 @@ async function runOnBus(
|
|
|
1262
1127
|
// Empty when the short-circuit didn't fire → byte-identical.
|
|
1263
1128
|
restoreTier: shortCircuit.restoreTier,
|
|
1264
1129
|
})
|
|
1265
|
-
graphDone
|
|
1130
|
+
stop.graphDone()
|
|
1131
|
+
reprioritize = undefined
|
|
1132
|
+
graphOver = true
|
|
1266
1133
|
|
|
1267
1134
|
// Which persistent children outlive the graph, and the bounded SIGTERM
|
|
1268
1135
|
// of the rest, before the summary prints. Scoped to the real CLI
|
|
@@ -1272,49 +1139,19 @@ async function runOnBus(
|
|
|
1272
1139
|
// expect run() to return, not block on a server.
|
|
1273
1140
|
const foreground = options.log === undefined && (options.handleSignals ?? true)
|
|
1274
1141
|
// An aborted run's children are already being torn down: nothing to hold.
|
|
1275
|
-
const hold = options.holdPersistent === true && !
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
![...outcomes.values()].every((o) => isPassStatus(o.status)) ||
|
|
1286
|
-
[...persistentRegistry.values()].some(
|
|
1287
|
-
(c) => hasEnded(c) && c.exitCode !== 0 && !keepAlive.children.includes(c),
|
|
1288
|
-
)
|
|
1289
|
-
if (failedElsewhere && !hold && options.continueMode !== 'always')
|
|
1290
|
-
keepAlive = { nodes: [], children: [] }
|
|
1291
|
-
const crashedPersistent = (
|
|
1292
|
-
await shutdownPersistent(persistentRegistry, keepAlive.children)
|
|
1293
|
-
).filter((c) => endedBeforeStop?.has(persistentRegistry.get(c.id)!) ?? true)
|
|
1142
|
+
const hold = options.holdPersistent === true && !stop.signal.aborted
|
|
1143
|
+
const { keepAlive, crashed: crashedPersistent } = await disposeServers({
|
|
1144
|
+
registry: persistentRegistry,
|
|
1145
|
+
nodes,
|
|
1146
|
+
outcomes,
|
|
1147
|
+
keep: foreground || hold,
|
|
1148
|
+
hold,
|
|
1149
|
+
continueAlways: options.continueMode === 'always',
|
|
1150
|
+
endedBeforeStop: stop.endedBeforeStop,
|
|
1151
|
+
})
|
|
1294
1152
|
|
|
1295
1153
|
mark('run graph')
|
|
1296
1154
|
|
|
1297
|
-
// A server that died on its own before the stop failed, whatever its
|
|
1298
|
-
// outcome said when it became ready: the footer counted it a success
|
|
1299
|
-
// over the run's red exit (item 1071). A kept one that has already
|
|
1300
|
-
// died is the same; the keep-alive wait below names it. Its code is its
|
|
1301
|
-
// own, or the shell's report of the signal that took it.
|
|
1302
|
-
const failServer = (id: string, code: number | string): void => {
|
|
1303
|
-
const o = outcomes.get(id)
|
|
1304
|
-
if (o === undefined) return
|
|
1305
|
-
// In place: the renderer holds this outcome (events carry live refs)
|
|
1306
|
-
// and closes the server's output block from it at runEnd, where a
|
|
1307
|
-
// copy left it reading `running` under its exit line (WD-10).
|
|
1308
|
-
o.status = 'failed'
|
|
1309
|
-
o.exitCode = typeof code === 'number' ? code : signalExitCode(code)
|
|
1310
|
-
}
|
|
1311
|
-
for (const c of crashedPersistent) failServer(c.id, c.code)
|
|
1312
|
-
keepAlive.nodes.forEach((n, i) => {
|
|
1313
|
-
const child = keepAlive.children[i]!
|
|
1314
|
-
if (!hasEnded(child) || child.exitCode === 0) return
|
|
1315
|
-
if (endedBeforeStop !== undefined && !endedBeforeStop.has(child)) return
|
|
1316
|
-
failServer(n.id, child.exitCode ?? child.signalCode ?? 'unknown')
|
|
1317
|
-
})
|
|
1318
1155
|
// Clear the status line for good before the summary prints.
|
|
1319
1156
|
log.runEnd?.()
|
|
1320
1157
|
const list = [...outcomes.values()]
|
|
@@ -1340,7 +1177,7 @@ async function runOnBus(
|
|
|
1340
1177
|
// 1102).
|
|
1341
1178
|
for (const c of crashedPersistent) {
|
|
1342
1179
|
const code = typeof c.code === 'number' ? c.code : signalExitCode(c.code)
|
|
1343
|
-
log
|
|
1180
|
+
warnOf(log)(`vx: ${c.id} exited with code ${code} before the run stopped it`)
|
|
1344
1181
|
}
|
|
1345
1182
|
// Outputs that never came home are not an error, but a silent `dist/`
|
|
1346
1183
|
// that is empty-or-stale would be: name every task whose bytes are
|
|
@@ -1356,105 +1193,20 @@ async function runOnBus(
|
|
|
1356
1193
|
// change the run's exit code — the run already happened.
|
|
1357
1194
|
// Written again after the keep-alive wait: a kept server's crash or a
|
|
1358
1195
|
// Ctrl-C there is the process's exit, and the first write said ok.
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
// uncached task's log. Then the report every plugin's teardown reads.
|
|
1362
|
-
const keepRunBytes = async (final: readonly TaskOutcome[]): Promise<RunReport> => {
|
|
1363
|
-
const keep = prepared.keepRunEntry
|
|
1364
|
-
const tasks: RunReportTask[] = []
|
|
1365
|
-
const work: (() => Promise<void>)[] = []
|
|
1366
|
-
for (const o of final) {
|
|
1367
|
-
if (isGroupTask(o.node)) continue
|
|
1368
|
-
const task: { -readonly [K in keyof RunReportTask]: RunReportTask[K] } = {
|
|
1369
|
-
id: o.node.id,
|
|
1370
|
-
status: o.status,
|
|
1371
|
-
durationMs: Math.round(o.durationMs),
|
|
1372
|
-
...(o.hash !== undefined ? { key: o.hash } : {}),
|
|
1373
|
-
}
|
|
1374
|
-
tasks.push(task)
|
|
1375
|
-
if (keep === undefined) continue
|
|
1376
|
-
const runLog = o.runLog
|
|
1377
|
-
if (runLog !== undefined) {
|
|
1378
|
-
work.push(async () => {
|
|
1379
|
-
const text = decodeOutputLog(runLog)
|
|
1380
|
-
.map((c) => c.text)
|
|
1381
|
-
.join('')
|
|
1382
|
-
if (text.length === 0) return
|
|
1383
|
-
const name = runEntryName(runId, o.node.id, 'log')
|
|
1384
|
-
if (await keep(name, new TextEncoder().encode(text))) task.log = name
|
|
1385
|
-
})
|
|
1386
|
-
}
|
|
1387
|
-
if (o.status === 'failed' && o.where === undefined) {
|
|
1388
|
-
work.push(async () => {
|
|
1389
|
-
const bytes = await packRunOutputs(
|
|
1390
|
-
prepared.workspaceRoot,
|
|
1391
|
-
await outputsLeft(
|
|
1392
|
-
o.node,
|
|
1393
|
-
prepared.workspaceRoot,
|
|
1394
|
-
prepared.nestedDirsByProject.get(o.node.projectName) ?? [],
|
|
1395
|
-
),
|
|
1396
|
-
)
|
|
1397
|
-
if (bytes === null) return
|
|
1398
|
-
const name = runEntryName(runId, o.node.id, 'outputs')
|
|
1399
|
-
if (await keep(name, bytes)) task.outputs = name
|
|
1400
|
-
})
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
for (let i = 0; i < work.length; i += 8) {
|
|
1404
|
-
await Promise.all(work.slice(i, i + 8).map((w) => w()))
|
|
1405
|
-
}
|
|
1406
|
-
return { runId, commit: head.commitSha, ciRun: ciContext, ok, tasks }
|
|
1407
|
-
}
|
|
1408
|
-
let json: RunSummaryJson | undefined
|
|
1409
|
-
const summarize = async (runOk: boolean, final = list): Promise<void> => {
|
|
1410
|
-
if (options.json === true) {
|
|
1411
|
-
json = runSummaryJson({
|
|
1412
|
-
runId,
|
|
1413
|
-
startedAtMs: endedAtMsAtStart,
|
|
1414
|
-
endedAtMs,
|
|
1415
|
-
totalMs,
|
|
1416
|
-
ok: runOk,
|
|
1417
|
-
...(stoppedBy !== undefined && { exitCode: signalExitCode(stoppedBy) }),
|
|
1418
|
-
outcomes: final,
|
|
1419
|
-
flaky,
|
|
1420
|
-
})
|
|
1421
|
-
}
|
|
1422
|
-
if (options.summarize === undefined) return
|
|
1423
|
-
// The rewrite after the keep-alive wait says nothing: the footer is
|
|
1424
|
-
// the run's last word.
|
|
1425
|
-
const quiet = final !== list
|
|
1426
|
-
try {
|
|
1427
|
-
const wrote = await writeRunSummary({
|
|
1428
|
-
target: options.summarize,
|
|
1429
|
-
cacheDir,
|
|
1430
|
-
cwd: options.cwd,
|
|
1431
|
-
runId,
|
|
1432
|
-
startedAtMs: endedAtMsAtStart,
|
|
1433
|
-
endedAtMs,
|
|
1434
|
-
totalMs,
|
|
1435
|
-
ok: runOk,
|
|
1436
|
-
...(stoppedBy !== undefined && { exitCode: signalExitCode(stoppedBy) }),
|
|
1437
|
-
outcomes: final,
|
|
1438
|
-
flaky,
|
|
1439
|
-
})
|
|
1440
|
-
if (!quiet) log.status(`vx: summary written to ${wrote}`)
|
|
1441
|
-
} catch (err) {
|
|
1442
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
1443
|
-
log.status(`vx: failed to write summary: ${msg}`)
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
await summarize(ok)
|
|
1447
|
-
if (options.profile !== undefined) {
|
|
1196
|
+
const profile = options.profile
|
|
1197
|
+
if (profile !== undefined) {
|
|
1448
1198
|
try {
|
|
1449
|
-
const wrote = await
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1199
|
+
const wrote = await step('vx:profile', () =>
|
|
1200
|
+
writeRunProfile({
|
|
1201
|
+
target: profile,
|
|
1202
|
+
cwd: options.cwd,
|
|
1203
|
+
outcomes: list,
|
|
1204
|
+
}),
|
|
1205
|
+
)
|
|
1454
1206
|
log.status(`vx: profile written to ${wrote}`)
|
|
1455
1207
|
} catch (err) {
|
|
1456
1208
|
const msg = err instanceof Error ? err.message : String(err)
|
|
1457
|
-
log
|
|
1209
|
+
warnOf(log)(`vx: failed to write profile: ${msg}`)
|
|
1458
1210
|
}
|
|
1459
1211
|
}
|
|
1460
1212
|
// Record each task to the run history in a single SQLite transaction
|
|
@@ -1482,11 +1234,11 @@ async function runOnBus(
|
|
|
1482
1234
|
concurrency,
|
|
1483
1235
|
flow: options.flow ?? null,
|
|
1484
1236
|
forwardArgs: options.forwardArgs,
|
|
1485
|
-
tags: maskTags(options.tags ?? {}),
|
|
1486
1237
|
git: gitContext,
|
|
1487
1238
|
ci: ciContext,
|
|
1488
1239
|
host: hostContext,
|
|
1489
1240
|
withTelemetry: telemetry !== undefined,
|
|
1241
|
+
remoteTraffic: prepared.localCache.traffic.keys,
|
|
1490
1242
|
})
|
|
1491
1243
|
const records = recordsOf(list, ok)
|
|
1492
1244
|
const recordHistory = (write: () => void): void => {
|
|
@@ -1498,27 +1250,21 @@ async function runOnBus(
|
|
|
1498
1250
|
// an unprivileged user on a 2 MiB disk, 2026-09-16). The run said
|
|
1499
1251
|
// what happened; `vx last` and the flaky list will not know this one.
|
|
1500
1252
|
const message = err instanceof Error ? err.message : String(err)
|
|
1501
|
-
log
|
|
1253
|
+
warnOf(log)(`[vx] run history not recorded: ${message} — the verdict above stands`)
|
|
1502
1254
|
}
|
|
1503
1255
|
}
|
|
1504
1256
|
// A server kept in the foreground can still fail the run after the
|
|
1505
1257
|
// cache closes: its history is written once the wait has decided, or it
|
|
1506
1258
|
// read `ok` over the exit 1 and fed the flaky list a pass (X-23).
|
|
1507
|
-
const historyAfterWait = keepAlive.children.length > 0 && !hold && stoppedBy === undefined
|
|
1508
|
-
// A failed task's output, for `vx last` and an agent
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
log.status(`[vx] failed tasks' output not kept: ${err.message} — the verdict above stands`)
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
if (stoppedBy === undefined && !historyAfterWait) {
|
|
1520
|
-
recordHistory(() => cache.recordRunBundle(records))
|
|
1521
|
-
recordFailures(list)
|
|
1259
|
+
const historyAfterWait = keepAlive.children.length > 0 && !hold && stop.stoppedBy === undefined
|
|
1260
|
+
// A failed task's output, for `vx last` and an agent (run-failures.ts).
|
|
1261
|
+
const recordFailures = (final: readonly TaskOutcome[]): void =>
|
|
1262
|
+
writeRunFailures(prepared.cacheDir, runId, final, warnOf(log))
|
|
1263
|
+
if (stop.stoppedBy === undefined && !historyAfterWait) {
|
|
1264
|
+
stepSync('vx:history', () => {
|
|
1265
|
+
recordHistory(() => cache.recordRunBundle(records))
|
|
1266
|
+
recordFailures(list)
|
|
1267
|
+
})
|
|
1522
1268
|
}
|
|
1523
1269
|
mark('record history')
|
|
1524
1270
|
// Drain any still-in-flight background prefetches before closing the
|
|
@@ -1536,14 +1282,20 @@ async function runOnBus(
|
|
|
1536
1282
|
// channel shut from under the upload — silently losing every remote
|
|
1537
1283
|
// write with nothing but a warning. The seam's contract is that a
|
|
1538
1284
|
// cache layer stays usable until the run's uploads have settled.
|
|
1539
|
-
await prefetchDone
|
|
1540
1285
|
// Every deferred save has settled by here: the scheduler finishes a
|
|
1541
1286
|
// task only once its `settledOf` promise (the lane's `landed`) has,
|
|
1542
1287
|
// so `runGraph` resolved with every save in the cache. A second
|
|
1543
1288
|
// drain of the lane stood here until item 634; deleting it changed
|
|
1544
1289
|
// nothing the suite could see, because this gate is the rule.
|
|
1545
|
-
await
|
|
1546
|
-
|
|
1290
|
+
await step('vx:uploads', async () => {
|
|
1291
|
+
await prefetchDone
|
|
1292
|
+
await Promise.all([cache.drainUploads?.(), keeper?.settled()])
|
|
1293
|
+
})
|
|
1294
|
+
writeRunSteps(prepared.cacheDir, runId, steps.ended, warnOf(log))
|
|
1295
|
+
// An upload that failed after the rows went in: its row says so too.
|
|
1296
|
+
if (prepared.localCache.uploads.failed > 0 && stop.stoppedBy === undefined && !historyAfterWait)
|
|
1297
|
+
recordHistory(() => recordLateRemoteErrors(prepared.localCache, records.runs, runId))
|
|
1298
|
+
const report = reportOf(list, { runId, commit: head.commitSha, ciRun: ciContext, ok }, keeper)
|
|
1547
1299
|
// Hand the per-run summary to the telemetry sinks + drain them. Only
|
|
1548
1300
|
// when a sink is active (telemetry !== undefined) — otherwise this
|
|
1549
1301
|
// whole block is skipped and the run is byte-identical to before.
|
|
@@ -1562,72 +1314,52 @@ async function runOnBus(
|
|
|
1562
1314
|
uploads: prepared.localCache.uploads,
|
|
1563
1315
|
})
|
|
1564
1316
|
telemetry.emitSummary(summary)
|
|
1565
|
-
|
|
1317
|
+
const sinks = telemetry
|
|
1318
|
+
await sinks.flush(step)
|
|
1566
1319
|
}
|
|
1567
1320
|
// End-of-run plugin lifecycle: each plugin's teardown(). Crash-isolated
|
|
1568
1321
|
// + time-bounded inside teardownPlugins, so a faulty plugin can neither
|
|
1569
1322
|
// fail nor hang the run. Here on the normal path, after the drains
|
|
1570
1323
|
// above; every other exit takes it through `teardown` too.
|
|
1571
|
-
// The miss path's output-directory snapshots
|
|
1572
|
-
|
|
1573
|
-
// miss-save.ts). A few at a time: each is an lstat + readdir per
|
|
1574
|
-
// prefix; the rows land together at close, in one transaction.
|
|
1575
|
-
// Each walk is held to its entry's rows (item 1087), read here in one
|
|
1576
|
-
// batch: read per snapshot, each read also flushed the snapshots before
|
|
1577
|
-
// it, one transaction apiece (+110 ms at 1,000).
|
|
1578
|
-
const snapshotRows = new Map<string, ReadonlyArray<{ path: string }>>()
|
|
1579
|
-
for (let i = 0; i < outputDirSnapshots.length; i += 900) {
|
|
1580
|
-
const batch = cache.loadOutputFilesBatch(
|
|
1581
|
-
outputDirSnapshots.slice(i, i + 900).map((s) => s.hash),
|
|
1582
|
-
)
|
|
1583
|
-
for (const [hash, rows] of batch) snapshotRows.set(hash, rows)
|
|
1584
|
-
}
|
|
1585
|
-
for (let i = 0; i < outputDirSnapshots.length; i += 32) {
|
|
1586
|
-
await Promise.all(
|
|
1587
|
-
outputDirSnapshots
|
|
1588
|
-
.slice(i, i + 32)
|
|
1589
|
-
.map((s) =>
|
|
1590
|
-
cache.recordOutputDirs?.(s.hash, s.projectDir, s.prefixes, (files) =>
|
|
1591
|
-
s.holds(files, snapshotRows.get(s.hash) ?? []),
|
|
1592
|
-
),
|
|
1593
|
-
),
|
|
1594
|
-
)
|
|
1595
|
-
}
|
|
1324
|
+
// The miss path's output-directory snapshots first (miss-save.ts).
|
|
1325
|
+
await step('vx:snapshots', () => recordOutputDirSnapshots(cache, outputDirSnapshots))
|
|
1596
1326
|
mark('output dir snapshots')
|
|
1597
1327
|
// Not on a stopped run: one stopped while it waited on another run's
|
|
1598
1328
|
// lock never held it, and its prune evicted under that run (item 858).
|
|
1599
|
-
if (!
|
|
1329
|
+
if (!stop.signal.aborted) await step('vx:retention', () => applyCacheRetention(prepared))
|
|
1600
1330
|
// A plugin hears the run until its teardown and nothing after: released
|
|
1601
1331
|
// only in the finally, its handlers heard a kept server through the
|
|
1602
1332
|
// whole keep-alive wait below (C-66). Idempotent; the finally's stay.
|
|
1603
1333
|
disposePlugins?.()
|
|
1604
1334
|
telemetry?.dispose()
|
|
1605
1335
|
await teardown(undefined, report)
|
|
1606
|
-
await closeCache
|
|
1336
|
+
await step('vx:close', closeCache)
|
|
1607
1337
|
mark('close')
|
|
1608
1338
|
printTimings()
|
|
1609
1339
|
|
|
1610
1340
|
// Tear down SRT's network bridge + (on macOS) log monitor. No-op if
|
|
1611
1341
|
// no task was sandboxed; otherwise SRT keeps proxy servers alive and
|
|
1612
1342
|
// the next vx run would init on top of stale state.
|
|
1613
|
-
await
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1343
|
+
await step('vx:sandbox', async () => {
|
|
1344
|
+
await prewarming
|
|
1345
|
+
if (sandboxArmer?.armed) {
|
|
1346
|
+
try {
|
|
1347
|
+
await resetSandbox()
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
1350
|
+
warnOf(log)(`vx: sandbox cleanup failed: ${msg}`)
|
|
1351
|
+
}
|
|
1620
1352
|
}
|
|
1621
|
-
}
|
|
1353
|
+
})
|
|
1622
1354
|
|
|
1623
1355
|
// The footer is the run's last word (owner, 2026-10-06): it prints once
|
|
1624
1356
|
// the run's own work is done, history, uploads, plugin teardown and the
|
|
1625
1357
|
// sandbox reset included, so any warning they raise lands above it. A
|
|
1626
1358
|
// task's own facts (flaky, blocked) ride its row. Only a kept server's
|
|
1627
1359
|
// own output follows.
|
|
1360
|
+
// The footer replaces the live region in place.
|
|
1361
|
+
log.runDone?.()
|
|
1628
1362
|
if (options.summaryTable === true) for (const line of formatOutcomeTable(list)) log.status(line)
|
|
1629
|
-
const above = options.beforeFooter?.(list, ok)
|
|
1630
|
-
if (above) log.status(above.replace(/\n$/, ''))
|
|
1631
1363
|
if (options.footer !== false)
|
|
1632
1364
|
for (const line of formatRunSummary(list, totalMs, colors, runContext)) log.status(line)
|
|
1633
1365
|
|
|
@@ -1646,57 +1378,18 @@ async function runOnBus(
|
|
|
1646
1378
|
// ended the session and its code — the summary above said `success`
|
|
1647
1379
|
// for it, and an exit 1 with no word about why is a mystery in a log.
|
|
1648
1380
|
if (keepAlive.children.length > 0 && hold) {
|
|
1649
|
-
const held = keepAlive.children
|
|
1650
|
-
// A held server that dies on its own is said, as the foreground's
|
|
1651
|
-
// keep-alive says it: `vx watch` sat on "watching" over a dead dev
|
|
1652
|
-
// server. The holder's own stop() is not such a death.
|
|
1653
|
-
let stopping = false
|
|
1654
|
-
// Taken by a later run (`RunOptions.keep`): kept or stopped there.
|
|
1655
|
-
const handed = new Set<ReturnType<typeof Bun.spawn>>()
|
|
1656
|
-
const servers = new Map<string, HeldServer>()
|
|
1657
|
-
keepAlive.nodes.forEach((n, i) => {
|
|
1658
|
-
const child = held[i]!
|
|
1659
|
-
servers.set(n.id, {
|
|
1660
|
-
child,
|
|
1661
|
-
config: serverConfig(n, options.forwardArgs),
|
|
1662
|
-
out: serverOuts.get(n.id) ?? { bus },
|
|
1663
|
-
handOver: () => void handed.add(child),
|
|
1664
|
-
})
|
|
1665
|
-
// One that died during the graph was said then.
|
|
1666
|
-
if (hasEnded(child)) return
|
|
1667
|
-
void child.exited.then((code) => {
|
|
1668
|
-
if (!stopping && !handed.has(child) && code !== 0)
|
|
1669
|
-
log.status(`vx: ${n.id} exited with code ${code}`)
|
|
1670
|
-
})
|
|
1671
|
-
})
|
|
1672
1381
|
return {
|
|
1673
1382
|
ok,
|
|
1674
1383
|
outcomes: list,
|
|
1675
|
-
persistent: {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
return terminateChildren(() => held.filter((c) => !handed.has(c)), signal)
|
|
1681
|
-
},
|
|
1682
|
-
},
|
|
1384
|
+
persistent: holdServers(keepAlive, {
|
|
1385
|
+
forwardArgs: options.forwardArgs,
|
|
1386
|
+
outOf: (id) => serverOuts.get(id) ?? { bus },
|
|
1387
|
+
warn: warnOf(log),
|
|
1388
|
+
}),
|
|
1683
1389
|
}
|
|
1684
1390
|
}
|
|
1685
1391
|
if (keepAlive.children.length > 0) {
|
|
1686
|
-
|
|
1687
|
-
// forked and returned (schema.md), not the end of the session: a
|
|
1688
|
-
// `db: 'docker compose up -d'` under `app#dev` stopped the dev server
|
|
1689
|
-
// right after the summary (WD-3). It ends the wait only as the last.
|
|
1690
|
-
const first = await new Promise<{ code: number; i: number }>((resolve) => {
|
|
1691
|
-
let left = keepAlive.children.length
|
|
1692
|
-
keepAlive.children.forEach((c, i) => {
|
|
1693
|
-
void c.exited.then((code) => {
|
|
1694
|
-
left--
|
|
1695
|
-
const n = keepAlive.nodes[i]!
|
|
1696
|
-
if (code !== 0 || n.requested || n.surfaced === true || left === 0) resolve({ code, i })
|
|
1697
|
-
})
|
|
1698
|
-
})
|
|
1699
|
-
})
|
|
1392
|
+
const first = await sessionEnd(keepAlive)
|
|
1700
1393
|
const node = keepAlive.nodes[first.i]!
|
|
1701
1394
|
// Only the ones still up are stopped: a kept server already dead was
|
|
1702
1395
|
// counted as one (WD-11).
|
|
@@ -1704,8 +1397,8 @@ async function runOnBus(
|
|
|
1704
1397
|
// Not when the run was stopped: the server ended because the user
|
|
1705
1398
|
// stopped it, and "exited with code 130" read as a crash after every
|
|
1706
1399
|
// Ctrl-C once a stop let run() finish its own path (item 852).
|
|
1707
|
-
if (!
|
|
1708
|
-
log
|
|
1400
|
+
if (!stop.signal.aborted) {
|
|
1401
|
+
warnOf(log)(
|
|
1709
1402
|
`vx: ${node.id} exited with code ${first.code}` +
|
|
1710
1403
|
(others > 0
|
|
1711
1404
|
? `; stopping ${others} other persistent task${others === 1 ? '' : 's'}`
|
|
@@ -1715,14 +1408,11 @@ async function runOnBus(
|
|
|
1715
1408
|
// Under a stop the abort is already taking them down with the stop's
|
|
1716
1409
|
// signal; a second SIGTERM in its grace cut short the graceful
|
|
1717
1410
|
// shutdown of a server that reads it as "quit now" (WD-16).
|
|
1718
|
-
if (
|
|
1411
|
+
if (stop.signal.aborted) await stop.aborting?.catch(() => {})
|
|
1719
1412
|
else await terminateChildren(() => keepAlive.children)
|
|
1720
|
-
// The server that ended the session on its own, not cleanly, failed
|
|
1721
|
-
|
|
1722
|
-
// and `failed: 0`, and so did the outcomes `--report` renders (C-53).
|
|
1723
|
-
if (!stopRun.signal.aborted && first.code !== 0) failServer(node.id, first.code)
|
|
1413
|
+
// The server that ended the session on its own, not cleanly, failed.
|
|
1414
|
+
if (!stop.signal.aborted && first.code !== 0) failServer(outcomes, node.id, first.code)
|
|
1724
1415
|
const final = [...outcomes.values()]
|
|
1725
|
-
await summarize(ok && first.code === 0, final)
|
|
1726
1416
|
if (historyAfterWait) {
|
|
1727
1417
|
recordHistory(() => {
|
|
1728
1418
|
// A writer's open: another vx version may have reset the index
|
|
@@ -1736,10 +1426,10 @@ async function runOnBus(
|
|
|
1736
1426
|
})
|
|
1737
1427
|
recordFailures(final)
|
|
1738
1428
|
}
|
|
1739
|
-
return { ok: ok && first.code === 0, outcomes: final
|
|
1429
|
+
return { ok: ok && first.code === 0, outcomes: final }
|
|
1740
1430
|
}
|
|
1741
1431
|
|
|
1742
|
-
return { ok, outcomes: list
|
|
1432
|
+
return { ok, outcomes: list }
|
|
1743
1433
|
} finally {
|
|
1744
1434
|
// Idempotent. The status-line ticker starts in runStart, and every
|
|
1745
1435
|
// call between it and the success path's runEnd is crash-isolated
|
|
@@ -1749,9 +1439,8 @@ async function runOnBus(
|
|
|
1749
1439
|
// here), so deleting this line reddens nothing (item 635). It is the
|
|
1750
1440
|
// floor for a throw a later change adds, not a path a row can drive.
|
|
1751
1441
|
log.runEnd?.()
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
stopRun.signal.removeEventListener('abort', onAbort)
|
|
1442
|
+
log.runDone?.()
|
|
1443
|
+
stop.detach()
|
|
1755
1444
|
// Plugins installed at the top of run() get their bus subscriptions
|
|
1756
1445
|
// released here. Idempotent; safe even if installPlugins threw.
|
|
1757
1446
|
disposePlugins?.()
|
|
@@ -1768,9 +1457,9 @@ async function runOnBus(
|
|
|
1768
1457
|
} catch {
|
|
1769
1458
|
// teardown must not throw on the way out
|
|
1770
1459
|
}
|
|
1771
|
-
await aborting?.catch(() => {})
|
|
1460
|
+
await stop.aborting?.catch(() => {})
|
|
1772
1461
|
// Last: a process signal waits for this before it exits.
|
|
1773
|
-
|
|
1462
|
+
stop.left()
|
|
1774
1463
|
}
|
|
1775
1464
|
}
|
|
1776
1465
|
|
|
@@ -1815,10 +1504,10 @@ function noneAffected(prepared: PreparedRun, options: RunOptions): string {
|
|
|
1815
1504
|
* Planning mode. Same setup as `run()` — workspace discovery, config
|
|
1816
1505
|
* load, package graph, task graph — but stops short of execution.
|
|
1817
1506
|
* Returns a `RunPlan` predicting the cache hit/miss outcome of every
|
|
1818
|
-
* task. Used by `--dry
|
|
1507
|
+
* task. Used by `--dry`.
|
|
1819
1508
|
*
|
|
1820
1509
|
* Side-effects are limited to opening + closing the local Cache handle
|
|
1821
|
-
* (and running `
|
|
1510
|
+
* (and running `inputs.runtime` probe commands, which key
|
|
1822
1511
|
* derivation requires). Cache probing is the byte-free `cache.has()`
|
|
1823
1512
|
* existence check — no artifact download, no ingest, no accessed_at bump.
|
|
1824
1513
|
*/
|
|
@@ -1890,7 +1579,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1890
1579
|
} finally {
|
|
1891
1580
|
// The plan called the cache and executor factories; teardown releases
|
|
1892
1581
|
// what they opened, as a run's end does (item 1021).
|
|
1893
|
-
await teardownPlugins(prepared.plugins, (
|
|
1582
|
+
await teardownPlugins(prepared.plugins, warnOf(log))
|
|
1894
1583
|
prepared.cache.close()
|
|
1895
1584
|
// The same table a run prints: a dry run is how the prepare stages
|
|
1896
1585
|
// (discovery, config load, the git enumeration) get profiled on a
|