@vzn/vx 0.0.652 → 0.0.653
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 +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +17 -3
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +46 -25
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lstatSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import type { TaskConfig } from '../config.js'
|
|
3
|
+
import type { InputStability, TaskConfig } from '../config.js'
|
|
4
4
|
import type { FoldMemo, ProjectFilesCache, WorkspaceFilesCache } from '../cache/index.js'
|
|
5
5
|
import {
|
|
6
6
|
ABSENT_INPUT,
|
|
@@ -9,19 +9,27 @@ import {
|
|
|
9
9
|
behindLink,
|
|
10
10
|
filesToken,
|
|
11
11
|
foldHead,
|
|
12
|
+
hashFileSeen,
|
|
12
13
|
type InputListing,
|
|
13
14
|
type CacheKeyInput,
|
|
14
15
|
type CacheLayer,
|
|
15
16
|
FILE_HASH_RACY_MS,
|
|
16
17
|
racyWindowMs,
|
|
17
18
|
resolveInputs,
|
|
19
|
+
stampGrainMs,
|
|
18
20
|
type ResolveInputsArgs,
|
|
19
21
|
type GitFilesCache,
|
|
20
22
|
type TaskInputComponent,
|
|
21
23
|
WORKSPACE_OUTPUT_PREFIX,
|
|
22
24
|
} from '../cache/index.js'
|
|
23
25
|
import type { InputFile, TaskInputs } from '../exec/index.js'
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
isGroupTask,
|
|
28
|
+
KeyFailed,
|
|
29
|
+
standInsOf,
|
|
30
|
+
type TaskNode,
|
|
31
|
+
type TaskOutcome,
|
|
32
|
+
} from '../graph/index.js'
|
|
25
33
|
import { GROUP_VIA } from '../history/index.js'
|
|
26
34
|
import {
|
|
27
35
|
isUserError,
|
|
@@ -32,6 +40,7 @@ import {
|
|
|
32
40
|
UserError,
|
|
33
41
|
xxh3hex,
|
|
34
42
|
} from '../util/index.js'
|
|
43
|
+
import { outputsLeft } from './run-report.js'
|
|
35
44
|
import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
|
|
36
45
|
|
|
37
46
|
/**
|
|
@@ -63,8 +72,41 @@ export interface HashCache {
|
|
|
63
72
|
graph?: string | undefined
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
/**
|
|
76
|
+
* `.vx/workspace`'s `outputs.clean`, after the config stage, by the run's
|
|
77
|
+
* hash cache: a task without its own runs under it, so its key folds it
|
|
78
|
+
* (`hashableConfig`). Beside `HashCache`, not on it: that type is the
|
|
79
|
+
* package's surface.
|
|
80
|
+
*/
|
|
81
|
+
const workspaceClean = new WeakMap<HashCache, boolean>()
|
|
82
|
+
|
|
83
|
+
/** The workspace's `outputs.clean` the run's keys were derived under. */
|
|
84
|
+
export function workspaceCleanOf(hashCache: HashCache): boolean | undefined {
|
|
85
|
+
return workspaceClean.get(hashCache)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `inputs.stability.strategy` of `VX_INPUTS_STABILITY_STRATEGY` (`run`,
|
|
90
|
+
* beating a task's own) and of `.vx/workspace` (beaten by it), by the run's
|
|
91
|
+
* hash cache: it decides which saves are kept, so a key folds the one a
|
|
92
|
+
* task runs under (`hashableConfig`). Beside `HashCache` as the clean is.
|
|
93
|
+
*/
|
|
94
|
+
const inputStrategies = new WeakMap<HashCache, InputStrategies>()
|
|
95
|
+
|
|
96
|
+
type InputStrategy = InputStability['strategy']
|
|
97
|
+
type InputStrategies = { run?: InputStrategy; workspace?: InputStrategy }
|
|
98
|
+
|
|
99
|
+
/** The run-level `inputs.stability.strategy` values the run's keys were derived under. */
|
|
100
|
+
export function inputStrategiesOf(hashCache: HashCache): InputStrategies | undefined {
|
|
101
|
+
return inputStrategies.get(hashCache)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createHashCache(
|
|
105
|
+
folds?: FoldMemo,
|
|
106
|
+
cleanOutputs?: boolean,
|
|
107
|
+
strategies?: InputStrategies,
|
|
108
|
+
): HashCache {
|
|
109
|
+
const hashCache: HashCache = {
|
|
68
110
|
taskConfig: new WeakMap(),
|
|
69
111
|
runtime: new Map(),
|
|
70
112
|
workspaceRuntime: new Map(),
|
|
@@ -72,6 +114,10 @@ export function createHashCache(folds?: FoldMemo): HashCache {
|
|
|
72
114
|
projectFiles: new Map(),
|
|
73
115
|
folds,
|
|
74
116
|
}
|
|
117
|
+
if (cleanOutputs !== undefined) workspaceClean.set(hashCache, cleanOutputs)
|
|
118
|
+
if (strategies?.run !== undefined || strategies?.workspace !== undefined)
|
|
119
|
+
inputStrategies.set(hashCache, strategies)
|
|
120
|
+
return hashCache
|
|
75
121
|
}
|
|
76
122
|
|
|
77
123
|
export interface ComputeHashArgs {
|
|
@@ -99,6 +145,15 @@ export interface ComputeHashArgs {
|
|
|
99
145
|
probesAfterWrites?: ReadonlySet<string>
|
|
100
146
|
}
|
|
101
147
|
|
|
148
|
+
/** A key derivation whose throw is the task's `KeyFailed`: its command never ran. */
|
|
149
|
+
export async function keyOf(derived: Promise<string>): Promise<string> {
|
|
150
|
+
try {
|
|
151
|
+
return await derived
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new KeyFailed(err)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
/**
|
|
103
158
|
* Resolve every input the cache key depends on (file content, env
|
|
104
159
|
* values, upstream hashes, config bytes, forwardArgs) and ask the cache layer to combine them. Used by both
|
|
@@ -174,6 +229,12 @@ export interface InputFact {
|
|
|
174
229
|
path: string
|
|
175
230
|
digest: string
|
|
176
231
|
since: number
|
|
232
|
+
/**
|
|
233
|
+
* The ctime of the stat its digest came from, for a file hashed at the
|
|
234
|
+
* describe (an index OID was not stat'ed): a ctime still equal to it is
|
|
235
|
+
* not a write since, though the command's start shares its coarse tick.
|
|
236
|
+
*/
|
|
237
|
+
ctimeMs?: number
|
|
177
238
|
/**
|
|
178
239
|
* For a link, the content behind it (`behindLink`), which the key folds
|
|
179
240
|
* after the link's own target string. An edit there leaves the link
|
|
@@ -196,14 +257,21 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
196
257
|
inputs: TaskInputs
|
|
197
258
|
facts: InputFact[]
|
|
198
259
|
describedAt: number
|
|
260
|
+
/** Lists `inputs.upstream` with a non-cacheable upstream's outputs on disk; before any read of it. */
|
|
261
|
+
readUpstream: () => Promise<void>
|
|
199
262
|
/** An input file the key did not fold that exists now (see `addedInput`). */
|
|
200
263
|
added: () => string | undefined
|
|
201
264
|
}> {
|
|
202
265
|
const hashedAt = Date.now()
|
|
203
266
|
const { input, listings } = await resolveKeyInput(args)
|
|
204
267
|
const sorted = [...input.inputFiles].sort()
|
|
268
|
+
const ctimes = new Map<string, number>()
|
|
205
269
|
const digests = await Promise.all(
|
|
206
|
-
sorted.map(
|
|
270
|
+
sorted.map(
|
|
271
|
+
(f) =>
|
|
272
|
+
input.fileHashes?.get(f) ??
|
|
273
|
+
hashFileSeen(args.cache, f, (c) => ctimes.set(f, c)).catch(absentOr),
|
|
274
|
+
),
|
|
207
275
|
)
|
|
208
276
|
// Learned BEFORE the key folds it: an edit in between then reads as a
|
|
209
277
|
// move at the re-check (a withheld save), never as content the key
|
|
@@ -229,6 +297,8 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
229
297
|
digest: digests[i]!,
|
|
230
298
|
since: input.fileHashes?.has(f) === true ? indexedAt : hashedAt,
|
|
231
299
|
}
|
|
300
|
+
const ctimeMs = ctimes.get(f)
|
|
301
|
+
if (ctimeMs !== undefined) fact.ctimeMs = ctimeMs
|
|
232
302
|
const linked = behind.get(f)
|
|
233
303
|
if (linked !== undefined) fact.behind = linked
|
|
234
304
|
return fact
|
|
@@ -241,22 +311,44 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
241
311
|
a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0,
|
|
242
312
|
)
|
|
243
313
|
// Output lists come from the local index — one SELECT for every upstream
|
|
244
|
-
// entry.
|
|
314
|
+
// entry. A non-cacheable upstream keeps no entry: its list is its
|
|
315
|
+
// declared outputs as they are on disk now (`readUpstream`), as it ran
|
|
316
|
+
// here and a local dependant reads them here. Left empty, a remote
|
|
317
|
+
// executor shipped the dependant without them and its result was saved
|
|
318
|
+
// under the key.
|
|
245
319
|
// Read on first access: the local floor never reads it, and its every
|
|
246
320
|
// miss paid a query over the whole closure (execute-task reads it up
|
|
247
321
|
// front for a plugin executor).
|
|
248
|
-
const listUpstream = (): TaskInputs['upstream'] => {
|
|
322
|
+
const listUpstream = (onDisk?: ReadonlyMap<string, string[]>): TaskInputs['upstream'] => {
|
|
249
323
|
const rows = args.cache.loadOutputFilesBatch(graft.map((g) => g.hash))
|
|
250
324
|
return graft.map((g) => {
|
|
251
|
-
const outputs =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
325
|
+
const outputs =
|
|
326
|
+
onDisk?.get(g.taskId) ??
|
|
327
|
+
(rows.get(g.hash) ?? []).map((r) =>
|
|
328
|
+
r.path.startsWith(WORKSPACE_OUTPUT_PREFIX)
|
|
329
|
+
? r.path.slice(WORKSPACE_OUTPUT_PREFIX.length)
|
|
330
|
+
: relPosix(input.workspaceRoot, path.join(g.projectDir, r.path)),
|
|
331
|
+
)
|
|
256
332
|
return { taskId: g.taskId, hash: g.hash, outputs }
|
|
257
333
|
})
|
|
258
334
|
}
|
|
259
335
|
let upstream: TaskInputs['upstream'] | undefined
|
|
336
|
+
const readUpstream = async (): Promise<void> => {
|
|
337
|
+
if (upstream !== undefined) return
|
|
338
|
+
const uncached = expandGroupUpstream(args.upstream).filter(
|
|
339
|
+
(u) => u.hash !== undefined && u.node.config.cache !== true,
|
|
340
|
+
)
|
|
341
|
+
// No nested project is cut out: each file found is on this disk, where
|
|
342
|
+
// a local run of the dependant reads it too.
|
|
343
|
+
const onDisk = new Map(
|
|
344
|
+
await Promise.all(
|
|
345
|
+
uncached.map(
|
|
346
|
+
async (u) => [u.node.id, await outputsLeft(u.node, input.workspaceRoot, [])] as const,
|
|
347
|
+
),
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
upstream ??= listUpstream(onDisk)
|
|
351
|
+
}
|
|
260
352
|
// A listing from the run's enumeration is as old as it; one listed with
|
|
261
353
|
// no run cache was listed during this describe.
|
|
262
354
|
const listedAt = args.gitFilesCache === undefined ? hashedAt : indexedAt
|
|
@@ -264,6 +356,7 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
264
356
|
hash,
|
|
265
357
|
facts,
|
|
266
358
|
describedAt: hashedAt,
|
|
359
|
+
readUpstream,
|
|
267
360
|
added: () => addedInput(listings, new Set(input.inputFiles), listedAt),
|
|
268
361
|
inputs: {
|
|
269
362
|
files,
|
|
@@ -287,11 +380,15 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
287
380
|
* a file whose ctime is older than its fact (by the racy window, as git
|
|
288
381
|
* judges its index) has not been written since, and one that is not gets
|
|
289
382
|
* hashed again and compared. ctime, because no writer can set it back. A
|
|
290
|
-
* file that is gone has moved. One written
|
|
291
|
-
*
|
|
292
|
-
* input changed and changed BACK while the
|
|
293
|
-
* again, and the entry saved the other
|
|
294
|
-
* 1015). A write before it — an
|
|
383
|
+
* file that is gone has moved. One written after `commandFrom` (read off
|
|
384
|
+
* the file system's clock, `fsClockNow`, just after the describe) has moved
|
|
385
|
+
* whatever it holds now: an input changed and changed BACK while the
|
|
386
|
+
* command ran matched its digest again, and the entry saved the other
|
|
387
|
+
* content's output under the key (item 1015). A write before it — an
|
|
388
|
+
* upstream's — is judged by content, as before. A coarse clock dates a
|
|
389
|
+
* write in `commandFrom`'s own tick at it, before or after: such a ctime is
|
|
390
|
+
* the upstream's when the describe's stat saw it already
|
|
391
|
+
* (`InputFact.ctimeMs`), else the command's.
|
|
295
392
|
* `byBytes` (`inputs.stability.strategy: 'mutable'`) judges a write during
|
|
296
393
|
* the command by content too, when it answers true for that file and its
|
|
297
394
|
* ctime: the task says the same bytes are its own identical rewrite
|
|
@@ -318,9 +415,14 @@ export async function movedInput(
|
|
|
318
415
|
// Back since the key folded it gone.
|
|
319
416
|
if (f.digest === ABSENT_INPUT) return f.path
|
|
320
417
|
// A whole-second stamp may stand for any write in its second (two for
|
|
321
|
-
// an even one, FAT's tick), one during the command included
|
|
322
|
-
//
|
|
323
|
-
|
|
418
|
+
// an even one, FAT's tick), one during the command included. No lag
|
|
419
|
+
// widens it: `commandFrom` is on the file system's clock already.
|
|
420
|
+
const grainMs = stampGrainMs(ctimeMs)
|
|
421
|
+
if (
|
|
422
|
+
commandFrom !== undefined &&
|
|
423
|
+
(ctimeMs > commandFrom ||
|
|
424
|
+
(ctimeMs >= commandFrom - grainMs && (grainMs > 0 || f.ctimeMs !== ctimeMs)))
|
|
425
|
+
) {
|
|
324
426
|
if (byBytes?.(f.path, ctimeMs) !== true) return f.path
|
|
325
427
|
suspects.push(f)
|
|
326
428
|
continue
|
|
@@ -340,14 +442,27 @@ export async function movedInput(
|
|
|
340
442
|
return undefined
|
|
341
443
|
}
|
|
342
444
|
|
|
343
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* A task's `filesToken`, as its key's fold memo probe takes it: the fields
|
|
447
|
+
* of `inputsArgs` the token reads, and no more. Built through `inputsArgs`,
|
|
448
|
+
* two spread objects per call were ~3 ms of 3,202 (2026-11-07).
|
|
449
|
+
*/
|
|
344
450
|
export function taskFilesToken(
|
|
345
|
-
args: Pick<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
>,
|
|
451
|
+
args: Pick<ComputeHashArgs, 'node' | 'workspaceRoot' | 'nestedProjectDirs'> & {
|
|
452
|
+
gitFilesCache: GitFilesCache
|
|
453
|
+
},
|
|
349
454
|
): string | undefined {
|
|
350
|
-
|
|
455
|
+
const node = args.node
|
|
456
|
+
const out = node.config.outputs
|
|
457
|
+
return filesToken({
|
|
458
|
+
projectDir: node.projectDir,
|
|
459
|
+
workspaceRoot: args.workspaceRoot,
|
|
460
|
+
inputs: node.config.inputs,
|
|
461
|
+
ownOutputs: out?.files ?? [],
|
|
462
|
+
ownWorkspaceOutputs: out?.workspaceFiles ?? [],
|
|
463
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
464
|
+
gitFilesCache: args.gitFilesCache,
|
|
465
|
+
})
|
|
351
466
|
}
|
|
352
467
|
|
|
353
468
|
/** What `resolveInputs` is asked for a task's key. */
|
|
@@ -397,6 +512,7 @@ async function resolveKeyInput(
|
|
|
397
512
|
cfg.inputs?.tasks,
|
|
398
513
|
args.node.projectName,
|
|
399
514
|
args.node.id,
|
|
515
|
+
standInsOf(args.node),
|
|
400
516
|
)
|
|
401
517
|
const upstreamHashes = upstreamPairs.map(([, hash]) => hash)
|
|
402
518
|
// hash → upstream task id, for capture-row naming only (not folded).
|
|
@@ -487,7 +603,7 @@ export function hashTaskConfig(cfg: TaskConfig, hashCache?: HashCache): string {
|
|
|
487
603
|
if (hashCache) {
|
|
488
604
|
const cached = hashCache.taskConfig.get(cfg)
|
|
489
605
|
if (cached !== undefined) return cached
|
|
490
|
-
const hash = xxh3hex(JSON.stringify(hashableConfig(cfg)))
|
|
606
|
+
const hash = xxh3hex(JSON.stringify(hashableConfig(asRun(cfg, hashCache))))
|
|
491
607
|
hashCache.taskConfig.set(cfg, hash)
|
|
492
608
|
return hash
|
|
493
609
|
}
|
|
@@ -495,7 +611,36 @@ export function hashTaskConfig(cfg: TaskConfig, hashCache?: HashCache): string {
|
|
|
495
611
|
}
|
|
496
612
|
|
|
497
613
|
/**
|
|
498
|
-
*
|
|
614
|
+
* The config as the task runs it, where a run-level value changes what is
|
|
615
|
+
* saved: folded as the task's own field would be, and only then, so every
|
|
616
|
+
* key that never ran under one stays.
|
|
617
|
+
*
|
|
618
|
+
* A workspace `outputs.clean: false` saves whatever the outputs held before
|
|
619
|
+
* the command, a leftover included: a key that missed it hit that entry
|
|
620
|
+
* once the clean was back on. An `inputs.stability.strategy` other than
|
|
621
|
+
* `'immutable'` keeps a save the check withholds (`'unchecked'`, one whose
|
|
622
|
+
* input moved under the command): unfolded, that entry was hit once the
|
|
623
|
+
* check was back on, and a formatter it covered never ran.
|
|
624
|
+
*/
|
|
625
|
+
function asRun(cfg: TaskConfig, hashCache: HashCache): TaskConfig {
|
|
626
|
+
let out = cfg
|
|
627
|
+
if (workspaceClean.get(hashCache) === false && cfg.outputs && cfg.outputs.clean === undefined) {
|
|
628
|
+
out = { ...out, outputs: { ...cfg.outputs, clean: false } }
|
|
629
|
+
}
|
|
630
|
+
const strategies = inputStrategies.get(hashCache)
|
|
631
|
+
const inputs = cfg.inputs
|
|
632
|
+
if (strategies !== undefined && inputs !== undefined && cfg.exec !== undefined) {
|
|
633
|
+
const own = inputs.stability?.strategy
|
|
634
|
+
const runs = strategies.run ?? own ?? strategies.workspace ?? 'immutable'
|
|
635
|
+
if (runs !== (own ?? 'immutable')) {
|
|
636
|
+
out = { ...out, inputs: { ...inputs, stability: { ...inputs.stability, strategy: runs } } }
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return out
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Project the config for hashing: `exec.remote` (and `exec.worker`) is a pure PLACEMENT hint
|
|
499
644
|
* with zero effect on outputs, so it is stripped — pinning a task to this
|
|
500
645
|
* machine never busts a cache. It must not: the whole contract of a remote
|
|
501
646
|
* executor is that the same command over the same inputs produces the
|
|
@@ -521,12 +666,24 @@ function hashableConfig(cfg: TaskConfig): unknown {
|
|
|
521
666
|
// writes a config the lock never sorted (2026-10-25).
|
|
522
667
|
const env = cfg.inputs?.env
|
|
523
668
|
const pass = cfg.exec?.env?.passThrough
|
|
524
|
-
|
|
669
|
+
const secret = cfg.exec?.env?.secret
|
|
670
|
+
if (
|
|
671
|
+
cfg.exec?.remote === undefined &&
|
|
672
|
+
cfg.exec?.worker === undefined &&
|
|
673
|
+
sortedNames(env) &&
|
|
674
|
+
sortedNames(pass) &&
|
|
675
|
+
sortedNames(secret)
|
|
676
|
+
)
|
|
525
677
|
return keysSorted(cfg)
|
|
526
|
-
const {
|
|
678
|
+
const {
|
|
679
|
+
remote: _remote,
|
|
680
|
+
worker: _worker,
|
|
681
|
+
...execRest
|
|
682
|
+
} = cfg.exec ?? ({} as NonNullable<TaskConfig['exec']>)
|
|
527
683
|
// Env names are sets: each value is read by its name, so their order
|
|
528
684
|
// keys nothing (conformance env/declared-order-irrelevant).
|
|
529
685
|
if (!sortedNames(pass)) execRest.env = { ...execRest.env, passThrough: [...pass!].sort() }
|
|
686
|
+
if (!sortedNames(secret)) execRest.env = { ...execRest.env, secret: [...secret!].sort() }
|
|
530
687
|
const out: TaskConfig = cfg.exec === undefined ? { ...cfg } : { ...cfg, exec: execRest }
|
|
531
688
|
if (!sortedNames(env)) out.inputs = { ...cfg.inputs!, env: [...env!].sort() }
|
|
532
689
|
return keysSorted(out)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// See docs/design/observability-architecture-2026-06.md §1, §3.
|
|
13
13
|
|
|
14
14
|
import type { EventBus } from '../report/index.js'
|
|
15
|
-
import type { TaskNode } from '../graph/index.js'
|
|
15
|
+
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
16
16
|
import type {
|
|
17
17
|
RunContextRecord,
|
|
18
18
|
RunSummaryRecord,
|
|
@@ -31,6 +31,8 @@ export interface TelemetryHandle {
|
|
|
31
31
|
emitSummary(summary: RunSummaryRecord): void
|
|
32
32
|
/** Await every sink's flush (crash-isolated, time-bounded by the sink), each as a `<plugin>:telemetry` step. */
|
|
33
33
|
flush(step?: <T>(id: string, work: () => Promise<T>) => Promise<T>): Promise<void>
|
|
34
|
+
/** Send the held `task.end` of each server not in `keep`; see `TelemetrySource.endServers`. */
|
|
35
|
+
endServers(final: readonly TaskOutcome[], keep?: ReadonlySet<string>): void
|
|
34
36
|
/** Remove the bus subscription. Idempotent. */
|
|
35
37
|
dispose(): void
|
|
36
38
|
/** Sample a running task's process tree; see `TelemetrySource.track`. */
|
|
@@ -47,12 +49,17 @@ const RECORD_KINDS: Record<TelemetryRecord['kind'], true> = {
|
|
|
47
49
|
'run.end': true,
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Reject anything that is not sink-shaped, naming what arrived, and return
|
|
54
|
+
* a copy of its members read once here. The sink is user input: a getter
|
|
55
|
+
* read again downstream, outside this try, threw past every guard and
|
|
56
|
+
* failed a green run.
|
|
57
|
+
*/
|
|
51
58
|
function checkSink(sink: unknown): TelemetrySink {
|
|
52
59
|
if (typeof sink !== 'object' || sink === null) {
|
|
53
60
|
throw new Error(`telemetry sink must be an object, got ${sink === null ? 'null' : typeof sink}`)
|
|
54
61
|
}
|
|
55
|
-
const wants =
|
|
62
|
+
const { name, wants, onRecord, onRunSummary, flush } = sink as TelemetrySink
|
|
56
63
|
if (wants !== undefined && !Array.isArray(wants)) {
|
|
57
64
|
throw new Error(`telemetry sink 'wants' must be an array, got ${typeof wants}`)
|
|
58
65
|
}
|
|
@@ -69,13 +76,24 @@ function checkSink(sink: unknown): TelemetrySink {
|
|
|
69
76
|
// subscribed to the bus and heard nothing — the plugin was "on" and did
|
|
70
77
|
// nothing. Refused here, which the caller turns into the same warning
|
|
71
78
|
// a throwing hook gets: never a failed run.
|
|
72
|
-
|
|
73
|
-
if (typeof s.onRecord !== 'function' && typeof s.onRunSummary !== 'function') {
|
|
79
|
+
if (typeof onRecord !== 'function' && typeof onRunSummary !== 'function') {
|
|
74
80
|
throw new Error(
|
|
75
81
|
'telemetry sink handles nothing: neither onRecord nor onRunSummary is a function',
|
|
76
82
|
)
|
|
77
83
|
}
|
|
78
|
-
|
|
84
|
+
for (const [key, fn] of Object.entries({ onRecord, onRunSummary, flush })) {
|
|
85
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
86
|
+
throw new Error(`telemetry sink '${key}' must be a function, got ${typeof fn}`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Bound: a sink written as a class reads its own state through `this`.
|
|
90
|
+
return {
|
|
91
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
92
|
+
...(wants !== undefined ? { wants: [...wants] } : {}),
|
|
93
|
+
...(onRecord !== undefined ? { onRecord: onRecord.bind(sink) } : {}),
|
|
94
|
+
...(onRunSummary !== undefined ? { onRunSummary: onRunSummary.bind(sink) } : {}),
|
|
95
|
+
...(flush !== undefined ? { flush: flush.bind(sink) } : {}),
|
|
96
|
+
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
/**
|
|
@@ -147,6 +165,7 @@ export async function subscribeTelemetry(
|
|
|
147
165
|
return {
|
|
148
166
|
emitSummary: (summary) => source.emitSummary(summary),
|
|
149
167
|
flush: (step) => source.flush(step),
|
|
168
|
+
endServers: (final, keep) => source.endServers(final, keep),
|
|
150
169
|
dispose,
|
|
151
170
|
...(source.track !== undefined ? { track: source.track } : {}),
|
|
152
171
|
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// rendering-oriented `WireEvent` stream.
|
|
11
11
|
//
|
|
12
12
|
// A sink is observe-only BY CONSTRUCTION: its only input is a plain-data
|
|
13
|
-
// record (one object every sink receives,
|
|
13
|
+
// record (one object every sink receives, frozen once, deep: a sink that
|
|
14
|
+
// cleared a `task.end`'s `failedAttempts` cleared the outcome's own and
|
|
15
|
+
// crashed the run's failure log); its `TelemetryContext` carries read-only metadata and NO mutable
|
|
14
16
|
// run handle (no bus, no Cache, no RunRequest). There is no API path from a
|
|
15
17
|
// sink back into scheduling/caching/exec — telemetry provably cannot change
|
|
16
18
|
// what or how tasks run. Contrast `cache`/`executor`, which return objects
|
|
@@ -34,7 +36,8 @@ import {
|
|
|
34
36
|
teardownTimeoutMs,
|
|
35
37
|
thrownMessage,
|
|
36
38
|
} from '../util/index.js'
|
|
37
|
-
import type
|
|
39
|
+
import { neverStarted, type RunEvent, type RunEventSubscriber } from '../report/index.js'
|
|
40
|
+
import { freezeDeep } from './read-only.js'
|
|
38
41
|
|
|
39
42
|
/** Bumped when the record shape changes. Readers MUST check `v`. */
|
|
40
43
|
export const TELEMETRY_SCHEMA_VERSION = 4
|
|
@@ -256,14 +259,20 @@ export interface RunSummaryRecord {
|
|
|
256
259
|
startedAt: number
|
|
257
260
|
endedAt: number
|
|
258
261
|
totalDurationMs: number
|
|
262
|
+
/** Every task the run had, as the terminal's total: `tasks` plus `notRunCount`. */
|
|
259
263
|
taskCount: number
|
|
260
264
|
failedCount: number
|
|
261
265
|
/**
|
|
262
|
-
* Tasks a shutdown signal or an embedder's abort killed (v3)
|
|
263
|
-
*
|
|
264
|
-
*
|
|
266
|
+
* Tasks a shutdown signal or an embedder's abort killed once started (v3),
|
|
267
|
+
* each in `tasks` as `aborted`: without the count a stopped run read as a
|
|
268
|
+
* failure with nothing failed (item 851).
|
|
265
269
|
*/
|
|
266
270
|
abortedCount: number
|
|
271
|
+
/**
|
|
272
|
+
* Tasks the stop reached before they started: the terminal's `not run`.
|
|
273
|
+
* Not in `tasks`, since they never ran. Absent when none. Additive.
|
|
274
|
+
*/
|
|
275
|
+
notRunCount?: number
|
|
267
276
|
hitCount: number
|
|
268
277
|
/** Hits by the layer that answered, up-to-date ones included. */
|
|
269
278
|
hitLocalCount: number
|
|
@@ -318,8 +327,9 @@ export interface RunStage {
|
|
|
318
327
|
* computed: both a local `run()` and the distributed controller build their
|
|
319
328
|
* `TaskTelemetry[]` (each via `deriveCacheSource`) and call this, so a
|
|
320
329
|
* distributed run and a local run produce byte-identical summaries and land in
|
|
321
|
-
* the same ingest. The per-task tallies (
|
|
322
|
-
* hitLocal|Remote|Count) derive from `tasks
|
|
330
|
+
* the same ingest. The per-task tallies (failedCount /
|
|
331
|
+
* hitLocal|Remote|Count) derive from `tasks`, and `taskCount` from `tasks`
|
|
332
|
+
* plus `notRunCount`; `totalDurationMs` (wall time) and
|
|
323
333
|
* `exitOk` (the run's overall verdict — which counts skipped tasks beyond the
|
|
324
334
|
* recorded task list) are run-level facts and are passed in.
|
|
325
335
|
*/
|
|
@@ -332,6 +342,7 @@ export function assembleRunSummary(
|
|
|
332
342
|
totalDurationMs: number
|
|
333
343
|
exitOk: boolean
|
|
334
344
|
abortedCount: number
|
|
345
|
+
notRunCount?: number
|
|
335
346
|
stages?: readonly RunStage[]
|
|
336
347
|
uploads?: { count: number; bytes: number; ms: number; failed: number }
|
|
337
348
|
machines?: readonly MachineReport[]
|
|
@@ -360,9 +371,14 @@ export function assembleRunSummary(
|
|
|
360
371
|
startedAt: timing.startedAt,
|
|
361
372
|
endedAt: timing.endedAt,
|
|
362
373
|
totalDurationMs: timing.totalDurationMs,
|
|
363
|
-
|
|
374
|
+
// The terminal's total: the tasks a stop reached first are not in
|
|
375
|
+
// `tasks`, yet a summary of 2 beside a terminal's 3 read as a lost task.
|
|
376
|
+
taskCount: tasks.length + (timing.notRunCount ?? 0),
|
|
364
377
|
failedCount,
|
|
365
378
|
abortedCount: timing.abortedCount,
|
|
379
|
+
...(timing.notRunCount !== undefined && timing.notRunCount > 0
|
|
380
|
+
? { notRunCount: timing.notRunCount }
|
|
381
|
+
: {}),
|
|
366
382
|
hitCount: hitLocalCount + hitRemoteCount,
|
|
367
383
|
hitLocalCount,
|
|
368
384
|
hitRemoteCount,
|
|
@@ -383,7 +399,11 @@ export function assembleRunSummary(
|
|
|
383
399
|
}
|
|
384
400
|
}
|
|
385
401
|
|
|
386
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* A telemetry consumer. Observe-only: receives records, frozen, holds no
|
|
404
|
+
* run handle. A write to a record throws, and the sink is switched off for
|
|
405
|
+
* the run as for any throw.
|
|
406
|
+
*/
|
|
387
407
|
export interface TelemetrySink {
|
|
388
408
|
readonly name?: string
|
|
389
409
|
/**
|
|
@@ -395,7 +415,11 @@ export interface TelemetrySink {
|
|
|
395
415
|
readonly wants?: ReadonlyArray<TelemetryRecord['kind']>
|
|
396
416
|
/** A streaming record. MUST return promptly — buffer; do NOT await I/O. */
|
|
397
417
|
onRecord?(record: TelemetryRecord): void
|
|
398
|
-
/**
|
|
418
|
+
/**
|
|
419
|
+
* The per-run summary, once at run:end; a requested persistent task the
|
|
420
|
+
* foreground keeps sends it when that task exits, so it reads the run's
|
|
421
|
+
* final verdict. MUST return promptly.
|
|
422
|
+
*/
|
|
399
423
|
onRunSummary?(summary: RunSummaryRecord): void
|
|
400
424
|
/**
|
|
401
425
|
* Drain buffered data. Awaited at run:end under a deadline — a sink that
|
|
@@ -433,6 +457,12 @@ export interface TelemetrySource {
|
|
|
433
457
|
* each as the step `<plugin>:telemetry` through `step`.
|
|
434
458
|
*/
|
|
435
459
|
flush(step?: <T>(id: string, work: () => Promise<T>) => Promise<T>): Promise<void>
|
|
460
|
+
/**
|
|
461
|
+
* Send the held `task.end` of each persistent task not in `keep`, read
|
|
462
|
+
* from `final` (by id): a server's outcome can change after its ready,
|
|
463
|
+
* when it dies before the run stops it or ends a kept session.
|
|
464
|
+
*/
|
|
465
|
+
endServers(final: readonly TaskOutcome[], keep?: ReadonlySet<string>): void
|
|
436
466
|
/**
|
|
437
467
|
* Sample this task's process tree every `SAMPLE_MS` until the returned
|
|
438
468
|
* function is called, the root exits, or the run ends. Present only when
|
|
@@ -602,6 +632,9 @@ export function createTelemetrySource(args: {
|
|
|
602
632
|
}
|
|
603
633
|
|
|
604
634
|
function deliver(record: TelemetryRecord): void {
|
|
635
|
+
// Once, for every sink: a `task.end` holds the outcome's own arrays, and
|
|
636
|
+
// a sink that cleared `failedAttempts` crashed the run's failure log.
|
|
637
|
+
freezeDeep(record)
|
|
605
638
|
for (const sink of sinks) {
|
|
606
639
|
if (disabled.has(sink) || sink.onRecord === undefined) continue
|
|
607
640
|
const kinds = sink.wants ?? DEFAULT_KINDS
|
|
@@ -667,6 +700,16 @@ export function createTelemetrySource(args: {
|
|
|
667
700
|
case 'task:complete': {
|
|
668
701
|
const { node, outcome } = event
|
|
669
702
|
if (node.config.exec === undefined) return // group task
|
|
703
|
+
// The stop reached it first: no task.start, and the summary counts
|
|
704
|
+
// it in `notRunCount`, not `tasks`, so no task.end either.
|
|
705
|
+
if (neverStarted(outcome)) return
|
|
706
|
+
// A server's end waits for its final outcome (`endServers`): one
|
|
707
|
+
// task.end per task, and the one sent at ready read `success` over
|
|
708
|
+
// a crash after it.
|
|
709
|
+
if (node.config.exec.persistent !== undefined) {
|
|
710
|
+
heldEnds.set(node.id, { ts, outcome })
|
|
711
|
+
return
|
|
712
|
+
}
|
|
670
713
|
deliver({
|
|
671
714
|
v: TELEMETRY_SCHEMA_VERSION,
|
|
672
715
|
kind: 'task.end',
|
|
@@ -687,10 +730,26 @@ export function createTelemetrySource(args: {
|
|
|
687
730
|
}
|
|
688
731
|
}
|
|
689
732
|
|
|
733
|
+
const heldEnds = new Map<string, { ts: number; outcome: TaskOutcome }>()
|
|
690
734
|
return {
|
|
691
735
|
subscriber,
|
|
692
736
|
...(wantsSample ? { track } : {}),
|
|
737
|
+
endServers(final, keep) {
|
|
738
|
+
for (const [id, held] of heldEnds) {
|
|
739
|
+
if (keep?.has(id) === true) continue
|
|
740
|
+
heldEnds.delete(id)
|
|
741
|
+
const outcome = final.find((o) => o.node.id === id) ?? held.outcome
|
|
742
|
+
deliver({
|
|
743
|
+
v: TELEMETRY_SCHEMA_VERSION,
|
|
744
|
+
kind: 'task.end',
|
|
745
|
+
runId,
|
|
746
|
+
ts: held.ts,
|
|
747
|
+
...taskTelemetryOf(outcome),
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
},
|
|
693
751
|
emitSummary(summary: RunSummaryRecord): void {
|
|
752
|
+
freezeDeep(summary)
|
|
694
753
|
for (const sink of sinks) {
|
|
695
754
|
if (disabled.has(sink) || sink.onRunSummary === undefined) continue
|
|
696
755
|
try {
|
|
@@ -712,9 +771,12 @@ export function createTelemetrySource(args: {
|
|
|
712
771
|
// accessed_at bumps and every later plugin's teardown lost with it.
|
|
713
772
|
// Sinks race concurrently, so each still gets the whole budget.
|
|
714
773
|
const deadline = new AbortController()
|
|
774
|
+
// The deadline's warning names the sinks still out, as a throw names its own.
|
|
775
|
+
const pending = new Set<TelemetrySink>()
|
|
715
776
|
const all = Promise.all(
|
|
716
777
|
sinks.map(async (sink) => {
|
|
717
778
|
if (disabled.has(sink) || sink.flush === undefined) return
|
|
779
|
+
pending.add(sink)
|
|
718
780
|
try {
|
|
719
781
|
await step(`${args.owners?.get(sink) ?? label(sink)}:telemetry`, () =>
|
|
720
782
|
sink.flush!(deadline.signal),
|
|
@@ -723,12 +785,17 @@ export function createTelemetrySource(args: {
|
|
|
723
785
|
// A flush failure can never break the run — but it still costs
|
|
724
786
|
// this sink its export, so say so rather than dropping it.
|
|
725
787
|
warn?.(`[vx] telemetry sink '${label(sink)}' failed to flush: ${thrownMessage(err)}`)
|
|
788
|
+
} finally {
|
|
789
|
+
pending.delete(sink)
|
|
726
790
|
}
|
|
727
791
|
}),
|
|
728
792
|
)
|
|
729
793
|
if (!(await settleWithin(all, ms))) {
|
|
730
794
|
deadline.abort(new Error(`telemetry flush deadline (${ms}ms)`))
|
|
731
|
-
|
|
795
|
+
const names = [...pending].map((s) => `'${label(s)}'`).join(', ')
|
|
796
|
+
warn?.(
|
|
797
|
+
`[vx] telemetry flush timed out after ${ms}ms (sink${pending.size === 1 ? '' : 's'} ${names}); buffered records lost`,
|
|
798
|
+
)
|
|
732
799
|
// A sink told of the abort says what it lost as its request ends; the
|
|
733
800
|
// run ended first and that line was lost (vx-otel, on CI). Briefly.
|
|
734
801
|
await settleWithin(all, ABORT_GRACE_MS)
|
|
@@ -767,7 +834,8 @@ export function taskTelemetryOf(o: TaskOutcome): TaskTelemetry {
|
|
|
767
834
|
project: o.node.projectName,
|
|
768
835
|
task: o.node.taskName,
|
|
769
836
|
status: o.status,
|
|
770
|
-
|
|
837
|
+
// A hit whose restore failed is in no cache column: neither a miss nor a hit.
|
|
838
|
+
cacheSource: o.restoreFailed === true ? 'none' : deriveCacheSource(o.status),
|
|
771
839
|
exitCode: o.exitCode,
|
|
772
840
|
durationMs: o.durationMs,
|
|
773
841
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
selectFoldedDeps,
|
|
3
|
+
type StandIns,
|
|
3
4
|
type FoldCandidate,
|
|
4
5
|
type TaskNode,
|
|
5
6
|
type TaskOutcome,
|
|
@@ -72,10 +73,11 @@ export function filterUpstreamHashes(
|
|
|
72
73
|
filter: readonly string[] | undefined,
|
|
73
74
|
selfProjectName: string,
|
|
74
75
|
selfTaskId: string,
|
|
76
|
+
standIns?: StandIns,
|
|
75
77
|
): Array<[upstreamTaskId: string, hash: string]> {
|
|
76
78
|
const candidates: FoldCandidate[] = []
|
|
77
79
|
for (const u of upstream) if (u.hash) candidates.push({ node: u.node, unit: u.hash })
|
|
78
|
-
return selectFoldedDeps(candidates, filter, selfProjectName, selfTaskId).map((c) => [
|
|
80
|
+
return selectFoldedDeps(candidates, filter, selfProjectName, selfTaskId, standIns).map((c) => [
|
|
79
81
|
c.node.id,
|
|
80
82
|
c.unit,
|
|
81
83
|
])
|