@vzn/vx 0.0.652 → 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.
Files changed (167) hide show
  1. package/README.md +2 -2
  2. package/launcher.sh +2 -1
  3. package/package.json +5 -5
  4. package/schemas/error.json +1 -1
  5. package/schemas/show.json +1 -0
  6. package/skills/vx/SKILL.md +3 -3
  7. package/src/bin.ts +25 -3
  8. package/src/cache/archive.ts +196 -31
  9. package/src/cache/cache.ts +863 -225
  10. package/src/cache/checkpointer.ts +102 -0
  11. package/src/cache/file-hashes.ts +2 -1
  12. package/src/cache/fold-memo.ts +24 -4
  13. package/src/cache/fs-clock.ts +26 -0
  14. package/src/cache/git-inputs.ts +188 -22
  15. package/src/cache/history-stream.ts +22 -5
  16. package/src/cache/index.ts +10 -1
  17. package/src/cache/inputs.ts +573 -152
  18. package/src/cache/key-fold.ts +1 -1
  19. package/src/cache/layer.ts +23 -10
  20. package/src/cache/layered-cache.ts +28 -7
  21. package/src/cache/output-index.ts +106 -44
  22. package/src/cache/plugin-cache.ts +2 -2
  23. package/src/cache/policy.ts +6 -2
  24. package/src/cache/remote-lane.ts +3 -1
  25. package/src/cache/run-history.ts +50 -10
  26. package/src/cache/schema.ts +42 -9
  27. package/src/cli/cache.ts +147 -34
  28. package/src/cli/completions.ts +2 -2
  29. package/src/cli/core-alias.ts +9 -2
  30. package/src/cli/docs.ts +4 -2
  31. package/src/cli/help.ts +25 -2
  32. package/src/cli/index.ts +38 -7
  33. package/src/cli/info.ts +4 -3
  34. package/src/cli/init.ts +55 -20
  35. package/src/cli/last.ts +184 -41
  36. package/src/cli/lock.ts +187 -24
  37. package/src/cli/plan-format.ts +24 -21
  38. package/src/cli/plugin-commands.ts +6 -1
  39. package/src/cli/plugin-templates.ts +1 -1
  40. package/src/cli/run.ts +68 -34
  41. package/src/cli/select.ts +160 -64
  42. package/src/cli/show.ts +63 -18
  43. package/src/cli/task-verb.ts +12 -7
  44. package/src/cli/upgrade.ts +210 -76
  45. package/src/cli/watch-filter.ts +88 -15
  46. package/src/cli/watch-fs.ts +34 -47
  47. package/src/cli/watch-judge.ts +72 -8
  48. package/src/cli/watch-set.ts +6 -4
  49. package/src/cli/watch.ts +179 -25
  50. package/src/cli/why.ts +107 -33
  51. package/src/cli/workspace-config.ts +11 -1
  52. package/src/config.ts +9 -0
  53. package/src/exec/env.ts +3 -1
  54. package/src/exec/executor.ts +76 -15
  55. package/src/exec/kill-tree.ts +7 -7
  56. package/src/exec/runner.ts +270 -108
  57. package/src/exec/sandbox-binds.ts +109 -18
  58. package/src/exec/sandbox-runtime.ts +134 -37
  59. package/src/exec/sandbox-violations.ts +250 -46
  60. package/src/exec/spawn-off.ts +10 -5
  61. package/src/exec/warm-shell.ts +29 -7
  62. package/src/graph/folds.ts +89 -16
  63. package/src/graph/index.ts +8 -0
  64. package/src/graph/priorities.ts +8 -5
  65. package/src/graph/scheduler.ts +173 -38
  66. package/src/graph/stand-ins.ts +36 -0
  67. package/src/graph/task-graph.ts +327 -141
  68. package/src/graph/task-status.ts +5 -1
  69. package/src/handoff.ts +45 -10
  70. package/src/history/failure-mode.ts +15 -5
  71. package/src/history/history.ts +103 -30
  72. package/src/history/index.ts +1 -0
  73. package/src/history/metrics.ts +189 -69
  74. package/src/index.ts +2 -1
  75. package/src/orchestrator/admission.ts +13 -9
  76. package/src/orchestrator/affected-tasks.ts +35 -13
  77. package/src/orchestrator/command-windows.ts +8 -5
  78. package/src/orchestrator/deferred-outputs.ts +41 -12
  79. package/src/orchestrator/doctor.ts +21 -9
  80. package/src/orchestrator/download-policy.ts +3 -3
  81. package/src/orchestrator/excluded-keys.ts +14 -2
  82. package/src/orchestrator/execute-args.ts +9 -2
  83. package/src/orchestrator/execute-task.ts +564 -123
  84. package/src/orchestrator/graph-memo.ts +26 -4
  85. package/src/orchestrator/graph-stage.ts +27 -4
  86. package/src/orchestrator/held-servers.ts +36 -14
  87. package/src/orchestrator/hit-restore.ts +136 -87
  88. package/src/orchestrator/index.ts +4 -1
  89. package/src/orchestrator/keyed-projects.ts +25 -0
  90. package/src/orchestrator/local-floor.ts +21 -3
  91. package/src/orchestrator/lockfile-claim.ts +30 -4
  92. package/src/orchestrator/miss-save.ts +33 -53
  93. package/src/orchestrator/options.ts +13 -0
  94. package/src/orchestrator/output-host.ts +16 -4
  95. package/src/orchestrator/output-log.ts +1 -54
  96. package/src/orchestrator/persistent-task.ts +31 -17
  97. package/src/orchestrator/persistent.ts +152 -15
  98. package/src/orchestrator/placement.ts +81 -15
  99. package/src/orchestrator/plan.ts +37 -15
  100. package/src/orchestrator/plugin-host.ts +232 -108
  101. package/src/orchestrator/plugin.ts +53 -27
  102. package/src/orchestrator/prepare.ts +194 -38
  103. package/src/orchestrator/projects.ts +89 -10
  104. package/src/orchestrator/read-only.ts +115 -0
  105. package/src/orchestrator/run-context.ts +6 -2
  106. package/src/orchestrator/run-failures.ts +43 -14
  107. package/src/orchestrator/run-id.ts +13 -2
  108. package/src/orchestrator/run-keys.ts +87 -28
  109. package/src/orchestrator/run-live.ts +16 -13
  110. package/src/orchestrator/run-lock.ts +27 -4
  111. package/src/orchestrator/run-records.ts +12 -10
  112. package/src/orchestrator/run-report.ts +2 -1
  113. package/src/orchestrator/run-stop.ts +17 -9
  114. package/src/orchestrator/run.ts +443 -107
  115. package/src/orchestrator/sandbox-fix.ts +40 -12
  116. package/src/orchestrator/sandbox-request.ts +286 -59
  117. package/src/orchestrator/save-lane.ts +24 -13
  118. package/src/orchestrator/shell-verdict.ts +20 -1
  119. package/src/orchestrator/signals.ts +103 -8
  120. package/src/orchestrator/stable-keys.ts +17 -9
  121. package/src/orchestrator/task-artifacts.ts +15 -2
  122. package/src/orchestrator/task-hash.ts +187 -30
  123. package/src/orchestrator/telemetry-host.ts +25 -6
  124. package/src/orchestrator/telemetry.ts +80 -12
  125. package/src/orchestrator/upstream.ts +3 -1
  126. package/src/report/events.ts +20 -3
  127. package/src/report/forecast.ts +103 -37
  128. package/src/report/framed-output.ts +165 -157
  129. package/src/report/index.ts +2 -1
  130. package/src/report/logger.ts +107 -30
  131. package/src/report/plain-output.ts +13 -9
  132. package/src/report/status-line.ts +28 -55
  133. package/src/report/summary.ts +57 -14
  134. package/src/settings.ts +42 -6
  135. package/src/ui/components.ts +36 -42
  136. package/src/ui/index.ts +3 -1
  137. package/src/ui/live-region.ts +67 -8
  138. package/src/ui/terminal.ts +12 -0
  139. package/src/ui/theme.ts +16 -1
  140. package/src/util/config-frame.ts +17 -3
  141. package/src/util/edit-distance.ts +17 -1
  142. package/src/util/errors.ts +23 -7
  143. package/src/util/foreign-cache-dir.ts +60 -0
  144. package/src/util/git-ignored.ts +59 -5
  145. package/src/util/index.ts +15 -1
  146. package/src/util/paths.ts +50 -6
  147. package/src/util/plain-text.ts +19 -0
  148. package/src/util/secret-mask.ts +57 -1
  149. package/src/util/settle.ts +16 -0
  150. package/src/util/size.ts +12 -2
  151. package/src/util/write-file.ts +86 -9
  152. package/src/workspace/affected.ts +225 -27
  153. package/src/workspace/config-bytecode.ts +23 -4
  154. package/src/workspace/config-eval.ts +90 -16
  155. package/src/workspace/config-imports.ts +96 -41
  156. package/src/workspace/config-schema.ts +126 -25
  157. package/src/workspace/dependencies-lock.ts +7 -14
  158. package/src/workspace/filter.ts +10 -1
  159. package/src/workspace/index.ts +3 -1
  160. package/src/workspace/json-data.ts +34 -8
  161. package/src/workspace/lockfile.ts +86 -37
  162. package/src/workspace/migration.ts +46 -25
  163. package/src/workspace/module-state.ts +62 -0
  164. package/src/workspace/nested-dirs.ts +8 -0
  165. package/src/workspace/project-loader.ts +276 -32
  166. package/src/workspace/repo-id.ts +29 -6
  167. package/src/workspace/workspace.ts +190 -30
@@ -8,7 +8,7 @@
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'
@@ -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 }
@@ -283,6 +284,8 @@ export interface OutcomeView {
283
284
  isGroup?: boolean
284
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
- /** `outcomeWord` with the exit code a failure carries: `failed (exit 9)`. */
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
@@ -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
- const cost = new Map<string, number>()
82
- for (const n of order) {
83
- const p = isGroupTask(n) ? undefined : p50s.get(n.id)
84
- if (p !== undefined) cost.set(n.id, p)
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 position = new Map(order.map((n, i) => [n.id, i]))
90
- const before = (a: string, b: string): boolean => {
91
- const d = (rank.get(a) ?? 0) - (rank.get(b) ?? 0)
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: string | undefined
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 (const n of order) {
108
- if (finished.has(n.id)) continue
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: string | undefined
111
- for (const d of n.deps) {
112
- const e = end.get(d)
113
- if (e !== undefined && (via === undefined || e > start)) {
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 since = running.get(n.id)
119
- if (since !== undefined) head.set(n.id, n.id)
120
- else if (via !== undefined && head.has(via)) head.set(n.id, head.get(via)!)
121
- const by =
122
- since === undefined ? (head.get(n.id) ?? n.deps.find((d) => end.has(d))) : undefined
123
- if (by !== undefined && !isGroupTask(n)) {
124
- waiting++
125
- let at = held.length
126
- while (at > 0 && before(n.id, held[at - 1]!.id)) at--
127
- if (at < workers) {
128
- held.splice(at, 0, { id: n.id, by })
129
- if (held.length > workers) held.pop()
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.get(n.id)
198
+ const p = cost[i]!
133
199
  let own = 0
134
- if (p !== undefined) {
200
+ if (p === p) {
135
201
  known = true
136
- own = since === undefined ? p : Math.max(0, p - (nowMs - since))
202
+ own = runs ? Math.max(0, p - (nowMs - s)) : p
137
203
  }
138
204
  const e = start + own
139
- end.set(n.id, e)
140
- if (e > chain || chainEnd === undefined) {
205
+ end[i] = e
206
+ if (e > chain || chainEnd < 0) {
141
207
  chain = e
142
- chainEnd = n.id
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 === undefined ? undefined : head.get(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 } : {}),