@vzn/vx 0.0.636 → 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 +39 -11
- package/docs/caching.md +213 -207
- package/docs/cli.md +699 -655
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +26 -25
- package/docs/schema.md +376 -198
- 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 +5 -3
- 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 -63
- 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 +103 -149
- 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 +169 -70
- 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 +24 -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 +158 -63
- 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 +42 -29
- 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 +113 -72
- 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 +121 -95
- 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 +209 -268
- 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 +20 -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
|
@@ -5,7 +5,9 @@ import {
|
|
|
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,26 +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
|
|
178
187
|
/**
|
|
179
188
|
* The run's `inputStability` layers around the task's own: `run` from
|
|
180
|
-
* the env vars (above the task), `workspace` from
|
|
189
|
+
* the env vars (above the task), `workspace` from `.vx/workspace` (below).
|
|
181
190
|
*/
|
|
182
191
|
inputStability?: { run: InputStability; workspace?: InputStability }
|
|
192
|
+
/** `cache.outputs.clean` for tasks without their own: its variable, else `.vx/workspace`. */
|
|
193
|
+
cleanOutputs?: boolean
|
|
183
194
|
/** Per-run memo for `git ls-files` (one entry per project dir). */
|
|
184
195
|
gitFilesCache?: GitFilesCache
|
|
185
|
-
/** Per-run memo for derived hashes (
|
|
196
|
+
/** Per-run memo for derived hashes (task config, file lists). */
|
|
186
197
|
hashCache?: HashCache
|
|
187
198
|
/** The run's `probesAfterWrites`, handed to every key this task takes. */
|
|
188
199
|
probesAfterWrites?: ReadonlySet<string>
|
|
@@ -247,8 +258,8 @@ export interface ExecuteArgs {
|
|
|
247
258
|
*/
|
|
248
259
|
taintedUpstream?: boolean
|
|
249
260
|
/**
|
|
250
|
-
* Whether a task has rewritten a file
|
|
251
|
-
* 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
|
|
252
263
|
* on the old digest is probed or saved.
|
|
253
264
|
*/
|
|
254
265
|
fingerprintWatch?: FingerprintWatch
|
|
@@ -260,6 +271,8 @@ export interface ExecuteArgs {
|
|
|
260
271
|
noDependants?: true
|
|
261
272
|
/** The task holds vx's terminal (`terminalHolders`): its stdio is vx's own. */
|
|
262
273
|
terminal?: true
|
|
274
|
+
/** When each command of the run ran and where it may write (command-windows.ts). */
|
|
275
|
+
commandWindows?: CommandWindows
|
|
263
276
|
}
|
|
264
277
|
|
|
265
278
|
/**
|
|
@@ -360,9 +373,14 @@ function inputStabilityOf(
|
|
|
360
373
|
}
|
|
361
374
|
}
|
|
362
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 ?? []
|
|
379
|
+
}
|
|
380
|
+
|
|
363
381
|
/**
|
|
364
|
-
*
|
|
365
|
-
* 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.
|
|
366
384
|
*/
|
|
367
385
|
function fingerprintMoved(args: ExecuteArgs): boolean {
|
|
368
386
|
if (args.fingerprintWatch?.moved() === undefined) return false
|
|
@@ -390,7 +408,6 @@ async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
390
408
|
node: args.node,
|
|
391
409
|
upstream: args.upstream,
|
|
392
410
|
workspaceRoot: args.workspaceRoot,
|
|
393
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
394
411
|
cache: args.cache,
|
|
395
412
|
forwardArgs: args.forwardArgs,
|
|
396
413
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -486,7 +503,6 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
486
503
|
node,
|
|
487
504
|
upstream: args.upstream,
|
|
488
505
|
workspaceRoot: args.workspaceRoot,
|
|
489
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
490
506
|
cache: args.cache,
|
|
491
507
|
forwardArgs: args.forwardArgs,
|
|
492
508
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -548,6 +564,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
548
564
|
const serverSecrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
549
565
|
const serverOut = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStdout(node, t))
|
|
550
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))
|
|
551
569
|
const persistentOpts: Parameters<typeof runPersistent>[0] = {
|
|
552
570
|
command,
|
|
553
571
|
cwd: node.projectDir,
|
|
@@ -566,7 +584,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
566
584
|
}
|
|
567
585
|
// For a persistent task the timeout bounds the readiness wait. Per-task
|
|
568
586
|
// `exec.timeout` wins; else the run-level default (env/workspace/--timeout).
|
|
569
|
-
const effectiveTimeout = step
|
|
587
|
+
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
570
588
|
if (effectiveTimeout !== undefined) {
|
|
571
589
|
persistentOpts.timeoutMs = effectiveTimeout
|
|
572
590
|
}
|
|
@@ -617,7 +635,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
617
635
|
// it fails, and with no `exec.timeout` the wait never ends, so a run
|
|
618
636
|
// whose `readyWhen` never matched showed nothing at all.
|
|
619
637
|
const readyWhen = step.persistent.readyWhen
|
|
620
|
-
|
|
638
|
+
// How long a server may take to match `readyWhen` before vx says it waits.
|
|
639
|
+
const noticeMs = args.readyNotice ?? 10_000
|
|
621
640
|
const notice =
|
|
622
641
|
readyWhen === undefined
|
|
623
642
|
? undefined
|
|
@@ -660,7 +679,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
660
679
|
const message = err instanceof Error ? err.message : String(err)
|
|
661
680
|
// The task's OWN stream, not the process's: the frame is where a
|
|
662
681
|
// reader looks for why a task failed, and a run with a custom logger
|
|
663
|
-
// (an embedder
|
|
682
|
+
// (an embedder) never saw a bare stderr write at all.
|
|
664
683
|
log.taskStderr(node, `\n[vx] ${node.id}: persistent task failed to become ready: ${message}\n`)
|
|
665
684
|
// A server is never traced, so the sandbox's refusals reach no report:
|
|
666
685
|
// a dev server that died on a file outside its grants read only as the
|
|
@@ -724,7 +743,9 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
724
743
|
}
|
|
725
744
|
args.persistentRegistry?.set(node.id, spawn.child)
|
|
726
745
|
forgetUndeclaredWrites(args, undeclaredWriteReach(node, args.workspaceRoot))
|
|
727
|
-
if (mayWriteFingerprint(node, args.workspaceRoot))
|
|
746
|
+
if (mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))) {
|
|
747
|
+
args.fingerprintWatch?.wrote()
|
|
748
|
+
}
|
|
728
749
|
return {
|
|
729
750
|
node,
|
|
730
751
|
status: 'success',
|
|
@@ -795,10 +816,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
795
816
|
const outputs = cacheCfg?.outputs.files ?? []
|
|
796
817
|
const wsOutputs = cacheCfg?.outputs.workspaceFiles ?? []
|
|
797
818
|
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
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
|
|
802
822
|
// `signal` stops a plugin executor on the run's stop AND on
|
|
803
823
|
// `exec.timeout`: core cannot kill a process an executor spawned, so a
|
|
804
824
|
// declared timeout meant nothing on one that kept its own clock (H-12).
|
|
@@ -910,7 +930,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
910
930
|
node,
|
|
911
931
|
upstream,
|
|
912
932
|
workspaceRoot: args.workspaceRoot,
|
|
913
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
914
933
|
cache,
|
|
915
934
|
forwardArgs: args.forwardArgs,
|
|
916
935
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -941,8 +960,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
941
960
|
projectDir: node.projectDir,
|
|
942
961
|
outputs,
|
|
943
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),
|
|
944
969
|
}
|
|
945
|
-
const wsCleanArgs = { workspaceRoot: args.workspaceRoot, outputs: wsOutputs }
|
|
946
970
|
// An ADDITIVE task (its outputs overlap an upstream's, with the edge that
|
|
947
971
|
// orders them; item 588) does not clean by glob — the glob selects the
|
|
948
972
|
// upstream's files it adds beside. Its outputs are stamped once before
|
|
@@ -952,6 +976,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
952
976
|
const additive = (node.addsToOutputsOf?.length ?? 0) > 0
|
|
953
977
|
let stampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
954
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
|
|
955
985
|
|
|
956
986
|
// Cache lookup. On hit, time the user-perceived restore op
|
|
957
987
|
// (clean+restore+log-replay) — that's what the framed-block footer
|
|
@@ -990,7 +1020,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
990
1020
|
node,
|
|
991
1021
|
upstream,
|
|
992
1022
|
workspaceRoot: args.workspaceRoot,
|
|
993
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
994
1023
|
cache,
|
|
995
1024
|
forwardArgs: args.forwardArgs,
|
|
996
1025
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -1002,6 +1031,41 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1002
1031
|
: {}),
|
|
1003
1032
|
})
|
|
1004
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
|
+
|
|
1005
1069
|
// A name only `exec.env.secret` makes secret is not one the name rule
|
|
1006
1070
|
// sees later: its row carries the mark, so `vx why` hides its hash (M-63).
|
|
1007
1071
|
const named = node.config.exec?.env?.secret
|
|
@@ -1061,7 +1125,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1061
1125
|
let withheld: WithheldLink[] = []
|
|
1062
1126
|
|
|
1063
1127
|
// Cache miss path (or caching disabled), up to `1 + retries` attempts.
|
|
1064
|
-
// Explicit config wins over the run-level `--
|
|
1128
|
+
// Explicit config wins over the run-level `--retries` default, including
|
|
1065
1129
|
// an explicit `retries: 0`.
|
|
1066
1130
|
const maxAttempts = 1 + (step.retries ?? args.retries ?? 0)
|
|
1067
1131
|
let attempt = 0
|
|
@@ -1092,7 +1156,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1092
1156
|
const endStamp = span('miss: stamp outputs')
|
|
1093
1157
|
stampedBefore ??= await stampOutputs(cleanArgs)
|
|
1094
1158
|
endStamp()
|
|
1159
|
+
} else if (
|
|
1160
|
+
keepOutputs &&
|
|
1161
|
+
attempt === 1 &&
|
|
1162
|
+
willWrite &&
|
|
1163
|
+
!deferralRequested &&
|
|
1164
|
+
outputs.length > 0
|
|
1165
|
+
) {
|
|
1166
|
+
keptBefore = await stampOutputs(cleanArgs)
|
|
1095
1167
|
} else if (willWrite && !deferralRequested && outputs.length > 0) {
|
|
1168
|
+
keptBefore = undefined
|
|
1096
1169
|
// Mark the wiped paths, exactly as the workspace twin below does. The
|
|
1097
1170
|
// git snapshot still lists them with their committed index OIDs, and
|
|
1098
1171
|
// resolveFiles SKIPS its existence probe for any path carrying a
|
|
@@ -1102,7 +1175,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1102
1175
|
const endClean = span('miss: clean outputs')
|
|
1103
1176
|
const cleanedRels = await cleanOutputs(cleanArgs)
|
|
1104
1177
|
endClean()
|
|
1105
|
-
args.gitFilesCache?.noteClean(node.id, node.projectDir, cleanedRels)
|
|
1106
1178
|
args.gitFilesCache?.markOutputsChanged(node.projectDir, cleanedRels)
|
|
1107
1179
|
}
|
|
1108
1180
|
if (additive && willWrite && !deferralRequested && wsOutputs.length > 0) {
|
|
@@ -1110,7 +1182,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1110
1182
|
// glob clean deleted the upstream's files before this task, which
|
|
1111
1183
|
// reads them, ran (A-43).
|
|
1112
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)
|
|
1113
1193
|
} else if (willWrite && !deferralRequested && wsOutputs.length > 0) {
|
|
1194
|
+
wsKeptBefore = undefined
|
|
1114
1195
|
// Root-anchored deletions can land in other projects' dirs; mark
|
|
1115
1196
|
// them so stale per-project git snapshots can't survive the wipe.
|
|
1116
1197
|
const cleanedWsRels = await cleanWorkspaceOutputs(wsCleanArgs)
|
|
@@ -1139,6 +1220,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1139
1220
|
// the throw is its rejection, the value its resolution, so both reach
|
|
1140
1221
|
// the naming catch and the cleanup in finally below.
|
|
1141
1222
|
let running: Promise<unknown>
|
|
1223
|
+
const closeWindow =
|
|
1224
|
+
args.commandWindows?.open(
|
|
1225
|
+
node.id,
|
|
1226
|
+
node.projectDir,
|
|
1227
|
+
commandWriteReach(node, args.workspaceRoot),
|
|
1228
|
+
) ?? (() => {})
|
|
1142
1229
|
try {
|
|
1143
1230
|
running = Promise.resolve(args.executor.execute(req))
|
|
1144
1231
|
} catch (thrown) {
|
|
@@ -1177,6 +1264,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1177
1264
|
throw err
|
|
1178
1265
|
})
|
|
1179
1266
|
.finally(() => {
|
|
1267
|
+
closeWindow()
|
|
1180
1268
|
clearTimeout(timeoutTimer)
|
|
1181
1269
|
unlistenStop?.()
|
|
1182
1270
|
unlistenStop = undefined
|
|
@@ -1267,18 +1355,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1267
1355
|
// The line quotes the command's first word, which a config may have
|
|
1268
1356
|
// built from a secret: masked as the task's own output is (L-37).
|
|
1269
1357
|
if (verdict !== undefined) log.taskStderr(node, `\n${secrets?.mask(verdict) ?? verdict}\n`)
|
|
1270
|
-
// A committed file another task's clean removed, still gone: vx
|
|
1271
|
-
// cleans outputs before a run where Turbo does not, so a reader with
|
|
1272
|
-
// no edge to the producer fails naming only the file (A-48).
|
|
1273
|
-
if (code !== 0) {
|
|
1274
|
-
for (const c of (args.gitFilesCache?.trackedCleansMissing(node.id) ?? []).slice(0, 3)) {
|
|
1275
|
-
const rel = relPosix(args.workspaceRoot, c.path)
|
|
1276
|
-
log.taskStderr(
|
|
1277
|
-
node,
|
|
1278
|
-
`\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`,
|
|
1279
|
-
)
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
1358
|
}
|
|
1283
1359
|
return { result: res, exitCode: code }
|
|
1284
1360
|
}
|
|
@@ -1327,7 +1403,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1327
1403
|
const writeReach =
|
|
1328
1404
|
args.executor.remote === true ? 'none' : undeclaredWriteReach(node, args.workspaceRoot)
|
|
1329
1405
|
const writesFingerprint =
|
|
1330
|
-
args.executor.remote !== true &&
|
|
1406
|
+
args.executor.remote !== true &&
|
|
1407
|
+
mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))
|
|
1331
1408
|
// A stop that landed during the awaits above (the key, the probe, the
|
|
1332
1409
|
// input description): the first attempt would wipe the last build and
|
|
1333
1410
|
// hand the executor a request after the run had stopped. Through a call,
|
|
@@ -1474,7 +1551,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1474
1551
|
const wallclockEndNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
1475
1552
|
|
|
1476
1553
|
const stability = inputStabilityOf(args.node.config.cache, args.inputStability)
|
|
1477
|
-
let movedFails
|
|
1554
|
+
let movedFails: string | undefined
|
|
1478
1555
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1479
1556
|
if (effectiveExitCode === 0 && willSave && deferralRequested) {
|
|
1480
1557
|
// The outputs never landed here: no artifact, no rows. A partial local
|
|
@@ -1503,6 +1580,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1503
1580
|
} else if (
|
|
1504
1581
|
effectiveExitCode === 0 &&
|
|
1505
1582
|
willSave &&
|
|
1583
|
+
!leftBehind() &&
|
|
1506
1584
|
(await keyStillTrue()) &&
|
|
1507
1585
|
(own = await ownOutputs()) !== undefined
|
|
1508
1586
|
) {
|
|
@@ -1527,6 +1605,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1527
1605
|
deferSave: args.deferSave,
|
|
1528
1606
|
// A sink listens: say what the save cost.
|
|
1529
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
|
+
: {}),
|
|
1530
1618
|
})
|
|
1531
1619
|
args.deferredSaves?.set(node.id, landed)
|
|
1532
1620
|
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants !== true) {
|
|
@@ -1554,6 +1642,18 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1554
1642
|
}
|
|
1555
1643
|
}
|
|
1556
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
|
+
|
|
1557
1657
|
/**
|
|
1558
1658
|
* What an additive run saves as its own, or undefined when it removed a
|
|
1559
1659
|
* file it found: no entry replays a removal (`ownOutputsSince`), so the
|
|
@@ -1586,10 +1686,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1586
1686
|
if (described!.hash !== hash) return null
|
|
1587
1687
|
return (
|
|
1588
1688
|
(await movedInput(
|
|
1589
|
-
described!.facts,
|
|
1689
|
+
keyedInputs(described!.facts),
|
|
1590
1690
|
cache,
|
|
1591
1691
|
described!.describedAt,
|
|
1592
|
-
stability.strategy === 'mutable',
|
|
1692
|
+
stability.strategy === 'mutable' ? ownRewrite : undefined,
|
|
1593
1693
|
)) ?? described!.added()
|
|
1594
1694
|
)
|
|
1595
1695
|
}
|
|
@@ -1619,24 +1719,25 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1619
1719
|
endCheck()
|
|
1620
1720
|
if (moved === undefined) return true
|
|
1621
1721
|
const what =
|
|
1622
|
-
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`)
|
|
1623
1725
|
// A task that regenerates its own input (TanStack Router's committed
|
|
1624
1726
|
// `routeTree.gen.ts`, rewritten to the same bytes by every build) is
|
|
1625
1727
|
// never saved (item 1015), and the line said so without the way out.
|
|
1626
|
-
// Not for `package.json`, which an output may not cover.
|
|
1627
1728
|
const hint =
|
|
1628
|
-
moved === null ||
|
|
1729
|
+
moved === null || rewroteBy !== undefined
|
|
1629
1730
|
? ''
|
|
1630
1731
|
: '; if the task writes it, declare it in cache.outputs'
|
|
1631
1732
|
if (stability.severity === 'error') {
|
|
1632
1733
|
log.status(
|
|
1633
|
-
`[vx] ${node.id}: ${what}
|
|
1734
|
+
`[vx] ${node.id}: ${what} — the result is not saved, ` +
|
|
1634
1735
|
`and inputStability.severity is 'error', so the task fails${hint}`,
|
|
1635
1736
|
)
|
|
1636
|
-
movedFails =
|
|
1737
|
+
movedFails = moved === null ? '' : printable(relPosix(args.workspaceRoot, moved))
|
|
1637
1738
|
} else {
|
|
1638
1739
|
log.status(
|
|
1639
|
-
`[vx] ${node.id}: ${what}
|
|
1740
|
+
`[vx] ${node.id}: ${what} — the result stands, ` +
|
|
1640
1741
|
`but is not saved under a key that no longer describes it${hint}`,
|
|
1641
1742
|
)
|
|
1642
1743
|
}
|
|
@@ -1645,7 +1746,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1645
1746
|
return false
|
|
1646
1747
|
}
|
|
1647
1748
|
|
|
1648
|
-
if (movedFails) effectiveExitCode = 1
|
|
1749
|
+
if (movedFails !== undefined) effectiveExitCode = 1
|
|
1649
1750
|
|
|
1650
1751
|
if (unkeyedUpstream !== undefined && willWrite && effectiveExitCode === 0) {
|
|
1651
1752
|
log.status(
|
|
@@ -1674,6 +1775,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1674
1775
|
? { failedOutput: failedOutputLog(output, willSave) }
|
|
1675
1776
|
: {}),
|
|
1676
1777
|
...(result.timedOut === true && effectiveExitCode !== 0 ? { timedOut: true as const } : {}),
|
|
1778
|
+
...(movedFails !== undefined ? { inputMoved: movedFails } : {}),
|
|
1677
1779
|
...(result.cpuMs !== undefined ? { cpuMs: result.cpuMs } : {}),
|
|
1678
1780
|
...(result.peakRssBytes !== undefined ? { peakRssBytes: result.peakRssBytes } : {}),
|
|
1679
1781
|
...(result.where !== undefined ? { where: result.where } : {}),
|
|
@@ -1704,7 +1806,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1704
1806
|
/**
|
|
1705
1807
|
* Drop the run's facts about files a task may have written without
|
|
1706
1808
|
* declaring them (`undeclaredWriteReach`): the project's git snapshot and
|
|
1707
|
-
* its index OIDs
|
|
1809
|
+
* its index OIDs. The next reader
|
|
1708
1810
|
* re-enumerates the project (one `git ls-files`) and hashes its files by
|
|
1709
1811
|
* content; the per-declaration file-list memos are keyed on the snapshot
|
|
1710
1812
|
* array, so they miss with it (item 743).
|
|
@@ -1713,12 +1815,10 @@ function forgetUndeclaredWrites(args: ExecuteArgs, reach: 'none' | 'project' | '
|
|
|
1713
1815
|
if (reach === 'none') return
|
|
1714
1816
|
if (reach === 'workspace') {
|
|
1715
1817
|
args.gitFilesCache?.clear()
|
|
1716
|
-
args.hashCache?.packageJson.clear()
|
|
1717
1818
|
return
|
|
1718
1819
|
}
|
|
1719
1820
|
args.gitFilesCache?.delete(args.node.projectDir)
|
|
1720
1821
|
args.gitFilesCache?.invalidateWorkspacePartition()
|
|
1721
|
-
args.hashCache?.packageJson.delete(args.node.projectDir)
|
|
1722
1822
|
}
|
|
1723
1823
|
|
|
1724
1824
|
/** The two bin directories a task's PATH starts with: its own, then the root's (once when they coincide). */
|
|
@@ -1765,16 +1865,11 @@ function taskEnv(node: TaskNode, step: ExecConfig, workspaceRoot: string): NodeJ
|
|
|
1765
1865
|
return env
|
|
1766
1866
|
}
|
|
1767
1867
|
|
|
1768
|
-
|
|
1769
|
-
|
|
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)!
|
|
1770
1871
|
}
|
|
1771
1872
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
* `VX_READY_NOTICE_MS` overrides it, as `VX_KILL_GRACE_MS` does the grace.
|
|
1775
|
-
*/
|
|
1776
|
-
function readyNoticeMs(): number {
|
|
1777
|
-
const raw = process.env['VX_READY_NOTICE_MS']
|
|
1778
|
-
if (raw !== undefined && /^[0-9]+$/.test(raw) && Number(raw) > 0) return Number(raw)
|
|
1779
|
-
return 10_000
|
|
1873
|
+
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
1874
|
+
return signal?.aborted === true
|
|
1780
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
|
}
|
|
@@ -12,6 +12,8 @@ export interface LiveForecast {
|
|
|
12
12
|
critical?: string
|
|
13
13
|
/** Each running task's unfinished dependents, direct and transitive. */
|
|
14
14
|
blocks: ReadonlyMap<string, number>
|
|
15
|
+
/** Each running task's p50, where history knows it: the row's expected duration. */
|
|
16
|
+
expected?: ReadonlyMap<string, number>
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export type Forecast = (
|
|
@@ -90,7 +92,12 @@ export function createForecast(
|
|
|
90
92
|
|
|
91
93
|
return (finished, running, nowMs) => {
|
|
92
94
|
const blocks = new Map<string, number>()
|
|
93
|
-
|
|
95
|
+
const expected = new Map<string, number>()
|
|
96
|
+
for (const id of running.keys()) {
|
|
97
|
+
blocks.set(id, blocked(id, finished))
|
|
98
|
+
const p = cost.get(id)
|
|
99
|
+
if (p !== undefined) expected.set(id, p)
|
|
100
|
+
}
|
|
94
101
|
const end = new Map<string, number>()
|
|
95
102
|
// The running task each node's longest chain waits on.
|
|
96
103
|
const head = new Map<string, string>()
|
|
@@ -128,12 +135,13 @@ export function createForecast(
|
|
|
128
135
|
}
|
|
129
136
|
work += own
|
|
130
137
|
}
|
|
131
|
-
if (!known || overrun) return { blocks }
|
|
138
|
+
if (!known || overrun) return { blocks, expected }
|
|
132
139
|
const critical = chainEnd === undefined ? undefined : head.get(chainEnd)
|
|
133
140
|
return {
|
|
134
141
|
etaMs: Math.max(chain, work / workers),
|
|
135
142
|
...(critical !== undefined ? { critical } : {}),
|
|
136
143
|
blocks,
|
|
144
|
+
expected,
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
}
|