@vzn/vx 0.0.645 → 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 +39 -50
- 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 +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -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/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- 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 +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- 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 +326 -65
- 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 +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- 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 +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- 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 +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- 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 +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- 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 +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- 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 +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- 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 +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- 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 +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -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 +65 -4
- 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 +282 -0
- 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 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/exec/executor.ts
CHANGED
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
// request here; an executor decides WHERE/HOW the process runs. Core's own
|
|
4
4
|
// `local-executor.ts` is the in-process spawn, and it sits at the TAIL of
|
|
5
5
|
// every executor list — what a plugin executor declines runs here.
|
|
6
|
-
// Persistent tasks (`exec.persistent`) never reach an executor
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Persistent tasks (`exec.persistent`) never reach an executor as tasks: a
|
|
7
|
+
// worker cannot hand the submitter a listening port. A `servers` executor
|
|
8
|
+
// is handed a task's persistent dependencies instead (`ExecuteRequest.servers`)
|
|
9
|
+
// and starts them where it runs the task; the rest stay on `runPersistent`. An `exec.interactive` task reaches the local one
|
|
9
10
|
// only: no plugin can hand it this terminal.
|
|
10
11
|
//
|
|
11
12
|
// Lives in `exec/` (not `orchestrator/`) so the contract depends only on
|
|
12
13
|
// process primitives — the module-boundary matrix forbids `exec` → `cache`,
|
|
13
14
|
// which is why sandbox baselines arrive pre-resolved on the request.
|
|
14
15
|
|
|
16
|
+
import type { SandboxConfig } from '../config.js'
|
|
15
17
|
import type { CaptureConfig, RunResult } from './runner.js'
|
|
16
18
|
import type { ResolvedSandboxConfig, SandboxViolation } from './sandbox-runtime.js'
|
|
17
|
-
import { UserError } from '../util/index.js'
|
|
19
|
+
import { thrownMessage, UserError, type ui } from '../util/index.js'
|
|
18
20
|
|
|
19
21
|
/** Sandbox baselines + the user's resolved sandbox block, when the task is sandboxed. */
|
|
20
22
|
export interface ExecuteSandbox {
|
|
@@ -34,6 +36,12 @@ export interface ExecuteSandbox {
|
|
|
34
36
|
*/
|
|
35
37
|
readonly reportLinked: readonly string[]
|
|
36
38
|
readonly config: ResolvedSandboxConfig
|
|
39
|
+
/**
|
|
40
|
+
* A cached task's: the project directories its key answers for, which
|
|
41
|
+
* an executor that builds the request again where it runs, on that
|
|
42
|
+
* machine's paths and platform, hands `sandboxRequestFor`.
|
|
43
|
+
*/
|
|
44
|
+
readonly keyed?: readonly string[]
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/** One declared input file, as the cache key saw it. */
|
|
@@ -77,6 +85,24 @@ export interface TaskInputs {
|
|
|
77
85
|
readonly configDigest: string
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
/** A persistent dependency a `servers` executor starts where it runs the task, as `runPersistent` does. */
|
|
89
|
+
export interface ServerSpec {
|
|
90
|
+
readonly taskId: string
|
|
91
|
+
readonly command: string
|
|
92
|
+
readonly cwd: string
|
|
93
|
+
readonly env: NodeJS.ProcessEnv
|
|
94
|
+
/** `exec.persistent.readyWhen`: ready at the first output line it matches; absent, at spawn. */
|
|
95
|
+
readonly readyWhen?: string
|
|
96
|
+
/** Bounds the readiness wait. */
|
|
97
|
+
readonly timeoutMs?: number
|
|
98
|
+
/**
|
|
99
|
+
* Its `exec.sandbox`, and the directories of the projects nested in its
|
|
100
|
+
* own (a wall its grants do not reach): it starts inside that sandbox,
|
|
101
|
+
* built where it runs, as a one-shot task's.
|
|
102
|
+
*/
|
|
103
|
+
readonly sandbox?: { readonly config: SandboxConfig; readonly nested: readonly string[] }
|
|
104
|
+
}
|
|
105
|
+
|
|
80
106
|
export interface ExecuteRequest {
|
|
81
107
|
/** `${project}#${task}` — for executors that route or log by task. */
|
|
82
108
|
readonly taskId: string
|
|
@@ -90,6 +116,15 @@ export interface ExecuteRequest {
|
|
|
90
116
|
* this machine — needs a stable address, and the key is that address.
|
|
91
117
|
*/
|
|
92
118
|
readonly cacheKey?: string
|
|
119
|
+
/**
|
|
120
|
+
* Core saves no entry under `cacheKey` after this attempt: the run's
|
|
121
|
+
* policy writes no layer (`--no-cache`, `--cache-policy local:r`), or an
|
|
122
|
+
* upstream failed under `--continue=always`, ran unkeyed or is keyless.
|
|
123
|
+
* An executor that saves a task's result for its dependants keeps it off
|
|
124
|
+
* that key too: a distributed run's worker saved it, and the next run
|
|
125
|
+
* hit an entry the user had said not to write (2026-10-24).
|
|
126
|
+
*/
|
|
127
|
+
readonly unsaved?: boolean
|
|
93
128
|
/**
|
|
94
129
|
* Cache READS are disabled for this run (`--force` / `--no-cache`). An
|
|
95
130
|
* executor that keeps its OWN remote record of executions must not serve
|
|
@@ -104,6 +139,11 @@ export interface ExecuteRequest {
|
|
|
104
139
|
* execution under the same key.
|
|
105
140
|
*/
|
|
106
141
|
readonly remoteOnly?: boolean
|
|
142
|
+
/**
|
|
143
|
+
* The platform to run it on, one of the task's `exec.platforms` and the
|
|
144
|
+
* executor's `platforms`: its key includes it. Absent when the task names none.
|
|
145
|
+
*/
|
|
146
|
+
readonly platform?: string
|
|
107
147
|
/**
|
|
108
148
|
* Whether this task's outputs must land on the submitter's disk now.
|
|
109
149
|
* 'eager' (or absent) = materialise as today. 'deferred' = leave them
|
|
@@ -162,6 +202,12 @@ export interface ExecuteRequest {
|
|
|
162
202
|
* core's local executor is ever handed such a task (placement.ts).
|
|
163
203
|
*/
|
|
164
204
|
readonly terminal?: true
|
|
205
|
+
/**
|
|
206
|
+
* The persistent tasks it depends on, through groups, for a `servers`
|
|
207
|
+
* executor: each started where the command runs, ready before it, and
|
|
208
|
+
* shared by the tasks that run there. Absent for any other executor.
|
|
209
|
+
*/
|
|
210
|
+
readonly servers?: readonly ServerSpec[]
|
|
165
211
|
}
|
|
166
212
|
|
|
167
213
|
export interface ExecuteResult extends RunResult {
|
|
@@ -173,16 +219,40 @@ export interface ExecuteResult extends RunResult {
|
|
|
173
219
|
* existed. `deferred` means they are NOT on disk and `materialize()`
|
|
174
220
|
* fetches them; core calls it lazily, at most once, and then saves an
|
|
175
221
|
* ordinary cache entry, so a deferred task leaves no permanent third
|
|
176
|
-
* state behind.
|
|
222
|
+
* state behind. An executor may answer `deferred` to an `eager` request
|
|
223
|
+
* too, when it cannot bring them here: core saves nothing from this disk.
|
|
177
224
|
*/
|
|
178
225
|
readonly outputs?: { kind: 'disk' } | { kind: 'deferred'; materialize: () => Promise<void> }
|
|
226
|
+
/**
|
|
227
|
+
* Tracked files the command changed where it ran, workspace-relative
|
|
228
|
+
* POSIX: an executor that runs it elsewhere says so, as this disk shows
|
|
229
|
+
* none of it. One the key read withholds the save under the key, as an
|
|
230
|
+
* input changed here does.
|
|
231
|
+
*/
|
|
232
|
+
readonly changed?: readonly string[]
|
|
179
233
|
/**
|
|
180
234
|
* Executor-reported placement label — which machine ran the command
|
|
181
235
|
* (a REAPI worker id, a pool member). Absent = this host. Rides
|
|
182
236
|
* `TaskOutcome.where` into telemetry so a task can be attributed to
|
|
183
|
-
* its worker
|
|
237
|
+
* its worker, onto its row and frame (`<id>@<where>`), and into the
|
|
238
|
+
* run history (v44).
|
|
184
239
|
*/
|
|
185
240
|
readonly where?: string
|
|
241
|
+
/**
|
|
242
|
+
* The cores and memory of the machine `where` names, as it reports
|
|
243
|
+
* them: the run history keeps them, so a task that used all of one
|
|
244
|
+
* reads as wanting a bigger one (`TaskResources.saturated*`).
|
|
245
|
+
*/
|
|
246
|
+
readonly machine?: {
|
|
247
|
+
readonly cores: number
|
|
248
|
+
readonly memoryBytes: number
|
|
249
|
+
/**
|
|
250
|
+
* The share of a core each runnable thread got there over the task's
|
|
251
|
+
* run (1 when a core was free). On a crowded machine the CPU a task
|
|
252
|
+
* used is its share, not its need: the run history divides by this.
|
|
253
|
+
*/
|
|
254
|
+
readonly cpuShare?: number
|
|
255
|
+
}
|
|
186
256
|
}
|
|
187
257
|
|
|
188
258
|
/** What an executor sees when a task is PLACED — once per task, before scheduling. */
|
|
@@ -193,9 +263,10 @@ export interface TaskPlacement {
|
|
|
193
263
|
readonly command: string
|
|
194
264
|
/**
|
|
195
265
|
* Must run on this machine: the task is persistent, depends (transitively)
|
|
196
|
-
* on a persistent or sandboxed
|
|
266
|
+
* on a persistent task this executor does not carry or a sandboxed one, is sandboxed itself, declares
|
|
197
267
|
* `exec.remote: false`, or folds a runtime probe into its key (this
|
|
198
|
-
* machine's `node -v`)
|
|
268
|
+
* machine's `node -v`) and this executor is not `runtime`. A `remote`
|
|
269
|
+
* executor is never offered such a task.
|
|
199
270
|
*/
|
|
200
271
|
readonly pinnedLocal: boolean
|
|
201
272
|
/** Declares `cache` — the only tasks whose input set is described, and so the only ones that can ship. */
|
|
@@ -207,6 +278,54 @@ export interface TaskExecutor {
|
|
|
207
278
|
readonly name: string
|
|
208
279
|
/** Runs the command somewhere else. Never offered a `pinnedLocal` task. */
|
|
209
280
|
readonly remote?: boolean
|
|
281
|
+
/**
|
|
282
|
+
* A `remote` executor that enforces `ExecuteRequest.sandbox` where it
|
|
283
|
+
* runs the command, as the local one does. It is offered sandboxed tasks
|
|
284
|
+
* and their dependants; the request's violations come back on the result.
|
|
285
|
+
*/
|
|
286
|
+
readonly sandbox?: boolean
|
|
287
|
+
/**
|
|
288
|
+
* A `remote` executor that starts a task's persistent dependencies where
|
|
289
|
+
* it runs the task (`ExecuteRequest.servers`). It is offered the
|
|
290
|
+
* dependants of a persistent task with no `exec.sandbox` and no
|
|
291
|
+
* `exec.remote: false`; that task then runs here only for a dependant
|
|
292
|
+
* placed here (2026-10-22: nothing runs on a distributed run's
|
|
293
|
+
* orchestrator).
|
|
294
|
+
*/
|
|
295
|
+
readonly servers?: boolean
|
|
296
|
+
/**
|
|
297
|
+
* A `remote` executor that answers a task's `inputs.runtime` and
|
|
298
|
+
* `workspaceRuntime` probes where it runs the task (`answerRuntime`) and
|
|
299
|
+
* fails it when one answers other than `TaskInputs` says this machine
|
|
300
|
+
* did. It is offered tasks whose key folds a probe, which otherwise run
|
|
301
|
+
* here: their key holds this machine's answer (C-2).
|
|
302
|
+
*/
|
|
303
|
+
readonly runtime?: boolean
|
|
304
|
+
/**
|
|
305
|
+
* A `remote` executor that runs every task of the run, so none runs on
|
|
306
|
+
* this machine; only cache hits restore here (2026-10-24: "no
|
|
307
|
+
* task should ever run on the orchestrator"). Nothing is pinned for it:
|
|
308
|
+
* it is offered `exec.remote: false` and `exec.interactive` tasks too,
|
|
309
|
+
* and starts every server a dependant needs. A task it cannot take (a
|
|
310
|
+
* platform it does not offer, one it declines, a server requested on its
|
|
311
|
+
* own) refuses the run, and a task it gives back fails.
|
|
312
|
+
*/
|
|
313
|
+
readonly exclusive?: boolean
|
|
314
|
+
/**
|
|
315
|
+
* A `remote` executor that enforces `ExecuteRequest.timeoutMs` where it
|
|
316
|
+
* runs the command, timed from its start, and reports `timedOut`. Core
|
|
317
|
+
* then arms no clock of its own: armed at the request, it counted the
|
|
318
|
+
* wait for a worker still booting and killed a 1 s task with a 3 s
|
|
319
|
+
* timeout before it began (2026-10-24). The run's stop still aborts.
|
|
320
|
+
*/
|
|
321
|
+
readonly timeouts?: boolean
|
|
322
|
+
/**
|
|
323
|
+
* The platforms this executor runs tasks on (`exec.platforms`' names). It
|
|
324
|
+
* is offered a task that declares `exec.platforms` only when one of them
|
|
325
|
+
* is listed, and runs it on `ExecuteRequest.platform`; an executor that
|
|
326
|
+
* declares none is never offered such a task off this machine.
|
|
327
|
+
*/
|
|
328
|
+
readonly platforms?: readonly string[]
|
|
210
329
|
/**
|
|
211
330
|
* How many tasks this executor runs at once. Its tasks then occupy a pool
|
|
212
331
|
* of this size instead of the local worker slots, so a remote pool is not
|
|
@@ -217,9 +336,10 @@ export interface TaskExecutor {
|
|
|
217
336
|
/** Per-task opt-out at placement time. Absent = accepts every task it is offered. */
|
|
218
337
|
accepts?(task: TaskPlacement): boolean
|
|
219
338
|
/**
|
|
220
|
-
* The tasks placed here that have NOT finished yet — called
|
|
221
|
-
*
|
|
222
|
-
*
|
|
339
|
+
* The tasks placed here that will run and have NOT finished yet — called
|
|
340
|
+
* first once the run's cache has said which placed tasks it holds (those
|
|
341
|
+
* restore, so they are left out), then again after each completion (any
|
|
342
|
+
* outcome, a failure too) and when a task it left out executes after all.
|
|
223
343
|
*
|
|
224
344
|
* An executor that provisions something per task — a container, an
|
|
225
345
|
* allocation, a pod — otherwise has to guess. `capacity` says how much it
|
|
@@ -228,11 +348,80 @@ export interface TaskExecutor {
|
|
|
228
348
|
* moment the run can no longer use it, rather than holding it until
|
|
229
349
|
* teardown. Absent = the executor does not care and nothing is computed
|
|
230
350
|
* for it.
|
|
351
|
+
*
|
|
352
|
+
* `missed`: of `remaining`, those whose keys the run's up-front probe
|
|
353
|
+
* found in no store (each store asked answered for every key), or, when
|
|
354
|
+
* the remote has no batch probe, those the index keeps no hit of: they
|
|
355
|
+
* will run. The rest may still restore, or were not probed (a key an upstream's outputs can
|
|
356
|
+
* change). The same set each call, changed in place.
|
|
231
357
|
*/
|
|
232
|
-
demand?(remaining: ReadonlySet<string>): void
|
|
358
|
+
demand?(remaining: ReadonlySet<string>, missed: ReadonlySet<string>): void
|
|
359
|
+
/**
|
|
360
|
+
* Rows the run's footer shows for this executor, `[label, text]`, the
|
|
361
|
+
* text plain or a `ui` inline (a tone colors it): what only it knows, such as the machines it ran tasks on. Asked on every
|
|
362
|
+
* frame of the live footer and once for the final one, so it is cheap;
|
|
363
|
+
* a throw shows nothing.
|
|
364
|
+
*/
|
|
365
|
+
footer?(): readonly (readonly [label: string, text: ui.Inline])[]
|
|
366
|
+
/**
|
|
367
|
+
* The machines it ran the run's tasks on (`small#1`, `local`), for the
|
|
368
|
+
* run summary telemetry sinks read; asked once at the end, only when a
|
|
369
|
+
* sink is on. A throw reports none.
|
|
370
|
+
*/
|
|
371
|
+
machines?(): readonly MachineReport[]
|
|
372
|
+
/**
|
|
373
|
+
* The run's end had it placed its tasks perfectly, from the run's
|
|
374
|
+
* start: what each took, replayed on the machines it could have had, for
|
|
375
|
+
* the run summary beside the real time. Asked once at the end, only when a sink is on. A throw, or
|
|
376
|
+
* anything but a finite number, reports none.
|
|
377
|
+
*/
|
|
378
|
+
idealMs?(): number | undefined
|
|
379
|
+
/**
|
|
380
|
+
* Its own Chrome-trace events for `vx run --profile` (the machines it ran
|
|
381
|
+
* tasks on, in time), asked once at the end, only when the flag is set.
|
|
382
|
+
* `ts` is in microseconds of `performance.now()`; `pid` from 1, which
|
|
383
|
+
* the run moves past its own process. A throw adds none.
|
|
384
|
+
*/
|
|
385
|
+
profile?(): readonly Record<string, unknown>[]
|
|
386
|
+
/**
|
|
387
|
+
* Where a task it was handed runs (`small#2`), for the live row; asked
|
|
388
|
+
* per frame. `null` while it holds the task for a machine to begin it:
|
|
389
|
+
* the row says `waiting`, and its clock starts when a machine begins it.
|
|
390
|
+
*/
|
|
391
|
+
running?(taskId: string): string | null | undefined
|
|
233
392
|
execute(req: ExecuteRequest): Promise<ExecuteResult>
|
|
234
393
|
}
|
|
235
394
|
|
|
395
|
+
/** One machine an executor ran tasks on, as the run summary reports it. */
|
|
396
|
+
export interface MachineReport {
|
|
397
|
+
/** As task rows name it (`small#1`). */
|
|
398
|
+
name: string
|
|
399
|
+
/** `online`, or `gone` for one that left before the run ended. */
|
|
400
|
+
status: string
|
|
401
|
+
cores: number
|
|
402
|
+
memoryBytes: number
|
|
403
|
+
done: number
|
|
404
|
+
failed: number
|
|
405
|
+
/** From when it joined to when it left or the run ended: warm-up not counted. */
|
|
406
|
+
onlineMs: number
|
|
407
|
+
/** Time with at least one task on it, from its send to its result. */
|
|
408
|
+
busyMs: number
|
|
409
|
+
/** Cores in use at once, at the most and on average over its time online. */
|
|
410
|
+
cpuPeak: number
|
|
411
|
+
cpuAvg: number
|
|
412
|
+
/** Bytes in use at once, at the most and on average. */
|
|
413
|
+
memoryPeakBytes: number
|
|
414
|
+
memoryAvgBytes: number
|
|
415
|
+
/** Core-time its tasks used over the core-time it had, 0 to 1. */
|
|
416
|
+
utilization: number
|
|
417
|
+
/** Cores the executor booked for the tasks it ran, on average over its time online. */
|
|
418
|
+
bookedAvg?: number
|
|
419
|
+
/** From when the executor asked for it to when it joined, for one it started. */
|
|
420
|
+
warmUpMs?: number
|
|
421
|
+
/** When it was asked for (else when it joined), from the executor's start. */
|
|
422
|
+
startMs?: number
|
|
423
|
+
}
|
|
424
|
+
|
|
236
425
|
/**
|
|
237
426
|
* What a remote executor rejects with when it gives a task back: core runs
|
|
238
427
|
* the same request on the local floor and says `reason` once (a remote that
|
|
@@ -267,6 +456,8 @@ export function assertExecuteResult(
|
|
|
267
456
|
`executor '${executor}' returned an invalid result for ${taskId}: ${what} — a plugin bug, not a task failure`,
|
|
268
457
|
)
|
|
269
458
|
}
|
|
459
|
+
const shown = (v: unknown): string =>
|
|
460
|
+
typeof v === 'object' && v !== null ? 'an object' : String(v as string)
|
|
270
461
|
if (typeof res !== 'object' || res === null)
|
|
271
462
|
fail(`${res === null ? 'null' : typeof res} (expected an object)`)
|
|
272
463
|
const r = res as Record<string, unknown>
|
|
@@ -284,6 +475,46 @@ export function assertExecuteResult(
|
|
|
284
475
|
}
|
|
285
476
|
if (!Array.isArray(r['violations']))
|
|
286
477
|
fail(`violations is ${typeof r['violations']} (expected an array)`)
|
|
478
|
+
// The optional fields reach the task's row, telemetry and the run
|
|
479
|
+
// history: a `where` object lost the whole run's history to a bind error
|
|
480
|
+
// naming no executor, and a `null` violation failed the task as a TypeError.
|
|
481
|
+
for (const [i, v] of (r['violations'] as unknown[]).entries()) {
|
|
482
|
+
if (typeof v !== 'object' || v === null || typeof (v as { line?: unknown }).line !== 'string')
|
|
483
|
+
fail(`violations[${i}] is not an object with a string line`)
|
|
484
|
+
}
|
|
485
|
+
const where = r['where']
|
|
486
|
+
if (where !== undefined && (typeof where !== 'string' || where === ''))
|
|
487
|
+
fail(`where is ${where === '' ? 'empty' : typeof where} (expected a non-empty string)`)
|
|
488
|
+
for (const key of ['cpuMs', 'peakRssBytes']) {
|
|
489
|
+
const n = r[key]
|
|
490
|
+
if (n !== undefined && !(typeof n === 'number' && Number.isFinite(n) && n >= 0))
|
|
491
|
+
fail(`${key} is ${shown(n)} (expected a non-negative number)`)
|
|
492
|
+
}
|
|
493
|
+
const signal = r['signal']
|
|
494
|
+
if (
|
|
495
|
+
signal !== undefined &&
|
|
496
|
+
typeof signal !== 'string' &&
|
|
497
|
+
!(typeof signal === 'number' && Number.isInteger(signal) && signal > 0)
|
|
498
|
+
)
|
|
499
|
+
fail(`signal is ${shown(signal)} (expected a name or a signal number)`)
|
|
500
|
+
if (r['timedOut'] !== undefined && typeof r['timedOut'] !== 'boolean')
|
|
501
|
+
fail(`timedOut is ${typeof r['timedOut']} (expected a boolean)`)
|
|
502
|
+
const machine = r['machine']
|
|
503
|
+
if (machine !== undefined) {
|
|
504
|
+
if (typeof machine !== 'object' || machine === null) fail('machine is not an object')
|
|
505
|
+
const m = machine as Record<string, unknown>
|
|
506
|
+
// A worker that cannot read its memory reports 0 (vx-dte's join takes it).
|
|
507
|
+
for (const [key, least] of [
|
|
508
|
+
['cores', 'positive'],
|
|
509
|
+
['memoryBytes', 'non-negative'],
|
|
510
|
+
['cpuShare', 'positive'],
|
|
511
|
+
] as const) {
|
|
512
|
+
const n = m[key]
|
|
513
|
+
if (key === 'cpuShare' && n === undefined) continue
|
|
514
|
+
if (!(typeof n === 'number' && Number.isFinite(n) && (least === 'positive' ? n > 0 : n >= 0)))
|
|
515
|
+
fail(`machine.${key} is ${shown(n)} (expected a ${least} number)`)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
287
518
|
const outputs = r['outputs'] as { kind?: unknown; materialize?: unknown } | undefined
|
|
288
519
|
if (outputs !== undefined) {
|
|
289
520
|
if (typeof outputs !== 'object' || outputs === null) fail('outputs is not an object')
|
|
@@ -293,6 +524,12 @@ export function assertExecuteResult(
|
|
|
293
524
|
} else if (outputs.kind !== 'disk')
|
|
294
525
|
fail(`outputs.kind is ${String(outputs.kind)} (expected disk or deferred)`)
|
|
295
526
|
}
|
|
527
|
+
const changed = r['changed']
|
|
528
|
+
if (
|
|
529
|
+
changed !== undefined &&
|
|
530
|
+
(!Array.isArray(changed) || changed.some((f) => typeof f !== 'string'))
|
|
531
|
+
)
|
|
532
|
+
fail('changed is not an array of paths')
|
|
296
533
|
}
|
|
297
534
|
|
|
298
535
|
/**
|
|
@@ -307,13 +544,18 @@ export function selectExecutor(
|
|
|
307
544
|
executors: readonly TaskExecutor[],
|
|
308
545
|
task: TaskPlacement,
|
|
309
546
|
label: (executor: TaskExecutor) => string = (e) => `executor '${e.name}'`,
|
|
547
|
+
/** `task.pinnedLocal` as `executor` sees it: one that enforces the sandbox or carries servers lifts some pins. */
|
|
548
|
+
pinnedFor: (executor: TaskExecutor) => boolean = () => task.pinnedLocal,
|
|
310
549
|
): TaskExecutor {
|
|
311
550
|
for (const executor of executors) {
|
|
312
|
-
|
|
551
|
+
const pinned = pinnedFor(executor)
|
|
552
|
+
if (pinned && executor.remote === true) continue
|
|
313
553
|
if (executor.accepts === undefined) return executor
|
|
314
554
|
let taken: unknown
|
|
315
555
|
try {
|
|
316
|
-
taken = executor.accepts(
|
|
556
|
+
taken = executor.accepts(
|
|
557
|
+
pinned === task.pinnedLocal ? task : { ...task, pinnedLocal: pinned },
|
|
558
|
+
)
|
|
317
559
|
// An `async accepts` answers a Promise: truthy, so the executor took
|
|
318
560
|
// every task, and a rejection went unheard (the shape of H-16's admit).
|
|
319
561
|
if (taken instanceof Promise) {
|
|
@@ -321,7 +563,7 @@ export function selectExecutor(
|
|
|
321
563
|
throw new Error('returned a Promise; accepts is synchronous')
|
|
322
564
|
}
|
|
323
565
|
} catch (err) {
|
|
324
|
-
const m =
|
|
566
|
+
const m = thrownMessage(err)
|
|
325
567
|
throw new UserError(`${label(executor)} failed in accepts for ${task.taskId}: ${m}`)
|
|
326
568
|
}
|
|
327
569
|
if (taken) return executor
|
|
@@ -330,3 +572,8 @@ export function selectExecutor(
|
|
|
330
572
|
`no executor accepted ${task.taskId} (declared: ${executors.map((e) => e.name).join(', ')}). Core's local executor accepts every task, so this means it is missing from the list.`,
|
|
331
573
|
)
|
|
332
574
|
}
|
|
575
|
+
|
|
576
|
+
/** This machine as `exec.platforms` names it: `os-arch` as Bun reports them, `linux-x64`. */
|
|
577
|
+
export function hostPlatform(): string {
|
|
578
|
+
return `${process.platform}-${process.arch}`
|
|
579
|
+
}
|
package/src/exec/index.ts
CHANGED
|
@@ -9,7 +9,9 @@ export {
|
|
|
9
9
|
VX_RUN_WORKSPACE_ENV,
|
|
10
10
|
} from './env.js'
|
|
11
11
|
export {
|
|
12
|
+
lineEnds,
|
|
12
13
|
runCommand,
|
|
14
|
+
restoreTerminalModes,
|
|
13
15
|
runPersistent,
|
|
14
16
|
PersistentReadyError,
|
|
15
17
|
withForwardArgs,
|
|
@@ -19,6 +21,7 @@ export {
|
|
|
19
21
|
type CaptureConfig,
|
|
20
22
|
execWord,
|
|
21
23
|
maskCaptured,
|
|
24
|
+
isDroppedOutputChunk,
|
|
22
25
|
BoundedCapture,
|
|
23
26
|
type CapturedChunk,
|
|
24
27
|
} from './runner.js'
|
|
@@ -48,17 +51,20 @@ export {
|
|
|
48
51
|
} from './sandbox-paths.js'
|
|
49
52
|
export { bindableWrites, punchWalls } from './sandbox-binds.js'
|
|
50
53
|
export { isLocalExecutor, localExecutor } from './local-executor.js'
|
|
51
|
-
export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
|
|
52
|
-
export { sampleTrees } from './proc-sample.js'
|
|
54
|
+
export { holdGroups, killTree, pauseTree, untilGroupsGone } from './kill-tree.js'
|
|
55
|
+
export { sampleTrees, type TreeUsage } from './proc-sample.js'
|
|
53
56
|
export {
|
|
54
57
|
assertExecuteResult,
|
|
55
58
|
executorFallback,
|
|
59
|
+
hostPlatform,
|
|
56
60
|
isExecutorFallback,
|
|
57
61
|
selectExecutor,
|
|
58
62
|
type ExecuteRequest,
|
|
59
63
|
type ExecuteResult,
|
|
60
64
|
type ExecuteSandbox,
|
|
61
65
|
type InputFile,
|
|
66
|
+
type MachineReport,
|
|
67
|
+
type ServerSpec,
|
|
62
68
|
type TaskExecutor,
|
|
63
69
|
type TaskInputs,
|
|
64
70
|
type TaskPlacement,
|
package/src/exec/kill-tree.ts
CHANGED
|
@@ -328,6 +328,21 @@ export function killTree(child: Child, signal: 'SIGINT' | 'SIGTERM' | 'SIGKILL')
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Stop or continue `child`'s group with vx (Ctrl-Z, `fg`). Straight to
|
|
333
|
+
* the group, never down a sandbox's channel: its watcher passes one
|
|
334
|
+
* signal and is gone. Under a Linux sandbox that stops bwrap, and the
|
|
335
|
+
* command inside blocks once its output pipe fills.
|
|
336
|
+
*/
|
|
337
|
+
export function pauseTree(child: Child, signal: 'SIGSTOP' | 'SIGCONT'): void {
|
|
338
|
+
if (!(child.pid > 0) || goneGroups.has(child)) return
|
|
339
|
+
try {
|
|
340
|
+
process.kill(-child.pid, signal)
|
|
341
|
+
} catch {
|
|
342
|
+
// The group is gone, or not ours: nothing of it to stop.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
331
346
|
/**
|
|
332
347
|
* How often a group whose leader has exited is asked again whether any
|
|
333
348
|
* member is left. Only a group that outlived its leader is polled.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// process. Not a plugin — "run it on this machine" is not a capability
|
|
3
3
|
// someone else supplies differently, it is what running MEANS when no
|
|
4
4
|
// plugin claims the task, so it sits at the TAIL of every executor list
|
|
5
|
-
// (
|
|
5
|
+
// (2026-09-05). An executor plugin places work ELSEWHERE; its
|
|
6
6
|
// absence is "here".
|
|
7
7
|
import { runCommand, signalExitCode, stopSignal } from './runner.js'
|
|
8
8
|
import { runSandboxed } from './sandbox-runtime.js'
|
package/src/exec/proc-sample.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// counted; nor is the CPU of a descendant that already exited.
|
|
9
9
|
|
|
10
10
|
import { readdir, readFile } from 'node:fs/promises'
|
|
11
|
-
import { procfsIsOwn } from '../util/index.js'
|
|
11
|
+
import { helperEnv, procfsIsOwn } from '../util/index.js'
|
|
12
12
|
|
|
13
13
|
export interface TreeUsage {
|
|
14
14
|
/** CPU time of the live tree, user + system, in ms. */
|
|
@@ -19,7 +19,10 @@ export interface TreeUsage {
|
|
|
19
19
|
|
|
20
20
|
interface Proc {
|
|
21
21
|
ppid: number
|
|
22
|
+
/** Its own CPU and, on Linux, what its reaped children used (`cutime`, `cstime`). */
|
|
22
23
|
cpuMs: number
|
|
24
|
+
/** The executable's name; Linux only. */
|
|
25
|
+
comm?: string
|
|
23
26
|
/** Resident KiB when the table carries it; Linux reads it per tree member. */
|
|
24
27
|
rssKiB?: number
|
|
25
28
|
}
|
|
@@ -32,7 +35,11 @@ interface Proc {
|
|
|
32
35
|
const TICK_MS = 10
|
|
33
36
|
|
|
34
37
|
/** Each root's tree usage; a root no longer alive has no entry. */
|
|
35
|
-
export async function sampleTrees(
|
|
38
|
+
export async function sampleTrees(
|
|
39
|
+
roots: readonly number[],
|
|
40
|
+
/** Members not counted (their children still are): the sandbox's own tracer. */
|
|
41
|
+
skip?: ReadonlySet<string>,
|
|
42
|
+
): Promise<Map<number, TreeUsage>> {
|
|
36
43
|
const out = new Map<number, TreeUsage>()
|
|
37
44
|
if (roots.length === 0) return out
|
|
38
45
|
const table = process.platform === 'linux' ? await linuxTable() : await psTable()
|
|
@@ -49,7 +56,8 @@ export async function sampleTrees(roots: readonly number[]): Promise<Map<number,
|
|
|
49
56
|
const stack = [root]
|
|
50
57
|
while (stack.length > 0) {
|
|
51
58
|
const pid = stack.pop()!
|
|
52
|
-
|
|
59
|
+
const comm = table.get(pid)!.comm
|
|
60
|
+
if (skip === undefined || comm === undefined || !skip.has(comm)) tree.push(pid)
|
|
53
61
|
for (const c of children.get(pid) ?? []) stack.push(c)
|
|
54
62
|
}
|
|
55
63
|
let cpuMs = 0
|
|
@@ -86,12 +94,16 @@ async function linuxTable(): Promise<Map<number, Proc> | undefined> {
|
|
|
86
94
|
}
|
|
87
95
|
// `<pid> (<comm>) <state> <ppid> …` — comm may hold spaces and parens,
|
|
88
96
|
// so the fields start after the LAST ')': state is [0], ppid [1],
|
|
89
|
-
// utime [11], stime [12].
|
|
90
|
-
|
|
97
|
+
// utime [11], stime [12], cutime [13], cstime [14]. The reaped
|
|
98
|
+
// children's share keeps a tree's sum from dropping when a member
|
|
99
|
+
// exits: what a task's short-lived processes used stays counted.
|
|
100
|
+
const close = stat.lastIndexOf(')')
|
|
101
|
+
const f = stat.slice(close + 2).split(' ')
|
|
91
102
|
if (f[0] === 'Z' || f[0] === 'X') return
|
|
92
103
|
table.set(Number(entry), {
|
|
93
104
|
ppid: Number(f[1]),
|
|
94
|
-
cpuMs: (Number(f[11]) + Number(f[12])) * TICK_MS,
|
|
105
|
+
cpuMs: (Number(f[11]) + Number(f[12]) + Number(f[13]) + Number(f[14])) * TICK_MS,
|
|
106
|
+
comm: stat.slice(stat.indexOf('(') + 1, close),
|
|
95
107
|
})
|
|
96
108
|
}),
|
|
97
109
|
)
|
|
@@ -113,7 +125,9 @@ async function linuxRssKiB(pid: number): Promise<number> {
|
|
|
113
125
|
async function psTable(): Promise<Map<number, Proc> | undefined> {
|
|
114
126
|
let text: string
|
|
115
127
|
try {
|
|
128
|
+
// It runs beside the tasks, readable by them (`ps eww`): PATH alone.
|
|
116
129
|
const ps = Bun.spawn(['ps', '-A', '-o', 'pid=,ppid=,rss=,time='], {
|
|
130
|
+
env: helperEnv(),
|
|
117
131
|
stdin: 'ignore',
|
|
118
132
|
stdout: 'pipe',
|
|
119
133
|
stderr: 'ignore',
|
|
@@ -153,3 +167,53 @@ export function psTimeMs(s: string): number | undefined {
|
|
|
153
167
|
for (const n of parts) seconds = seconds * 60 + n
|
|
154
168
|
return Math.round((days * 86_400 + seconds) * 1000)
|
|
155
169
|
}
|
|
170
|
+
|
|
171
|
+
/** How often a watched tree is looked at; one timer serves every tree. */
|
|
172
|
+
const WATCH_EVERY_MS = 250
|
|
173
|
+
|
|
174
|
+
const watched = new Map<number, TreeUsage & { looks: number }>()
|
|
175
|
+
let watchTimer: ReturnType<typeof setInterval> | undefined
|
|
176
|
+
let looking = false
|
|
177
|
+
const SKIP = new Set(['strace'])
|
|
178
|
+
|
|
179
|
+
async function look(): Promise<void> {
|
|
180
|
+
if (looking) return
|
|
181
|
+
looking = true
|
|
182
|
+
try {
|
|
183
|
+
const usage = await sampleTrees([...watched.keys()], SKIP)
|
|
184
|
+
for (const [pid, u] of usage) {
|
|
185
|
+
const w = watched.get(pid)
|
|
186
|
+
if (w === undefined) continue
|
|
187
|
+
w.cpuMs = Math.max(w.cpuMs, u.cpuMs)
|
|
188
|
+
w.rssBytes = Math.max(w.rssBytes, u.rssBytes)
|
|
189
|
+
w.looks++
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// A failed look is a missing point.
|
|
193
|
+
} finally {
|
|
194
|
+
looking = false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Watch a running tree until `stop`, which answers the most CPU and the
|
|
200
|
+
* peak resident memory any look saw; none when no look landed (a task
|
|
201
|
+
* shorter than one interval). The sandbox's answer on Linux, where the
|
|
202
|
+
* wait never sees what bwrap's pid namespace used.
|
|
203
|
+
*/
|
|
204
|
+
export function watchTree(pid: number): { stop(): TreeUsage | undefined } {
|
|
205
|
+
const w = { cpuMs: 0, rssBytes: 0, looks: 0 }
|
|
206
|
+
watched.set(pid, w)
|
|
207
|
+
watchTimer ??= setInterval(() => void look(), WATCH_EVERY_MS)
|
|
208
|
+
watchTimer.unref?.()
|
|
209
|
+
return {
|
|
210
|
+
stop() {
|
|
211
|
+
if (watched.get(pid) === w) watched.delete(pid)
|
|
212
|
+
if (watched.size === 0 && watchTimer !== undefined) {
|
|
213
|
+
clearInterval(watchTimer)
|
|
214
|
+
watchTimer = undefined
|
|
215
|
+
}
|
|
216
|
+
return w.looks === 0 ? undefined : { cpuMs: w.cpuMs, rssBytes: w.rssBytes }
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
}
|