@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
package/src/report/logger.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
executedCells,
|
|
13
13
|
formatTaskExecutedLine,
|
|
14
14
|
flakyNote,
|
|
15
|
-
pathNote,
|
|
16
15
|
formatTaskHashLine,
|
|
17
16
|
formatTaskHitLine,
|
|
18
17
|
formatTaskSkippedLine,
|
|
@@ -32,12 +31,14 @@ import {
|
|
|
32
31
|
type RunContext,
|
|
33
32
|
type TaskBucket,
|
|
34
33
|
} from './summary.js'
|
|
35
|
-
import { isGroupTask } from '../graph/index.js'
|
|
34
|
+
import { isGroupTask, isKeyFailed } from '../graph/index.js'
|
|
36
35
|
import {
|
|
37
36
|
appendTail,
|
|
38
37
|
createTail,
|
|
39
38
|
formatElapsed,
|
|
39
|
+
maskStreams,
|
|
40
40
|
resetTail,
|
|
41
|
+
secretMask,
|
|
41
42
|
tailText,
|
|
42
43
|
type Tail,
|
|
43
44
|
envFlagOn,
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
type StatusStream,
|
|
59
60
|
} from '../ui/index.js'
|
|
60
61
|
import { isCacheHit } from '../graph/index.js'
|
|
61
|
-
import {
|
|
62
|
+
import { outcomeLabel, outcomeWord, ranNoCache } from './events.js'
|
|
62
63
|
import type { BlockedTask, Forecast } from './forecast.js'
|
|
63
64
|
|
|
64
65
|
export interface Logger {
|
|
@@ -204,10 +205,17 @@ function ghaProperty(value: string): string {
|
|
|
204
205
|
* one could simply be printed by the task — so it is a per-run UUID.
|
|
205
206
|
*/
|
|
206
207
|
function ghaFence(body: string, token: string): string {
|
|
207
|
-
|
|
208
|
+
const [open, close] = ghaFenceEnds(token)
|
|
209
|
+
return `${open}${body}${close}`
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
|
|
212
|
+
/** The lines `ghaFence` puts before and after the text. */
|
|
213
|
+
function ghaFenceEnds(token: string): [open: string, close: string] {
|
|
214
|
+
return [`::stop-commands::${token}\n`, `::${token}::\n`]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const cacheWordOf = (o: TaskOutcome): 'miss' | 'no-cache' | '' =>
|
|
218
|
+
o.restoreFailed === true || isKeyFailed(o) ? '' : ranNoCache(o) ? 'no-cache' : 'miss'
|
|
211
219
|
|
|
212
220
|
export function resolveOutputView(
|
|
213
221
|
options: {
|
|
@@ -239,6 +247,9 @@ const NOTIFY_AFTER_MS = 10_000
|
|
|
239
247
|
// A step of vx's own quicker than this shows nowhere (2026-10-15).
|
|
240
248
|
const STEP_SHOWN_MS = 150
|
|
241
249
|
|
|
250
|
+
/** A frame this long is written in pieces, never copied into one string. */
|
|
251
|
+
const PIECEWISE_CHARS = 1024 * 1024
|
|
252
|
+
|
|
242
253
|
export function defaultLogger(
|
|
243
254
|
colors: ColorSupport = detectColors(),
|
|
244
255
|
view: OutputView = { mode: 'full' },
|
|
@@ -373,15 +384,19 @@ export function defaultLogger(
|
|
|
373
384
|
let slots: (WorkerSlot | null)[] = []
|
|
374
385
|
const slotQueue: WorkerSlot[] = []
|
|
375
386
|
let succeeded = 0
|
|
376
|
-
// Executed
|
|
387
|
+
// Executed without `cache: true`: never a miss, counted apart.
|
|
377
388
|
let noCache = 0
|
|
378
389
|
let upToDate = 0
|
|
379
390
|
let restoredLocal = 0
|
|
380
391
|
let restoredRemote = 0
|
|
392
|
+
// Hits whose restore failed: failed, and in no cache count.
|
|
393
|
+
let restoreFailed = 0
|
|
394
|
+
// A task with no key asked no cache: not a miss.
|
|
395
|
+
let keyFailed = 0
|
|
381
396
|
let saved = 0
|
|
382
397
|
let notSaved = 0
|
|
383
398
|
let skippedCount = 0
|
|
384
|
-
// Each finished task's cache bucket by its key: its
|
|
399
|
+
// Each finished task's cache bucket by its key, with a remote: its traffic moves it.
|
|
385
400
|
const buckets = new Map<string, TaskBucket>()
|
|
386
401
|
// The graph is done and vx's own closing steps run: the region stays,
|
|
387
402
|
// footer and step rows, until the footer replaces it (2026-10-15).
|
|
@@ -405,7 +420,7 @@ export function defaultLogger(
|
|
|
405
420
|
// The pinned zone above the worker rows: persistent tasks pin at
|
|
406
421
|
// ready (their outcome lands while the child keeps running) and live
|
|
407
422
|
// until runEnd kills the region. A failure is not pinned: it is a
|
|
408
|
-
// permanent
|
|
423
|
+
// permanent failed row the moment it happens, and its full frame is
|
|
409
424
|
// deferred to runEnd (owner: '✗ line, continue, all full frames at
|
|
410
425
|
// the end'). 'full'/CI keeps frames inline.
|
|
411
426
|
const deferredFailures: string[] = []
|
|
@@ -566,8 +581,9 @@ export function defaultLogger(
|
|
|
566
581
|
restoredRemote,
|
|
567
582
|
saved,
|
|
568
583
|
notSaved,
|
|
569
|
-
miss: succeeded + failed - noCache,
|
|
584
|
+
miss: succeeded + failed - noCache - restoreFailed - keyFailed,
|
|
570
585
|
noCache,
|
|
586
|
+
...(restoreFailed > 0 ? { restoreFailed } : {}),
|
|
571
587
|
left: total - done - stopped - notRun,
|
|
572
588
|
...(running > 0 ? { running } : {}),
|
|
573
589
|
...(scheduled > 0 ? { scheduled } : {}),
|
|
@@ -676,8 +692,11 @@ export function defaultLogger(
|
|
|
676
692
|
if (chunk.length > 0) endsBlank = chunk.endsWith('\n\n')
|
|
677
693
|
if (!logOpened && chunk.length > 0) {
|
|
678
694
|
logOpened = true
|
|
679
|
-
|
|
680
|
-
|
|
695
|
+
if (chunk.length < PIECEWISE_CHARS) {
|
|
696
|
+
writer.write(`\n${chunk}`)
|
|
697
|
+
return
|
|
698
|
+
}
|
|
699
|
+
writer.write('\n')
|
|
681
700
|
}
|
|
682
701
|
writer.write(chunk)
|
|
683
702
|
}
|
|
@@ -706,8 +725,7 @@ export function defaultLogger(
|
|
|
706
725
|
if (panelOpen === 'Warnings') closePanel()
|
|
707
726
|
inPanel = true
|
|
708
727
|
if (panelOpen !== 'Tasks') {
|
|
709
|
-
emitLine(ui.panelTop('Tasks', colors))
|
|
710
|
-
emitLine(formatListHeader(colors))
|
|
728
|
+
emitLine(ui.panelTop('Tasks', colors, undefined, undefined, formatListHeader(colors)))
|
|
711
729
|
panelOpen = 'Tasks'
|
|
712
730
|
}
|
|
713
731
|
emitLine(line)
|
|
@@ -715,11 +733,20 @@ export function defaultLogger(
|
|
|
715
733
|
}
|
|
716
734
|
// formatTaskBlock returns '' for group tasks (no exec) — skip the
|
|
717
735
|
// write so a stray newline doesn't sneak into the output.
|
|
718
|
-
|
|
736
|
+
// `head` and `tail` wrap the block. A big one is written in pieces:
|
|
737
|
+
// joined into one string, a frame of a 100 MB log was copied whole again
|
|
738
|
+
// (200 MB, two bytes a character once the frame's corners are in it).
|
|
739
|
+
const emitBlock = (block: string, head = '', tail = ''): void => {
|
|
719
740
|
if (block.length === 0) return
|
|
720
741
|
endStreamLine()
|
|
721
742
|
const lead = lineEmitted || streamedSinceBlock ? '\n' : ''
|
|
722
|
-
taskWrite(`${lead}${block}\n`)
|
|
743
|
+
if (block.length < PIECEWISE_CHARS) taskWrite(`${lead}${head}${block}${tail}\n`)
|
|
744
|
+
else {
|
|
745
|
+
logWrite(lead + head)
|
|
746
|
+
logWrite(block)
|
|
747
|
+
logWrite(`${tail}\n`)
|
|
748
|
+
endsBlank = (tail || block).endsWith('\n')
|
|
749
|
+
}
|
|
723
750
|
lineEmitted = false
|
|
724
751
|
streamedSinceBlock = false
|
|
725
752
|
}
|
|
@@ -760,10 +787,12 @@ export function defaultLogger(
|
|
|
760
787
|
const failureRow = (node: TaskNode, outcome: TaskOutcome): string =>
|
|
761
788
|
formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(outcome), {
|
|
762
789
|
...executedCells(outcome, blocksOf(node.id)),
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
790
|
+
// A failed restore ran no command: nothing to compare.
|
|
791
|
+
...(outcome.restoreFailed === true
|
|
792
|
+
? {}
|
|
793
|
+
: { usual: usualCell(outcome.durationMs, usualMsOf(node.id), colors) }),
|
|
794
|
+
...(onPath.has(node.id) ? { critical: true } : {}),
|
|
795
|
+
}) + flakyNote(outcome, colors)
|
|
767
796
|
|
|
768
797
|
return {
|
|
769
798
|
showsSteps: writer.enabled,
|
|
@@ -780,8 +809,13 @@ export function defaultLogger(
|
|
|
780
809
|
// summary row or "exited with code" can follow its partial line.
|
|
781
810
|
endStreamLine()
|
|
782
811
|
closePanel()
|
|
783
|
-
|
|
784
|
-
|
|
812
|
+
// A block's own trailing blank stands for the first blank status line; a second one is meant
|
|
813
|
+
// (two before the Summary).
|
|
814
|
+
if (line === '' && endsBlank) {
|
|
815
|
+
endsBlank = false
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
endsBlank = false
|
|
785
819
|
writer.write(`${line}\n`)
|
|
786
820
|
},
|
|
787
821
|
warn(line) {
|
|
@@ -957,7 +991,7 @@ export function defaultLogger(
|
|
|
957
991
|
}
|
|
958
992
|
}
|
|
959
993
|
// Failures end the log: every deferred frame replays here, right
|
|
960
|
-
// above the summary — the
|
|
994
|
+
// above the summary — the failed rows marked them in the stream,
|
|
961
995
|
// the full diagnostics read last where eyes land.
|
|
962
996
|
// Guarded for repeat runEnd calls.
|
|
963
997
|
if (!flushedFailures && deferredFailures.length > 0) {
|
|
@@ -1008,12 +1042,23 @@ export function defaultLogger(
|
|
|
1008
1042
|
const preReady = persistentTails.get(node.id)
|
|
1009
1043
|
// In the order the task wrote it; a bounded tail kept its streams
|
|
1010
1044
|
// apart, so its stdout reads first.
|
|
1011
|
-
|
|
1045
|
+
let output: OutputChunk[] = preReady
|
|
1012
1046
|
? [
|
|
1013
1047
|
{ text: tailText(preReady.out), err: false },
|
|
1014
1048
|
{ text: tailText(preReady.err), err: true },
|
|
1015
1049
|
]
|
|
1016
1050
|
: takeChunks(node.id)
|
|
1051
|
+
// The live mask lets a held piece go after a pause, so a value the
|
|
1052
|
+
// task wrote in two pieces arrived as two clean chunks. The frame
|
|
1053
|
+
// prints now with the whole text in hand: each stream is masked as
|
|
1054
|
+
// a whole, as the stored log is. A hit's replay already was.
|
|
1055
|
+
if (!isCacheHit(outcome.status) && output.some((c) => c.text.length > 0)) {
|
|
1056
|
+
const mask = secretMask(
|
|
1057
|
+
[process.env, node.config.exec?.env?.define],
|
|
1058
|
+
node.config.exec?.env?.secret,
|
|
1059
|
+
)
|
|
1060
|
+
if (mask !== null) output = maskStreams(output, mask)
|
|
1061
|
+
}
|
|
1017
1062
|
const dropped =
|
|
1018
1063
|
preReady && (preReady.out.dropped > 0 || preReady.err.dropped > 0)
|
|
1019
1064
|
? { droppedStdout: preReady.out.dropped, droppedStderr: preReady.err.dropped }
|
|
@@ -1041,9 +1086,13 @@ export function defaultLogger(
|
|
|
1041
1086
|
// the terminal where a frame would have: a task that traps Ctrl-C
|
|
1042
1087
|
// and exits 0 is aborted since item 962, and its frame was dropped
|
|
1043
1088
|
// with the count. A task that never started opened no frame.
|
|
1089
|
+
// A list view prints a failure's output; a killed task's partial
|
|
1090
|
+
// output is what tells where a run hung, so it prints too.
|
|
1044
1091
|
const framed =
|
|
1045
1092
|
!neverStarted(outcome) &&
|
|
1046
1093
|
(view.mode === 'full' ||
|
|
1094
|
+
view.mode === 'broad' ||
|
|
1095
|
+
view.mode === 'errors-only' ||
|
|
1047
1096
|
(view.mode === 'focused' && isPrimary(node)) ||
|
|
1048
1097
|
(framesTerminal && holdsTerminal(node)))
|
|
1049
1098
|
if (framed && streamsLive(node)) {
|
|
@@ -1051,6 +1100,23 @@ export function defaultLogger(
|
|
|
1051
1100
|
return
|
|
1052
1101
|
}
|
|
1053
1102
|
if (framed && output.some((c) => c.text.length > 0)) {
|
|
1103
|
+
// A list view treats it as a failure: the row now, the frame
|
|
1104
|
+
// with the deferred failures above the summary.
|
|
1105
|
+
if (view.mode === 'broad' || view.mode === 'errors-only') {
|
|
1106
|
+
emitRow(formatTaskAbortedLine(node, outcome, colors, blocksOf(node.id)))
|
|
1107
|
+
deferredFailures.push(
|
|
1108
|
+
formatTaskBlock(
|
|
1109
|
+
node,
|
|
1110
|
+
outcome,
|
|
1111
|
+
{ output, ...dropped },
|
|
1112
|
+
colors,
|
|
1113
|
+
true,
|
|
1114
|
+
forwardArgs,
|
|
1115
|
+
links,
|
|
1116
|
+
),
|
|
1117
|
+
)
|
|
1118
|
+
return
|
|
1119
|
+
}
|
|
1054
1120
|
emitBlock(
|
|
1055
1121
|
fenced(
|
|
1056
1122
|
formatTaskBlock(
|
|
@@ -1078,7 +1144,9 @@ export function defaultLogger(
|
|
|
1078
1144
|
done++
|
|
1079
1145
|
if (outcome.status === 'failed') {
|
|
1080
1146
|
failed++
|
|
1081
|
-
if (
|
|
1147
|
+
if (outcome.restoreFailed === true) restoreFailed++
|
|
1148
|
+
else if (isKeyFailed(outcome)) keyFailed++
|
|
1149
|
+
else if (ranNoCache(outcome)) noCache++
|
|
1082
1150
|
} else if (node.config.exec?.persistent !== undefined && outcome.status === 'success') {
|
|
1083
1151
|
// A persistent task's outcome arrives at READY; the child
|
|
1084
1152
|
// keeps running until the orchestrator SIGTERMs it at run
|
|
@@ -1101,7 +1169,9 @@ export function defaultLogger(
|
|
|
1101
1169
|
if (qi >= 0) slotQueue.splice(qi, 1)
|
|
1102
1170
|
}
|
|
1103
1171
|
}
|
|
1104
|
-
|
|
1172
|
+
// Only a remote's traffic moves a bucket: without one, an entry per
|
|
1173
|
+
// task was held for the run and never read.
|
|
1174
|
+
const bucket = runContext?.cache?.remote === true ? cacheBucketOf(outcome) : undefined
|
|
1105
1175
|
if (bucket !== undefined) {
|
|
1106
1176
|
const entry = { task: node.id, bucket }
|
|
1107
1177
|
if (outcome.hash !== undefined) buckets.set(outcome.hash, entry)
|
|
@@ -1126,7 +1196,10 @@ export function defaultLogger(
|
|
|
1126
1196
|
skippedCount++
|
|
1127
1197
|
break
|
|
1128
1198
|
}
|
|
1129
|
-
if (
|
|
1199
|
+
if (
|
|
1200
|
+
outcome.status === 'success' ||
|
|
1201
|
+
(outcome.status === 'failed' && outcome.restoreFailed !== true)
|
|
1202
|
+
) {
|
|
1130
1203
|
spreadMax = Math.max(spreadMax, outcome.durationMs)
|
|
1131
1204
|
spreadMin = Math.min(spreadMin, outcome.durationMs)
|
|
1132
1205
|
spreadSum += outcome.durationMs
|
|
@@ -1175,7 +1248,7 @@ export function defaultLogger(
|
|
|
1175
1248
|
// their replay buffers are deliberately dropped; the counts
|
|
1176
1249
|
// surface in the end-of-run summary.
|
|
1177
1250
|
if (outcome.status === 'failed') {
|
|
1178
|
-
//
|
|
1251
|
+
// failed row now; the full frame replays at runEnd.
|
|
1179
1252
|
emitRow(failureRow(node, outcome))
|
|
1180
1253
|
deferredFailures.push(
|
|
1181
1254
|
formatTaskBlock(
|
|
@@ -1258,7 +1331,7 @@ export function defaultLogger(
|
|
|
1258
1331
|
return
|
|
1259
1332
|
}
|
|
1260
1333
|
// Dependency-pulled nodes: silent on success; a skip gets its
|
|
1261
|
-
// row, failures the
|
|
1334
|
+
// row, failures the failed row now and their frame replayed at runEnd.
|
|
1262
1335
|
if (outcome.status === 'skipped') {
|
|
1263
1336
|
emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
|
|
1264
1337
|
} else if (outcome.status === 'failed') {
|
|
@@ -1307,20 +1380,24 @@ export function defaultLogger(
|
|
|
1307
1380
|
// The block is the task's own output — fenced, so a task that
|
|
1308
1381
|
// prints `::endgroup::` can't close vx's group early and an
|
|
1309
1382
|
// `::error` line in its output can't become a real annotation.
|
|
1310
|
-
const
|
|
1383
|
+
const [open, close] = ghaFenceEnds(ghaToken)
|
|
1311
1384
|
// Failed tasks stay pre-expanded in the Actions viewer:
|
|
1312
1385
|
// an ::error annotation instead of a collapsed group.
|
|
1313
1386
|
if (outcome.status === 'failed') {
|
|
1314
1387
|
// The message is ours (a literal, an integer and a signal
|
|
1315
1388
|
// name); only the title carries user input.
|
|
1316
1389
|
emitBlock(
|
|
1317
|
-
|
|
1390
|
+
block,
|
|
1391
|
+
`::error title=${ghaProperty(node.id)}::${outcomeLabel(outcome)}\n${open}`,
|
|
1392
|
+
close,
|
|
1318
1393
|
)
|
|
1319
1394
|
} else {
|
|
1320
1395
|
emitBlock(
|
|
1396
|
+
block,
|
|
1321
1397
|
`::group::${ghaData(
|
|
1322
1398
|
`${node.id} (${outcomeWord(outcome)} ${formatElapsed(outcome.durationMs)})`,
|
|
1323
|
-
)}\n${
|
|
1399
|
+
)}\n${open}`,
|
|
1400
|
+
`${close}::endgroup::\n`,
|
|
1324
1401
|
)
|
|
1325
1402
|
}
|
|
1326
1403
|
return
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { TaskNode } from '../graph/index.js'
|
|
2
|
+
import { stripEscapes } from '../util/index.js'
|
|
2
3
|
import type { Logger } from './logger.js'
|
|
3
4
|
|
|
4
|
-
// A prefix that may yet complete as a CSI, OSC
|
|
5
|
-
// (`tput sgr0` writes `\x1b(B`). An escape cut at a
|
|
6
|
-
// the next chunk: stripped alone, its remainder
|
|
7
|
-
// as text.
|
|
5
|
+
// A prefix that may yet complete as a CSI, string (OSC, DCS, SOS, PM, APC)
|
|
6
|
+
// or charset sequence (`tput sgr0` writes `\x1b(B`). An escape cut at a
|
|
7
|
+
// chunk's end is held for the next chunk: stripped alone, its remainder
|
|
8
|
+
// (`1m`, `B`) reached the log as text. A string escape ends at its line
|
|
9
|
+
// (stripEscapes), so it is held only within one, and only up to HOLD_MAX:
|
|
10
|
+
// an unbounded hold joined every later chunk and lost them at the task's end.
|
|
8
11
|
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
9
|
-
const PARTIAL_ESCAPE = /^\x1b(?:\[[0-?]*[ -/]
|
|
12
|
+
const PARTIAL_ESCAPE = /^\x1b(?:\[[0-?]*[ -/]*|[\]P_^X][^\x07\x1b\n]*\x1b?|[ -/]+)?$/
|
|
13
|
+
const HOLD_MAX = 4096
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* `sink` with terminal escapes stripped from task output. Tasks run with
|
|
@@ -21,17 +25,17 @@ export function plainOutput(sink: Logger): Logger {
|
|
|
21
25
|
if (prior !== undefined) held.delete(key)
|
|
22
26
|
const esc = text.lastIndexOf('\x1b')
|
|
23
27
|
if (esc === -1) return text
|
|
24
|
-
if (PARTIAL_ESCAPE.test(text.slice(esc))) {
|
|
28
|
+
if (text.length - esc <= HOLD_MAX && PARTIAL_ESCAPE.test(text.slice(esc))) {
|
|
25
29
|
held.set(key, text.slice(esc))
|
|
26
|
-
return
|
|
30
|
+
return stripEscapes(text.slice(0, esc))
|
|
27
31
|
}
|
|
28
|
-
return
|
|
32
|
+
return stripEscapes(text)
|
|
29
33
|
}
|
|
30
34
|
const flush = (node: TaskNode, key: string, write: (n: TaskNode, c: string) => void): void => {
|
|
31
35
|
const rest = held.get(key)
|
|
32
36
|
if (rest === undefined) return
|
|
33
37
|
held.delete(key)
|
|
34
|
-
const text =
|
|
38
|
+
const text = stripEscapes(rest)
|
|
35
39
|
if (text !== '') write(node, text)
|
|
36
40
|
}
|
|
37
41
|
// Delegated one by one, never spread: an embedder's logger may be a class
|
|
@@ -57,6 +61,8 @@ export function plainOutput(sink: Logger): Logger {
|
|
|
57
61
|
if (text !== '' || chunk === '') sink.taskStderr(node, text)
|
|
58
62
|
},
|
|
59
63
|
taskComplete(node, outcome) {
|
|
64
|
+
// Nearly every task ends with no escape held: its keys go unbuilt.
|
|
65
|
+
if (held.size === 0) return sink.taskComplete(node, outcome)
|
|
60
66
|
flush(node, `${node.id}\0out`, (n, c) => sink.taskStdout(n, c))
|
|
61
67
|
flush(node, `${node.id}\0err`, (n, c) => sink.taskStderr(n, c))
|
|
62
68
|
sink.taskComplete(node, outcome)
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// The live region's panels: Warnings, Running and the live Summary,
|
|
2
2
|
// redrawn in place under the log by the ui module's writer.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { divider, paint, type ColorSupport } from '../ui/index.js'
|
|
5
5
|
import {
|
|
6
6
|
formatExpected,
|
|
7
|
-
formatRunningHeader,
|
|
8
7
|
formatTaskRow,
|
|
9
8
|
paintIdParts,
|
|
10
|
-
pathNote,
|
|
11
9
|
type RowCells,
|
|
12
10
|
unknownUsual,
|
|
13
11
|
} from './framed-output.js'
|
|
@@ -93,18 +91,18 @@ function paintPinnedId(id: string, colors: ColorSupport): string {
|
|
|
93
91
|
/**
|
|
94
92
|
* Permanent one-liner logged the moment a task fails (owner design:
|
|
95
93
|
* "log the failure and continue, full frames at the end"). On the grid:
|
|
96
|
-
*
|
|
94
|
+
* `failed` in red + exec time + `miss` + id. The exit code and
|
|
97
95
|
* output live in the framed block that replays at runEnd, not the line.
|
|
96
|
+
* A hit whose restore failed has `''`, shown `—`: neither a miss nor a hit.
|
|
98
97
|
*/
|
|
99
98
|
export function formatFailureLine(
|
|
100
99
|
id: string,
|
|
101
100
|
durationMs: number | null,
|
|
102
101
|
colors: ColorSupport = NO_COLOR,
|
|
103
|
-
cacheWord: 'miss' | 'no-cache' = 'miss',
|
|
102
|
+
cacheWord: 'miss' | 'no-cache' | '' = 'miss',
|
|
104
103
|
cells: RowCells = {},
|
|
105
104
|
): string {
|
|
106
105
|
return formatTaskRow(
|
|
107
|
-
paint('error', '\u25fc\uFE0E', colors),
|
|
108
106
|
durationMs,
|
|
109
107
|
'failed',
|
|
110
108
|
'error',
|
|
@@ -131,7 +129,6 @@ export function formatStepLine(
|
|
|
131
129
|
colors: ColorSupport = NO_COLOR,
|
|
132
130
|
): string {
|
|
133
131
|
return formatTaskRow(
|
|
134
|
-
ok ? paint('ok', '\u23fa\uFE0E', colors) : paint('error', '\u25fc\uFE0E', colors),
|
|
135
132
|
durationMs,
|
|
136
133
|
ok ? 'success' : 'failed',
|
|
137
134
|
ok ? 'ok' : 'error',
|
|
@@ -144,9 +141,6 @@ export function formatStepLine(
|
|
|
144
141
|
}
|
|
145
142
|
|
|
146
143
|
/** `rows` in a panel titled `title`, counted. */
|
|
147
|
-
function panel(title: string, count: string, rows: readonly string[], c: ColorSupport): string[] {
|
|
148
|
-
return [panelTop(title, c, count), ...rows]
|
|
149
|
-
}
|
|
150
144
|
|
|
151
145
|
/**
|
|
152
146
|
* Worker region: pinned ready persistent tasks, one row per worker
|
|
@@ -171,26 +165,18 @@ export function formatStatusRegion(
|
|
|
171
165
|
}
|
|
172
166
|
// The Running panel's rows; its box and header go round them below.
|
|
173
167
|
const lines: string[] = []
|
|
174
|
-
// Persistent (dev-server) rows:
|
|
168
|
+
// Persistent (dev-server) rows: `running`, no elapsed, no
|
|
175
169
|
// cache state (it's still alive).
|
|
176
170
|
for (const id of s.pinnedPersistent) {
|
|
177
171
|
lines.push(
|
|
178
|
-
formatTaskRow(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
'running',
|
|
182
|
-
'info',
|
|
183
|
-
'',
|
|
184
|
-
'',
|
|
185
|
-
paintPinnedId(id, colors),
|
|
186
|
-
colors,
|
|
187
|
-
{ worker: 'local' },
|
|
188
|
-
),
|
|
172
|
+
formatTaskRow(null, 'running', 'warn', '', '', paintPinnedId(id, colors), colors, {
|
|
173
|
+
worker: 'local',
|
|
174
|
+
}),
|
|
189
175
|
)
|
|
190
176
|
}
|
|
191
177
|
|
|
192
|
-
// Running rows:
|
|
193
|
-
// the ticking time IS the motion),
|
|
178
|
+
// Running rows: `running` in yellow, then the live elapsed time (no
|
|
179
|
+
// spinner; the ticking time IS the motion), the usual duration,
|
|
194
180
|
// how many tasks wait on it, and the full id (never truncated — name is
|
|
195
181
|
// the last column). The rows no task runs in hold the tasks planned
|
|
196
182
|
// next, each nested under what it waits on. No blank rows pad the list
|
|
@@ -218,7 +204,6 @@ export function formatStatusRegion(
|
|
|
218
204
|
if (s.stopping === true) {
|
|
219
205
|
lines.push(
|
|
220
206
|
formatTaskRow(
|
|
221
|
-
' ',
|
|
222
207
|
Math.max(0, s.nowMs - slot.startedMs),
|
|
223
208
|
'aborting',
|
|
224
209
|
'warn',
|
|
@@ -233,28 +218,26 @@ export function formatStatusRegion(
|
|
|
233
218
|
}
|
|
234
219
|
const at = s.where?.(slot.id)
|
|
235
220
|
const use = s.usage?.(slot.id)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
},
|
|
252
|
-
) + (s.critical === slot.id ? pathNote(colors) : ''),
|
|
221
|
+
const row = formatTaskRow(
|
|
222
|
+
Math.max(0, s.nowMs - slot.startedMs),
|
|
223
|
+
'running',
|
|
224
|
+
'warn',
|
|
225
|
+
slot.cache ?? 'miss',
|
|
226
|
+
'',
|
|
227
|
+
paintPinnedId(slot.id, colors),
|
|
228
|
+
colors,
|
|
229
|
+
{
|
|
230
|
+
usual: usualOf(slot.id),
|
|
231
|
+
...(use !== undefined ? { cpu: use.cores, memBytes: use.rssBytes } : {}),
|
|
232
|
+
blocks: String(s.blockedBy?.get(slot.id) ?? 0),
|
|
233
|
+
worker: at ?? 'local',
|
|
234
|
+
...(s.critical === slot.id ? { critical: true } : {}),
|
|
235
|
+
},
|
|
253
236
|
)
|
|
237
|
+
lines.push(row)
|
|
254
238
|
for (const b of nested.get(slot.id) ?? []) {
|
|
255
239
|
lines.push(
|
|
256
240
|
formatTaskRow(
|
|
257
|
-
' ',
|
|
258
241
|
null,
|
|
259
242
|
'blocked',
|
|
260
243
|
'',
|
|
@@ -270,10 +253,9 @@ export function formatStatusRegion(
|
|
|
270
253
|
for (const step of s.steps ?? []) {
|
|
271
254
|
lines.push(
|
|
272
255
|
formatTaskRow(
|
|
273
|
-
' ',
|
|
274
256
|
Math.max(0, s.nowMs - step.startedMs),
|
|
275
257
|
'running',
|
|
276
|
-
'
|
|
258
|
+
'warn',
|
|
277
259
|
INTERNAL,
|
|
278
260
|
'',
|
|
279
261
|
paintPinnedId(step.id, colors),
|
|
@@ -285,16 +267,7 @@ export function formatStatusRegion(
|
|
|
285
267
|
// Panels top to bottom: what runs now, then the live Summary, the same panel the final printout is, so the region
|
|
286
268
|
// BECOMES the summary when the run ends.
|
|
287
269
|
const out: string[] = []
|
|
288
|
-
if (lines.length > 0)
|
|
289
|
-
out.push(
|
|
290
|
-
'',
|
|
291
|
-
...panel(
|
|
292
|
-
'Running',
|
|
293
|
-
String(s.pinnedPersistent.length + running.length + (s.steps?.length ?? 0)),
|
|
294
|
-
[formatRunningHeader(colors), ...lines],
|
|
295
|
-
colors,
|
|
296
|
-
),
|
|
297
|
-
)
|
|
270
|
+
if (lines.length > 0) out.push(divider(colors), '', ...lines)
|
|
298
271
|
out.push(...s.summaryLines)
|
|
299
272
|
return out
|
|
300
273
|
}
|