@vzn/vx 0.0.651 → 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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- 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 +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- 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 +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- 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 +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- 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 +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- 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 +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- 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-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- 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 +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- 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 +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- 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 +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -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/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- 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 +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- 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 +142 -6
- 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 +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- 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 +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- 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 +58 -31
- 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,12 +380,16 @@ 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
|
|
295
|
-
*
|
|
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.
|
|
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
|
|
298
395
|
* (tsdown's package.json), not an edit undone, unless another command that
|
|
@@ -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,27 +442,40 @@ 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. */
|
|
354
469
|
function inputsArgs(args: ComputeHashArgs): ResolveInputsArgs {
|
|
355
470
|
const cfg = args.node.config
|
|
356
|
-
const
|
|
471
|
+
const cfgOut = cfg.outputs
|
|
357
472
|
return {
|
|
358
473
|
projectDir: args.node.projectDir,
|
|
359
474
|
workspaceRoot: args.workspaceRoot,
|
|
360
475
|
envSource: process.env,
|
|
361
476
|
inputs: cfg.inputs,
|
|
362
|
-
ownOutputs:
|
|
363
|
-
ownWorkspaceOutputs:
|
|
477
|
+
ownOutputs: cfgOut?.files ?? [],
|
|
478
|
+
ownWorkspaceOutputs: cfgOut?.workspaceFiles ?? [],
|
|
364
479
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
365
480
|
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
366
481
|
...(args.probesAfterWrites?.has(args.node.id) === true ? { runtimeScope: args.node.id } : {}),
|
|
@@ -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
|
}
|