@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
package/src/orchestrator/run.ts
CHANGED
|
@@ -8,11 +8,13 @@ import { EMPTY_LOCK, inputStabilityEnv, LOCK_INVALID, lockEntryBroken } from '..
|
|
|
8
8
|
import { realpathSync } from 'node:fs'
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import {
|
|
11
|
+
indexDb,
|
|
11
12
|
Cache,
|
|
12
13
|
type CachePolicy,
|
|
13
14
|
cachesNothing,
|
|
14
15
|
formatCachePolicy,
|
|
15
16
|
networkConcurrency,
|
|
17
|
+
keepRunNotes,
|
|
16
18
|
stopRuntimeProbes,
|
|
17
19
|
sweepStaging,
|
|
18
20
|
} from '../cache/index.js'
|
|
@@ -37,11 +39,12 @@ import {
|
|
|
37
39
|
type TaskNode,
|
|
38
40
|
type TaskOutcome,
|
|
39
41
|
} from '../graph/index.js'
|
|
42
|
+
import { graphCounts } from './graph-memo.js'
|
|
40
43
|
import {
|
|
41
44
|
mark,
|
|
42
45
|
stageTimes,
|
|
43
46
|
MAX_TIMEOUT_MS,
|
|
44
|
-
|
|
47
|
+
timingLines,
|
|
45
48
|
ulid,
|
|
46
49
|
isLiteralPattern,
|
|
47
50
|
listed,
|
|
@@ -51,12 +54,17 @@ import {
|
|
|
51
54
|
secretMask,
|
|
52
55
|
isFsRefusal,
|
|
53
56
|
isUserError,
|
|
54
|
-
detectColors,
|
|
55
|
-
type ColorSupport,
|
|
56
57
|
SecretBroker,
|
|
57
58
|
thrownMessage,
|
|
58
|
-
type ui,
|
|
59
59
|
} from '../util/index.js'
|
|
60
|
+
import * as ui from '../ui/index.js'
|
|
61
|
+
|
|
62
|
+
/** The stage table `VX_DEBUG_TIMING=1` asks for, on stderr. */
|
|
63
|
+
function printTimings(): void {
|
|
64
|
+
const lines = timingLines()
|
|
65
|
+
if (lines !== undefined) ui.print('err', ui.trace(lines))
|
|
66
|
+
}
|
|
67
|
+
import { detectColors, type ColorSupport } from '../ui/index.js'
|
|
60
68
|
import { keyedProjects } from './keyed-projects.js'
|
|
61
69
|
import { isDefaultBuild } from '../graph/index.js'
|
|
62
70
|
import { commandWriteReach, prepareSandbox } from './sandbox-request.js'
|
|
@@ -66,6 +74,7 @@ import { settleProbedHit, unkeyedGroupOutcome, type ExecuteArgs } from './execut
|
|
|
66
74
|
import { executeArgsOf, type RunScope } from './execute-args.js'
|
|
67
75
|
import { excludedTaint } from './excluded-keys.js'
|
|
68
76
|
import { dependedOn, keyUpstream } from './upstream.js'
|
|
77
|
+
import { createLiveUsage } from './live-usage.js'
|
|
69
78
|
import {
|
|
70
79
|
busLogger,
|
|
71
80
|
createEventBus,
|
|
@@ -93,7 +102,6 @@ import {
|
|
|
93
102
|
} from './plugin-host.js'
|
|
94
103
|
import { createSteps } from './steps.js'
|
|
95
104
|
import { liveSocketOf, serveLive } from './run-live.js'
|
|
96
|
-
import { writeRunSteps, writeRunWarnings } from './run-steps.js'
|
|
97
105
|
import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
98
106
|
import { claimOutput, guardedOutput } from './output-host.js'
|
|
99
107
|
import { assembleRunSummary } from './telemetry.js'
|
|
@@ -353,6 +361,11 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
353
361
|
const detach = (): void => {
|
|
354
362
|
unsubscribeTerminal()
|
|
355
363
|
unsubscribeWarned()
|
|
364
|
+
// A run refused or thrown before its tasks (an unknown task, a plugin
|
|
365
|
+
// stage that threw) never reached the end that takes the live region
|
|
366
|
+
// down: a load slow enough to show its step left `vx:load running`
|
|
367
|
+
// on screen, and the CLI's refusal was written onto its last line.
|
|
368
|
+
terminal?.runDone?.()
|
|
356
369
|
terminal?.settle()
|
|
357
370
|
}
|
|
358
371
|
// A plugin's `output` renders the run from its start; core's renderer
|
|
@@ -910,7 +923,40 @@ async function runOnBus(
|
|
|
910
923
|
const fault = executorFaults(warnOf(log))
|
|
911
924
|
const footers = executors.filter((e) => e.footer !== undefined)
|
|
912
925
|
const placers = executors.filter((e) => e.running !== undefined)
|
|
926
|
+
// The scheduler's dependents count, and every row's `blocks` (the live
|
|
927
|
+
// list's and the results'): one closure over the graph, built on the
|
|
928
|
+
// first that asks.
|
|
929
|
+
let counted: ReadonlyMap<string, number> | undefined
|
|
930
|
+
const blocksNow = (): ReadonlyMap<string, number> =>
|
|
931
|
+
(counted ??= graphCounts(nodes, hashCache.folds, hashCache.graph))
|
|
932
|
+
// Every row's `usual` and the forecast's timings: history read once, on
|
|
933
|
+
// the first row that asks, so a run that executes nothing never reads
|
|
934
|
+
// it. It was read only a second into a live run, so rows before that,
|
|
935
|
+
// a short run's and every log's showed none (2026-10-29).
|
|
936
|
+
let p50s: ReadonlyMap<string, number> | undefined
|
|
937
|
+
const p50sNow = (): ReadonlyMap<string, number> => {
|
|
938
|
+
if (p50s !== undefined) return p50s
|
|
939
|
+
const ids: string[] = []
|
|
940
|
+
for (const n of nodes.values()) if (!isGroupTask(n)) ids.push(n.id)
|
|
941
|
+
try {
|
|
942
|
+
p50s = new LocalHistoryProvider(
|
|
943
|
+
indexDb(prepared.localCache),
|
|
944
|
+
undefined,
|
|
945
|
+
prepared.branch(),
|
|
946
|
+
taskDefinitions(nodes.values()),
|
|
947
|
+
).p50sNow(ids)
|
|
948
|
+
} catch {
|
|
949
|
+
// History is observability: an unreadable one knows no task.
|
|
950
|
+
p50s = new Map()
|
|
951
|
+
}
|
|
952
|
+
return p50s
|
|
953
|
+
}
|
|
954
|
+
// The live rows' `cpu` and `mem`; it looks only once the live list asks.
|
|
955
|
+
const liveUsage = createLiveUsage()
|
|
913
956
|
const runContext = {
|
|
957
|
+
blocks: blocksNow,
|
|
958
|
+
usual: (id: string) => p50sNow().get(id),
|
|
959
|
+
usage: liveUsage.usage,
|
|
914
960
|
...(footers.length > 0
|
|
915
961
|
? { executorRows: () => footers.flatMap((e) => executorFooter(e, fault)) }
|
|
916
962
|
: {}),
|
|
@@ -967,18 +1013,8 @@ async function runOnBus(
|
|
|
967
1013
|
// history's only if the region is still up a second in, so a warm run
|
|
968
1014
|
// never reads history.
|
|
969
1015
|
terminal?.forecast(
|
|
970
|
-
() => createForecast(nodes, new Map(), concurrency),
|
|
971
|
-
async () =>
|
|
972
|
-
const ids: string[] = []
|
|
973
|
-
for (const n of nodes.values()) if (!isGroupTask(n)) ids.push(n.id)
|
|
974
|
-
const p50s = await new LocalHistoryProvider(
|
|
975
|
-
prepared.localCache.dbHandle(),
|
|
976
|
-
undefined,
|
|
977
|
-
prepared.branch(),
|
|
978
|
-
taskDefinitions(nodes.values()),
|
|
979
|
-
).p50sFor(ids)
|
|
980
|
-
return createForecast(nodes, p50s, concurrency)
|
|
981
|
-
},
|
|
1016
|
+
() => createForecast(nodes, new Map(), concurrency, blocksNow()),
|
|
1017
|
+
async () => createForecast(nodes, p50sNow(), concurrency, blocksNow()),
|
|
982
1018
|
)
|
|
983
1019
|
|
|
984
1020
|
// Remote-only: kick off background prefetches so remote-GET latency
|
|
@@ -1112,7 +1148,7 @@ async function runOnBus(
|
|
|
1112
1148
|
// Why a task missed, for a sink: a read of the history per miss, so only
|
|
1113
1149
|
// when a sink exists.
|
|
1114
1150
|
const explainMiss =
|
|
1115
|
-
telemetry === undefined ? undefined : createMissExplainer(prepared.localCache
|
|
1151
|
+
telemetry === undefined ? undefined : createMissExplainer(indexDb(prepared.localCache))
|
|
1116
1152
|
const lateProbes = probesAfterWrites(nodes, workspaceRoot)
|
|
1117
1153
|
// The scheduler's fail-fast stop, as the retry loop of a task already
|
|
1118
1154
|
// in flight sees it.
|
|
@@ -1143,7 +1179,15 @@ async function runOnBus(
|
|
|
1143
1179
|
if (options.retries !== undefined) scope.retries = options.retries
|
|
1144
1180
|
if (taskTimeoutDefault !== undefined) scope.timeout = taskTimeoutDefault
|
|
1145
1181
|
if (cleanOutputs !== undefined) scope.cleanOutputs = cleanOutputs
|
|
1146
|
-
|
|
1182
|
+
const sinkTrack = telemetry?.track
|
|
1183
|
+
scope.track = (taskId, pid) => {
|
|
1184
|
+
const untrack = liveUsage.track(taskId, pid)
|
|
1185
|
+
const sunk = sinkTrack?.(taskId, pid)
|
|
1186
|
+
return () => {
|
|
1187
|
+
untrack()
|
|
1188
|
+
return sunk?.()
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1147
1191
|
if (explainMiss !== undefined) scope.explainMiss = explainMiss
|
|
1148
1192
|
if (keepRun !== undefined) scope.keepRun = keepRun
|
|
1149
1193
|
if (sandboxArmer !== null) scope.armSandbox = () => sandboxArmer.arm()
|
|
@@ -1215,7 +1259,7 @@ async function runOnBus(
|
|
|
1215
1259
|
// rows land, so the row that prints next carries the verdict: nothing
|
|
1216
1260
|
// prints below the footer. A hit or a skip asks nothing; a green miss
|
|
1217
1261
|
// is one probe of the failed-row index.
|
|
1218
|
-
const historyDb = prepared.localCache
|
|
1262
|
+
const historyDb = indexDb(prepared.localCache)
|
|
1219
1263
|
const cacheOff = cachesNothing(policy)
|
|
1220
1264
|
const judgeFlaky = (o: TaskOutcome): void => {
|
|
1221
1265
|
const candidates = flakyCandidates([o], taintedRan)
|
|
@@ -1249,10 +1293,12 @@ async function runOnBus(
|
|
|
1249
1293
|
}
|
|
1250
1294
|
}
|
|
1251
1295
|
let lockBroken: boolean | undefined
|
|
1296
|
+
prepared.localCache.storeHeldBlobSizes()
|
|
1252
1297
|
const outcomes = await runGraph({
|
|
1253
1298
|
nodes,
|
|
1254
1299
|
concurrency,
|
|
1255
1300
|
restoreConcurrency: options.restoreConcurrency ?? 2 * concurrency,
|
|
1301
|
+
reverseDeps: blocksNow,
|
|
1256
1302
|
settledOf: (o) => deferredSaves.get(o.node.id),
|
|
1257
1303
|
...(hasPooledExecutor(executors) ? { poolOf: poolOfPlacement(placements) } : {}),
|
|
1258
1304
|
...(admit !== undefined ? { admit } : {}),
|
|
@@ -1361,6 +1407,9 @@ async function runOnBus(
|
|
|
1361
1407
|
const hold =
|
|
1362
1408
|
options.holdPersistent === true &&
|
|
1363
1409
|
(!stop.signal.aborted || stop.signal.reason === CYCLE_INTERRUPT)
|
|
1410
|
+
// A stop already signalled every server once and waits out its grace:
|
|
1411
|
+
// a SIGTERM here, while a server ran its cleanup, cut it short.
|
|
1412
|
+
if (stop.signal.aborted) await stop.aborting?.catch(() => {})
|
|
1364
1413
|
const { keepAlive, crashed: crashedPersistent } = await disposeServers({
|
|
1365
1414
|
registry: persistentRegistry,
|
|
1366
1415
|
nodes,
|
|
@@ -1469,6 +1518,7 @@ async function runOnBus(
|
|
|
1469
1518
|
ci: ciContext,
|
|
1470
1519
|
host: hostContext,
|
|
1471
1520
|
withTelemetry: telemetry !== undefined,
|
|
1521
|
+
blocks: blocksNow(),
|
|
1472
1522
|
remoteTraffic: prepared.localCache.traffic.keys,
|
|
1473
1523
|
})
|
|
1474
1524
|
const records = recordsOf(list, ok)
|
|
@@ -1492,8 +1542,11 @@ async function runOnBus(
|
|
|
1492
1542
|
// A cache directory this user cannot write keeps none of the run's
|
|
1493
1543
|
// records: the run's one notice said so.
|
|
1494
1544
|
const keeps = prepared.localCache.readOnlyNotice() === null
|
|
1495
|
-
const recordFailures = (
|
|
1496
|
-
|
|
1545
|
+
const recordFailures = (
|
|
1546
|
+
final: readonly TaskOutcome[],
|
|
1547
|
+
into: Cache = prepared.localCache,
|
|
1548
|
+
): void => {
|
|
1549
|
+
if (keeps) writeRunFailures(into, runId, final, warnOf(log))
|
|
1497
1550
|
}
|
|
1498
1551
|
if (stop.stoppedBy === undefined && !historyAfterWait) {
|
|
1499
1552
|
stepSync('vx:history', () => {
|
|
@@ -1535,8 +1588,18 @@ async function runOnBus(
|
|
|
1535
1588
|
await Promise.all([cache.drainUploads?.(), keeper?.settled()])
|
|
1536
1589
|
})
|
|
1537
1590
|
if (keeps) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1591
|
+
// Beside the run's rows (cache/run-notes.ts); a refused write costs only them.
|
|
1592
|
+
try {
|
|
1593
|
+
keepRunNotes(indexDb(prepared.localCache), runId, {
|
|
1594
|
+
steps: steps.ended,
|
|
1595
|
+
warnings: warned.map((w) => Bun.stripANSI(w)),
|
|
1596
|
+
})
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1599
|
+
warnOf(log)(
|
|
1600
|
+
`[vx] background steps and warnings not kept: ${message} — the verdict above stands`,
|
|
1601
|
+
)
|
|
1602
|
+
}
|
|
1540
1603
|
}
|
|
1541
1604
|
// An upload that failed after the rows went in: its row says so too.
|
|
1542
1605
|
if (prepared.localCache.uploads.failed > 0 && stop.stoppedBy === undefined && !historyAfterWait)
|
|
@@ -1670,11 +1733,11 @@ async function runOnBus(
|
|
|
1670
1733
|
const local = new Cache(prepared.cacheDir, undefined, prepared.workspaceRoot)
|
|
1671
1734
|
try {
|
|
1672
1735
|
local.recordRunBundle(recordsOf(final, ok && !crashed))
|
|
1736
|
+
recordFailures(final, local)
|
|
1673
1737
|
} finally {
|
|
1674
1738
|
local.close()
|
|
1675
1739
|
}
|
|
1676
1740
|
})
|
|
1677
|
-
recordFailures(final)
|
|
1678
1741
|
}
|
|
1679
1742
|
return { ok: ok && !crashed, outcomes: final }
|
|
1680
1743
|
}
|
|
@@ -1789,7 +1852,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1789
1852
|
// inspection command, so the history read's cost is fine here even
|
|
1790
1853
|
// though it stays OFF the default run path.
|
|
1791
1854
|
history: new LocalHistoryProvider(
|
|
1792
|
-
prepared.localCache
|
|
1855
|
+
indexDb(prepared.localCache),
|
|
1793
1856
|
undefined,
|
|
1794
1857
|
prepared.branch(),
|
|
1795
1858
|
taskDefinitions(prepared.nodes.values()),
|
|
@@ -446,8 +446,8 @@ export function withheldLinkLine(taskId: string, w: WithheldLink): string {
|
|
|
446
446
|
return (
|
|
447
447
|
`vx: ${taskId} read \`${w.target}\` through \`${w.link}\`, and its key includes no task of ` +
|
|
448
448
|
`${w.name}, so an edit there would not re-run it. Add a \`dependsOn\` edge that ` +
|
|
449
|
-
`reaches one (\`^build
|
|
450
|
-
|
|
449
|
+
`reaches one (\`^build\`, where ${w.name}#build keys its sources), or grant and key ` +
|
|
450
|
+
`the files yourself (\`allow.read\` plus ` +
|
|
451
451
|
`\`inputs.workspaceFiles\`).`
|
|
452
452
|
)
|
|
453
453
|
}
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
// them to the runner around every spawn.
|
|
15
15
|
|
|
16
16
|
import { signalExitCode } from '../exec/index.js'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
claimExitForSignal,
|
|
19
|
+
dieOf,
|
|
20
|
+
hangupIgnored,
|
|
21
|
+
killGraceMs,
|
|
22
|
+
settleWithin,
|
|
23
|
+
} from '../util/index.js'
|
|
18
24
|
import {
|
|
19
25
|
holdGroups,
|
|
20
26
|
killTree,
|
|
@@ -154,10 +160,7 @@ export function forwardSignals(args: {
|
|
|
154
160
|
// and a 130 exit ran the script's next line. The code stays the
|
|
155
161
|
// fallback should the signal not land.
|
|
156
162
|
if (args.handsTerminal !== true) {
|
|
157
|
-
process.once('exit', () =>
|
|
158
|
-
process.removeAllListeners(signal)
|
|
159
|
-
process.kill(process.pid, signal)
|
|
160
|
-
})
|
|
163
|
+
process.once('exit', () => dieOf(signal))
|
|
161
164
|
}
|
|
162
165
|
process.exit(signalExitCode(signal))
|
|
163
166
|
}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
//
|
|
7
7
|
// A run shows only the steps it waits on, before its first task or its end
|
|
8
8
|
// (2026-10-16). One that runs beside the tasks (`vx:prefetch`) is a
|
|
9
|
-
// `background` step: never shown, and kept for `vx last` (run-
|
|
9
|
+
// `background` step: never shown, and kept for `vx last` (cache/run-notes.ts).
|
|
10
10
|
import type { Logger } from '../report/index.js'
|
|
11
|
-
import type { RunStep } from '
|
|
11
|
+
import type { RunStep } from '../cache/index.js'
|
|
12
12
|
|
|
13
13
|
export interface Steps {
|
|
14
14
|
/** Time `work` as step `id` when steps show; else run it as it is. */
|
|
@@ -519,9 +519,23 @@ function hashableConfig(cfg: TaskConfig): unknown {
|
|
|
519
519
|
// Keys sorted: moving `cache` above `exec`, or a `define` in another
|
|
520
520
|
// order, missed every key of the task, and a `project` or `graph` plugin
|
|
521
521
|
// writes a config the lock never sorted (2026-10-25).
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
522
|
+
const env = cfg.inputs?.env
|
|
523
|
+
const pass = cfg.exec?.env?.passThrough
|
|
524
|
+
if (cfg.exec?.remote === undefined && sortedNames(env) && sortedNames(pass))
|
|
525
|
+
return keysSorted(cfg)
|
|
526
|
+
const { remote: _remote, ...execRest } = cfg.exec ?? ({} as NonNullable<TaskConfig['exec']>)
|
|
527
|
+
// Env names are sets: each value is read by its name, so their order
|
|
528
|
+
// keys nothing (conformance env/declared-order-irrelevant).
|
|
529
|
+
if (!sortedNames(pass)) execRest.env = { ...execRest.env, passThrough: [...pass!].sort() }
|
|
530
|
+
const out: TaskConfig = cfg.exec === undefined ? { ...cfg } : { ...cfg, exec: execRest }
|
|
531
|
+
if (!sortedNames(env)) out.inputs = { ...cfg.inputs!, env: [...env!].sort() }
|
|
532
|
+
return keysSorted(out)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function sortedNames(names: readonly string[] | undefined): boolean {
|
|
536
|
+
if (names === undefined) return true
|
|
537
|
+
for (let i = 1; i < names.length; i++) if (names[i - 1]! > names[i]!) return false
|
|
538
|
+
return true
|
|
525
539
|
}
|
|
526
540
|
|
|
527
541
|
/**
|
package/src/report/forecast.ts
CHANGED
|
@@ -12,8 +12,6 @@ export interface LiveForecast {
|
|
|
12
12
|
critical?: string
|
|
13
13
|
/** Tasks not started because a dependency has not finished: the tasks line's `blocked`. */
|
|
14
14
|
waiting: number
|
|
15
|
-
/** Each running task's p50, where history knows it: the row's expected duration. */
|
|
16
|
-
expected?: ReadonlyMap<string, number>
|
|
17
15
|
/**
|
|
18
16
|
* Tasks not started because a dependency has not finished, in the order
|
|
19
17
|
* the scheduler would start them, at most one per worker: what an idle
|
|
@@ -21,9 +19,9 @@ export interface LiveForecast {
|
|
|
21
19
|
*/
|
|
22
20
|
blocked?: readonly BlockedTask[]
|
|
23
21
|
/**
|
|
24
|
-
* Each running task's count of
|
|
25
|
-
*
|
|
26
|
-
* cell, why they are not running. A
|
|
22
|
+
* Each running task's count of what is downstream of it, the
|
|
23
|
+
* scheduler's own count (`computeReverseDepCount`): its row's `blocks`
|
|
24
|
+
* cell, why they are not running. A node downstream of two running tasks
|
|
27
25
|
* counts under both.
|
|
28
26
|
*/
|
|
29
27
|
blockedBy: ReadonlyMap<string, number>
|
|
@@ -53,6 +51,8 @@ export function createForecast(
|
|
|
53
51
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
54
52
|
p50s: ReadonlyMap<string, number>,
|
|
55
53
|
concurrency: number,
|
|
54
|
+
/** Every node's dependents count, the scheduler's baseline and the rows' `blocks`; computed here when absent. */
|
|
55
|
+
rank: ReadonlyMap<string, number> = computeReverseDepCount(nodes),
|
|
56
56
|
): Forecast {
|
|
57
57
|
// Dependencies before dependents, once: each call is one pass over it.
|
|
58
58
|
const order: TaskNode[] = []
|
|
@@ -84,38 +84,16 @@ export function createForecast(
|
|
|
84
84
|
if (p !== undefined) cost.set(n.id, p)
|
|
85
85
|
}
|
|
86
86
|
const workers = Math.max(1, concurrency)
|
|
87
|
-
// The scheduler's own baseline ranking, most dependents first,
|
|
88
|
-
// graph order
|
|
89
|
-
const rank = computeReverseDepCount(nodes)
|
|
87
|
+
// The scheduler's own baseline ranking (`rank`), most dependents first,
|
|
88
|
+
// ties in graph order.
|
|
90
89
|
const position = new Map(order.map((n, i) => [n.id, i]))
|
|
91
|
-
// The tasks each node waits on directly, a group's own looked through.
|
|
92
|
-
const direct = new Map<string, readonly string[]>()
|
|
93
|
-
const through = (id: string, into: Set<string>, seen: Set<string>): void => {
|
|
94
|
-
if (seen.has(id)) return
|
|
95
|
-
seen.add(id)
|
|
96
|
-
const d = nodes.get(id)
|
|
97
|
-
if (d === undefined || !isGroupTask(d)) into.add(id)
|
|
98
|
-
else for (const x of d.deps) through(x, into, seen)
|
|
99
|
-
}
|
|
100
|
-
for (const n of order) {
|
|
101
|
-
if (isGroupTask(n) || n.deps.length === 0) continue
|
|
102
|
-
const into = new Set<string>()
|
|
103
|
-
const seen = new Set<string>()
|
|
104
|
-
for (const d of n.deps) through(d, into, seen)
|
|
105
|
-
direct.set(n.id, [...into])
|
|
106
|
-
}
|
|
107
90
|
const before = (a: string, b: string): boolean => {
|
|
108
91
|
const d = (rank.get(a) ?? 0) - (rank.get(b) ?? 0)
|
|
109
92
|
return d !== 0 ? d > 0 : position.get(a)! < position.get(b)!
|
|
110
93
|
}
|
|
111
94
|
|
|
112
95
|
return (finished, running, nowMs) => {
|
|
113
|
-
const expected = new Map<string, number>()
|
|
114
96
|
let waiting = 0
|
|
115
|
-
for (const id of running.keys()) {
|
|
116
|
-
const p = cost.get(id)
|
|
117
|
-
if (p !== undefined) expected.set(id, p)
|
|
118
|
-
}
|
|
119
97
|
const end = new Map<string, number>()
|
|
120
98
|
// The running task each node's longest chain waits on.
|
|
121
99
|
const head = new Map<string, string>()
|
|
@@ -144,8 +122,6 @@ export function createForecast(
|
|
|
144
122
|
since === undefined ? (head.get(n.id) ?? n.deps.find((d) => end.has(d))) : undefined
|
|
145
123
|
if (by !== undefined && !isGroupTask(n)) {
|
|
146
124
|
waiting++
|
|
147
|
-
for (const on of direct.get(n.id) ?? [])
|
|
148
|
-
if (running.has(on)) blockedBy.set(on, (blockedBy.get(on) ?? 0) + 1)
|
|
149
125
|
let at = held.length
|
|
150
126
|
while (at > 0 && before(n.id, held[at - 1]!.id)) at--
|
|
151
127
|
if (at < workers) {
|
|
@@ -167,13 +143,16 @@ export function createForecast(
|
|
|
167
143
|
}
|
|
168
144
|
work += own
|
|
169
145
|
}
|
|
170
|
-
|
|
146
|
+
for (const id of running.keys()) {
|
|
147
|
+
const d = rank.get(id)
|
|
148
|
+
if (d !== undefined && d > 0) blockedBy.set(id, d)
|
|
149
|
+
}
|
|
150
|
+
if (!known) return { waiting, blocked: held, blockedBy }
|
|
171
151
|
const critical = chainEnd === undefined ? undefined : head.get(chainEnd)
|
|
172
152
|
return {
|
|
173
153
|
etaMs: Math.max(chain, work / workers),
|
|
174
154
|
...(critical !== undefined ? { critical } : {}),
|
|
175
155
|
waiting,
|
|
176
|
-
expected,
|
|
177
156
|
blocked: held,
|
|
178
157
|
blockedBy,
|
|
179
158
|
}
|