@vzn/vx 0.0.650 → 0.0.652
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 +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- 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 +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- 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-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- 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 +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- 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/graph/priorities.ts
CHANGED
|
@@ -156,29 +156,47 @@ export function computeReverseDepCount(
|
|
|
156
156
|
/** Count only these dependents (the exec tier); every one when absent. */
|
|
157
157
|
counted?: ReadonlyMap<string, unknown>,
|
|
158
158
|
): Map<string, number> {
|
|
159
|
+
return reverseDepCountsOver(nodes, [counted])[0]!
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `computeReverseDepCount` once per entry of `counted`, from ONE closure:
|
|
164
|
+
* the closure is the cost (~100 ms at 11k nodes, 2026-10-29), each count
|
|
165
|
+
* one popcount pass over it.
|
|
166
|
+
*/
|
|
167
|
+
function reverseDepCountsOver(
|
|
168
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
169
|
+
counted: ReadonlyArray<ReadonlyMap<string, unknown> | undefined>,
|
|
170
|
+
): Map<string, number>[] {
|
|
159
171
|
// Exact transitive-dependent COUNTS need closure SETS (diamonds
|
|
160
172
|
// double-count under naive summing). Set-of-strings closures are
|
|
161
173
|
// O(N²) entries and took 8.5s on a 1090-package, 100-layer repo;
|
|
162
|
-
// bitsets make the same closure O(E·N/32) time
|
|
163
|
-
//
|
|
174
|
+
// bitsets make the same closure O(E·N/32) time over N²/8 bits
|
|
175
|
+
// (3270 tasks ≈ 1.3 MB) — single-digit ms at that scale.
|
|
164
176
|
const ids = [...nodes.keys()]
|
|
165
|
-
const index = new Map<string, number>()
|
|
166
|
-
for (let i = 0; i < ids.length; i++) index.set(ids[i]!, i)
|
|
167
177
|
const n = ids.length
|
|
168
|
-
const
|
|
178
|
+
const index = new Map<string, number>()
|
|
179
|
+
for (let i = 0; i < n; i++) index.set(ids[i]!, i)
|
|
169
180
|
|
|
170
|
-
// Direct dependents as
|
|
171
|
-
const
|
|
181
|
+
// Direct dependents as one flat list (offsets per node) + in-degree.
|
|
182
|
+
const from: number[] = []
|
|
183
|
+
const to: number[] = []
|
|
184
|
+
const offset = new Int32Array(n + 1)
|
|
172
185
|
const indegree = new Uint32Array(n)
|
|
173
|
-
for (
|
|
174
|
-
const
|
|
175
|
-
for (const dep of node.deps) {
|
|
186
|
+
for (let ni = 0; ni < n; ni++) {
|
|
187
|
+
for (const dep of nodes.get(ids[ni]!)!.deps) {
|
|
176
188
|
const di = index.get(dep)
|
|
177
189
|
if (di === undefined) continue
|
|
178
|
-
|
|
190
|
+
from.push(di)
|
|
191
|
+
to.push(ni)
|
|
192
|
+
offset[di + 1]!++
|
|
179
193
|
indegree[ni]!++
|
|
180
194
|
}
|
|
181
195
|
}
|
|
196
|
+
for (let i = 0; i < n; i++) offset[i + 1]! += offset[i]!
|
|
197
|
+
const next = offset.slice(0, n)
|
|
198
|
+
const dependents = new Int32Array(from.length)
|
|
199
|
+
for (let e = 0; e < from.length; e++) dependents[next[from[e]!]!++] = to[e]!
|
|
182
200
|
|
|
183
201
|
// Kahn topo order over dependency edges (deps before dependents).
|
|
184
202
|
// Insertion order is topo today, but the closure's correctness
|
|
@@ -189,48 +207,84 @@ export function computeReverseDepCount(
|
|
|
189
207
|
for (let i = 0; i < n; i++) if (indegree[i] === 0) topo[tail++] = i
|
|
190
208
|
while (head < tail) {
|
|
191
209
|
const v = topo[head++]!
|
|
192
|
-
for (
|
|
210
|
+
for (let k = offset[v]!; k < offset[v + 1]!; k++) {
|
|
211
|
+
const r = dependents[k]!
|
|
193
212
|
if (--indegree[r]! === 0) topo[tail++] = r
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// POSITIONS of the node at position t's transitive dependents. A
|
|
200
|
-
// dependent sits after its dependency, so a closure holds only bits
|
|
201
|
-
// past its own position, and each OR starts at the dependent's word:
|
|
202
|
-
// half the words of indexing by insertion order on a layered graph.
|
|
216
|
+
// The same lists by topo POSITION. A dependent sits after its
|
|
217
|
+
// dependency, so a closure holds only bits past its own position.
|
|
203
218
|
const pos = new Int32Array(n)
|
|
204
219
|
for (let t = 0; t < tail; t++) pos[topo[t]!] = t
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
else
|
|
208
|
-
for (let t = 0; t < tail; t++) if (counted.has(ids[topo[t]!]!)) mask[t >>> 5]! |= 1 << (t & 31)
|
|
209
|
-
const closure = new Uint32Array(n * words)
|
|
210
|
-
const counts = new Map<string, number>()
|
|
211
|
-
for (let t = tail - 1; t >= 0; t--) {
|
|
220
|
+
const start = new Int32Array(tail + 1)
|
|
221
|
+
for (let t = 0; t < tail; t++) {
|
|
212
222
|
const i = topo[t]!
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
start[t + 1] = start[t]! + offset[i + 1]! - offset[i]!
|
|
224
|
+
}
|
|
225
|
+
const rev = new Int32Array(start[tail]!)
|
|
226
|
+
for (let t = 0; t < tail; t++) {
|
|
227
|
+
const i = topo[t]!
|
|
228
|
+
let k = start[t]!
|
|
229
|
+
for (let j = offset[i]!; j < offset[i + 1]!; j++) rev[k++] = pos[dependents[j]!]!
|
|
230
|
+
}
|
|
231
|
+
const words = (tail + 31) >>> 5
|
|
232
|
+
const masks = counted.map((only) => {
|
|
233
|
+
const mask = new Uint32Array(words)
|
|
234
|
+
if (only === undefined) mask.fill(0xffffffff)
|
|
235
|
+
else
|
|
236
|
+
for (let t = 0; t < tail; t++) if (only.has(ids[topo[t]!]!)) mask[t >>> 5]! |= 1 << (t & 31)
|
|
237
|
+
return mask
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Reverse-topo sweeps, one per block of `span` words: every node's
|
|
241
|
+
// closure restricted to the block's positions, its count summed over
|
|
242
|
+
// the blocks. All N rows of all words (9,603 tasks: 11.5 MB) missed
|
|
243
|
+
// the cache on nearly every OR, and the first call, the only one a run
|
|
244
|
+
// makes, took 77 ms; a block of ~256 KB took 37 (2026-10-29). A
|
|
245
|
+
// dependent past the block holds no bit in it, so its edge is skipped.
|
|
246
|
+
const span = Math.max(1, Math.min(words, Math.floor(65536 / Math.max(1, tail))))
|
|
247
|
+
const totals = masks.map(() => new Int32Array(tail))
|
|
248
|
+
const closure = new Uint32Array(tail * span)
|
|
249
|
+
for (let w0 = 0; w0 < words; w0 += span) {
|
|
250
|
+
const width = Math.min(words, w0 + span) - w0
|
|
251
|
+
const end = Math.min(tail, (w0 + width) << 5)
|
|
252
|
+
if (w0 > 0) closure.fill(0, 0, end * span)
|
|
253
|
+
for (let t = end - 1; t >= 0; t--) {
|
|
254
|
+
const base = t * span
|
|
255
|
+
for (let k = start[t]!; k < start[t + 1]!; k++) {
|
|
256
|
+
const r = rev[k]!
|
|
257
|
+
if (r >= end) continue
|
|
258
|
+
const rw = r >>> 5
|
|
259
|
+
if (rw >= w0) closure[base + rw - w0]! |= 1 << (r & 31)
|
|
260
|
+
const rbase = r * span
|
|
261
|
+
for (let w = rw > w0 ? rw - w0 : 0; w < width; w++)
|
|
262
|
+
closure[base + w]! |= closure[rbase + w]!
|
|
263
|
+
}
|
|
264
|
+
for (let m = 0; m < masks.length; m++) {
|
|
265
|
+
const mask = masks[m]!
|
|
266
|
+
let count = 0
|
|
267
|
+
for (let w = t >>> 5 > w0 ? (t >>> 5) - w0 : 0; w < width; w++) {
|
|
268
|
+
let v = closure[base + w]! & mask[w0 + w]!
|
|
269
|
+
if (v === 0) continue
|
|
270
|
+
v = v - ((v >>> 1) & 0x55555555)
|
|
271
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
|
|
272
|
+
count += (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24
|
|
273
|
+
}
|
|
274
|
+
totals[m]![t]! += count
|
|
275
|
+
}
|
|
226
276
|
}
|
|
227
|
-
counts.set(ids[i]!, count)
|
|
228
277
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
278
|
+
const all = totals.map((total) => {
|
|
279
|
+
const counts = new Map<string, number>()
|
|
280
|
+
for (let t = 0; t < tail; t++) counts.set(ids[topo[t]!]!, total[t]!)
|
|
281
|
+
// Cycle-stranded nodes (never topo-visited) can't occur — the graph
|
|
282
|
+
// builder rejects cycles — but a missing Map entry would silently
|
|
283
|
+
// sort as undefined, so default them defensively to 0.
|
|
284
|
+
for (const id of ids) if (!counts.has(id)) counts.set(id, 0)
|
|
285
|
+
return counts
|
|
286
|
+
})
|
|
287
|
+
return all
|
|
234
288
|
}
|
|
235
289
|
|
|
236
290
|
/**
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
isUserError,
|
|
6
6
|
OUT_OF_FDS_HINT,
|
|
7
7
|
thrownMessage,
|
|
8
|
-
ui,
|
|
9
8
|
} from '../util/index.js'
|
|
9
|
+
import * as ui from '../ui/index.js'
|
|
10
10
|
import {
|
|
11
11
|
computeReverseDepCount,
|
|
12
12
|
mergePriorities,
|
|
@@ -89,7 +89,8 @@ export interface TaskOutcome {
|
|
|
89
89
|
*/
|
|
90
90
|
artifactBytes?: number
|
|
91
91
|
/**
|
|
92
|
-
* The raw bytes of
|
|
92
|
+
* The raw bytes of the task's outputs: on a hit, the entry's files as
|
|
93
|
+
* restored; on a miss, the files its save stored (the row waits for it).
|
|
93
94
|
*/
|
|
94
95
|
outputBytes?: number
|
|
95
96
|
/** On a remote hit this run pulled: the download and its ingest. */
|
|
@@ -185,7 +186,7 @@ export interface TaskOutcome {
|
|
|
185
186
|
serverExit?: number
|
|
186
187
|
/**
|
|
187
188
|
* The input that changed while the command ran, failing the task under
|
|
188
|
-
* `
|
|
189
|
+
* `inputs.stability.severity: 'error'` (workspace-relative; '' when vx
|
|
189
190
|
* could not name it). The exit is vx's 1; the label reads this instead.
|
|
190
191
|
*/
|
|
191
192
|
inputMoved?: string
|
|
@@ -230,6 +231,8 @@ export interface TaskOutcome {
|
|
|
230
231
|
attempts?: number
|
|
231
232
|
/** The attempts that failed and were run again, in order: when each ended (epoch ms) and how. */
|
|
232
233
|
failedAttempts?: readonly { endedAt: number; exitCode: number; timedOut?: true }[]
|
|
234
|
+
/** Each of `failedAttempts`' output, as `failedOutput` encodes it: what `vx last --log` shows. */
|
|
235
|
+
attemptOutputs?: readonly string[]
|
|
233
236
|
/**
|
|
234
237
|
* Set when this run proved the task flaky: its key failed after it had
|
|
235
238
|
* passed (this run's included) or it passed only on a retry. Its row carries it.
|
|
@@ -308,8 +311,7 @@ export interface ScheduleOptions {
|
|
|
308
311
|
*/
|
|
309
312
|
onFailFast?: () => void
|
|
310
313
|
execute: (node: TaskNode, upstream: TaskOutcome[]) => Promise<TaskOutcome>
|
|
311
|
-
|
|
312
|
-
onStart?: (node: TaskNode, queuedMs?: number) => void
|
|
314
|
+
onStart?: (node: TaskNode) => void
|
|
313
315
|
onFinish?: (outcome: TaskOutcome) => void
|
|
314
316
|
/**
|
|
315
317
|
* Where the line naming a rejected `execute` goes, before its failed
|
|
@@ -355,6 +357,11 @@ export interface ScheduleOptions {
|
|
|
355
357
|
* Undefined or `most-work` is the reverse-deps count above.
|
|
356
358
|
*/
|
|
357
359
|
strategy?: ScheduleStrategy
|
|
360
|
+
/**
|
|
361
|
+
* The `most-work` baseline already counted (`computeReverseDepCount`, read
|
|
362
|
+
* back with the graph): asked only when that baseline is the one used.
|
|
363
|
+
*/
|
|
364
|
+
reverseDeps?: () => ReadonlyMap<string, number>
|
|
358
365
|
/**
|
|
359
366
|
* Restore-tier task ids: confirmed stable-key LOCAL cache hits (the
|
|
360
367
|
* local short-circuit). A restore-tier task:
|
|
@@ -598,7 +605,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
598
605
|
? strategyPriorities(nodes, options.strategy)
|
|
599
606
|
: options.restoreTier !== undefined && options.restoreTier.size > 0
|
|
600
607
|
? tieredReverseDepCount(nodes, options.restoreTier)
|
|
601
|
-
: computeReverseDepCount(nodes)
|
|
608
|
+
: (options.reverseDeps?.() ?? computeReverseDepCount(nodes))
|
|
602
609
|
const priority: ReadonlyMap<string, number> = options.priorities
|
|
603
610
|
? mergePriorities(baseline, options.priorities)
|
|
604
611
|
: baseline
|
|
@@ -964,10 +971,9 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
964
971
|
return undefined
|
|
965
972
|
}
|
|
966
973
|
}
|
|
967
|
-
const started = (node: TaskNode
|
|
974
|
+
const started = (node: TaskNode): void => {
|
|
968
975
|
try {
|
|
969
|
-
|
|
970
|
-
else onStart?.(node)
|
|
976
|
+
onStart?.(node)
|
|
971
977
|
} catch (err) {
|
|
972
978
|
const m = thrownMessage(err)
|
|
973
979
|
ui.say('warn', `[vx] onStart observer threw for ${node.id}: ${m}\n`)
|
|
@@ -1106,7 +1112,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1106
1112
|
const settle = settleInPlace(node, upstream)
|
|
1107
1113
|
let now: TaskOutcome | undefined
|
|
1108
1114
|
if (settle !== undefined) {
|
|
1109
|
-
if (!demoted.has(id)) started(node
|
|
1115
|
+
if (!demoted.has(id)) started(node)
|
|
1110
1116
|
try {
|
|
1111
1117
|
now = settle()
|
|
1112
1118
|
} catch {
|
|
@@ -1136,7 +1142,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1136
1142
|
// the dispatch loop (it would strand the tick with the slot held).
|
|
1137
1143
|
// A demoted task's second dispatch is the same task, already started,
|
|
1138
1144
|
// as is one whose settle threw.
|
|
1139
|
-
if (!demoted.has(id) && settle === undefined) started(node
|
|
1145
|
+
if (!demoted.has(id) && settle === undefined) started(node)
|
|
1140
1146
|
|
|
1141
1147
|
// `.then(onFulfilled, onRejected)` — NOT `.then(f).catch(g)`. The
|
|
1142
1148
|
// rejection arm handles ONLY `execute()` rejecting; a throw from
|
package/src/graph/task-graph.ts
CHANGED
|
@@ -294,7 +294,7 @@ export interface BuildGraphOptions {
|
|
|
294
294
|
*/
|
|
295
295
|
undeclaredDeps?: (taskId: string, name: string) => void
|
|
296
296
|
/**
|
|
297
|
-
* The workspace root, where `
|
|
297
|
+
* The workspace root, where `outputs.workspaceFiles` is anchored.
|
|
298
298
|
* Given, a root-anchored output is also compared with every project's
|
|
299
299
|
* `files` outputs (item 1088); absent (a graph built for a unit row), only
|
|
300
300
|
* with other root-anchored ones.
|
|
@@ -335,6 +335,31 @@ export function undeclaredDepsError(
|
|
|
335
335
|
)
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* The `pkg#task` to offer for a cross-project miss: the task nearest
|
|
340
|
+
* `task` in the project named, or in the one nearest `project` — or the
|
|
341
|
+
* one whose path ends in it, since a project is named by its path unless
|
|
342
|
+
* named, and `a#build` is how a `packages/a` is first typed.
|
|
343
|
+
*/
|
|
344
|
+
function nearestTarget(
|
|
345
|
+
projects: ReadonlyMap<string, ProjectEntry>,
|
|
346
|
+
project: string,
|
|
347
|
+
task: string,
|
|
348
|
+
): string | undefined {
|
|
349
|
+
const holder =
|
|
350
|
+
projects.get(project) ??
|
|
351
|
+
projects.get(nearest(project, projects.keys()) ?? '') ??
|
|
352
|
+
onlyOne([...projects.values()].filter((p) => p.name.endsWith(`/${project}`)))
|
|
353
|
+
if (holder === undefined) return undefined
|
|
354
|
+
const tasks = Object.keys(holder.config.tasks ?? {})
|
|
355
|
+
const name = tasks.includes(task) ? task : nearest(task, tasks)
|
|
356
|
+
return name === undefined ? undefined : taskId(holder.name, name)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function onlyOne<T>(list: readonly T[]): T | undefined {
|
|
360
|
+
return list.length === 1 ? list[0] : undefined
|
|
361
|
+
}
|
|
362
|
+
|
|
338
363
|
/** One task mid-expansion in `buildTaskGraph`'s walk. */
|
|
339
364
|
interface Frame {
|
|
340
365
|
node: TaskNode
|
|
@@ -656,8 +681,9 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
656
681
|
`${id}: dependsOn //#${spec.task}, the workspace root's task, but the root declares no such task: add a vx.config at the root declaring it and run \`vx lock\``,
|
|
657
682
|
'VX_E_DEPENDS_ON',
|
|
658
683
|
)
|
|
684
|
+
const meant = nearestTarget(projects, spec.project, spec.task)
|
|
659
685
|
throw new UserError(
|
|
660
|
-
`${id}: dependsOn ${taskId(spec.project, spec.task)} but no such project or task is declared`,
|
|
686
|
+
`${id}: dependsOn ${taskId(spec.project, spec.task)} but no such project or task is declared${meant === undefined ? '' : `. Did you mean ${meant}?`}`,
|
|
661
687
|
'VX_E_DEPENDS_ON',
|
|
662
688
|
)
|
|
663
689
|
}
|
|
@@ -1248,7 +1274,7 @@ function detectOutputCollisions(
|
|
|
1248
1274
|
const byProject = new Map<string, TaskNode[]>()
|
|
1249
1275
|
const wsDeclarers: TaskNode[] = []
|
|
1250
1276
|
for (const n of nodes.values()) {
|
|
1251
|
-
const outs = n.config.
|
|
1277
|
+
const outs = n.config.outputs
|
|
1252
1278
|
if ((outs?.files?.length ?? 0) > 0) {
|
|
1253
1279
|
const bucket = byProject.get(n.projectName)
|
|
1254
1280
|
if (bucket) bucket.push(n)
|
|
@@ -1264,9 +1290,9 @@ function detectOutputCollisions(
|
|
|
1264
1290
|
const positiveOf = (globs: readonly string[] | undefined): readonly string[] | undefined =>
|
|
1265
1291
|
globs === undefined ? undefined : splitNegations(globs).positive
|
|
1266
1292
|
const filesOf = (n: TaskNode): readonly string[] | undefined =>
|
|
1267
|
-
positiveOf(n.config.
|
|
1293
|
+
positiveOf(n.config.outputs?.files)
|
|
1268
1294
|
const wsFilesOf = (n: TaskNode): readonly string[] | undefined =>
|
|
1269
|
-
positiveOf(n.config.
|
|
1295
|
+
positiveOf(n.config.outputs?.workspaceFiles)
|
|
1270
1296
|
for (const bucket of byProject.values()) {
|
|
1271
1297
|
// Most projects hold one task with outputs, and a call per project
|
|
1272
1298
|
// cost the cold build a measured 0.4 ms at 1,000 projects.
|
|
@@ -1320,7 +1346,7 @@ function detectOutputCollisions(
|
|
|
1320
1346
|
* captures, so the pair cannot collide there.
|
|
1321
1347
|
*/
|
|
1322
1348
|
function takenBackOf(n: TaskNode, field: 'files' | 'workspaceFiles'): string[] {
|
|
1323
|
-
return splitNegations(n.config.
|
|
1349
|
+
return splitNegations(n.config.outputs?.[field] ?? []).negative
|
|
1324
1350
|
}
|
|
1325
1351
|
|
|
1326
1352
|
/**
|
|
@@ -1348,9 +1374,9 @@ function collideAcross(
|
|
|
1348
1374
|
if (!outputsOverlap(ga, gb) || outputTakenBack(gb, wsBack) || outputTakenBack(ga, filesBack))
|
|
1349
1375
|
continue
|
|
1350
1376
|
const head =
|
|
1351
|
-
`${ws.id} declares the output ${JSON.stringify(ga)} in
|
|
1377
|
+
`${ws.id} declares the output ${JSON.stringify(ga)} in outputs.workspaceFiles, ` +
|
|
1352
1378
|
`inside ${files.id}'s ${JSON.stringify(gb.slice(rel === '' ? 0 : rel.length + 1))} in ` +
|
|
1353
|
-
`
|
|
1379
|
+
`outputs.files — vx cleans a task's declared outputs before it runs and before a ` +
|
|
1354
1380
|
`cache-hit restore, so whichever of these runs second DELETES the other's output`
|
|
1355
1381
|
if (exclusive && (reaches(files.id, ws.id) || reaches(ws.id, files.id))) {
|
|
1356
1382
|
throw new UserError(`${head}. ${EXCLUSIVE_OUTPUTS_FIX}`, 'VX_E_OUTPUT_OVERLAP')
|
|
@@ -1374,7 +1400,7 @@ function collideAcross(
|
|
|
1374
1400
|
const outside = wsGlobs[own.indexOf(null)]!
|
|
1375
1401
|
throw new UserError(
|
|
1376
1402
|
`${head}. ${ws.id} depends on ${files.id}, but adding to its output needs every ` +
|
|
1377
|
-
`
|
|
1403
|
+
`outputs.workspaceFiles entry inside ${rel === '' ? 'the workspace root' : rel}, ` +
|
|
1378
1404
|
`and ${JSON.stringify(outside)} is not. Give each task its own output path.`,
|
|
1379
1405
|
'VX_E_OUTPUT_OVERLAP',
|
|
1380
1406
|
)
|
|
@@ -1614,7 +1640,7 @@ function collide(
|
|
|
1614
1640
|
const head =
|
|
1615
1641
|
`${a.id} and ${b.id} both declare the output ${JSON.stringify(ga)}` +
|
|
1616
1642
|
(ga === gb ? '' : ` / ${JSON.stringify(gb)}`) +
|
|
1617
|
-
` in
|
|
1643
|
+
` in outputs.${field} — vx cleans a task's declared outputs before it runs and ` +
|
|
1618
1644
|
`before a cache-hit restore, so whichever of these runs second DELETES the other's ` +
|
|
1619
1645
|
`output`
|
|
1620
1646
|
// An overlap WITH an edge is the addition shape (item 588): the
|
|
@@ -1626,7 +1652,7 @@ function collide(
|
|
|
1626
1652
|
const [up, down] = reaches(b.id, a.id) ? [a, b] : reaches(a.id, b.id) ? [b, a] : []
|
|
1627
1653
|
const parts = {
|
|
1628
1654
|
headline: `${a.id} and ${b.id} both write ${ga === gb ? ga : `${ga} / ${gb}`}`,
|
|
1629
|
-
...(field === 'files' ? {} : { what: ['in
|
|
1655
|
+
...(field === 'files' ? {} : { what: ['in outputs.workspaceFiles'] }),
|
|
1630
1656
|
why: "vx clears a task's outputs before it runs, so one deletes the other's",
|
|
1631
1657
|
}
|
|
1632
1658
|
if (up !== undefined && exclusive)
|
|
@@ -1639,9 +1665,8 @@ function collide(
|
|
|
1639
1665
|
)
|
|
1640
1666
|
if (up !== undefined && down !== undefined) {
|
|
1641
1667
|
const downGlobs = splitNegations(
|
|
1642
|
-
(field === 'files'
|
|
1643
|
-
|
|
1644
|
-
: down.config.cache?.outputs.workspaceFiles) ?? [],
|
|
1668
|
+
(field === 'files' ? down.config.outputs?.files : down.config.outputs?.workspaceFiles) ??
|
|
1669
|
+
[],
|
|
1645
1670
|
).positive
|
|
1646
1671
|
;(down.addsToOutputsOf ??= []).push(up.id)
|
|
1647
1672
|
;(up.outputsAddedToBy ??= []).push(...(downGlobs ?? []))
|
|
@@ -1680,13 +1705,13 @@ export function outputTakenBack(output: string, negatives: readonly string[]): b
|
|
|
1680
1705
|
* and its key read nothing (hunt 8). Always refused, whatever the rules.
|
|
1681
1706
|
*/
|
|
1682
1707
|
function refuseSelfClean(node: TaskNode): void {
|
|
1683
|
-
const
|
|
1708
|
+
const declaredOutputs = node.config.outputs
|
|
1684
1709
|
const declared = node.config.inputs
|
|
1685
|
-
if (
|
|
1710
|
+
if (declaredOutputs === undefined || declared === undefined) return
|
|
1686
1711
|
for (const field of ['files', 'workspaceFiles'] as const) {
|
|
1687
|
-
if (!coversAny(declared[field],
|
|
1712
|
+
if (!coversAny(declared[field], declaredOutputs[field])) continue
|
|
1688
1713
|
const inputs = splitNegations(declared[field] ?? [])
|
|
1689
|
-
const out = splitNegations(
|
|
1714
|
+
const out = splitNegations(declaredOutputs[field] ?? [])
|
|
1690
1715
|
const outputs = out.positive
|
|
1691
1716
|
if (outputs.length === 0) continue
|
|
1692
1717
|
const roots = outputs.map((g) => rootless(staticPrefix(g)))
|
|
@@ -1797,11 +1822,11 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
|
|
|
1797
1822
|
return list
|
|
1798
1823
|
}
|
|
1799
1824
|
for (const n of nodes.values()) {
|
|
1800
|
-
const
|
|
1825
|
+
const declaredOutputs = n.config.outputs
|
|
1801
1826
|
const inputs = n.config.inputs
|
|
1802
1827
|
if (inputs === undefined) continue
|
|
1803
|
-
const outFiles = positives(
|
|
1804
|
-
const outWs = positives(
|
|
1828
|
+
const outFiles = positives(declaredOutputs?.files ?? [])
|
|
1829
|
+
const outWs = positives(declaredOutputs?.workspaceFiles ?? [])
|
|
1805
1830
|
if (outFiles.length > 0) {
|
|
1806
1831
|
const anchor = workspaceRoot === undefined ? undefined : relPosix(workspaceRoot, n.projectDir)
|
|
1807
1832
|
bucket(n.projectName).push({ node: n, globs: outFiles, reads: false, anchor })
|
|
@@ -1869,7 +1894,7 @@ function readsRootedOutputs(
|
|
|
1869
1894
|
if (workspaceRoot !== undefined) {
|
|
1870
1895
|
for (const node of filesWriters) {
|
|
1871
1896
|
const rel = relPosix(workspaceRoot, node.projectDir)
|
|
1872
|
-
const globs = positives(node.config.
|
|
1897
|
+
const globs = positives(node.config.outputs!.files).map((g) =>
|
|
1873
1898
|
rel === '' ? g : `${rel}/${g}`,
|
|
1874
1899
|
)
|
|
1875
1900
|
rooted.push({ node, globs, reads: false, rel })
|
|
@@ -1999,14 +2024,16 @@ function takenBack(
|
|
|
1999
2024
|
let tb = memo.bySide.get(reader)
|
|
2000
2025
|
if (tb === undefined) {
|
|
2001
2026
|
const inputs = reader.node.config.inputs!
|
|
2002
|
-
const
|
|
2027
|
+
const declaredOutputs = reader.node.config.outputs
|
|
2003
2028
|
// A reader rebased to the root (X-135) speaks in its project's terms.
|
|
2004
2029
|
const at = reader.rel === undefined || reader.rel === '' ? '' : `${reader.rel}/`
|
|
2005
2030
|
// What the reader's key never reads: its `!` entries and its own outputs.
|
|
2006
2031
|
const list = [
|
|
2007
2032
|
...splitNegations(field === 'files' ? inputs.files : (inputs.workspaceFiles ?? [])).negative,
|
|
2008
2033
|
...splitNegations(
|
|
2009
|
-
field === 'files'
|
|
2034
|
+
field === 'files'
|
|
2035
|
+
? (declaredOutputs?.files ?? [])
|
|
2036
|
+
: (declaredOutputs?.workspaceFiles ?? []),
|
|
2010
2037
|
).positive,
|
|
2011
2038
|
].map((g) => at + g)
|
|
2012
2039
|
const key = list.join('\0')
|
package/src/handoff.ts
CHANGED
|
@@ -223,15 +223,19 @@ export async function handOff(bin: string, args: readonly string[]): Promise<nev
|
|
|
223
223
|
// hides the env on every verb, is never reached here. Imported here:
|
|
224
224
|
// a start that hands off nothing pays nothing.
|
|
225
225
|
await (await import('./util/index.js')).hideEnvFromTasks()
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
// The handlers come first: a signal landing between the spawn and them
|
|
227
|
+
// killed this vx and left the local one running, orphaned (main
|
|
228
|
+
// dc9d49c). A handler runs on the event loop, after the spawn returned.
|
|
229
|
+
let child: Bun.Subprocess | undefined
|
|
230
230
|
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
231
231
|
process.on(sig, () => {
|
|
232
|
-
if (!inForeground()) child
|
|
232
|
+
if (!inForeground()) child?.kill(sig)
|
|
233
233
|
})
|
|
234
234
|
}
|
|
235
|
+
child = Bun.spawn([bin, ...args], {
|
|
236
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
237
|
+
env: { ...process.env, VX_HANDED_OFF: process.execPath },
|
|
238
|
+
})
|
|
235
239
|
const code = await child.exited
|
|
236
240
|
const signal = child.signalCode
|
|
237
241
|
if (signal === null) process.exit(code)
|
|
@@ -240,8 +244,7 @@ export async function handOff(bin: string, args: readonly string[]): Promise<nev
|
|
|
240
244
|
// SIGINT, and an exit 130 ran the script's next line. The code below is
|
|
241
245
|
// the rest, and the fallback should the signal not land.
|
|
242
246
|
if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP') {
|
|
243
|
-
|
|
244
|
-
process.kill(process.pid, signal)
|
|
247
|
+
;(await import('./util/index.js')).dieOf(signal)
|
|
245
248
|
}
|
|
246
249
|
if (typeof signal === 'number') process.exit(128 + signal)
|
|
247
250
|
const { constants } = await import('node:os')
|
|
@@ -30,7 +30,7 @@ export type FailureMode = 'stable' | 'flaky-recoverable' | 'flaky-fatal'
|
|
|
30
30
|
* `status`, `cache_hit`, `cached` columns) with how often it failed, how often it
|
|
31
31
|
* passed — an executed success or a cache hit, which replays a success —
|
|
32
32
|
* and how often it failed AFTER its first pass (`relapses`). A task with
|
|
33
|
-
* no cache
|
|
33
|
+
* no `cache: true` keys on its config alone and runs every time, so its key
|
|
34
34
|
* says nothing about its inputs: no reader counts it, as no run's row
|
|
35
35
|
* calls it flaky. Nor is a row that names a `blocked_by`: it ran behind a
|
|
36
36
|
* failure (`--continue=always`), on bytes its key never named, so its
|
|
@@ -198,6 +198,29 @@ export function detectFlaky(db: Database, candidates: readonly FlakyCandidate[])
|
|
|
198
198
|
return out
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Whether a task that just failed on `hash` is known to be flaky: this key
|
|
203
|
+
* passed before (the failure itself is the flake), or the retained history
|
|
204
|
+
* holds a key of the task that failed after it passed, or a pass that took
|
|
205
|
+
* a retry. Asked only after a failure, so a green run never reads it.
|
|
206
|
+
*/
|
|
207
|
+
export function knownFlaky(db: Database, project: string, task: string, hash: string): boolean {
|
|
208
|
+
const passed = db
|
|
209
|
+
.query(
|
|
210
|
+
`SELECT 1 FROM ${OUTCOMES} WHERE hash = ? AND project = ? AND task = ?
|
|
211
|
+
AND (status = 'success' OR status IN ${HIT_STATUSES}) LIMIT 1`,
|
|
212
|
+
)
|
|
213
|
+
.get(hash, project, task)
|
|
214
|
+
if (passed !== null) return true
|
|
215
|
+
const flaked = db
|
|
216
|
+
.query(
|
|
217
|
+
`SELECT 1 FROM (${keyOutcomesSql(OUTCOMES, ' AND project = ? AND task = ?')})
|
|
218
|
+
WHERE relapses > 0 OR retried_failures > 0 LIMIT 1`,
|
|
219
|
+
)
|
|
220
|
+
.get(project, task)
|
|
221
|
+
return flaked !== null
|
|
222
|
+
}
|
|
223
|
+
|
|
201
224
|
/** One task the recorded history shows failing on a key that had passed. */
|
|
202
225
|
export interface FlakyTask {
|
|
203
226
|
/** `project#task`. */
|
package/src/history/history.ts
CHANGED
|
@@ -311,6 +311,11 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
311
311
|
* against 4 for the executed successes' durations alone.
|
|
312
312
|
*/
|
|
313
313
|
async p50sFor(taskIds: readonly string[]): Promise<ReadonlyMap<string, number>> {
|
|
314
|
+
return this.p50sNow(taskIds)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `p50sFor`, at once: a row drawn now reads it (the rows' `usual`). */
|
|
318
|
+
p50sNow(taskIds: readonly string[]): ReadonlyMap<string, number> {
|
|
314
319
|
if (taskIds.length === 0) return new Map()
|
|
315
320
|
return this.forecasts(new Set(taskIds))
|
|
316
321
|
}
|
package/src/history/index.ts
CHANGED
package/src/history/metrics.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface RunSummaryRow {
|
|
|
39
39
|
cacheHit: boolean | null
|
|
40
40
|
/** On a hit: true when outputs were restored, false when up-to-date (v31); null otherwise. */
|
|
41
41
|
restored: boolean | null
|
|
42
|
-
/** Whether the task declared
|
|
42
|
+
/** Whether the task declared `cache: true`; null on rows older than the column. */
|
|
43
43
|
cached: boolean | null
|
|
44
44
|
hash: string
|
|
45
45
|
cpuMs: number | null
|
|
@@ -50,6 +50,10 @@ export interface RunSummaryRow {
|
|
|
50
50
|
avgRssBytes: number | null
|
|
51
51
|
/** Where an executor ran it (`small#1`, v44); null = here. */
|
|
52
52
|
placement: string | null
|
|
53
|
+
/** The bytes of its outputs: what its save stored, or its hit restored (v51). */
|
|
54
|
+
outputBytes: number | null
|
|
55
|
+
/** How many tasks were downstream of it (v51). */
|
|
56
|
+
blocks: number | null
|
|
53
57
|
// High-precision spans relative to run start. Decimal strings on the wire
|
|
54
58
|
// (bigints aren't JSON-safe); use BigInt or Number on the client.
|
|
55
59
|
wallclockStartNs: string | null
|
|
@@ -65,6 +69,8 @@ export interface RunSummaryRow {
|
|
|
65
69
|
remoteError: string | null
|
|
66
70
|
/** The attempts a retried task took (>1, v23); null when it ran once. */
|
|
67
71
|
attempts: number | null
|
|
72
|
+
/** Milliseconds it waited, ready, for a slot (v51); null when it never queued. */
|
|
73
|
+
queuedMs: number | null
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
export interface ListRunsArgs {
|
|
@@ -123,10 +129,12 @@ export function listRuns(db: Database, args: ListRunsArgs = {}): RunSummaryRow[]
|
|
|
123
129
|
cache_hit AS cacheHit, restored, cached, hash,
|
|
124
130
|
cpu_ms AS cpuMs, peak_rss_bytes AS peakRssBytes,
|
|
125
131
|
peak_cores AS peakCores, avg_rss_bytes AS avgRssBytes, placement,
|
|
132
|
+
output_bytes AS outputBytes, blocks,
|
|
126
133
|
wallclock_start_ns AS wallclockStartNs, wallclock_end_ns AS wallclockEndNs,
|
|
127
134
|
blocked_by AS blockedBy, timed_out AS timedOut,
|
|
128
135
|
sandbox_violations AS sandboxViolations, not_ready AS notReady,
|
|
129
|
-
forward_args IS NOT NULL AS forwarded, remote_error AS remoteError, attempts
|
|
136
|
+
forward_args IS NOT NULL AS forwarded, remote_error AS remoteError, attempts,
|
|
137
|
+
queued_ms AS queuedMs
|
|
130
138
|
FROM runs ${clause} ORDER BY id DESC LIMIT ?`,
|
|
131
139
|
)
|
|
132
140
|
.all(...params, limit) as RawRow[]
|
|
@@ -374,7 +382,7 @@ export interface WhyDidThisRerun {
|
|
|
374
382
|
cacheHit: boolean | null
|
|
375
383
|
/** On a hit: outputs restored (true) or up-to-date (false); null otherwise. */
|
|
376
384
|
restored: boolean | null
|
|
377
|
-
/** Whether the task declared
|
|
385
|
+
/** Whether the task declared `cache: true`; null on rows older than the column. */
|
|
378
386
|
cached: boolean | null
|
|
379
387
|
startedAt: number
|
|
380
388
|
}
|
|
@@ -606,9 +614,9 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
606
614
|
// one's is new each run, so every dependent misses: "folded by
|
|
607
615
|
// dependents" read as if they could hit.
|
|
608
616
|
this_.keylessBy === null
|
|
609
|
-
? 'this task declares no `cache`
|
|
617
|
+
? 'this task declares no `cache: true` — it runs on every invocation; its key is folded by dependents only'
|
|
610
618
|
: this_.keylessBy === taskId
|
|
611
|
-
? 'this task declares no `cache`
|
|
619
|
+
? 'this task declares no `cache: true` — it runs on every invocation; it declares no `inputs` either, so each run derives a new key and every task that depends on it misses; declare its `inputs`'
|
|
612
620
|
: `this task declares no \`cache\` block — it runs on every invocation; its key includes ${this_.keylessBy}, which declares no \`inputs\`, so each run derives a new key and every task that depends on it misses; declare inputs on ${this_.keylessBy}`
|
|
613
621
|
: this_.keylessBy !== null
|
|
614
622
|
? keylessNote(taskId, this_.keylessBy)
|
|
@@ -881,7 +889,7 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
881
889
|
this_.cached === 0
|
|
882
890
|
? // The verdict already says the task is uncached; this line says
|
|
883
891
|
// why no input can be named.
|
|
884
|
-
'
|
|
892
|
+
'without `cache: true` it saves no entry, so no input fingerprints are kept to say which of its `inputs` moved its key; with no `inputs`, every run gives it a new key'
|
|
885
893
|
: unsaved !== null
|
|
886
894
|
? `cache key changed but input fingerprints are unavailable — ${unsaved} and saved no entry; only the key change is known`
|
|
887
895
|
: 'cache key changed but input fingerprints are unavailable — the entry was pruned (or the row predates the `cached` column); only the key change is known',
|