@vzn/vx 0.0.646 → 0.0.648
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 +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
// RS (U+001E) + `e` / `o` where the stream switches to stderr / stdout, and
|
|
5
5
|
// a literal RS doubled. Output that never touched stderr is its own text.
|
|
6
6
|
|
|
7
|
-
import { BoundedCapture, type CapturedChunk } from '../exec/index.js'
|
|
7
|
+
import { type BoundedCapture, type CapturedChunk, isDroppedOutputChunk } from '../exec/index.js'
|
|
8
|
+
import { MASKED, type SecretMask } from '../util/index.js'
|
|
8
9
|
|
|
9
10
|
const RS = '\x1e'
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* `chunks` as one stored string; it starts on stdout. `mask` (the task's
|
|
14
|
+
* secrets) is applied to each stream's whole text (`maskStreams`).
|
|
15
|
+
*/
|
|
16
|
+
export function encodeOutputLog(chunks: readonly CapturedChunk[], mask?: SecretMask): string {
|
|
13
17
|
let out = ''
|
|
14
18
|
let err = false
|
|
15
|
-
for (const c of chunks) {
|
|
16
|
-
if (c.text.length === 0) continue
|
|
19
|
+
for (const c of maskStreams(chunks, mask)) {
|
|
17
20
|
if (c.err !== err) {
|
|
18
21
|
out += RS + (c.err ? 'e' : 'o')
|
|
19
22
|
err = c.err
|
|
@@ -23,6 +26,76 @@ export function encodeOutputLog(chunks: readonly CapturedChunk[]): string {
|
|
|
23
26
|
return out
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* `encodeOutputLog` with `mask` applied, for a capture of raw output: a
|
|
31
|
+
* value the capture's dropped middle cut left a piece either side, which
|
|
32
|
+
* only `maskCut` sees, as `maskCaptured` does for core's own capture.
|
|
33
|
+
*/
|
|
34
|
+
export function maskedOutputLog(chunks: readonly CapturedChunk[], mask: SecretMask): string {
|
|
35
|
+
const at = chunks.findIndex(isDroppedOutputChunk)
|
|
36
|
+
if (at === -1) return encodeOutputLog(chunks, mask)
|
|
37
|
+
const before = chunks.slice(0, at)
|
|
38
|
+
const tail = encodeOutputLog(chunks.slice(at + 1))
|
|
39
|
+
// The dropped line is on stdout, where the tail's own log starts.
|
|
40
|
+
const lead = tail.startsWith(RS + 'e') ? RS + 'e' : ''
|
|
41
|
+
const [h, t] = mask.maskCut(encodeOutputLog(before), tail.slice(lead.length))
|
|
42
|
+
const back = before.findLast((c) => c.text.length > 0)?.err === true ? RS + 'o' : ''
|
|
43
|
+
return h + back + chunks[at]!.text + lead + t
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Each stream's run of adjacent non-empty chunks joined, with `mask`
|
|
48
|
+
* applied to each stream's text as a whole: a value is masked where it
|
|
49
|
+
* starts and its rest dropped from the runs it reaches. The live stream
|
|
50
|
+
* masks chunk by chunk and lets a held piece go after a pause, so a value
|
|
51
|
+
* written in two pieces reached the capture in two chunks, and a write to
|
|
52
|
+
* the other stream between them made them two runs. A hit replays each
|
|
53
|
+
* stream to its own descriptor, so `vx run > log` joined them again.
|
|
54
|
+
*/
|
|
55
|
+
export function maskStreams(
|
|
56
|
+
chunks: readonly CapturedChunk[],
|
|
57
|
+
mask: SecretMask | undefined,
|
|
58
|
+
): CapturedChunk[] {
|
|
59
|
+
if (mask === undefined) return chunks.filter((c) => c.text.length > 0)
|
|
60
|
+
const runs: CapturedChunk[] = []
|
|
61
|
+
for (const c of chunks) {
|
|
62
|
+
if (c.text.length === 0) continue
|
|
63
|
+
const last = runs.at(-1)
|
|
64
|
+
if (last !== undefined && last.err === c.err) last.text += c.text
|
|
65
|
+
else runs.push({ text: c.text, err: c.err })
|
|
66
|
+
}
|
|
67
|
+
for (const err of [false, true]) {
|
|
68
|
+
const own = runs.filter((r) => r.err === err)
|
|
69
|
+
const spans = mask.spans(own.map((r) => r.text).join(''))
|
|
70
|
+
if (spans.length === 0) continue
|
|
71
|
+
let at = 0
|
|
72
|
+
let i = 0
|
|
73
|
+
for (const r of own) {
|
|
74
|
+
const end = at + r.text.length
|
|
75
|
+
let text = ''
|
|
76
|
+
let from = at
|
|
77
|
+
for (; i < spans.length && spans[i]![0] < end; i++) {
|
|
78
|
+
const [s, e] = spans[i]!
|
|
79
|
+
if (s >= from) text += r.text.slice(from - at, s - at) + MASKED
|
|
80
|
+
from = Math.max(from, Math.min(e, end))
|
|
81
|
+
// A value that runs past this run is cut from the next one too.
|
|
82
|
+
if (e > end) break
|
|
83
|
+
}
|
|
84
|
+
r.text = text + r.text.slice(from - at)
|
|
85
|
+
at = end
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// A run emptied whole leaves its neighbours, the other stream, adjacent.
|
|
89
|
+
const kept: CapturedChunk[] = []
|
|
90
|
+
for (const r of runs) {
|
|
91
|
+
if (r.text.length === 0) continue
|
|
92
|
+
const last = kept.at(-1)
|
|
93
|
+
if (last !== undefined && last.err === r.err) last.text += r.text
|
|
94
|
+
else kept.push(r)
|
|
95
|
+
}
|
|
96
|
+
return kept
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
/** The chunks `encodeOutputLog` wrote, adjacent same-stream text joined. */
|
|
27
100
|
export function decodeOutputLog(log: string): CapturedChunk[] {
|
|
28
101
|
if (!log.includes(RS)) return log.length === 0 ? [] : [{ text: log, err: false }]
|
|
@@ -58,9 +131,16 @@ export const FAILED_OUTPUT_HEAD_CHARS = 8 * 1024
|
|
|
58
131
|
export const FAILED_OUTPUT_TAIL_CHARS = 56 * 1024
|
|
59
132
|
|
|
60
133
|
/** A failed attempt's capture, re-bounded to the failure's limits when it is the entry's. */
|
|
61
|
-
export function failedOutputLog(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
134
|
+
export function failedOutputLog(
|
|
135
|
+
capture: BoundedCapture,
|
|
136
|
+
entrySized: boolean,
|
|
137
|
+
mask?: SecretMask,
|
|
138
|
+
): string {
|
|
139
|
+
if (!entrySized) return encodeOutputLog(capture.chunks(), mask)
|
|
140
|
+
// Masked before the re-bound: its cut must not split a value first.
|
|
141
|
+
return encodeOutputLog(
|
|
142
|
+
capture
|
|
143
|
+
.rebound(FAILED_OUTPUT_HEAD_CHARS, FAILED_OUTPUT_TAIL_CHARS, (c) => maskStreams(c, mask))
|
|
144
|
+
.chunks(),
|
|
145
|
+
)
|
|
66
146
|
}
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExecConfig } from '../config.js'
|
|
6
6
|
import {
|
|
7
|
+
lineEnds,
|
|
7
8
|
PersistentReadyError,
|
|
8
9
|
releaseBridges,
|
|
9
10
|
runPersistent,
|
|
11
|
+
sandboxReads,
|
|
10
12
|
signalExitCode,
|
|
11
13
|
withForwardArgs,
|
|
12
14
|
wrapSandboxedCommand,
|
|
13
15
|
} from '../exec/index.js'
|
|
14
16
|
import type { TaskOutcome } from '../graph/index.js'
|
|
15
|
-
import {
|
|
17
|
+
import { maskedStreams, secretMask, testTimerMs } from '../util/index.js'
|
|
16
18
|
import { warnOf } from '../report/index.js'
|
|
17
19
|
import type { ExecuteArgs } from './execute-task.js'
|
|
18
20
|
import {
|
|
@@ -25,13 +27,43 @@ import {
|
|
|
25
27
|
undeclaredWriteReach,
|
|
26
28
|
untouchedPlaceholderLine,
|
|
27
29
|
} from './sandbox-request.js'
|
|
30
|
+
import { shellVerdict } from './shell-verdict.js'
|
|
28
31
|
import { forwardedSignal, terminateChildren } from './signals.js'
|
|
29
32
|
import { computeTaskHash } from './task-hash.js'
|
|
30
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
forgetUndeclaredWrites,
|
|
35
|
+
isAborted,
|
|
36
|
+
taskBinDirs,
|
|
37
|
+
taskEnv,
|
|
38
|
+
taskTimeoutMs,
|
|
39
|
+
} from './task-process.js'
|
|
31
40
|
|
|
32
41
|
/** How long a server may take to match `readyWhen` before vx says it waits. */
|
|
33
42
|
const READY_NOTICE_MS = 10_000
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A server is keyed on its inputs like any task, so a cached dependant
|
|
46
|
+
* behind a dev server re-runs when the server's sources change; with none
|
|
47
|
+
* it has no key and the dependant always runs (A-17). A server the watch
|
|
48
|
+
* loop keeps up is keyed too: its dependant folds the key either way.
|
|
49
|
+
*/
|
|
50
|
+
export function serverKey(args: ExecuteArgs): Promise<string | undefined> | string | undefined {
|
|
51
|
+
if (args.noDependants === true) return undefined
|
|
52
|
+
return (
|
|
53
|
+
args.upfrontKey ??
|
|
54
|
+
computeTaskHash({
|
|
55
|
+
node: args.node,
|
|
56
|
+
upstream: args.upstream,
|
|
57
|
+
workspaceRoot: args.workspaceRoot,
|
|
58
|
+
cache: args.cache,
|
|
59
|
+
forwardArgs: args.forwardArgs,
|
|
60
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
61
|
+
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
62
|
+
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
/**
|
|
36
68
|
* Persistent task: dev server / file watcher / daemon. Spawn, wait
|
|
37
69
|
* for ready (regex match or immediate), stash the subprocess in the
|
|
@@ -55,26 +87,10 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
55
87
|
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
56
88
|
const env = taskEnv(node, step, args.workspaceRoot)
|
|
57
89
|
const wallclockStartNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
58
|
-
|
|
59
|
-
// server re-runs when the server's sources change; with none it has no
|
|
60
|
-
// key and the dependant always runs (A-17).
|
|
61
|
-
const hash =
|
|
62
|
-
args.noDependants === true
|
|
63
|
-
? undefined
|
|
64
|
-
: (args.upfrontKey ??
|
|
65
|
-
(await computeTaskHash({
|
|
66
|
-
node,
|
|
67
|
-
upstream: args.upstream,
|
|
68
|
-
workspaceRoot: args.workspaceRoot,
|
|
69
|
-
cache: args.cache,
|
|
70
|
-
forwardArgs: args.forwardArgs,
|
|
71
|
-
nestedProjectDirs: args.nestedProjectDirs,
|
|
72
|
-
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
73
|
-
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
74
|
-
})))
|
|
90
|
+
const hash = await serverKey(args)
|
|
75
91
|
|
|
76
92
|
// Every dependant starts it where it runs (`ServerSpec`): nothing to spawn
|
|
77
|
-
// here, and nothing that ran (
|
|
93
|
+
// here, and nothing that ran (2026-10-22: nothing runs on a
|
|
78
94
|
// distributed run's orchestrator).
|
|
79
95
|
if (args.carried === true)
|
|
80
96
|
return {
|
|
@@ -82,6 +98,7 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
82
98
|
status: 'success',
|
|
83
99
|
exitCode: 0,
|
|
84
100
|
durationMs: 0,
|
|
101
|
+
carried: true,
|
|
85
102
|
...(hash === undefined ? {} : { hash }),
|
|
86
103
|
wallclockStartNs,
|
|
87
104
|
wallclockEndNs: wallclockStartNs,
|
|
@@ -106,6 +123,7 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
106
123
|
// server that dies before readiness is told the directory spelling — the
|
|
107
124
|
// same trap and the same answer as a one-shot task's (sandbox-request.ts).
|
|
108
125
|
let placeholders: Placeholder[] = []
|
|
126
|
+
let serverSandbox: Awaited<ReturnType<typeof sandboxRequestFor>>['sandbox'] | undefined
|
|
109
127
|
if (step.sandbox !== undefined) {
|
|
110
128
|
await args.armSandbox?.()
|
|
111
129
|
// A persistent task never declares `cache` (the loader refuses it), so
|
|
@@ -119,6 +137,7 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
119
137
|
args.cacheDir,
|
|
120
138
|
)
|
|
121
139
|
placeholders = sb.placeholders
|
|
140
|
+
serverSandbox = sb.sandbox
|
|
122
141
|
// A wrap that refuses (a port the host holds) leaves the server unspawned
|
|
123
142
|
// and the placeholders vx made for it in the project for every later run.
|
|
124
143
|
const wrapped = await wrapSandboxedCommand({
|
|
@@ -139,16 +158,21 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
139
158
|
// is matched on the raw chunks, before this. Its held tail is flushed by
|
|
140
159
|
// the idle timer: a server never ends on the way to a flush.
|
|
141
160
|
const serverSecrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
142
|
-
const serverOut = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStdout(node, t))
|
|
143
|
-
const serverErr = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStderr(node, t))
|
|
144
161
|
// A server writes until the run stops it: its window stays open.
|
|
145
162
|
args.commandWindows?.open(node.id, node.projectDir, commandWriteReach(node, args.workspaceRoot))
|
|
163
|
+
// Whether the server's output ends mid-line, for vx's never-ready note.
|
|
164
|
+
const ends = lineEnds(
|
|
165
|
+
(t) => log.taskStdout(node, t),
|
|
166
|
+
(t) => log.taskStderr(node, t),
|
|
167
|
+
)
|
|
168
|
+
// Masked as one: a stream's held tail goes out before the other's next chunk.
|
|
169
|
+
const serverMasked = serverSecrets && maskedStreams(serverSecrets, ends.onStdout!, ends.onStderr!)
|
|
146
170
|
const persistentOpts: Parameters<typeof runPersistent>[0] = {
|
|
147
171
|
command,
|
|
148
172
|
cwd: node.projectDir,
|
|
149
173
|
env,
|
|
150
|
-
onStdout:
|
|
151
|
-
onStderr:
|
|
174
|
+
onStdout: serverMasked ? serverMasked.out : ends.onStdout!,
|
|
175
|
+
onStderr: serverMasked ? serverMasked.err : ends.onStderr!,
|
|
152
176
|
...(args.liveChildren !== undefined ? { liveChildren: args.liveChildren } : {}),
|
|
153
177
|
...(args.track !== undefined
|
|
154
178
|
? { onSpawn: (pid: number) => void args.track!(node.id, pid) }
|
|
@@ -235,8 +259,11 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
235
259
|
// A shell that exited before ready can leave its group running
|
|
236
260
|
// (`server & …; exit`): never registered, so no teardown reached it,
|
|
237
261
|
// and it held its port against the next `vx watch` cycle's server.
|
|
238
|
-
if (err instanceof PersistentReadyError && err.reason === 'exited')
|
|
239
|
-
await terminateChildren(() => [spawn.child])
|
|
262
|
+
if (err instanceof PersistentReadyError && err.reason === 'exited' && spawn.child !== undefined)
|
|
263
|
+
await terminateChildren(() => [spawn.child!])
|
|
264
|
+
// What the masked streams hold back (a tail that could begin a secret)
|
|
265
|
+
// belongs above vx's own line: left to the idle flush, it came after it.
|
|
266
|
+
serverMasked?.end()
|
|
240
267
|
// A server the run's stop killed while it started is aborted, as any
|
|
241
268
|
// task the stop kills (item 962): it read `failed (never ready:
|
|
242
269
|
// exited, exit 130)` with a recap after every Ctrl-C (C-62). The stop
|
|
@@ -257,7 +284,30 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
257
284
|
// The task's OWN stream, not the process's: the frame is where a
|
|
258
285
|
// reader looks for why a task failed, and a run with a custom logger
|
|
259
286
|
// (an embedder) never saw a bare stderr write at all.
|
|
260
|
-
log.taskStderr(
|
|
287
|
+
log.taskStderr(
|
|
288
|
+
node,
|
|
289
|
+
ends.own(`[vx] ${node.id}: persistent task failed to become ready: ${message}\n`),
|
|
290
|
+
)
|
|
291
|
+
// A server's 127, 126 or signal death needs the line a one-shot task's
|
|
292
|
+
// gets (execute-task.ts): a dev server whose tool sits in a sibling's
|
|
293
|
+
// bin said only "exit 127".
|
|
294
|
+
if (
|
|
295
|
+
err instanceof PersistentReadyError &&
|
|
296
|
+
err.reason === 'exited' &&
|
|
297
|
+
err.exitCode !== undefined
|
|
298
|
+
) {
|
|
299
|
+
const signal = spawn.child?.signalCode
|
|
300
|
+
const verdict = shellVerdict({
|
|
301
|
+
code: err.exitCode,
|
|
302
|
+
command: step.command,
|
|
303
|
+
cwd: node.projectDir,
|
|
304
|
+
bins: taskBinDirs(node, args.workspaceRoot),
|
|
305
|
+
signal: signal ?? undefined,
|
|
306
|
+
hidden: serverSandbox && ((f: string) => !sandboxReads(serverSandbox, f)),
|
|
307
|
+
})
|
|
308
|
+
if (verdict !== undefined)
|
|
309
|
+
log.taskStderr(node, `${serverSecrets?.mask(verdict) ?? verdict}\n`)
|
|
310
|
+
}
|
|
261
311
|
// A server is never traced, so the sandbox's refusals reach no report:
|
|
262
312
|
// a dev server that died on a file outside its grants read only as the
|
|
263
313
|
// tool's own "not found" (2026-10-03). Said for a sandboxed server that
|
|
@@ -294,7 +344,7 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
294
344
|
// A server that traps the TERM and exits 0 still timed out, as a
|
|
295
345
|
// one-shot task that does so reports 143 (X-116).
|
|
296
346
|
exitCode:
|
|
297
|
-
ready?.reason === 'timeout'
|
|
347
|
+
ready?.reason === 'timeout' && spawn.child !== undefined
|
|
298
348
|
? (await spawn.child.exited) || signalExitCode('SIGTERM')
|
|
299
349
|
: (ready?.exitCode ?? 1),
|
|
300
350
|
durationMs: spawn.readyMs(),
|
|
@@ -318,16 +368,21 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
318
368
|
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
319
369
|
}
|
|
320
370
|
}
|
|
321
|
-
|
|
371
|
+
// Ready, so it spawned: only a failed spawn has no child.
|
|
372
|
+
const child = spawn.child!
|
|
373
|
+
args.persistentRegistry?.set(node.id, child)
|
|
322
374
|
forgetUndeclaredWrites(args, undeclaredWriteReach(node, args.workspaceRoot))
|
|
323
375
|
if (mayWriteFingerprint(node, args.workspaceRoot, args.fingerprintWatch?.names ?? [])) {
|
|
324
376
|
args.fingerprintWatch?.wrote()
|
|
325
377
|
}
|
|
378
|
+
// Ready by its clean exit (a setup step): nothing is running any more.
|
|
379
|
+
const exited = child.exitCode
|
|
326
380
|
return {
|
|
327
381
|
node,
|
|
328
382
|
status: 'success',
|
|
329
383
|
exitCode: 0,
|
|
330
384
|
durationMs: spawn.readyMs(),
|
|
385
|
+
...(exited === null ? {} : { serverExit: exited }),
|
|
331
386
|
...(hash !== undefined ? { hash } : {}),
|
|
332
387
|
wallclockStartNs,
|
|
333
388
|
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
@@ -176,9 +176,8 @@ export function sessionEnd(keepAlive: KeepAlive): Promise<{ code: number; i: num
|
|
|
176
176
|
|
|
177
177
|
export interface HoldArgs {
|
|
178
178
|
forwardArgs: readonly string[] | undefined
|
|
179
|
-
/** Where a held server's output
|
|
179
|
+
/** Where a held server's output, and the notice of its death, go. */
|
|
180
180
|
outOf: (id: string) => HeldServer['out']
|
|
181
|
-
warn: (message: string) => void
|
|
182
181
|
}
|
|
183
182
|
|
|
184
183
|
/** The kept servers handed to the caller (`RunOptions.hold`) instead of waited on. */
|
|
@@ -193,17 +192,24 @@ export function holdServers(keepAlive: KeepAlive, args: HoldArgs): HeldPersisten
|
|
|
193
192
|
const servers = new Map<string, HeldServer>()
|
|
194
193
|
keepAlive.nodes.forEach((n, i) => {
|
|
195
194
|
const child = held[i]!
|
|
195
|
+
const out = args.outOf(n.id)
|
|
196
196
|
servers.set(n.id, {
|
|
197
197
|
child,
|
|
198
198
|
config: serverConfig(n, args.forwardArgs),
|
|
199
|
-
out
|
|
199
|
+
out,
|
|
200
200
|
handOver: () => void handed.add(child),
|
|
201
201
|
})
|
|
202
202
|
// One that died during the graph was said then.
|
|
203
203
|
if (hasEnded(child)) return
|
|
204
|
+
// Said where its output goes: a later run that keeps it owns the
|
|
205
|
+
// terminal from its start, and this run's renderer wrote over its rows.
|
|
204
206
|
void child.exited.then((code) => {
|
|
205
207
|
if (!stopping && !handed.has(child) && code !== 0)
|
|
206
|
-
|
|
208
|
+
out.bus.emit({
|
|
209
|
+
kind: 'run:status',
|
|
210
|
+
line: `vx: ${n.id} exited with code ${code}`,
|
|
211
|
+
warn: true,
|
|
212
|
+
})
|
|
207
213
|
})
|
|
208
214
|
})
|
|
209
215
|
return {
|
|
@@ -260,6 +266,12 @@ export async function disposeServers(args: {
|
|
|
260
266
|
endedBeforeStop: ReadonlySet<Child> | undefined
|
|
261
267
|
}): Promise<{ keepAlive: KeepAlive; crashed: CrashedPersistent[] }> {
|
|
262
268
|
const { registry, outcomes, endedBeforeStop } = args
|
|
269
|
+
// One turn of the event loop first, so Bun reaps what has already died:
|
|
270
|
+
// `exitCode` stays null until it does. A server dead at once, with
|
|
271
|
+
// nothing between its spawn and here to yield, was judged still up: the
|
|
272
|
+
// footer counted it a success, pinned it `running`, and a teardown read
|
|
273
|
+
// the run ok over vx's exit 1.
|
|
274
|
+
if (registry.size > 0) await new Promise((resolve) => setTimeout(resolve, 0))
|
|
263
275
|
let keepAlive = selectKeepAlive(registry, args.nodes, args.keep)
|
|
264
276
|
const failedElsewhere =
|
|
265
277
|
![...outcomes.values()].every((o) => isPassStatus(o.status)) ||
|