@vzn/vx 0.0.635 → 0.0.637
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 +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import type { ExecConfig, TaskConfig, CacheConfig } from '../config.js'
|
|
2
|
+
import type { ExecConfig, TaskConfig, CacheConfig, InputStability } from '../config.js'
|
|
3
3
|
import {
|
|
4
4
|
ArtifactVanishedError,
|
|
5
5
|
type CacheEntry,
|
|
6
6
|
type CacheLayer,
|
|
7
7
|
type CachePolicy,
|
|
8
|
+
anyUntouched,
|
|
8
9
|
cleanOutputs,
|
|
10
|
+
committedFiles,
|
|
9
11
|
cleanWorkspaceOutputs,
|
|
10
12
|
type OutputStamp,
|
|
11
13
|
ownOutputsSince,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
stampWorkspaceOutputs,
|
|
15
17
|
FULL_CACHE_POLICY,
|
|
16
18
|
type GitFilesCache,
|
|
19
|
+
racyWindowMs,
|
|
17
20
|
} from '../cache/index.js'
|
|
18
21
|
import {
|
|
19
22
|
buildIsolatedEnv,
|
|
@@ -51,7 +54,9 @@ import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '..
|
|
|
51
54
|
import {
|
|
52
55
|
killGraceMs,
|
|
53
56
|
MASKED,
|
|
57
|
+
parseDuration,
|
|
54
58
|
maskedEmitter,
|
|
59
|
+
outputMatcher,
|
|
55
60
|
printable,
|
|
56
61
|
relPosix,
|
|
57
62
|
secretMask,
|
|
@@ -61,12 +66,15 @@ import {
|
|
|
61
66
|
} from '../util/index.js'
|
|
62
67
|
import { forwardedSignal, SIGNAL_SHUTDOWN_GRACE_MS, terminateChildren } from './signals.js'
|
|
63
68
|
import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
69
|
+
import type { CommandWindows } from './command-windows.js'
|
|
64
70
|
import {
|
|
71
|
+
commandWriteReach,
|
|
65
72
|
mayWriteFingerprint,
|
|
66
73
|
type Placeholder,
|
|
67
74
|
placeholderSweeper,
|
|
68
75
|
reachedWithheld,
|
|
69
76
|
sandboxRequestFor,
|
|
77
|
+
sandboxReadable,
|
|
70
78
|
sweepPlaceholders,
|
|
71
79
|
undeclaredWriteReach,
|
|
72
80
|
untouchedPlaceholderLine,
|
|
@@ -88,6 +96,7 @@ import {
|
|
|
88
96
|
computeTaskHash,
|
|
89
97
|
describeTaskInputs,
|
|
90
98
|
type HashCache,
|
|
99
|
+
type InputFact,
|
|
91
100
|
movedInput,
|
|
92
101
|
type TaskInputComponent,
|
|
93
102
|
type UpfrontGroupKey,
|
|
@@ -99,7 +108,6 @@ export interface ExecuteArgs {
|
|
|
99
108
|
node: TaskNode
|
|
100
109
|
upstream: TaskOutcome[]
|
|
101
110
|
workspaceRoot: string
|
|
102
|
-
workspaceFingerprint: string
|
|
103
111
|
cache: CacheLayer
|
|
104
112
|
/** Granular cache read/write policy. Undefined → everything on. */
|
|
105
113
|
cachePolicy?: CachePolicy
|
|
@@ -121,7 +129,7 @@ export interface ExecuteArgs {
|
|
|
121
129
|
*/
|
|
122
130
|
remoteOnly?: boolean
|
|
123
131
|
/**
|
|
124
|
-
* Plan-time `--download` decision for THIS task (run.ts). `'deferred'`
|
|
132
|
+
* Plan-time `--cache-download` decision for THIS task (run.ts). `'deferred'`
|
|
125
133
|
* means: do not clean, do not save — the executor is expected to leave
|
|
126
134
|
* the outputs in the remote store and hand back a closure. An executor
|
|
127
135
|
* that ignores it and materialises anyway simply gets no local entry;
|
|
@@ -163,21 +171,29 @@ export interface ExecuteArgs {
|
|
|
163
171
|
/** Name what a missed key changed (`createMissExplainer`); absent when nobody listens. */
|
|
164
172
|
explainMiss?: MissExplainer
|
|
165
173
|
/**
|
|
166
|
-
* Run-level retry default (`--
|
|
174
|
+
* Run-level retry default (`--retries <n>` / `RunOptions.retries`).
|
|
167
175
|
* Explicit `exec.retries` wins, including an explicit 0. Threaded as
|
|
168
176
|
* an option only — never folded into any hash, so cache keys are
|
|
169
177
|
* byte-identical with and without it.
|
|
170
178
|
*/
|
|
171
179
|
retries?: number
|
|
172
180
|
/**
|
|
173
|
-
*
|
|
174
|
-
* `
|
|
175
|
-
* `exec.timeout` wins. Threaded as an option only — never hashed.
|
|
181
|
+
* `run.timeout` (ms), already resolved from flag, variable and
|
|
182
|
+
* workspace. Per-task `exec.timeout` wins. Never hashed.
|
|
176
183
|
*/
|
|
177
184
|
timeout?: number
|
|
185
|
+
/** `run.readyNotice` (ms), resolved like `timeout`; unset, 10 s. */
|
|
186
|
+
readyNotice?: number
|
|
187
|
+
/**
|
|
188
|
+
* The run's `inputStability` layers around the task's own: `run` from
|
|
189
|
+
* the env vars (above the task), `workspace` from `.vx/workspace` (below).
|
|
190
|
+
*/
|
|
191
|
+
inputStability?: { run: InputStability; workspace?: InputStability }
|
|
192
|
+
/** `cache.outputs.clean` for tasks without their own: its variable, else `.vx/workspace`. */
|
|
193
|
+
cleanOutputs?: boolean
|
|
178
194
|
/** Per-run memo for `git ls-files` (one entry per project dir). */
|
|
179
195
|
gitFilesCache?: GitFilesCache
|
|
180
|
-
/** Per-run memo for derived hashes (
|
|
196
|
+
/** Per-run memo for derived hashes (task config, file lists). */
|
|
181
197
|
hashCache?: HashCache
|
|
182
198
|
/** The run's `probesAfterWrites`, handed to every key this task takes. */
|
|
183
199
|
probesAfterWrites?: ReadonlySet<string>
|
|
@@ -242,8 +258,8 @@ export interface ExecuteArgs {
|
|
|
242
258
|
*/
|
|
243
259
|
taintedUpstream?: boolean
|
|
244
260
|
/**
|
|
245
|
-
* Whether a task has rewritten a file
|
|
246
|
-
* since the run read it (fingerprint-watch.ts): past that, no key taken
|
|
261
|
+
* Whether a task has rewritten a root file a plugin claims (whose bytes
|
|
262
|
+
* the claimant folded) since the run read it (fingerprint-watch.ts): past that, no key taken
|
|
247
263
|
* on the old digest is probed or saved.
|
|
248
264
|
*/
|
|
249
265
|
fingerprintWatch?: FingerprintWatch
|
|
@@ -255,6 +271,8 @@ export interface ExecuteArgs {
|
|
|
255
271
|
noDependants?: true
|
|
256
272
|
/** The task holds vx's terminal (`terminalHolders`): its stdio is vx's own. */
|
|
257
273
|
terminal?: true
|
|
274
|
+
/** When each command of the run ran and where it may write (command-windows.ts). */
|
|
275
|
+
commandWindows?: CommandWindows
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
/**
|
|
@@ -338,20 +356,31 @@ async function restoreOrMiss(
|
|
|
338
356
|
}
|
|
339
357
|
|
|
340
358
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* the
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* 2026-10-12).
|
|
359
|
+
* What a write to an input during the command means, each field highest
|
|
360
|
+
* first: the env var (`layers.run`), the task's `cache.inputStability`,
|
|
361
|
+
* the workspace's, then `'immutable'` and `'warn'` (owner, 2026-10-12), so
|
|
362
|
+
* by default any such write withholds the save, on CI too, since a
|
|
363
|
+
* concurrent task can change an input and put it back.
|
|
347
364
|
*/
|
|
348
|
-
function
|
|
349
|
-
|
|
365
|
+
function inputStabilityOf(
|
|
366
|
+
cache: CacheConfig | undefined,
|
|
367
|
+
layers: ExecuteArgs['inputStability'],
|
|
368
|
+
): Required<InputStability> {
|
|
369
|
+
const task = cache?.inputStability
|
|
370
|
+
return {
|
|
371
|
+
strategy: layers?.run.strategy ?? task?.strategy ?? layers?.workspace?.strategy ?? 'immutable',
|
|
372
|
+
severity: layers?.run.severity ?? task?.severity ?? layers?.workspace?.severity ?? 'warn',
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The root files the plugins claim, as the run's watch holds them. */
|
|
377
|
+
function claimedFiles(args: ExecuteArgs): readonly string[] {
|
|
378
|
+
return args.fingerprintWatch?.names ?? []
|
|
350
379
|
}
|
|
351
380
|
|
|
352
381
|
/**
|
|
353
|
-
*
|
|
354
|
-
* rewrote the lockfile), said once per run.
|
|
382
|
+
* A claimed file every key here folded through its claimant has moved (a
|
|
383
|
+
* task rewrote the lockfile), said once per run.
|
|
355
384
|
*/
|
|
356
385
|
function fingerprintMoved(args: ExecuteArgs): boolean {
|
|
357
386
|
if (args.fingerprintWatch?.moved() === undefined) return false
|
|
@@ -379,7 +408,6 @@ async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
379
408
|
node: args.node,
|
|
380
409
|
upstream: args.upstream,
|
|
381
410
|
workspaceRoot: args.workspaceRoot,
|
|
382
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
383
411
|
cache: args.cache,
|
|
384
412
|
forwardArgs: args.forwardArgs,
|
|
385
413
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -475,7 +503,6 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
475
503
|
node,
|
|
476
504
|
upstream: args.upstream,
|
|
477
505
|
workspaceRoot: args.workspaceRoot,
|
|
478
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
479
506
|
cache: args.cache,
|
|
480
507
|
forwardArgs: args.forwardArgs,
|
|
481
508
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -537,6 +564,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
537
564
|
const serverSecrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
538
565
|
const serverOut = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStdout(node, t))
|
|
539
566
|
const serverErr = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStderr(node, t))
|
|
567
|
+
// A server writes until the run stops it: its window stays open.
|
|
568
|
+
args.commandWindows?.open(node.id, node.projectDir, commandWriteReach(node, args.workspaceRoot))
|
|
540
569
|
const persistentOpts: Parameters<typeof runPersistent>[0] = {
|
|
541
570
|
command,
|
|
542
571
|
cwd: node.projectDir,
|
|
@@ -555,7 +584,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
555
584
|
}
|
|
556
585
|
// For a persistent task the timeout bounds the readiness wait. Per-task
|
|
557
586
|
// `exec.timeout` wins; else the run-level default (env/workspace/--timeout).
|
|
558
|
-
const effectiveTimeout = step
|
|
587
|
+
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
559
588
|
if (effectiveTimeout !== undefined) {
|
|
560
589
|
persistentOpts.timeoutMs = effectiveTimeout
|
|
561
590
|
}
|
|
@@ -606,7 +635,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
606
635
|
// it fails, and with no `exec.timeout` the wait never ends, so a run
|
|
607
636
|
// whose `readyWhen` never matched showed nothing at all.
|
|
608
637
|
const readyWhen = step.persistent.readyWhen
|
|
609
|
-
|
|
638
|
+
// How long a server may take to match `readyWhen` before vx says it waits.
|
|
639
|
+
const noticeMs = args.readyNotice ?? 10_000
|
|
610
640
|
const notice =
|
|
611
641
|
readyWhen === undefined
|
|
612
642
|
? undefined
|
|
@@ -649,7 +679,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
649
679
|
const message = err instanceof Error ? err.message : String(err)
|
|
650
680
|
// The task's OWN stream, not the process's: the frame is where a
|
|
651
681
|
// reader looks for why a task failed, and a run with a custom logger
|
|
652
|
-
// (an embedder
|
|
682
|
+
// (an embedder) never saw a bare stderr write at all.
|
|
653
683
|
log.taskStderr(node, `\n[vx] ${node.id}: persistent task failed to become ready: ${message}\n`)
|
|
654
684
|
// A server is never traced, so the sandbox's refusals reach no report:
|
|
655
685
|
// a dev server that died on a file outside its grants read only as the
|
|
@@ -713,7 +743,9 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
713
743
|
}
|
|
714
744
|
args.persistentRegistry?.set(node.id, spawn.child)
|
|
715
745
|
forgetUndeclaredWrites(args, undeclaredWriteReach(node, args.workspaceRoot))
|
|
716
|
-
if (mayWriteFingerprint(node, args.workspaceRoot))
|
|
746
|
+
if (mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))) {
|
|
747
|
+
args.fingerprintWatch?.wrote()
|
|
748
|
+
}
|
|
717
749
|
return {
|
|
718
750
|
node,
|
|
719
751
|
status: 'success',
|
|
@@ -784,10 +816,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
784
816
|
const outputs = cacheCfg?.outputs.files ?? []
|
|
785
817
|
const wsOutputs = cacheCfg?.outputs.workspaceFiles ?? []
|
|
786
818
|
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
const effectiveTimeout = step.timeout ?? args.timeout
|
|
819
|
+
// Per-task `exec.timeout` beats `run.timeout` (flag, variable,
|
|
820
|
+
// workspace; resolved into `args.timeout` by run.ts).
|
|
821
|
+
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
791
822
|
// `signal` stops a plugin executor on the run's stop AND on
|
|
792
823
|
// `exec.timeout`: core cannot kill a process an executor spawned, so a
|
|
793
824
|
// declared timeout meant nothing on one that kept its own clock (H-12).
|
|
@@ -899,7 +930,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
899
930
|
node,
|
|
900
931
|
upstream,
|
|
901
932
|
workspaceRoot: args.workspaceRoot,
|
|
902
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
903
933
|
cache,
|
|
904
934
|
forwardArgs: args.forwardArgs,
|
|
905
935
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -930,8 +960,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
930
960
|
projectDir: node.projectDir,
|
|
931
961
|
outputs,
|
|
932
962
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
963
|
+
committed: committedFiles(args.gitFilesCache, node.projectDir),
|
|
964
|
+
}
|
|
965
|
+
const wsCleanArgs = {
|
|
966
|
+
workspaceRoot: args.workspaceRoot,
|
|
967
|
+
outputs: wsOutputs,
|
|
968
|
+
committed: committedFiles(args.gitFilesCache, args.workspaceRoot),
|
|
933
969
|
}
|
|
934
|
-
const wsCleanArgs = { workspaceRoot: args.workspaceRoot, outputs: wsOutputs }
|
|
935
970
|
// An ADDITIVE task (its outputs overlap an upstream's, with the edge that
|
|
936
971
|
// orders them; item 588) does not clean by glob — the glob selects the
|
|
937
972
|
// upstream's files it adds beside. Its outputs are stamped once before
|
|
@@ -941,6 +976,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
941
976
|
const additive = (node.addsToOutputsOf?.length ?? 0) > 0
|
|
942
977
|
let stampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
943
978
|
let wsStampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
979
|
+
// `cache.outputs.clean: false` leaves the outputs to the first attempt's
|
|
980
|
+
// command, stamped, so a save can tell what it found and never wrote.
|
|
981
|
+
// An additive task never cleans by glob anyway.
|
|
982
|
+
const keepOutputs = !additive && (cacheCfg?.outputs.clean ?? args.cleanOutputs ?? true) === false
|
|
983
|
+
let keptBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
984
|
+
let wsKeptBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
944
985
|
|
|
945
986
|
// Cache lookup. On hit, time the user-perceived restore op
|
|
946
987
|
// (clean+restore+log-replay) — that's what the framed-block footer
|
|
@@ -979,7 +1020,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
979
1020
|
node,
|
|
980
1021
|
upstream,
|
|
981
1022
|
workspaceRoot: args.workspaceRoot,
|
|
982
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
983
1023
|
cache,
|
|
984
1024
|
forwardArgs: args.forwardArgs,
|
|
985
1025
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -991,6 +1031,41 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
991
1031
|
: {}),
|
|
992
1032
|
})
|
|
993
1033
|
: undefined
|
|
1034
|
+
/** The other command that may have put a moved input back (`ownRewrite`). */
|
|
1035
|
+
let rewroteBy: string | undefined
|
|
1036
|
+
/**
|
|
1037
|
+
* Under `'mutable'`, a write during the command that left the key's bytes
|
|
1038
|
+
* is the task's own identical rewrite only if no other command that may
|
|
1039
|
+
* write the file ran between the describe and that write: one that did
|
|
1040
|
+
* may have changed it and put it back while this command read it.
|
|
1041
|
+
*/
|
|
1042
|
+
const ownRewrite = (file: string, ctimeMs: number): boolean => {
|
|
1043
|
+
rewroteBy = args.commandWindows?.writerDuring(
|
|
1044
|
+
node.id,
|
|
1045
|
+
file,
|
|
1046
|
+
described!.describedAt,
|
|
1047
|
+
ctimeMs + racyWindowMs(ctimeMs, 0),
|
|
1048
|
+
)
|
|
1049
|
+
return rewroteBy === undefined
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* A committed file under the task's own outputs is keyed (`committedFiles`)
|
|
1054
|
+
* and the task may rewrite it (TanStack Router's `routeTree.gen.ts`): the
|
|
1055
|
+
* bytes it leaves are an output the entry saves and a restore writes, and
|
|
1056
|
+
* they follow from the key, so their change is no moved input.
|
|
1057
|
+
*/
|
|
1058
|
+
const ownOutput = outputs.length === 0 ? undefined : outputMatcher(outputs)
|
|
1059
|
+
const ownWsOutput = wsOutputs.length === 0 ? undefined : outputMatcher(wsOutputs)
|
|
1060
|
+
const keyedInputs = (facts: readonly InputFact[]): readonly InputFact[] =>
|
|
1061
|
+
ownOutput === undefined && ownWsOutput === undefined
|
|
1062
|
+
? facts
|
|
1063
|
+
: facts.filter(
|
|
1064
|
+
(f) =>
|
|
1065
|
+
ownOutput?.(relPosix(node.projectDir, f.path)) !== true &&
|
|
1066
|
+
ownWsOutput?.(relPosix(args.workspaceRoot, f.path)) !== true,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
994
1069
|
// A name only `exec.env.secret` makes secret is not one the name rule
|
|
995
1070
|
// sees later: its row carries the mark, so `vx why` hides its hash (M-63).
|
|
996
1071
|
const named = node.config.exec?.env?.secret
|
|
@@ -1050,7 +1125,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1050
1125
|
let withheld: WithheldLink[] = []
|
|
1051
1126
|
|
|
1052
1127
|
// Cache miss path (or caching disabled), up to `1 + retries` attempts.
|
|
1053
|
-
// Explicit config wins over the run-level `--
|
|
1128
|
+
// Explicit config wins over the run-level `--retries` default, including
|
|
1054
1129
|
// an explicit `retries: 0`.
|
|
1055
1130
|
const maxAttempts = 1 + (step.retries ?? args.retries ?? 0)
|
|
1056
1131
|
let attempt = 0
|
|
@@ -1081,7 +1156,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1081
1156
|
const endStamp = span('miss: stamp outputs')
|
|
1082
1157
|
stampedBefore ??= await stampOutputs(cleanArgs)
|
|
1083
1158
|
endStamp()
|
|
1159
|
+
} else if (
|
|
1160
|
+
keepOutputs &&
|
|
1161
|
+
attempt === 1 &&
|
|
1162
|
+
willWrite &&
|
|
1163
|
+
!deferralRequested &&
|
|
1164
|
+
outputs.length > 0
|
|
1165
|
+
) {
|
|
1166
|
+
keptBefore = await stampOutputs(cleanArgs)
|
|
1084
1167
|
} else if (willWrite && !deferralRequested && outputs.length > 0) {
|
|
1168
|
+
keptBefore = undefined
|
|
1085
1169
|
// Mark the wiped paths, exactly as the workspace twin below does. The
|
|
1086
1170
|
// git snapshot still lists them with their committed index OIDs, and
|
|
1087
1171
|
// resolveFiles SKIPS its existence probe for any path carrying a
|
|
@@ -1091,7 +1175,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1091
1175
|
const endClean = span('miss: clean outputs')
|
|
1092
1176
|
const cleanedRels = await cleanOutputs(cleanArgs)
|
|
1093
1177
|
endClean()
|
|
1094
|
-
args.gitFilesCache?.noteClean(node.id, node.projectDir, cleanedRels)
|
|
1095
1178
|
args.gitFilesCache?.markOutputsChanged(node.projectDir, cleanedRels)
|
|
1096
1179
|
}
|
|
1097
1180
|
if (additive && willWrite && !deferralRequested && wsOutputs.length > 0) {
|
|
@@ -1099,7 +1182,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1099
1182
|
// glob clean deleted the upstream's files before this task, which
|
|
1100
1183
|
// reads them, ran (A-43).
|
|
1101
1184
|
wsStampedBefore ??= await stampWorkspaceOutputs(wsCleanArgs)
|
|
1185
|
+
} else if (
|
|
1186
|
+
keepOutputs &&
|
|
1187
|
+
attempt === 1 &&
|
|
1188
|
+
willWrite &&
|
|
1189
|
+
!deferralRequested &&
|
|
1190
|
+
wsOutputs.length > 0
|
|
1191
|
+
) {
|
|
1192
|
+
wsKeptBefore = await stampWorkspaceOutputs(wsCleanArgs)
|
|
1102
1193
|
} else if (willWrite && !deferralRequested && wsOutputs.length > 0) {
|
|
1194
|
+
wsKeptBefore = undefined
|
|
1103
1195
|
// Root-anchored deletions can land in other projects' dirs; mark
|
|
1104
1196
|
// them so stale per-project git snapshots can't survive the wipe.
|
|
1105
1197
|
const cleanedWsRels = await cleanWorkspaceOutputs(wsCleanArgs)
|
|
@@ -1128,6 +1220,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1128
1220
|
// the throw is its rejection, the value its resolution, so both reach
|
|
1129
1221
|
// the naming catch and the cleanup in finally below.
|
|
1130
1222
|
let running: Promise<unknown>
|
|
1223
|
+
const closeWindow =
|
|
1224
|
+
args.commandWindows?.open(
|
|
1225
|
+
node.id,
|
|
1226
|
+
node.projectDir,
|
|
1227
|
+
commandWriteReach(node, args.workspaceRoot),
|
|
1228
|
+
) ?? (() => {})
|
|
1131
1229
|
try {
|
|
1132
1230
|
running = Promise.resolve(args.executor.execute(req))
|
|
1133
1231
|
} catch (thrown) {
|
|
@@ -1166,6 +1264,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1166
1264
|
throw err
|
|
1167
1265
|
})
|
|
1168
1266
|
.finally(() => {
|
|
1267
|
+
closeWindow()
|
|
1169
1268
|
clearTimeout(timeoutTimer)
|
|
1170
1269
|
unlistenStop?.()
|
|
1171
1270
|
unlistenStop = undefined
|
|
@@ -1256,18 +1355,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1256
1355
|
// The line quotes the command's first word, which a config may have
|
|
1257
1356
|
// built from a secret: masked as the task's own output is (L-37).
|
|
1258
1357
|
if (verdict !== undefined) log.taskStderr(node, `\n${secrets?.mask(verdict) ?? verdict}\n`)
|
|
1259
|
-
// A committed file another task's clean removed, still gone: vx
|
|
1260
|
-
// cleans outputs before a run where Turbo does not, so a reader with
|
|
1261
|
-
// no edge to the producer fails naming only the file (A-48).
|
|
1262
|
-
if (code !== 0) {
|
|
1263
|
-
for (const c of (args.gitFilesCache?.trackedCleansMissing(node.id) ?? []).slice(0, 3)) {
|
|
1264
|
-
const rel = relPosix(args.workspaceRoot, c.path)
|
|
1265
|
-
log.taskStderr(
|
|
1266
|
-
node,
|
|
1267
|
-
`\n[vx] ${rel} is tracked by git, and ${c.by} removed it as an output before its run; it is still missing. A task that reads it needs dependsOn on ${c.by}.\n`,
|
|
1268
|
-
)
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
1358
|
}
|
|
1272
1359
|
return { result: res, exitCode: code }
|
|
1273
1360
|
}
|
|
@@ -1316,7 +1403,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1316
1403
|
const writeReach =
|
|
1317
1404
|
args.executor.remote === true ? 'none' : undeclaredWriteReach(node, args.workspaceRoot)
|
|
1318
1405
|
const writesFingerprint =
|
|
1319
|
-
args.executor.remote !== true &&
|
|
1406
|
+
args.executor.remote !== true &&
|
|
1407
|
+
mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))
|
|
1320
1408
|
// A stop that landed during the awaits above (the key, the probe, the
|
|
1321
1409
|
// input description): the first attempt would wipe the last build and
|
|
1322
1410
|
// hand the executor a request after the run had stopped. Through a call,
|
|
@@ -1462,6 +1550,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1462
1550
|
|
|
1463
1551
|
const wallclockEndNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
1464
1552
|
|
|
1553
|
+
const stability = inputStabilityOf(args.node.config.cache, args.inputStability)
|
|
1554
|
+
let movedFails: string | undefined
|
|
1465
1555
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1466
1556
|
if (effectiveExitCode === 0 && willSave && deferralRequested) {
|
|
1467
1557
|
// The outputs never landed here: no artifact, no rows. A partial local
|
|
@@ -1490,6 +1580,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1490
1580
|
} else if (
|
|
1491
1581
|
effectiveExitCode === 0 &&
|
|
1492
1582
|
willSave &&
|
|
1583
|
+
!leftBehind() &&
|
|
1493
1584
|
(await keyStillTrue()) &&
|
|
1494
1585
|
(own = await ownOutputs()) !== undefined
|
|
1495
1586
|
) {
|
|
@@ -1514,6 +1605,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1514
1605
|
deferSave: args.deferSave,
|
|
1515
1606
|
// A sink listens: say what the save cost.
|
|
1516
1607
|
measure: args.explainMiss !== undefined,
|
|
1608
|
+
...(userSandbox
|
|
1609
|
+
? {
|
|
1610
|
+
readable: sandboxReadable(
|
|
1611
|
+
node,
|
|
1612
|
+
cfg.exec!.sandbox!,
|
|
1613
|
+
args.workspaceRoot,
|
|
1614
|
+
args.nestedProjectDirs,
|
|
1615
|
+
),
|
|
1616
|
+
}
|
|
1617
|
+
: {}),
|
|
1517
1618
|
})
|
|
1518
1619
|
args.deferredSaves?.set(node.id, landed)
|
|
1519
1620
|
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants !== true) {
|
|
@@ -1541,6 +1642,18 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1541
1642
|
}
|
|
1542
1643
|
}
|
|
1543
1644
|
|
|
1645
|
+
/**
|
|
1646
|
+
* A file the kept outputs held that the run left as it was: saving would
|
|
1647
|
+
* file a leftover from an older build as this run's output, so nothing is
|
|
1648
|
+
* saved (`cache.outputs.clean: false`).
|
|
1649
|
+
*/
|
|
1650
|
+
function leftBehind(): boolean {
|
|
1651
|
+
return (
|
|
1652
|
+
(keptBefore !== undefined && anyUntouched(keptBefore)) ||
|
|
1653
|
+
(wsKeptBefore !== undefined && anyUntouched(wsKeptBefore))
|
|
1654
|
+
)
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1544
1657
|
/**
|
|
1545
1658
|
* What an additive run saves as its own, or undefined when it removed a
|
|
1546
1659
|
* file it found: no entry replays a removal (`ownOutputsSince`), so the
|
|
@@ -1573,10 +1686,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1573
1686
|
if (described!.hash !== hash) return null
|
|
1574
1687
|
return (
|
|
1575
1688
|
(await movedInput(
|
|
1576
|
-
described!.facts,
|
|
1689
|
+
keyedInputs(described!.facts),
|
|
1577
1690
|
cache,
|
|
1578
1691
|
described!.describedAt,
|
|
1579
|
-
|
|
1692
|
+
stability.strategy === 'mutable' ? ownRewrite : undefined,
|
|
1580
1693
|
)) ?? described!.added()
|
|
1581
1694
|
)
|
|
1582
1695
|
}
|
|
@@ -1597,29 +1710,44 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1597
1710
|
unkeyed = true
|
|
1598
1711
|
return false
|
|
1599
1712
|
}
|
|
1713
|
+
// `'unchecked'` skips the check of the command's own window; a key the
|
|
1714
|
+
// re-derive before the command already disagreed with still withholds,
|
|
1715
|
+
// since it never described the bytes the command read.
|
|
1716
|
+
if (stability.strategy === 'unchecked' && described!.hash === hash) return true
|
|
1600
1717
|
const endCheck = span('miss: recheck inputs')
|
|
1601
1718
|
const moved = await movedSinceKey()
|
|
1602
1719
|
endCheck()
|
|
1603
1720
|
if (moved === undefined) return true
|
|
1604
1721
|
const what =
|
|
1605
|
-
moved === null ? 'its inputs' : `\`${printable(relPosix(args.workspaceRoot, moved))}\``
|
|
1722
|
+
(moved === null ? 'its inputs' : `\`${printable(relPosix(args.workspaceRoot, moved))}\``) +
|
|
1723
|
+
' changed after its key was taken' +
|
|
1724
|
+
(rewroteBy === undefined ? '' : ` while ${rewroteBy} ran`)
|
|
1606
1725
|
// A task that regenerates its own input (TanStack Router's committed
|
|
1607
1726
|
// `routeTree.gen.ts`, rewritten to the same bytes by every build) is
|
|
1608
1727
|
// never saved (item 1015), and the line said so without the way out.
|
|
1609
|
-
// Not for `package.json`, which an output may not cover.
|
|
1610
1728
|
const hint =
|
|
1611
|
-
moved === null ||
|
|
1729
|
+
moved === null || rewroteBy !== undefined
|
|
1612
1730
|
? ''
|
|
1613
1731
|
: '; if the task writes it, declare it in cache.outputs'
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
`
|
|
1617
|
-
|
|
1732
|
+
if (stability.severity === 'error') {
|
|
1733
|
+
log.status(
|
|
1734
|
+
`[vx] ${node.id}: ${what} — the result is not saved, ` +
|
|
1735
|
+
`and inputStability.severity is 'error', so the task fails${hint}`,
|
|
1736
|
+
)
|
|
1737
|
+
movedFails = moved === null ? '' : printable(relPosix(args.workspaceRoot, moved))
|
|
1738
|
+
} else {
|
|
1739
|
+
log.status(
|
|
1740
|
+
`[vx] ${node.id}: ${what} — the result stands, ` +
|
|
1741
|
+
`but is not saved under a key that no longer describes it${hint}`,
|
|
1742
|
+
)
|
|
1743
|
+
}
|
|
1618
1744
|
forgetUndeclaredWrites(args, wsOutputs.length > 0 ? 'workspace' : 'project')
|
|
1619
1745
|
unkeyed = true
|
|
1620
1746
|
return false
|
|
1621
1747
|
}
|
|
1622
1748
|
|
|
1749
|
+
if (movedFails !== undefined) effectiveExitCode = 1
|
|
1750
|
+
|
|
1623
1751
|
if (unkeyedUpstream !== undefined && willWrite && effectiveExitCode === 0) {
|
|
1624
1752
|
log.status(
|
|
1625
1753
|
`[vx] ${node.id}: ran over ${unkeyedUpstream.node.id}'s outputs, which its key no longer ` +
|
|
@@ -1647,6 +1775,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1647
1775
|
? { failedOutput: failedOutputLog(output, willSave) }
|
|
1648
1776
|
: {}),
|
|
1649
1777
|
...(result.timedOut === true && effectiveExitCode !== 0 ? { timedOut: true as const } : {}),
|
|
1778
|
+
...(movedFails !== undefined ? { inputMoved: movedFails } : {}),
|
|
1650
1779
|
...(result.cpuMs !== undefined ? { cpuMs: result.cpuMs } : {}),
|
|
1651
1780
|
...(result.peakRssBytes !== undefined ? { peakRssBytes: result.peakRssBytes } : {}),
|
|
1652
1781
|
...(result.where !== undefined ? { where: result.where } : {}),
|
|
@@ -1677,7 +1806,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1677
1806
|
/**
|
|
1678
1807
|
* Drop the run's facts about files a task may have written without
|
|
1679
1808
|
* declaring them (`undeclaredWriteReach`): the project's git snapshot and
|
|
1680
|
-
* its index OIDs
|
|
1809
|
+
* its index OIDs. The next reader
|
|
1681
1810
|
* re-enumerates the project (one `git ls-files`) and hashes its files by
|
|
1682
1811
|
* content; the per-declaration file-list memos are keyed on the snapshot
|
|
1683
1812
|
* array, so they miss with it (item 743).
|
|
@@ -1686,12 +1815,10 @@ function forgetUndeclaredWrites(args: ExecuteArgs, reach: 'none' | 'project' | '
|
|
|
1686
1815
|
if (reach === 'none') return
|
|
1687
1816
|
if (reach === 'workspace') {
|
|
1688
1817
|
args.gitFilesCache?.clear()
|
|
1689
|
-
args.hashCache?.packageJson.clear()
|
|
1690
1818
|
return
|
|
1691
1819
|
}
|
|
1692
1820
|
args.gitFilesCache?.delete(args.node.projectDir)
|
|
1693
1821
|
args.gitFilesCache?.invalidateWorkspacePartition()
|
|
1694
|
-
args.hashCache?.packageJson.delete(args.node.projectDir)
|
|
1695
1822
|
}
|
|
1696
1823
|
|
|
1697
1824
|
/** The two bin directories a task's PATH starts with: its own, then the root's (once when they coincide). */
|
|
@@ -1738,16 +1865,11 @@ function taskEnv(node: TaskNode, step: ExecConfig, workspaceRoot: string): NodeJ
|
|
|
1738
1865
|
return env
|
|
1739
1866
|
}
|
|
1740
1867
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1868
|
+
/** A task's own `exec.timeout` in ms; the schema has checked its form. */
|
|
1869
|
+
function taskTimeoutMs(step: ExecConfig): number | undefined {
|
|
1870
|
+
return step.timeout === undefined ? undefined : parseDuration(step.timeout)!
|
|
1743
1871
|
}
|
|
1744
1872
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
* `VX_READY_NOTICE_MS` overrides it, as `VX_KILL_GRACE_MS` does the grace.
|
|
1748
|
-
*/
|
|
1749
|
-
function readyNoticeMs(): number {
|
|
1750
|
-
const raw = process.env['VX_READY_NOTICE_MS']
|
|
1751
|
-
if (raw !== undefined && /^[0-9]+$/.test(raw) && Number(raw) > 0) return Number(raw)
|
|
1752
|
-
return 10_000
|
|
1873
|
+
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
1874
|
+
return signal?.aborted === true
|
|
1753
1875
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// files — `pnpm install` without `--frozen-lockfile` — and every key taken
|
|
1
|
+
// The files the plugins claim (`VxPlugin.fingerprint`) are read once per
|
|
2
|
+
// run, before any task, and every key folds what their claimant takes
|
|
3
|
+
// from them (a lockfile plugin's per-project parts). A task in the root
|
|
4
|
+
// project may rewrite one of those files — `pnpm install` without `--frozen-lockfile` — and every key taken
|
|
5
5
|
// before it then names a lockfile the tree no longer holds: a save under
|
|
6
6
|
// one filed bytes built against the new install under the old lockfile's
|
|
7
7
|
// key, replayed whenever the tree went back (item 750). The watch answers
|
|
8
|
-
// "has a
|
|
8
|
+
// "has a claimed file moved since the run read it", re-learning the
|
|
9
9
|
// fact only after a task that may have written one ran (`wrote`), so a run
|
|
10
10
|
// with no such task never touches the files again.
|
|
11
11
|
|
|
12
12
|
import { readFileSync, statSync } from 'node:fs'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { FILE_HASH_RACY_MS, racyWindowMs } from '../cache/index.js'
|
|
15
|
-
import { WORKSPACE_FINGERPRINT_FILES, type WorkspaceFingerprints } from '../workspace/index.js'
|
|
16
15
|
import type { Logger } from './logger.js'
|
|
17
16
|
|
|
18
17
|
export class FingerprintWatch {
|
|
@@ -20,22 +19,26 @@ export class FingerprintWatch {
|
|
|
20
19
|
private movedFiles: string[] | undefined
|
|
21
20
|
private said = false
|
|
22
21
|
|
|
23
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* `names`: the claimed files; `read`: the bytes of those that existed;
|
|
24
|
+
* `at`: when the run started reading them (ms since the epoch).
|
|
25
|
+
*/
|
|
24
26
|
constructor(
|
|
25
27
|
private readonly workspaceRoot: string,
|
|
26
|
-
|
|
28
|
+
readonly names: readonly string[],
|
|
29
|
+
private readonly read: ReadonlyMap<string, Uint8Array>,
|
|
27
30
|
private readonly at: number,
|
|
28
31
|
) {}
|
|
29
32
|
|
|
30
|
-
/** A task that may rewrite a
|
|
33
|
+
/** A task that may rewrite a claimed file (`mayWriteFingerprint`) ran a command. */
|
|
31
34
|
wrote(): void {
|
|
32
35
|
this.pending = true
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
|
-
* The
|
|
39
|
+
* The claimed files that no longer hold what the run read, or
|
|
37
40
|
* undefined. Once one has, the run stays moved: every key already taken
|
|
38
|
-
* folded the old bytes. One `stat` per
|
|
41
|
+
* folded the old bytes. One `stat` per claimed file after each writer; a
|
|
39
42
|
* file written since the read (by ctime, with git's racy window) is read
|
|
40
43
|
* and compared. The stat follows a link as both reads do: a lockfile
|
|
41
44
|
* that is a symlink, rewritten through it, moves its target's ctime and
|
|
@@ -46,9 +49,9 @@ export class FingerprintWatch {
|
|
|
46
49
|
if (this.movedFiles !== undefined || !this.pending) return this.movedFiles
|
|
47
50
|
this.pending = false
|
|
48
51
|
const moved: string[] = []
|
|
49
|
-
for (const f of
|
|
52
|
+
for (const f of this.names) {
|
|
50
53
|
const abs = path.join(this.workspaceRoot, f)
|
|
51
|
-
const before = this.read.
|
|
54
|
+
const before = this.read.get(f)
|
|
52
55
|
let ctimeMs: number
|
|
53
56
|
try {
|
|
54
57
|
ctimeMs = statSync(abs).ctimeMs
|
|
@@ -81,8 +84,8 @@ export class FingerprintWatch {
|
|
|
81
84
|
if (this.said || this.movedFiles === undefined) return
|
|
82
85
|
this.said = true
|
|
83
86
|
log.status(
|
|
84
|
-
`[vx] a task rewrote \`${this.movedFiles.join('`, `')}\` during the run, which
|
|
85
|
-
`folds
|
|
87
|
+
`[vx] a task rewrote \`${this.movedFiles.join('`, `')}\` during the run, which a plugin ` +
|
|
88
|
+
`folds into every key: nothing keyed before it is restored or saved from here on`,
|
|
86
89
|
)
|
|
87
90
|
}
|
|
88
91
|
}
|