@vzn/vx 0.0.515 → 0.0.517

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.515",
3
+ "version": "0.0.517",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.515",
25
- "@vzn/vx-linux-arm64": "0.0.515",
26
- "@vzn/vx-darwin-x64": "0.0.515",
27
- "@vzn/vx-darwin-arm64": "0.0.515"
24
+ "@vzn/vx-linux-x64": "0.0.517",
25
+ "@vzn/vx-linux-arm64": "0.0.517",
26
+ "@vzn/vx-darwin-x64": "0.0.517",
27
+ "@vzn/vx-darwin-arm64": "0.0.517"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -139,6 +139,11 @@ export interface TaskOutcome {
139
139
  * flaky detection reads it off the outcome stream.
140
140
  */
141
141
  attempts?: number
142
+ /**
143
+ * Set when this run proved the task flaky: its key holds both outcomes on
144
+ * record (this run's included) or it needed a retry. Its row carries it.
145
+ */
146
+ flaky?: { passes: number; failures: number }
142
147
  /**
143
148
  * Count of sandbox violations captured during this task's exec (a task
144
149
  * declaring `exec.sandbox`). Non-zero means the task touched something
@@ -291,8 +291,7 @@ export function outcomeLabel(
291
291
 
292
292
  /**
293
293
  * Why a task was skipped, one copy for every surface: `blocked by lib#build`
294
- * (the failed or aborted task at the root of the block, what the footer's
295
- * Skipped section groups by), or '' for fail-fast's skip — the run stopped,
294
+ * (the failed or aborted task at the root of the block), or '' for fail-fast's skip — the run stopped,
296
295
  * nothing blocked it, and a label claiming an upstream failure would lie.
297
296
  */
298
297
  export function skippedReason(blockedBy: string | undefined): string {
@@ -344,22 +344,42 @@ function formatOutcomeRow(
344
344
  ): string {
345
345
  const st = statusOf(o)
346
346
  const ca = cacheOf(o)
347
- return formatTaskRow(
348
- taskGlyph(o, colors),
349
- ms,
350
- st.word,
351
- st.color,
352
- ca.word,
353
- ca.color,
354
- paintedId,
355
- colors,
347
+ return (
348
+ formatTaskRow(
349
+ taskGlyph(o, colors),
350
+ ms,
351
+ st.word,
352
+ st.color,
353
+ ca.word,
354
+ ca.color,
355
+ paintedId,
356
+ colors,
357
+ ) + flakyNote(o, colors)
356
358
  )
357
359
  }
358
360
 
361
+ /**
362
+ * The dim note a flaky task's row and frame carry (`flaky - failed 1×
363
+ * before`): nothing prints below the footer, so the task's own line is
364
+ * where the reader learns it. '' when the run did not prove it flaky.
365
+ */
366
+ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
367
+ if (o.flaky === undefined) return ''
368
+ const said: string[] = []
369
+ if (o.status === 'failed' && o.flaky.passes > 0)
370
+ said.push(`passed ${o.flaky.passes}\u00d7 before`)
371
+ if (o.status !== 'failed' && o.flaky.failures > 0) {
372
+ said.push(`failed ${o.flaky.failures}\u00d7 before`)
373
+ }
374
+ if ((o.attempts ?? 1) > 1) said.push(`${o.attempts} attempts`)
375
+ const text = said.length === 0 ? 'flaky' : `flaky - ${said.join(' \u00b7 ')}`
376
+ return ` ${paint('', text, colors, { dim: true })}`
377
+ }
378
+
359
379
  /**
360
380
  * Compact one-liner for a skipped task — it never ran (blank time). The
361
- * blocker rides the row (`• blocked by lib#build`): the footer's Skipped
362
- * section groups the same fact, but the row is where the reader's eye is.
381
+ * blocker rides the row (`• blocked by lib#build`): nothing prints below
382
+ * the footer, so the row is where the reader learns what blocked it.
363
383
  */
364
384
  export function formatTaskSkippedLine(
365
385
  node: TaskNode,
@@ -564,7 +584,7 @@ function formatBlockFooter(o: TaskOutcome, colors: ColorSupport): string {
564
584
  // formatStatusTag.
565
585
  const dur = paint('', `(${formatDuration(o.durationMs)})`, colors, { dim: true })
566
586
  const tag = formatStatusTag(o, colors)
567
- return ` ${dur} ${tag}`
587
+ return ` ${dur} ${tag}${flakyNote(o, colors)}`
568
588
  }
569
589
 
570
590
  function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
@@ -8,6 +8,7 @@ import {
8
8
  formatTaskAbortedLine,
9
9
  formatTaskBlock,
10
10
  formatTaskExecutedLine,
11
+ flakyNote,
11
12
  formatTaskHitLine,
12
13
  formatTaskSkippedLine,
13
14
  } from './framed-output.js'
@@ -431,6 +432,10 @@ export function defaultLogger(
431
432
  (framesTerminal && holdsTerminal(node)) ||
432
433
  (view.mode === 'focused' && isPrimary(node) && !isGroupTask(node) && requestedCount <= 1)
433
434
 
435
+ const failureRow = (node: TaskNode, outcome: TaskOutcome): string =>
436
+ formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)) +
437
+ flakyNote(outcome, colors)
438
+
434
439
  return {
435
440
  settle() {
436
441
  // The bus delivers one `run:end`, so the CLI's last word reaches here.
@@ -697,25 +702,31 @@ export function defaultLogger(
697
702
  return
698
703
  }
699
704
  case 'errors-only':
705
+ if (outcome.status === 'skipped') {
706
+ emitLine(formatTaskSkippedLine(node, colors, outcome.blockedBy))
707
+ return
708
+ }
700
709
  if (outcome.status !== 'failed') return
701
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
710
+ emitLine(failureRow(node, outcome))
702
711
  deferredFailures.push(
703
712
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
704
713
  )
705
714
  return
706
715
  case 'broad':
707
- // News only: executed work gets a one-liner, failures get
708
- // the full frame. Hits (including up-to-date) are silent —
716
+ // News only: executed work and skips get a one-liner, failures
717
+ // get the full frame. Hits (including up-to-date) are silent —
709
718
  // their replay buffers are deliberately dropped; the counts
710
719
  // surface in the end-of-run summary.
711
720
  if (outcome.status === 'failed') {
712
721
  // ◼ row now; the full frame replays at runEnd.
713
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
722
+ emitLine(failureRow(node, outcome))
714
723
  deferredFailures.push(
715
724
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
716
725
  )
717
726
  } else if (outcome.status === 'success') {
718
727
  emitLine(formatTaskExecutedLine(node, outcome, colors))
728
+ } else if (outcome.status === 'skipped') {
729
+ emitLine(formatTaskSkippedLine(node, colors, outcome.blockedBy))
719
730
  }
720
731
  return
721
732
  case 'focused':
@@ -745,7 +756,7 @@ export function defaultLogger(
745
756
  // to runEnd like everywhere else; everything else emits
746
757
  // ONE atomic block from the buffered output.
747
758
  if (outcome.status === 'failed') {
748
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
759
+ emitLine(failureRow(node, outcome))
749
760
  deferredFailures.push(
750
761
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
751
762
  )
@@ -756,10 +767,12 @@ export function defaultLogger(
756
767
  emitBlock(formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors, true))
757
768
  return
758
769
  }
759
- // Dependency-pulled nodes: silent on success; failures get
760
- // the ◼ row now and their frame replayed at runEnd.
761
- if (outcome.status === 'failed') {
762
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
770
+ // Dependency-pulled nodes: silent on success; a skip gets its
771
+ // row, failures the ◼ row now and their frame replayed at runEnd.
772
+ if (outcome.status === 'skipped') {
773
+ emitLine(formatTaskSkippedLine(node, colors, outcome.blockedBy))
774
+ } else if (outcome.status === 'failed') {
775
+ emitLine(failureRow(node, outcome))
763
776
  deferredFailures.push(
764
777
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
765
778
  )
@@ -33,7 +33,7 @@ export interface SummarizeArgs {
33
33
  /** The process's exit code where it is not `ok ? 0 : 1`: a stopping signal's. */
34
34
  exitCode?: number
35
35
  outcomes: readonly TaskOutcome[]
36
- /** What `detectFlaky` found this run; the footer's Flaky section, typed. */
36
+ /** What `detectFlaky` found this run; each row's flaky note, typed. */
37
37
  flaky?: readonly FlakyFinding[]
38
38
  }
39
39
 
@@ -25,7 +25,6 @@ import {
25
25
  type TaskOutcome,
26
26
  } from '../graph/index.js'
27
27
  import {
28
- formatBytes,
29
28
  mark,
30
29
  killGraceMs,
31
30
  MAX_TIMEOUT_MS,
@@ -52,7 +51,7 @@ import { buildAdmission, executorLabel, resolveExecutors, teardownPlugins } from
52
51
  import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
53
52
  import { assembleRunSummary, isPassStatus } from './telemetry.js'
54
53
  import type { RunContextRecord } from './telemetry.js'
55
- import { defaultLogger, resolveOutputView, type Logger } from './logger.js'
54
+ import { defaultLogger, resolveOutputView } from './logger.js'
56
55
  import { detectColors, type ColorSupport } from './colors.js'
57
56
  import { plainOutput } from './plain-output.js'
58
57
  import { formatPersistentList } from './framed-output.js'
@@ -90,8 +89,8 @@ import { assembleRunRecords } from './run-records.js'
90
89
  import { hasEnded, selectKeepAlive, shutdownPersistent } from './persistent.js'
91
90
  import { writeRunProfile, writeRunSummary } from './run-artifacts.js'
92
91
  import { createSaveLane } from './save-lane.js'
93
- import { formatFlakySection, formatRunSummary, formatSkippedSection } from './summary.js'
94
- import { detectFlaky, type FlakyCandidate } from './failure-mode.js'
92
+ import { formatRunSummary } from './summary.js'
93
+ import { detectFlaky, type FlakyCandidate, type FlakyFinding } from './failure-mode.js'
95
94
  import type { RunOptions, RunSummary } from './options.js'
96
95
 
97
96
  // Per run, never shared: a `vx watch` process runs many, and a shared map
@@ -973,6 +972,24 @@ async function runOnBus(
973
972
  })
974
973
 
975
974
  mark('classify + probe')
975
+ // Judged as each task finishes, against the history BEFORE this run's
976
+ // rows land, so the row that prints next carries the verdict: nothing
977
+ // prints below the footer. A hit or a skip asks nothing; a green miss
978
+ // is one probe of the failed-row index.
979
+ const flaky: FlakyFinding[] = []
980
+ const historyDb = prepared.localCache.dbHandle()
981
+ const judgeFlaky = (o: TaskOutcome): void => {
982
+ const candidates = flakyCandidates([o])
983
+ if (candidates.length === 0) return
984
+ try {
985
+ const found = detectFlaky(historyDb, candidates)[0]
986
+ if (found === undefined) return
987
+ flaky.push(found)
988
+ o.flaky = { passes: found.passes, failures: found.failures }
989
+ } catch {
990
+ // History is observability: an unreadable one judges nothing.
991
+ }
992
+ }
976
993
  const outcomes = await runGraph({
977
994
  nodes,
978
995
  concurrency,
@@ -988,6 +1005,7 @@ async function runOnBus(
988
1005
  },
989
1006
  onFinish: (o) => {
990
1007
  taint.settled(o)
1008
+ judgeFlaky(o)
991
1009
  log.taskComplete(o.node, o)
992
1010
  narrowDemand(o.node.id)
993
1011
  },
@@ -1069,9 +1087,8 @@ async function runOnBus(
1069
1087
  if (stillUp.length > 0) {
1070
1088
  for (const line of formatPersistentList(stillUp, colors)) log.status(line)
1071
1089
  }
1072
- for (const line of formatRunSummary(list, totalMs, colors, runContext)) log.status(line)
1073
1090
  // A dependency-only server that died before the end of the graph
1074
- // stopped it: the footer counts it failed, this says why.
1091
+ // stopped it: the footer below counts it failed, this says why.
1075
1092
  // A signal death is named as its exit code, as the keep-alive wait
1076
1093
  // names it: `code SIGTERM` read beside `code 143` for one event (item
1077
1094
  // 1102).
@@ -1079,20 +1096,11 @@ async function runOnBus(
1079
1096
  const code = typeof c.code === 'number' ? c.code : signalExitCode(c.code)
1080
1097
  log.status(`vx: ${c.id} exited with code ${code} before the run stopped it`)
1081
1098
  }
1082
- // The footer's "N skipped" names no task; this names each under the
1083
- // failure that blocked it.
1084
- for (const line of formatSkippedSection(list)) log.status(line)
1085
- // Judged against the history BEFORE this run's rows land, so the query
1086
- // is one scan over the executed tasks' keys and nothing at all on a run
1087
- // that executed none (every hit, every skip).
1088
- const flaky = detectFlaky(prepared.localCache.dbHandle(), flakyCandidates(list))
1089
- for (const line of formatFlakySection(flaky)) log.status(line)
1090
1099
  // Outputs that never came home are not an error, but a silent `dist/`
1091
1100
  // that is empty-or-stale would be: name every task whose bytes are
1092
1101
  // still remote.
1093
1102
  const stillDeferred = deferredOutputs.pending()
1094
1103
  if (stillDeferred.length > 0) {
1095
- log.status('')
1096
1104
  log.status(
1097
1105
  ` Deferred: ${stillDeferred.length} task(s) left outputs remote (--download=none): ${stillDeferred.join(', ')}`,
1098
1106
  )
@@ -1104,6 +1112,9 @@ async function runOnBus(
1104
1112
  // Ctrl-C there is the process's exit, and the first write said ok.
1105
1113
  const summarize = async (runOk: boolean, final = list): Promise<void> => {
1106
1114
  if (options.summarize === undefined) return
1115
+ // The rewrite after the keep-alive wait says nothing: the footer is
1116
+ // the run's last word.
1117
+ const quiet = final !== list
1107
1118
  try {
1108
1119
  const wrote = await writeRunSummary({
1109
1120
  target: options.summarize,
@@ -1118,7 +1129,7 @@ async function runOnBus(
1118
1129
  outcomes: final,
1119
1130
  flaky,
1120
1131
  })
1121
- log.status(`vx: summary written to ${wrote}`)
1132
+ if (!quiet) log.status(`vx: summary written to ${wrote}`)
1122
1133
  } catch (err) {
1123
1134
  const msg = err instanceof Error ? err.message : String(err)
1124
1135
  log.status(`vx: failed to write summary: ${msg}`)
@@ -1138,7 +1149,6 @@ async function runOnBus(
1138
1149
  log.status(`vx: failed to write profile: ${msg}`)
1139
1150
  }
1140
1151
  }
1141
-
1142
1152
  // Record each task to the run history in a single SQLite transaction
1143
1153
  // (one fsync instead of N), with the invocation header row alongside,
1144
1154
  // atomically via recordRunBundle. The Tier-3 input-fingerprint rows
@@ -1249,7 +1259,7 @@ async function runOnBus(
1249
1259
  mark('output dir snapshots')
1250
1260
  // Not on a stopped run: one stopped while it waited on another run's
1251
1261
  // lock never held it, and its prune evicted under that run (item 858).
1252
- if (!stopRun.signal.aborted) await applyCacheRetention(prepared, log)
1262
+ if (!stopRun.signal.aborted) await applyCacheRetention(prepared)
1253
1263
  // A plugin hears the run until its teardown and nothing after: released
1254
1264
  // only in the finally, its handlers heard a kept server through the
1255
1265
  // whole keep-alive wait below (C-66). Idempotent; the finally's stay.
@@ -1273,6 +1283,13 @@ async function runOnBus(
1273
1283
  }
1274
1284
  }
1275
1285
 
1286
+ // The footer is the run's last word (owner, 2026-10-06): it prints once
1287
+ // the run's own work is done, history, uploads, plugin teardown and the
1288
+ // sandbox reset included, so any warning they raise lands above it. A
1289
+ // task's own facts (flaky, blocked) ride its row. Only a kept server's
1290
+ // own output follows.
1291
+ for (const line of formatRunSummary(list, totalMs, colors, runContext)) log.status(line)
1292
+
1276
1293
  // Edge case the summary already reported: the user requested a
1277
1294
  // persistent task (dev server / watcher). The run is "done" in every
1278
1295
  // bookkeeping sense — summary printed, history recorded — but the
@@ -1377,10 +1394,10 @@ async function runOnBus(
1377
1394
  /**
1378
1395
  * The workspace's `cacheRetention`, after every save and upload of this run
1379
1396
  * has landed (so nothing this run wrote is mid-flight) and before the cache
1380
- * closes. Housekeeping, not the run's work: a failure is one warning, never
1397
+ * closes. Housekeeping, not the run's work: silent, and a failure is never
1381
1398
  * a failed run. Declared nowhere → one property read.
1382
1399
  */
1383
- async function applyCacheRetention(prepared: PreparedRun, log: Logger): Promise<void> {
1400
+ async function applyCacheRetention(prepared: PreparedRun): Promise<void> {
1384
1401
  const retention = prepared.workspaceConfig?.cacheRetention
1385
1402
  if (retention === undefined) return
1386
1403
  // Validated at load (`validateRetention`), so both parse.
@@ -1388,26 +1405,12 @@ async function applyCacheRetention(prepared: PreparedRun, log: Logger): Promise<
1388
1405
  retention.olderThan === undefined ? undefined : parseDuration(retention.olderThan)!
1389
1406
  const maxBytes = retention.maxSize === undefined ? undefined : parseSize(retention.maxSize)!
1390
1407
  try {
1391
- const result = await prepared.localCache.evictIfDue({
1408
+ await prepared.localCache.evictIfDue({
1392
1409
  ...(maxAgeMs !== undefined ? { maxAgeMs } : {}),
1393
1410
  ...(maxBytes !== undefined ? { maxBytes } : {}),
1394
1411
  })
1395
- if (result !== null && (result.evicted > 0 || result.orphans > 0)) {
1396
- const said: string[] = []
1397
- if (result.evicted > 0) {
1398
- said.push(
1399
- `evicted ${result.evicted} entr${result.evicted === 1 ? 'y' : 'ies'} (${formatBytes(result.bytesFreed)})`,
1400
- )
1401
- }
1402
- if (result.orphans > 0) {
1403
- said.push(
1404
- `reaped ${result.orphans} orphaned artifact${result.orphans === 1 ? '' : 's'} (${formatBytes(result.orphanBytes)})`,
1405
- )
1406
- }
1407
- log.status(`vx: cache retention ${said.join(', ')}`)
1408
- }
1409
- } catch (err) {
1410
- log.status(`vx: cache retention skipped: ${err instanceof Error ? err.message : String(err)}`)
1412
+ } catch {
1413
+ // Housekeeping says nothing (owner): the next due run tries again.
1411
1414
  }
1412
1415
  }
1413
1416
 
@@ -4,7 +4,6 @@
4
4
  // aren't real work and would inflate "N total" misleadingly.
5
5
 
6
6
  import type { TaskOutcome } from '../graph/index.js'
7
- import type { FlakyFinding } from './failure-mode.js'
8
7
  import { paint, type ColorSupport } from './colors.js'
9
8
  import { tallyOutcomes } from './tally.js'
10
9
  import { isGroupTask } from '../graph/index.js'
@@ -386,84 +385,6 @@ export function neverStarted(o: TaskOutcome): boolean {
386
385
  return o.status === 'aborted' && o.wallclockStartNs === undefined
387
386
  }
388
387
 
389
- /**
390
- * Post-summary section naming the tasks that never started, under the
391
- * failure that blocked them. The footer's "1 skipped" says a task was
392
- * blocked and nothing about which task or by what; the broad flow prints
393
- * no row for a skipped task on purpose (a wide red run would drown in
394
- * them), so this is where the reader learns what the failure cost
395
- * (item 266). The scheduler records the root of each block on the outcome
396
- * (`blockedBy`, item 267); a skip without one is fail-fast's. Empty when
397
- * nothing was skipped.
398
- */
399
- export function formatSkippedSection(outcomes: readonly TaskOutcome[]): string[] {
400
- // A group (no `exec`) never starts by definition and is not a task the
401
- // tally counts; listing a blocked one here named three "tasks" the legend
402
- // beside it counted as none (item 281). Its members' rows say enough.
403
- const skipped = outcomes.filter((o) => o.status === 'skipped' && !isGroupTask(o.node))
404
- if (skipped.length === 0) return []
405
- const byId = new Map(outcomes.map((o) => [o.node.id, o]))
406
- const causeOf = (o: TaskOutcome): string => {
407
- if (o.blockedBy === undefined) return 'after the run stopped (fail-fast)'
408
- const root = byId.get(o.blockedBy)
409
- return root?.status === 'aborted'
410
- ? `after ${o.blockedBy} was aborted`
411
- : `after ${o.blockedBy} failed`
412
- }
413
- const groups = new Map<string, string[]>()
414
- for (const o of skipped) {
415
- const cause = causeOf(o)
416
- const list = groups.get(cause) ?? []
417
- list.push(o.node.id)
418
- groups.set(cause, list)
419
- }
420
- // "Blocked upstream" only when every skip was: a fail-fast skip had no
421
- // blocker, and the header claimed one over its own cause line (C-55).
422
- const blocked = skipped.every((o) => o.blockedBy !== undefined) ? ' \u2014 blocked upstream' : ''
423
- const lines = [
424
- '',
425
- ` Skipped: ${skipped.length} task${skipped.length === 1 ? '' : 's'} never started${blocked}`,
426
- ]
427
- const NAMES = 8
428
- for (const [cause, ids] of [...groups].sort((a, b) => b[1].length - a[1].length)) {
429
- ids.sort()
430
- const shown = ids.slice(0, NAMES).join(', ')
431
- const more = ids.length > NAMES ? ` \u2026 +${ids.length - NAMES} more` : ''
432
- lines.push(` \u2298 ${cause}: ${shown}${more}`)
433
- }
434
- return lines
435
- }
436
-
437
- /**
438
- * Post-summary section naming the tasks this run proved flaky: identical
439
- * inputs, both outcomes on record (or a retry within the run). A red run
440
- * whose failure has passed on these exact inputs before is not a break to
441
- * bisect, and a green run that only passed on the second attempt is not
442
- * green — either way the footer's counts alone would say nothing. Empty
443
- * when nothing was.
444
- */
445
- export function formatFlakySection(findings: readonly FlakyFinding[]): string[] {
446
- if (findings.length === 0) return []
447
- const lines = [
448
- '',
449
- ` Flaky: ${findings.length} task${findings.length === 1 ? '' : 's'} with the same inputs both passing and failing on record`,
450
- ]
451
- for (const f of findings) {
452
- const times = (n: number): string => `${n}\u00d7`
453
- const history =
454
- f.status === 'failed'
455
- ? `failed on inputs that passed ${times(f.passes)} before`
456
- : f.failures > 0
457
- ? `passed on inputs that failed ${times(f.failures)} before`
458
- : 'passed'
459
- const retried = f.attempts > 1 ? ` \u00b7 ${f.attempts} attempts this run` : ''
460
- lines.push(
461
- ` ${f.status === 'failed' ? '\u2717' : '\u2713'} ${f.taskId} \u2014 ${history}${retried}`,
462
- )
463
- }
464
- return lines
465
- }
466
-
467
388
  export function formatDuration(ms: number): string {
468
389
  return formatElapsed(ms)
469
390
  }