@vzn/vx 0.0.651 → 0.0.653
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
// rendering-oriented `WireEvent` stream.
|
|
11
11
|
//
|
|
12
12
|
// A sink is observe-only BY CONSTRUCTION: its only input is a plain-data
|
|
13
|
-
// record (one object every sink receives,
|
|
13
|
+
// record (one object every sink receives, frozen once, deep: a sink that
|
|
14
|
+
// cleared a `task.end`'s `failedAttempts` cleared the outcome's own and
|
|
15
|
+
// crashed the run's failure log); its `TelemetryContext` carries read-only metadata and NO mutable
|
|
14
16
|
// run handle (no bus, no Cache, no RunRequest). There is no API path from a
|
|
15
17
|
// sink back into scheduling/caching/exec — telemetry provably cannot change
|
|
16
18
|
// what or how tasks run. Contrast `cache`/`executor`, which return objects
|
|
17
19
|
// core calls INTO; those are the behavior capabilities, kept separate.
|
|
18
20
|
|
|
19
|
-
import { sampleTrees, type MachineReport } from '../exec/index.js'
|
|
21
|
+
import { sampleTrees, type MachineReport, type WorkerAdvice } from '../exec/index.js'
|
|
20
22
|
import {
|
|
21
23
|
type CacheSource,
|
|
22
24
|
deriveCacheSource,
|
|
@@ -34,7 +36,8 @@ import {
|
|
|
34
36
|
teardownTimeoutMs,
|
|
35
37
|
thrownMessage,
|
|
36
38
|
} from '../util/index.js'
|
|
37
|
-
import type
|
|
39
|
+
import { neverStarted, type RunEvent, type RunEventSubscriber } from '../report/index.js'
|
|
40
|
+
import { freezeDeep } from './read-only.js'
|
|
38
41
|
|
|
39
42
|
/** Bumped when the record shape changes. Readers MUST check `v`. */
|
|
40
43
|
export const TELEMETRY_SCHEMA_VERSION = 4
|
|
@@ -256,14 +259,20 @@ export interface RunSummaryRecord {
|
|
|
256
259
|
startedAt: number
|
|
257
260
|
endedAt: number
|
|
258
261
|
totalDurationMs: number
|
|
262
|
+
/** Every task the run had, as the terminal's total: `tasks` plus `notRunCount`. */
|
|
259
263
|
taskCount: number
|
|
260
264
|
failedCount: number
|
|
261
265
|
/**
|
|
262
|
-
* Tasks a shutdown signal or an embedder's abort killed (v3)
|
|
263
|
-
*
|
|
264
|
-
*
|
|
266
|
+
* Tasks a shutdown signal or an embedder's abort killed once started (v3),
|
|
267
|
+
* each in `tasks` as `aborted`: without the count a stopped run read as a
|
|
268
|
+
* failure with nothing failed (item 851).
|
|
265
269
|
*/
|
|
266
270
|
abortedCount: number
|
|
271
|
+
/**
|
|
272
|
+
* Tasks the stop reached before they started: the terminal's `not run`.
|
|
273
|
+
* Not in `tasks`, since they never ran. Absent when none. Additive.
|
|
274
|
+
*/
|
|
275
|
+
notRunCount?: number
|
|
267
276
|
hitCount: number
|
|
268
277
|
/** Hits by the layer that answered, up-to-date ones included. */
|
|
269
278
|
hitLocalCount: number
|
|
@@ -301,6 +310,8 @@ export interface RunSummaryRecord {
|
|
|
301
310
|
* (`TaskExecutor.idealMs`), beside `totalDurationMs`. Additive.
|
|
302
311
|
*/
|
|
303
312
|
idealMs?: number
|
|
313
|
+
/** The workers that would have run it best (`TaskExecutor.advice`). Additive. */
|
|
314
|
+
advice?: readonly WorkerAdvice[]
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
/** One stage of a run, a wall window in epoch ms. */
|
|
@@ -316,8 +327,9 @@ export interface RunStage {
|
|
|
316
327
|
* computed: both a local `run()` and the distributed controller build their
|
|
317
328
|
* `TaskTelemetry[]` (each via `deriveCacheSource`) and call this, so a
|
|
318
329
|
* distributed run and a local run produce byte-identical summaries and land in
|
|
319
|
-
* the same ingest. The per-task tallies (
|
|
320
|
-
* hitLocal|Remote|Count) derive from `tasks
|
|
330
|
+
* the same ingest. The per-task tallies (failedCount /
|
|
331
|
+
* hitLocal|Remote|Count) derive from `tasks`, and `taskCount` from `tasks`
|
|
332
|
+
* plus `notRunCount`; `totalDurationMs` (wall time) and
|
|
321
333
|
* `exitOk` (the run's overall verdict — which counts skipped tasks beyond the
|
|
322
334
|
* recorded task list) are run-level facts and are passed in.
|
|
323
335
|
*/
|
|
@@ -330,10 +342,12 @@ export function assembleRunSummary(
|
|
|
330
342
|
totalDurationMs: number
|
|
331
343
|
exitOk: boolean
|
|
332
344
|
abortedCount: number
|
|
345
|
+
notRunCount?: number
|
|
333
346
|
stages?: readonly RunStage[]
|
|
334
347
|
uploads?: { count: number; bytes: number; ms: number; failed: number }
|
|
335
348
|
machines?: readonly MachineReport[]
|
|
336
349
|
idealMs?: number | undefined
|
|
350
|
+
advice?: readonly WorkerAdvice[]
|
|
337
351
|
},
|
|
338
352
|
): RunSummaryRecord {
|
|
339
353
|
let failedCount = 0
|
|
@@ -357,9 +371,14 @@ export function assembleRunSummary(
|
|
|
357
371
|
startedAt: timing.startedAt,
|
|
358
372
|
endedAt: timing.endedAt,
|
|
359
373
|
totalDurationMs: timing.totalDurationMs,
|
|
360
|
-
|
|
374
|
+
// The terminal's total: the tasks a stop reached first are not in
|
|
375
|
+
// `tasks`, yet a summary of 2 beside a terminal's 3 read as a lost task.
|
|
376
|
+
taskCount: tasks.length + (timing.notRunCount ?? 0),
|
|
361
377
|
failedCount,
|
|
362
378
|
abortedCount: timing.abortedCount,
|
|
379
|
+
...(timing.notRunCount !== undefined && timing.notRunCount > 0
|
|
380
|
+
? { notRunCount: timing.notRunCount }
|
|
381
|
+
: {}),
|
|
363
382
|
hitCount: hitLocalCount + hitRemoteCount,
|
|
364
383
|
hitLocalCount,
|
|
365
384
|
hitRemoteCount,
|
|
@@ -376,10 +395,15 @@ export function assembleRunSummary(
|
|
|
376
395
|
? { machines: timing.machines }
|
|
377
396
|
: {}),
|
|
378
397
|
...(timing.idealMs !== undefined ? { idealMs: timing.idealMs } : {}),
|
|
398
|
+
...(timing.advice !== undefined && timing.advice.length > 0 ? { advice: timing.advice } : {}),
|
|
379
399
|
}
|
|
380
400
|
}
|
|
381
401
|
|
|
382
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* A telemetry consumer. Observe-only: receives records, frozen, holds no
|
|
404
|
+
* run handle. A write to a record throws, and the sink is switched off for
|
|
405
|
+
* the run as for any throw.
|
|
406
|
+
*/
|
|
383
407
|
export interface TelemetrySink {
|
|
384
408
|
readonly name?: string
|
|
385
409
|
/**
|
|
@@ -391,7 +415,11 @@ export interface TelemetrySink {
|
|
|
391
415
|
readonly wants?: ReadonlyArray<TelemetryRecord['kind']>
|
|
392
416
|
/** A streaming record. MUST return promptly — buffer; do NOT await I/O. */
|
|
393
417
|
onRecord?(record: TelemetryRecord): void
|
|
394
|
-
/**
|
|
418
|
+
/**
|
|
419
|
+
* The per-run summary, once at run:end; a requested persistent task the
|
|
420
|
+
* foreground keeps sends it when that task exits, so it reads the run's
|
|
421
|
+
* final verdict. MUST return promptly.
|
|
422
|
+
*/
|
|
395
423
|
onRunSummary?(summary: RunSummaryRecord): void
|
|
396
424
|
/**
|
|
397
425
|
* Drain buffered data. Awaited at run:end under a deadline — a sink that
|
|
@@ -429,6 +457,12 @@ export interface TelemetrySource {
|
|
|
429
457
|
* each as the step `<plugin>:telemetry` through `step`.
|
|
430
458
|
*/
|
|
431
459
|
flush(step?: <T>(id: string, work: () => Promise<T>) => Promise<T>): Promise<void>
|
|
460
|
+
/**
|
|
461
|
+
* Send the held `task.end` of each persistent task not in `keep`, read
|
|
462
|
+
* from `final` (by id): a server's outcome can change after its ready,
|
|
463
|
+
* when it dies before the run stops it or ends a kept session.
|
|
464
|
+
*/
|
|
465
|
+
endServers(final: readonly TaskOutcome[], keep?: ReadonlySet<string>): void
|
|
432
466
|
/**
|
|
433
467
|
* Sample this task's process tree every `SAMPLE_MS` until the returned
|
|
434
468
|
* function is called, the root exits, or the run ends. Present only when
|
|
@@ -598,6 +632,9 @@ export function createTelemetrySource(args: {
|
|
|
598
632
|
}
|
|
599
633
|
|
|
600
634
|
function deliver(record: TelemetryRecord): void {
|
|
635
|
+
// Once, for every sink: a `task.end` holds the outcome's own arrays, and
|
|
636
|
+
// a sink that cleared `failedAttempts` crashed the run's failure log.
|
|
637
|
+
freezeDeep(record)
|
|
601
638
|
for (const sink of sinks) {
|
|
602
639
|
if (disabled.has(sink) || sink.onRecord === undefined) continue
|
|
603
640
|
const kinds = sink.wants ?? DEFAULT_KINDS
|
|
@@ -663,6 +700,16 @@ export function createTelemetrySource(args: {
|
|
|
663
700
|
case 'task:complete': {
|
|
664
701
|
const { node, outcome } = event
|
|
665
702
|
if (node.config.exec === undefined) return // group task
|
|
703
|
+
// The stop reached it first: no task.start, and the summary counts
|
|
704
|
+
// it in `notRunCount`, not `tasks`, so no task.end either.
|
|
705
|
+
if (neverStarted(outcome)) return
|
|
706
|
+
// A server's end waits for its final outcome (`endServers`): one
|
|
707
|
+
// task.end per task, and the one sent at ready read `success` over
|
|
708
|
+
// a crash after it.
|
|
709
|
+
if (node.config.exec.persistent !== undefined) {
|
|
710
|
+
heldEnds.set(node.id, { ts, outcome })
|
|
711
|
+
return
|
|
712
|
+
}
|
|
666
713
|
deliver({
|
|
667
714
|
v: TELEMETRY_SCHEMA_VERSION,
|
|
668
715
|
kind: 'task.end',
|
|
@@ -683,10 +730,26 @@ export function createTelemetrySource(args: {
|
|
|
683
730
|
}
|
|
684
731
|
}
|
|
685
732
|
|
|
733
|
+
const heldEnds = new Map<string, { ts: number; outcome: TaskOutcome }>()
|
|
686
734
|
return {
|
|
687
735
|
subscriber,
|
|
688
736
|
...(wantsSample ? { track } : {}),
|
|
737
|
+
endServers(final, keep) {
|
|
738
|
+
for (const [id, held] of heldEnds) {
|
|
739
|
+
if (keep?.has(id) === true) continue
|
|
740
|
+
heldEnds.delete(id)
|
|
741
|
+
const outcome = final.find((o) => o.node.id === id) ?? held.outcome
|
|
742
|
+
deliver({
|
|
743
|
+
v: TELEMETRY_SCHEMA_VERSION,
|
|
744
|
+
kind: 'task.end',
|
|
745
|
+
runId,
|
|
746
|
+
ts: held.ts,
|
|
747
|
+
...taskTelemetryOf(outcome),
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
},
|
|
689
751
|
emitSummary(summary: RunSummaryRecord): void {
|
|
752
|
+
freezeDeep(summary)
|
|
690
753
|
for (const sink of sinks) {
|
|
691
754
|
if (disabled.has(sink) || sink.onRunSummary === undefined) continue
|
|
692
755
|
try {
|
|
@@ -708,9 +771,12 @@ export function createTelemetrySource(args: {
|
|
|
708
771
|
// accessed_at bumps and every later plugin's teardown lost with it.
|
|
709
772
|
// Sinks race concurrently, so each still gets the whole budget.
|
|
710
773
|
const deadline = new AbortController()
|
|
774
|
+
// The deadline's warning names the sinks still out, as a throw names its own.
|
|
775
|
+
const pending = new Set<TelemetrySink>()
|
|
711
776
|
const all = Promise.all(
|
|
712
777
|
sinks.map(async (sink) => {
|
|
713
778
|
if (disabled.has(sink) || sink.flush === undefined) return
|
|
779
|
+
pending.add(sink)
|
|
714
780
|
try {
|
|
715
781
|
await step(`${args.owners?.get(sink) ?? label(sink)}:telemetry`, () =>
|
|
716
782
|
sink.flush!(deadline.signal),
|
|
@@ -719,12 +785,17 @@ export function createTelemetrySource(args: {
|
|
|
719
785
|
// A flush failure can never break the run — but it still costs
|
|
720
786
|
// this sink its export, so say so rather than dropping it.
|
|
721
787
|
warn?.(`[vx] telemetry sink '${label(sink)}' failed to flush: ${thrownMessage(err)}`)
|
|
788
|
+
} finally {
|
|
789
|
+
pending.delete(sink)
|
|
722
790
|
}
|
|
723
791
|
}),
|
|
724
792
|
)
|
|
725
793
|
if (!(await settleWithin(all, ms))) {
|
|
726
794
|
deadline.abort(new Error(`telemetry flush deadline (${ms}ms)`))
|
|
727
|
-
|
|
795
|
+
const names = [...pending].map((s) => `'${label(s)}'`).join(', ')
|
|
796
|
+
warn?.(
|
|
797
|
+
`[vx] telemetry flush timed out after ${ms}ms (sink${pending.size === 1 ? '' : 's'} ${names}); buffered records lost`,
|
|
798
|
+
)
|
|
728
799
|
// A sink told of the abort says what it lost as its request ends; the
|
|
729
800
|
// run ended first and that line was lost (vx-otel, on CI). Briefly.
|
|
730
801
|
await settleWithin(all, ABORT_GRACE_MS)
|
|
@@ -763,7 +834,8 @@ export function taskTelemetryOf(o: TaskOutcome): TaskTelemetry {
|
|
|
763
834
|
project: o.node.projectName,
|
|
764
835
|
task: o.node.taskName,
|
|
765
836
|
status: o.status,
|
|
766
|
-
|
|
837
|
+
// A hit whose restore failed is in no cache column: neither a miss nor a hit.
|
|
838
|
+
cacheSource: o.restoreFailed === true ? 'none' : deriveCacheSource(o.status),
|
|
767
839
|
exitCode: o.exitCode,
|
|
768
840
|
durationMs: o.durationMs,
|
|
769
841
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
selectFoldedDeps,
|
|
3
|
+
type StandIns,
|
|
3
4
|
type FoldCandidate,
|
|
4
5
|
type TaskNode,
|
|
5
6
|
type TaskOutcome,
|
|
@@ -72,10 +73,11 @@ export function filterUpstreamHashes(
|
|
|
72
73
|
filter: readonly string[] | undefined,
|
|
73
74
|
selfProjectName: string,
|
|
74
75
|
selfTaskId: string,
|
|
76
|
+
standIns?: StandIns,
|
|
75
77
|
): Array<[upstreamTaskId: string, hash: string]> {
|
|
76
78
|
const candidates: FoldCandidate[] = []
|
|
77
79
|
for (const u of upstream) if (u.hash) candidates.push({ node: u.node, unit: u.hash })
|
|
78
|
-
return selectFoldedDeps(candidates, filter, selfProjectName, selfTaskId).map((c) => [
|
|
80
|
+
return selectFoldedDeps(candidates, filter, selfProjectName, selfTaskId, standIns).map((c) => [
|
|
79
81
|
c.node.id,
|
|
80
82
|
c.unit,
|
|
81
83
|
])
|
package/src/report/events.ts
CHANGED
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
import { exitSignal } from '../exec/index.js'
|
|
10
10
|
import type { InputChanges, TaskNode, TaskOutcome } from '../graph/index.js'
|
|
11
|
-
import { isGroupTask } from '../graph/index.js'
|
|
11
|
+
import { isGroupTask, isKeyFailed, markOutputsFailed, outputsFailedOf } from '../graph/index.js'
|
|
12
12
|
import { heldSecrets, maskedCommand, type SecretMask, secretMask } from '../util/index.js'
|
|
13
13
|
import type { Logger } from './logger.js'
|
|
14
14
|
import type { RunContext } from './summary.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* A task no cache answered for: it
|
|
17
|
+
* A task no cache answered for: it has no `cache: true`, or the run's policy
|
|
18
18
|
* read and wrote nothing. Its run is `no-cache`, never a miss, on every
|
|
19
19
|
* surface — `--dry`'s plan, the frame, the footer, the run history.
|
|
20
20
|
*/
|
|
21
21
|
export function ranNoCache(o: { node: TaskNode; cacheOff?: boolean }): boolean {
|
|
22
|
-
return o.node.config.cache
|
|
22
|
+
return o.node.config.cache !== true || o.cacheOff === true
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Payload of the `run:start` event — mirrors the Logger.runStart hook. */
|
|
@@ -41,7 +41,8 @@ export interface RunStartInfo {
|
|
|
41
41
|
* refs — zero-copy, because the default terminal subscriber runs on the
|
|
42
42
|
* same thread and reads those objects directly. Crossing a thread or RPC
|
|
43
43
|
* needs the serializable WireEvent form (see `projectNode` / `toWireEvent`
|
|
44
|
-
* below); that mapping is the boundary's job, not the producer's.
|
|
44
|
+
* below); that mapping is the boundary's job, not the producer's. A
|
|
45
|
+
* plugin gets read-only views of them (orchestrator/read-only.ts).
|
|
45
46
|
*/
|
|
46
47
|
export type RunEvent =
|
|
47
48
|
| { kind: 'run:start'; info: RunStartInfo }
|
|
@@ -281,8 +282,10 @@ export interface OutcomeView {
|
|
|
281
282
|
* and counting groups as executed tasks drifts from the terminal.
|
|
282
283
|
*/
|
|
283
284
|
isGroup?: boolean
|
|
284
|
-
/** Executed
|
|
285
|
+
/** Executed without `cache: true`: never consulted the cache, not a miss. */
|
|
285
286
|
noCache?: boolean
|
|
287
|
+
/** A failed hit whose restore threw (see `TaskOutcome`): neither a miss nor a hit. */
|
|
288
|
+
restoreFailed?: true
|
|
286
289
|
/** Cache hits only: the exec time the entry was stored with — the work
|
|
287
290
|
* this hit skipped, as opposed to `durationMs`, the restore it cost. */
|
|
288
291
|
storedDurationMs?: number
|
|
@@ -344,12 +347,18 @@ export function outcomeWord(o: Pick<OutcomeView, 'status' | 'restored'>): string
|
|
|
344
347
|
}
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* `outcomeWord` with the exit code a failure carries: `failed (exit 9)`.
|
|
352
|
+
* A hit whose restore failed ran nothing, so it has no exit to name, nor
|
|
353
|
+
* does a task whose key could not be derived (`isKeyFailed`) or whose
|
|
354
|
+
* output clean or save vx refused (`outputsFailedOf`): the step reads.
|
|
355
|
+
*/
|
|
348
356
|
export function outcomeLabel(
|
|
349
357
|
o: Pick<
|
|
350
358
|
OutcomeView,
|
|
351
359
|
| 'status'
|
|
352
360
|
| 'restored'
|
|
361
|
+
| 'restoreFailed'
|
|
353
362
|
| 'exitCode'
|
|
354
363
|
| 'timedOut'
|
|
355
364
|
| 'sandboxViolations'
|
|
@@ -359,6 +368,11 @@ export function outcomeLabel(
|
|
|
359
368
|
| 'otherPlatform'
|
|
360
369
|
>,
|
|
361
370
|
): string {
|
|
371
|
+
if (o.status === 'failed' && o.restoreFailed === true) return 'failed (cache restore failed)'
|
|
372
|
+
if (o.status === 'failed' && isKeyFailed(o)) return 'failed (cache key not derived)'
|
|
373
|
+
const step = o.status === 'failed' ? outputsFailedOf(o) : undefined
|
|
374
|
+
if (step !== undefined)
|
|
375
|
+
return step === 'save' ? 'failed (cache save failed)' : 'failed (output clean failed)'
|
|
362
376
|
if (o.status === 'failed')
|
|
363
377
|
return failedLabel(o.exitCode, o.timedOut, o.sandboxViolations, o.notReady, o.inputMoved)
|
|
364
378
|
if (o.status === 'skipped') return skippedLabel(o)
|
|
@@ -449,6 +463,9 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
449
463
|
}
|
|
450
464
|
if (isGroupTask(outcome.node)) view.isGroup = true
|
|
451
465
|
else if (ranNoCache(outcome)) view.noCache = true
|
|
466
|
+
if (outcome.restoreFailed === true) view.restoreFailed = true
|
|
467
|
+
const step = outputsFailedOf(outcome)
|
|
468
|
+
if (step !== undefined) markOutputsFailed(view, step)
|
|
452
469
|
if (outcome.storedDurationMs !== undefined) view.storedDurationMs = outcome.storedDurationMs
|
|
453
470
|
if (outcome.storedCpuMs !== undefined) view.storedCpuMs = outcome.storedCpuMs
|
|
454
471
|
if (outcome.storedPeakRssBytes !== undefined) view.storedPeakRssBytes = outcome.storedPeakRssBytes
|
package/src/report/forecast.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface BlockedTask {
|
|
|
33
33
|
by: string
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* One pass over what is unfinished. Called again with the same `finished`
|
|
38
|
+
* set, that set has only grown: the pass reads only what the last one left
|
|
39
|
+
* unfinished. Another set starts over.
|
|
40
|
+
*/
|
|
36
41
|
export type Forecast = (
|
|
37
42
|
finished: ReadonlySet<string>,
|
|
38
43
|
running: ReadonlyMap<string, number>,
|
|
@@ -78,68 +83,129 @@ export function createForecast(
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
for (const n of nodes.values()) place(n)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
// Each node by its place in `order`, its facts in arrays. A pass built
|
|
87
|
+
// Maps of the whole graph and read every finished task again, and each
|
|
88
|
+
// task event forces a frame, each frame a pass: a 20,000-task run spent
|
|
89
|
+
// ~1.6 s of its main thread on them on a terminal (2026-11-07).
|
|
90
|
+
const size = order.length
|
|
91
|
+
const position = new Map(order.map((n, i) => [n.id, i]))
|
|
92
|
+
const deps = order.map((n) => {
|
|
93
|
+
const at: number[] = []
|
|
94
|
+
// A dependency outside the graph never has an end: leaving it out changes nothing.
|
|
95
|
+
for (const d of n.deps) {
|
|
96
|
+
const i = position.get(d)
|
|
97
|
+
if (i !== undefined) at.push(i)
|
|
98
|
+
}
|
|
99
|
+
return at
|
|
100
|
+
})
|
|
101
|
+
const ids = order.map((n) => n.id)
|
|
102
|
+
const group = Uint8Array.from(order, (n) => (isGroupTask(n) ? 1 : 0))
|
|
103
|
+
// NaN: no p50.
|
|
104
|
+
const cost = new Float64Array(size)
|
|
105
|
+
const ranks = new Float64Array(size)
|
|
106
|
+
for (let i = 0; i < size; i++) {
|
|
107
|
+
const n = order[i]!
|
|
108
|
+
cost[i] = (group[i] ? undefined : p50s.get(n.id)) ?? NaN
|
|
109
|
+
ranks[i] = rank.get(n.id) ?? 0
|
|
85
110
|
}
|
|
86
111
|
const workers = Math.max(1, concurrency)
|
|
87
112
|
// The scheduler's own baseline ranking (`rank`), most dependents first,
|
|
88
113
|
// ties in graph order.
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return d !== 0 ? d > 0 : position.get(a)! < position.get(b)!
|
|
114
|
+
const before = (a: number, b: number): boolean => {
|
|
115
|
+
const d = ranks[a]! - ranks[b]!
|
|
116
|
+
return d !== 0 ? d > 0 : a < b
|
|
93
117
|
}
|
|
118
|
+
// Reused by every pass. `end` NaN: no end yet (finished, or not reached);
|
|
119
|
+
// `since` NaN: not running; `head` -1: no running task its chain waits on.
|
|
120
|
+
const end = new Float64Array(size)
|
|
121
|
+
const since = new Float64Array(size)
|
|
122
|
+
const head = new Int32Array(size)
|
|
123
|
+
// What the last pass left unfinished, in order: a pass reads only these,
|
|
124
|
+
// not every finished task again. Rebuilt when `finished` is another set
|
|
125
|
+
// or shrank; the logger's only grows.
|
|
126
|
+
const live = new Int32Array(size)
|
|
127
|
+
let liveCount = -1
|
|
128
|
+
let lastFinished: ReadonlySet<string> | undefined
|
|
129
|
+
let lastSize = 0
|
|
94
130
|
|
|
95
131
|
return (finished, running, nowMs) => {
|
|
132
|
+
if (finished !== lastFinished || finished.size < lastSize || liveCount < 0) {
|
|
133
|
+
for (let i = 0; i < size; i++) live[i] = i
|
|
134
|
+
liveCount = size
|
|
135
|
+
}
|
|
136
|
+
let kept = 0
|
|
137
|
+
for (let r = 0; r < liveCount; r++) {
|
|
138
|
+
const i = live[r]!
|
|
139
|
+
if (!finished.has(ids[i]!)) live[kept++] = i
|
|
140
|
+
}
|
|
141
|
+
liveCount = kept
|
|
142
|
+
lastFinished = finished
|
|
143
|
+
lastSize = finished.size
|
|
144
|
+
end.fill(NaN)
|
|
145
|
+
since.fill(NaN)
|
|
146
|
+
head.fill(-1)
|
|
147
|
+
for (const [id, at] of running) {
|
|
148
|
+
const i = position.get(id)
|
|
149
|
+
if (i !== undefined) since[i] = at
|
|
150
|
+
}
|
|
96
151
|
let waiting = 0
|
|
97
|
-
const end = new Map<string, number>()
|
|
98
|
-
// The running task each node's longest chain waits on.
|
|
99
|
-
const head = new Map<string, string>()
|
|
100
152
|
let chain = 0
|
|
101
|
-
let chainEnd
|
|
153
|
+
let chainEnd = -1
|
|
102
154
|
let work = 0
|
|
103
155
|
let known = false
|
|
104
156
|
// The best `workers` blocked tasks, kept sorted as the pass meets them.
|
|
105
157
|
const held: BlockedTask[] = []
|
|
158
|
+
const heldAt: number[] = []
|
|
106
159
|
const blockedBy = new Map<string, number>()
|
|
107
|
-
for (
|
|
108
|
-
|
|
160
|
+
for (let r = 0; r < liveCount; r++) {
|
|
161
|
+
const i = live[r]!
|
|
162
|
+
const ds = deps[i]!
|
|
109
163
|
let start = 0
|
|
110
|
-
let via
|
|
111
|
-
for (const d of
|
|
112
|
-
const e = end
|
|
113
|
-
if (e
|
|
164
|
+
let via = -1
|
|
165
|
+
for (const d of ds) {
|
|
166
|
+
const e = end[d]!
|
|
167
|
+
if (e === e && (via < 0 || e > start)) {
|
|
114
168
|
start = e
|
|
115
169
|
via = d
|
|
116
170
|
}
|
|
117
171
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
172
|
+
const s = since[i]!
|
|
173
|
+
const runs = s === s
|
|
174
|
+
if (runs) head[i] = i
|
|
175
|
+
else if (via >= 0 && head[via]! >= 0) head[i] = head[via]!
|
|
176
|
+
if (!runs && !group[i]) {
|
|
177
|
+
let by = head[i]!
|
|
178
|
+
if (by < 0)
|
|
179
|
+
for (const d of ds)
|
|
180
|
+
if (end[d] === end[d]) {
|
|
181
|
+
by = d
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
if (by >= 0) {
|
|
185
|
+
waiting++
|
|
186
|
+
let at = held.length
|
|
187
|
+
while (at > 0 && before(i, heldAt[at - 1]!)) at--
|
|
188
|
+
if (at < workers) {
|
|
189
|
+
held.splice(at, 0, { id: ids[i]!, by: ids[by]! })
|
|
190
|
+
heldAt.splice(at, 0, i)
|
|
191
|
+
if (held.length > workers) {
|
|
192
|
+
held.pop()
|
|
193
|
+
heldAt.pop()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
130
196
|
}
|
|
131
197
|
}
|
|
132
|
-
const p = cost
|
|
198
|
+
const p = cost[i]!
|
|
133
199
|
let own = 0
|
|
134
|
-
if (p
|
|
200
|
+
if (p === p) {
|
|
135
201
|
known = true
|
|
136
|
-
own =
|
|
202
|
+
own = runs ? Math.max(0, p - (nowMs - s)) : p
|
|
137
203
|
}
|
|
138
204
|
const e = start + own
|
|
139
|
-
end
|
|
140
|
-
if (e > chain || chainEnd
|
|
205
|
+
end[i] = e
|
|
206
|
+
if (e > chain || chainEnd < 0) {
|
|
141
207
|
chain = e
|
|
142
|
-
chainEnd =
|
|
208
|
+
chainEnd = i
|
|
143
209
|
}
|
|
144
210
|
work += own
|
|
145
211
|
}
|
|
@@ -148,7 +214,7 @@ export function createForecast(
|
|
|
148
214
|
if (d !== undefined && d > 0) blockedBy.set(id, d)
|
|
149
215
|
}
|
|
150
216
|
if (!known) return { waiting, blocked: held, blockedBy }
|
|
151
|
-
const critical = chainEnd
|
|
217
|
+
const critical = chainEnd < 0 || head[chainEnd]! < 0 ? undefined : ids[head[chainEnd]!]!
|
|
152
218
|
return {
|
|
153
219
|
etaMs: Math.max(chain, work / workers),
|
|
154
220
|
...(critical !== undefined ? { critical } : {}),
|