@vzn/vx 0.0.646 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
|
@@ -36,11 +36,24 @@ export interface RunKeys {
|
|
|
36
36
|
uncachedKeys: Map<string, string>
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Where a run's keys are remembered: the selection's row, under its token. */
|
|
40
|
+
export interface RunKeysAt {
|
|
41
|
+
id: string
|
|
42
|
+
token: string
|
|
43
|
+
/**
|
|
44
|
+
* The token less each node's files, and each node's files token, when a
|
|
45
|
+
* remembered graph fixes every edge and config (`graph`): an edit then
|
|
46
|
+
* moves only the files of the projects it touched, and the keys it
|
|
47
|
+
* reaches are all that derive again (`readRunDelta`).
|
|
48
|
+
*/
|
|
49
|
+
delta?: () => { global: string; files: Map<string, string> }
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
/**
|
|
40
53
|
* The memo row's name (the selection) and the token its keys stand for, or
|
|
41
54
|
* undefined when a node's key reads something the token cannot name.
|
|
42
55
|
*/
|
|
43
|
-
export function runKeysToken(args: RunKeysArgs):
|
|
56
|
+
export function runKeysToken(args: RunKeysArgs): RunKeysAt | undefined {
|
|
44
57
|
if (args.hashCache.folds === undefined) return undefined
|
|
45
58
|
const parts: string[] = [
|
|
46
59
|
RUN_MEMO_VERSION,
|
|
@@ -52,8 +65,18 @@ export function runKeysToken(args: RunKeysArgs): { id: string; token: string } |
|
|
|
52
65
|
// Which outputs git ignores decides which a reader may see (`readableOutputs`).
|
|
53
66
|
[...args.gitFilesCache.ignoredOutputs].sort().join('\0'),
|
|
54
67
|
]
|
|
68
|
+
// A remembered graph's token names every node's edges and config, so
|
|
69
|
+
// each node adds only what may differ under it (4 of 8 ms at 1,206
|
|
70
|
+
// tasks, 2026-10-23).
|
|
71
|
+
const graph = args.hashCache.graph
|
|
72
|
+
parts.push(graph ?? '')
|
|
55
73
|
const ids: string[] = []
|
|
56
74
|
const envNames = new Set<string>()
|
|
75
|
+
// Tasks of one project mostly declare the same files and outputs, and the
|
|
76
|
+
// token is a function of that declaration (`filesToken`): one walk each,
|
|
77
|
+
// not one per task (5 of 12 ms at 1,206 tasks, 2026-10-23).
|
|
78
|
+
const tokens = new Map<string, string>()
|
|
79
|
+
const filesAt: number[] = []
|
|
57
80
|
for (const node of args.nodes.values()) {
|
|
58
81
|
const cfg = node.config
|
|
59
82
|
if (isKeyless(cfg) || node.excludedUpstream !== undefined) return undefined
|
|
@@ -66,19 +89,30 @@ export function runKeysToken(args: RunKeysArgs): { id: string; token: string } |
|
|
|
66
89
|
if ((inputs.workspaceFiles?.length ?? 0) > 0) return undefined
|
|
67
90
|
// No positive entry resolves no files (`filesPlan`).
|
|
68
91
|
if (inputs.files === undefined || inputs.files.some((e) => !e.startsWith('!'))) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const outputs = cfg.cache?.outputs
|
|
93
|
+
const declared = `${node.projectName}\0${JSON.stringify([inputs.files, outputs?.files, outputs?.workspaceFiles])}`
|
|
94
|
+
let token = tokens.get(declared)
|
|
95
|
+
if (token === undefined) {
|
|
96
|
+
token = taskFilesToken({
|
|
97
|
+
node,
|
|
98
|
+
workspaceRoot: args.workspaceRoot,
|
|
99
|
+
nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
|
|
100
|
+
gitFilesCache: args.gitFilesCache,
|
|
101
|
+
hashCache: args.hashCache,
|
|
102
|
+
})
|
|
103
|
+
if (token === undefined) return undefined
|
|
104
|
+
tokens.set(declared, token)
|
|
105
|
+
}
|
|
77
106
|
files = token
|
|
78
107
|
}
|
|
79
108
|
for (const name of inputs.env ?? []) envNames.add(name)
|
|
80
109
|
}
|
|
81
110
|
ids.push(node.id)
|
|
111
|
+
if (graph !== undefined) {
|
|
112
|
+
parts.push(node.id, node.requested ? '1' : '0', node.platform ?? '')
|
|
113
|
+
filesAt.push(parts.push(files) - 1)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
82
116
|
parts.push(
|
|
83
117
|
node.id,
|
|
84
118
|
node.projectName,
|
|
@@ -88,6 +122,7 @@ export function runKeysToken(args: RunKeysArgs): { id: string; token: string } |
|
|
|
88
122
|
node.requested ? '1' : '0',
|
|
89
123
|
hashTaskConfig(cfg, args.hashCache),
|
|
90
124
|
JSON.stringify(node.keyParts ?? []),
|
|
125
|
+
node.platform ?? '',
|
|
91
126
|
JSON.stringify(node.addsToOutputsOf ?? []),
|
|
92
127
|
JSON.stringify(node.outputsAddedToBy ?? []),
|
|
93
128
|
files,
|
|
@@ -97,7 +132,26 @@ export function runKeysToken(args: RunKeysArgs): { id: string; token: string } |
|
|
|
97
132
|
const value = process.env[name]
|
|
98
133
|
parts.push(value === undefined ? name : `${name}=${value}`)
|
|
99
134
|
}
|
|
100
|
-
|
|
135
|
+
const selection = xxh3hex(ids.join('\0'))
|
|
136
|
+
const at: RunKeysAt = { id: `\0run\0${selection}`, token: xxh3hex(parts.join('\0')) }
|
|
137
|
+
if (graph !== undefined) {
|
|
138
|
+
let delta: { global: string; files: Map<string, string> } | undefined
|
|
139
|
+
at.delta = () => {
|
|
140
|
+
if (delta !== undefined) return delta
|
|
141
|
+
const files = new Map<string, string>()
|
|
142
|
+
const hashed = new Map<string, string>()
|
|
143
|
+
const rest = [...parts]
|
|
144
|
+
filesAt.forEach((i, n) => {
|
|
145
|
+
const token = parts[i]!
|
|
146
|
+
let h = hashed.get(token)
|
|
147
|
+
if (h === undefined) hashed.set(token, (h = xxh3hex(token)))
|
|
148
|
+
files.set(ids[n]!, h)
|
|
149
|
+
rest[i] = ''
|
|
150
|
+
})
|
|
151
|
+
return (delta = { global: `${selection}\0${xxh3hex(rest.join('\0'))}`, files })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return at
|
|
101
155
|
}
|
|
102
156
|
|
|
103
157
|
/** The keys remembered under `token`, mapped back onto this run's nodes. */
|
|
@@ -122,6 +176,84 @@ export function readRunKeys(
|
|
|
122
176
|
return { stableKeys, uncachedKeys: new Map(parsed.u) }
|
|
123
177
|
}
|
|
124
178
|
|
|
179
|
+
/**
|
|
180
|
+
* What a run's derivation leaves for the next one to start from: every key
|
|
181
|
+
* it made, in the order it made them, and which became stable or uncached
|
|
182
|
+
* keys. Those verdicts are a function of the graph, the configs and what
|
|
183
|
+
* git ignores, all in the global token, so a run under the same global
|
|
184
|
+
* token keeps them and derives only the keys an edit reaches.
|
|
185
|
+
*/
|
|
186
|
+
export interface RunDerivation {
|
|
187
|
+
keys: Map<string, string>
|
|
188
|
+
stable: string[]
|
|
189
|
+
uncached: string[]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface DeltaRow {
|
|
193
|
+
k: [string, string][]
|
|
194
|
+
f: [string, string][]
|
|
195
|
+
s: string[]
|
|
196
|
+
u: string[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const deltaId = (at: RunKeysAt): string => `\0rund\0${at.id.slice('\0run\0'.length)}`
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The last derivation under this run's global token, and the tasks whose
|
|
203
|
+
* keys may have moved since: each one whose files token did, and every task
|
|
204
|
+
* folding one of theirs. Undefined when no such derivation is remembered.
|
|
205
|
+
*/
|
|
206
|
+
export function readRunDelta(
|
|
207
|
+
args: RunKeysArgs,
|
|
208
|
+
at: RunKeysAt,
|
|
209
|
+
): { derivation: RunDerivation; dirty: Set<string> } | undefined {
|
|
210
|
+
if (at.delta === undefined) return undefined
|
|
211
|
+
const { global, files } = at.delta()
|
|
212
|
+
const row = args.hashCache.folds?.get(deltaId(at), global)
|
|
213
|
+
if (row === undefined) return undefined
|
|
214
|
+
let parsed: DeltaRow
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(row)
|
|
217
|
+
} catch {
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
220
|
+
const was = new Map(parsed.f)
|
|
221
|
+
const dirty = new Set<string>()
|
|
222
|
+
for (const [id, token] of files) if (was.get(id) !== token) dirty.add(id)
|
|
223
|
+
const dependants = new Map<string, string[]>()
|
|
224
|
+
for (const node of args.nodes.values()) {
|
|
225
|
+
for (const dep of node.deps) {
|
|
226
|
+
const list = dependants.get(dep)
|
|
227
|
+
if (list === undefined) dependants.set(dep, [node.id])
|
|
228
|
+
else list.push(node.id)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const stack = [...dirty]
|
|
232
|
+
while (stack.length > 0) {
|
|
233
|
+
for (const up of dependants.get(stack.pop()!) ?? []) {
|
|
234
|
+
if (dirty.has(up)) continue
|
|
235
|
+
dirty.add(up)
|
|
236
|
+
stack.push(up)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
derivation: { keys: new Map(parsed.k), stable: parsed.s, uncached: parsed.u },
|
|
241
|
+
dirty,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function writeRunDelta(args: RunKeysArgs, at: RunKeysAt, derivation: RunDerivation): void {
|
|
246
|
+
if (at.delta === undefined || args.hashCache.folds === undefined) return
|
|
247
|
+
const { global, files } = at.delta()
|
|
248
|
+
const row: DeltaRow = {
|
|
249
|
+
k: [...derivation.keys],
|
|
250
|
+
f: [...files],
|
|
251
|
+
s: derivation.stable,
|
|
252
|
+
u: derivation.uncached,
|
|
253
|
+
}
|
|
254
|
+
args.hashCache.folds.set(deltaId(at), global, JSON.stringify(row))
|
|
255
|
+
}
|
|
256
|
+
|
|
125
257
|
export function writeRunKeys(
|
|
126
258
|
args: RunKeysArgs,
|
|
127
259
|
at: { id: string; token: string },
|
|
@@ -144,21 +276,25 @@ let identity: string | undefined
|
|
|
144
276
|
* checkout is every module under `src/`, so an edit to the derivation from
|
|
145
277
|
* source never reads back a key the old code made.
|
|
146
278
|
*/
|
|
147
|
-
function codeIdentity(): string {
|
|
279
|
+
export function codeIdentity(): string {
|
|
148
280
|
if (identity !== undefined) return identity
|
|
149
281
|
// `Bun.main`, not `import.meta.dir`: `--minify` inlines the build
|
|
150
282
|
// machine's source path, so an installed binary walked a `src/` that is
|
|
151
283
|
// not there, threw, and lost every warm run's short circuit (2026-10-22).
|
|
284
|
+
// ctime and inode with the mtime, as the file hash memo keys: an npm
|
|
285
|
+
// tarball stamps every file 1985-10-26 and extraction keeps that mtime,
|
|
286
|
+
// so an upgrade that kept a file's size left its stamp alone, and `cp
|
|
287
|
+
// -p` does the same to a binary. No write can set ctime back.
|
|
152
288
|
if (Bun.main.startsWith('/$bunfs/')) {
|
|
153
289
|
const st = statSync(process.execPath)
|
|
154
|
-
return (identity = `${process.execPath}:${st.size}:${st.mtimeMs}:${st.ino}`)
|
|
290
|
+
return (identity = `${process.execPath}:${st.size}:${st.mtimeMs}:${st.ctimeMs}:${st.ino}`)
|
|
155
291
|
}
|
|
156
292
|
const src = path.resolve(import.meta.dir, '..')
|
|
157
293
|
const stamps: string[] = []
|
|
158
294
|
for (const rel of readdirSync(src, { recursive: true, encoding: 'utf8' }).sort()) {
|
|
159
295
|
if (!rel.endsWith('.ts')) continue
|
|
160
296
|
const st = statSync(path.join(src, rel))
|
|
161
|
-
stamps.push(`${rel}:${st.size}:${st.mtimeMs}`)
|
|
297
|
+
stamps.push(`${rel}:${st.size}:${st.mtimeMs}:${st.ctimeMs}:${st.ino}`)
|
|
162
298
|
}
|
|
163
299
|
return (identity = xxh3hex(stamps.join('\0')))
|
|
164
300
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// The run in progress, for `vx last --live` (2026-10-28): an agent that
|
|
2
|
+
// started `vx run` in the background asks what runs, what already failed
|
|
3
|
+
// and what it was warned of while the run goes on; the history holds a
|
|
4
|
+
// run only once it ended.
|
|
5
|
+
//
|
|
6
|
+
// The run answers on a unix socket beside its run lock (run-lock.ts),
|
|
7
|
+
// `<lock>.sock`: in this user's own lock directory, so no other user
|
|
8
|
+
// reaches it, and taken only while the lock is held, so a socket a killed
|
|
9
|
+
// run left is this run's to replace. Nothing is written for it: the run
|
|
10
|
+
// keeps which task started when (one Map entry per task) and builds the
|
|
11
|
+
// snapshot only when asked. Output is not kept; a reader that wants it
|
|
12
|
+
// follows (`--follow`), and a follower is fed only while it is attached
|
|
13
|
+
// and keeps up: one that falls a megabyte behind is let go, so a stuck
|
|
14
|
+
// reader never holds the run's memory or its event loop.
|
|
15
|
+
//
|
|
16
|
+
// The path is left with the lock, before the run's last steps, and a
|
|
17
|
+
// follower stays attached through `run:done`.
|
|
18
|
+
//
|
|
19
|
+
// The protocol is one request line, `snapshot` or `follow`, then the
|
|
20
|
+
// snapshot as one JSON line; a follower then gets each `WireEvent` as a
|
|
21
|
+
// line until `run:done`, and the run closes the connection.
|
|
22
|
+
|
|
23
|
+
import { unlinkSync } from 'node:fs'
|
|
24
|
+
import type { Socket, UnixSocketListener } from 'bun'
|
|
25
|
+
import { isGroupTask, type TaskNode } from '../graph/index.js'
|
|
26
|
+
import {
|
|
27
|
+
type EventBus,
|
|
28
|
+
type OutcomeView,
|
|
29
|
+
projectOutcome,
|
|
30
|
+
toWireEvent,
|
|
31
|
+
type WireEvent,
|
|
32
|
+
} from '../report/index.js'
|
|
33
|
+
import { runLockPath } from './run-lock.js'
|
|
34
|
+
|
|
35
|
+
/** One task as a live reader sees it. */
|
|
36
|
+
export interface LiveTask {
|
|
37
|
+
taskId: string
|
|
38
|
+
/** `waiting`, `running`, or the status it ended with. */
|
|
39
|
+
status: 'waiting' | 'running' | OutcomeView['status']
|
|
40
|
+
/** Epoch ms it started; null while it waits (and for a skip, which never starts). */
|
|
41
|
+
startedAt: number | null
|
|
42
|
+
/** Null until it ends. */
|
|
43
|
+
durationMs: number | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What `vx last --live --format json` prints, the snapshot line. */
|
|
47
|
+
export interface LiveSnapshot {
|
|
48
|
+
live: { runId: string; command: string; pid: number; startedAt: number; elapsedMs: number }
|
|
49
|
+
tasks: LiveTask[]
|
|
50
|
+
warnings: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Where the run of `workspaceRoot` answers: beside its run lock. */
|
|
54
|
+
export function liveSocketPath(workspaceRoot: string, lockDir?: string): string {
|
|
55
|
+
return liveSocketOf(runLockPath(workspaceRoot, lockDir))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The live socket beside the run lock at `lockPath`. */
|
|
59
|
+
export function liveSocketOf(lockPath: string): string {
|
|
60
|
+
return `${lockPath}.sock`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A follower this far behind is let go. */
|
|
64
|
+
const FOLLOW_BACKLOG = 1 << 20
|
|
65
|
+
|
|
66
|
+
interface Peer {
|
|
67
|
+
follow: boolean
|
|
68
|
+
/** Bytes the socket did not take yet. */
|
|
69
|
+
pending: Uint8Array[]
|
|
70
|
+
pendingBytes: number
|
|
71
|
+
/** End the connection once `pending` drains. */
|
|
72
|
+
ending: boolean
|
|
73
|
+
request: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const encoder = new TextEncoder()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The sockets this process listens on. A signal exit is `process.exit`
|
|
80
|
+
* (signals.ts): `unlisten` never runs, and a second Ctrl-C left the
|
|
81
|
+
* socket beside the released lock. The `exit` event runs on every exit
|
|
82
|
+
* path, synchronously, as run-lock.ts's release does; this one runs
|
|
83
|
+
* first, so the path is left before the lock frees it for the next run.
|
|
84
|
+
*/
|
|
85
|
+
const listening = new Set<string>()
|
|
86
|
+
let exitHooked = false
|
|
87
|
+
function unlinkOnExit(socketPath: string): void {
|
|
88
|
+
listening.add(socketPath)
|
|
89
|
+
if (exitHooked) return
|
|
90
|
+
exitHooked = true
|
|
91
|
+
process.prependListener('exit', () => {
|
|
92
|
+
for (const p of listening) {
|
|
93
|
+
try {
|
|
94
|
+
unlinkSync(p)
|
|
95
|
+
} catch {
|
|
96
|
+
// Gone already.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Serve the run's live state on `socketPath` until the returned close.
|
|
104
|
+
* Never fails the run: a socket that cannot be made (a path too long, a
|
|
105
|
+
* second run in this process holding it) leaves the run unanswered.
|
|
106
|
+
*/
|
|
107
|
+
export function serveLive(opts: {
|
|
108
|
+
socketPath: string
|
|
109
|
+
bus: EventBus
|
|
110
|
+
runId: string
|
|
111
|
+
command: string
|
|
112
|
+
startedAt: number
|
|
113
|
+
nodes: () => Iterable<TaskNode>
|
|
114
|
+
warnings: () => readonly string[]
|
|
115
|
+
}): { unlisten(): void; dispose(): void } {
|
|
116
|
+
const started = new Map<string, number>()
|
|
117
|
+
const ended = new Map<
|
|
118
|
+
string,
|
|
119
|
+
OutcomeView['status'] | { status: OutcomeView['status']; durationMs: number }
|
|
120
|
+
>()
|
|
121
|
+
const peers = new Map<Socket<Peer>, Peer>()
|
|
122
|
+
let followers = 0
|
|
123
|
+
|
|
124
|
+
const flush = (sock: Socket<Peer>, peer: Peer): void => {
|
|
125
|
+
while (peer.pending.length > 0) {
|
|
126
|
+
const head = peer.pending[0]!
|
|
127
|
+
const n = sock.write(head)
|
|
128
|
+
if (n < 0) return
|
|
129
|
+
peer.pendingBytes -= n
|
|
130
|
+
if (n < head.length) {
|
|
131
|
+
peer.pending[0] = head.subarray(n)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
peer.pending.shift()
|
|
135
|
+
}
|
|
136
|
+
if (peer.ending) sock.end()
|
|
137
|
+
}
|
|
138
|
+
const send = (sock: Socket<Peer>, peer: Peer, line: string): void => {
|
|
139
|
+
const bytes = encoder.encode(`${line}\n`)
|
|
140
|
+
peer.pending.push(bytes)
|
|
141
|
+
peer.pendingBytes += bytes.length
|
|
142
|
+
if (peer.follow && peer.pendingBytes > FOLLOW_BACKLOG) {
|
|
143
|
+
drop(sock, peer)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (peer.pending.length === 1) flush(sock, peer)
|
|
147
|
+
}
|
|
148
|
+
const drop = (sock: Socket<Peer>, peer: Peer): void => {
|
|
149
|
+
if (peers.delete(sock) && peer.follow) followers--
|
|
150
|
+
sock.end()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const snapshot = (): LiveSnapshot => {
|
|
154
|
+
const now = Date.now()
|
|
155
|
+
const tasks: LiveTask[] = []
|
|
156
|
+
for (const node of opts.nodes()) {
|
|
157
|
+
if (isGroupTask(node)) continue
|
|
158
|
+
const at = started.get(node.id) ?? null
|
|
159
|
+
const end = ended.get(node.id)
|
|
160
|
+
tasks.push(
|
|
161
|
+
end !== undefined && typeof end === 'object'
|
|
162
|
+
? { taskId: node.id, status: end.status, startedAt: at, durationMs: end.durationMs }
|
|
163
|
+
: at !== null
|
|
164
|
+
? { taskId: node.id, status: 'running', startedAt: at, durationMs: null }
|
|
165
|
+
: { taskId: node.id, status: 'waiting', startedAt: null, durationMs: null },
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
const rank = (t: LiveTask): number =>
|
|
169
|
+
t.status === 'running' ? 0 : t.status === 'waiting' ? 2 : 1
|
|
170
|
+
tasks.sort(
|
|
171
|
+
(a, b) => rank(a) - rank(b) || (a.taskId < b.taskId ? -1 : a.taskId > b.taskId ? 1 : 0),
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
live: {
|
|
175
|
+
runId: opts.runId,
|
|
176
|
+
command: opts.command,
|
|
177
|
+
pid: process.pid,
|
|
178
|
+
startedAt: opts.startedAt,
|
|
179
|
+
elapsedMs: now - opts.startedAt,
|
|
180
|
+
},
|
|
181
|
+
tasks,
|
|
182
|
+
warnings: opts.warnings().map((w) => Bun.stripANSI(w)),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const unsubscribe = opts.bus.subscribe((event) => {
|
|
187
|
+
if (event.kind === 'task:start') started.set(event.node.id, Date.now())
|
|
188
|
+
else if (event.kind === 'task:complete') {
|
|
189
|
+
ended.set(event.node.id, {
|
|
190
|
+
status: event.outcome.status,
|
|
191
|
+
durationMs: event.outcome.durationMs,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
if (followers === 0) return
|
|
195
|
+
if (
|
|
196
|
+
(event.kind === 'task:start' || event.kind === 'task:complete') &&
|
|
197
|
+
isGroupTask(event.node)
|
|
198
|
+
) {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
const wire: WireEvent =
|
|
202
|
+
event.kind === 'task:complete'
|
|
203
|
+
? { kind: 'task:complete', outcome: projectOutcome(event.outcome) }
|
|
204
|
+
: toWireEvent(event)
|
|
205
|
+
const line = JSON.stringify(wire)
|
|
206
|
+
for (const [sock, peer] of peers) {
|
|
207
|
+
if (!peer.follow) continue
|
|
208
|
+
send(sock, peer, line)
|
|
209
|
+
if (event.kind === 'run:done') peer.ending = true
|
|
210
|
+
if (peer.ending && peer.pending.length === 0) drop(sock, peer)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
let listener: UnixSocketListener<Peer> | undefined
|
|
215
|
+
try {
|
|
216
|
+
unlinkSync(opts.socketPath)
|
|
217
|
+
} catch {
|
|
218
|
+
// None there: the usual case.
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const made = Bun.listen<Peer>({
|
|
222
|
+
unix: opts.socketPath,
|
|
223
|
+
socket: {
|
|
224
|
+
open(sock) {
|
|
225
|
+
const peer: Peer = {
|
|
226
|
+
follow: false,
|
|
227
|
+
pending: [],
|
|
228
|
+
pendingBytes: 0,
|
|
229
|
+
ending: false,
|
|
230
|
+
request: '',
|
|
231
|
+
}
|
|
232
|
+
sock.data = peer
|
|
233
|
+
peers.set(sock, peer)
|
|
234
|
+
},
|
|
235
|
+
data(sock, chunk) {
|
|
236
|
+
const peer = sock.data
|
|
237
|
+
if (peer.request.includes('\n')) return
|
|
238
|
+
peer.request += chunk.toString()
|
|
239
|
+
if (!peer.request.includes('\n')) return
|
|
240
|
+
const what = peer.request.trim()
|
|
241
|
+
send(sock, peer, JSON.stringify(snapshot()))
|
|
242
|
+
if (what === 'follow') {
|
|
243
|
+
peer.follow = true
|
|
244
|
+
followers++
|
|
245
|
+
} else {
|
|
246
|
+
peer.ending = true
|
|
247
|
+
if (peer.pending.length === 0) drop(sock, peer)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
drain(sock) {
|
|
251
|
+
flush(sock, sock.data)
|
|
252
|
+
},
|
|
253
|
+
close(sock) {
|
|
254
|
+
const peer = peers.get(sock)
|
|
255
|
+
if (peer !== undefined) drop(sock, peer)
|
|
256
|
+
},
|
|
257
|
+
error(sock) {
|
|
258
|
+
const peer = peers.get(sock)
|
|
259
|
+
if (peer !== undefined) drop(sock, peer)
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
// The socket must never keep a run's process alive past its end.
|
|
264
|
+
made.unref()
|
|
265
|
+
listener = made
|
|
266
|
+
unlinkOnExit(opts.socketPath)
|
|
267
|
+
} catch {
|
|
268
|
+
listener = undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
// With the run lock: the next run may take the path the moment the lock
|
|
273
|
+
// is free, so this run stops taking connections and leaves the path
|
|
274
|
+
// first. Followers already attached stay, through `run:done`.
|
|
275
|
+
unlisten: () => {
|
|
276
|
+
if (listener === undefined) return
|
|
277
|
+
listener.stop()
|
|
278
|
+
listener = undefined
|
|
279
|
+
listening.delete(opts.socketPath)
|
|
280
|
+
try {
|
|
281
|
+
unlinkSync(opts.socketPath)
|
|
282
|
+
} catch {
|
|
283
|
+
// Gone already.
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
dispose: () => {
|
|
287
|
+
unsubscribe()
|
|
288
|
+
for (const [sock, peer] of [...peers]) {
|
|
289
|
+
if (peer.pending.length > 0) peer.ending = true
|
|
290
|
+
else drop(sock, peer)
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -126,6 +126,9 @@ export interface RunLockOptions {
|
|
|
126
126
|
signal?: AbortSignal | undefined
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** Releases a taken run lock; `path` is its directory (`runLockPath`). */
|
|
130
|
+
export type RunLockRelease = (() => Promise<void>) & { readonly path: string }
|
|
131
|
+
|
|
129
132
|
/**
|
|
130
133
|
* The lock directory for a workspace root: stable across this user's runs,
|
|
131
134
|
* private to this machine. Keyed by the REAL path: a CLI's cwd comes back
|
|
@@ -323,16 +326,18 @@ async function leave(lockDir: string, entry: string): Promise<void> {
|
|
|
323
326
|
/**
|
|
324
327
|
* Take the workspace's run lock, waiting for a live holder in another
|
|
325
328
|
* process to release it. Resolves to the release function; the caller
|
|
326
|
-
* runs it on every exit path.
|
|
329
|
+
* runs it on every exit path. It carries the lock's `path`, so a run's
|
|
330
|
+
* live socket beside it costs no second realpath and hash.
|
|
327
331
|
*/
|
|
328
332
|
export async function acquireRunLock(
|
|
329
333
|
workspaceRoot: string,
|
|
330
334
|
opts: RunLockOptions,
|
|
331
|
-
): Promise<
|
|
335
|
+
): Promise<RunLockRelease> {
|
|
332
336
|
const lockDir = runLockPath(workspaceRoot, opts.dir)
|
|
333
337
|
// A task's own process (`vx cache clean` in a task) would wait for the
|
|
334
338
|
// run that started it, which holds the lock until that task ends.
|
|
335
|
-
|
|
339
|
+
// Empty is unset: resolved, '' named the cwd.
|
|
340
|
+
const outer = process.env[VX_RUN_WORKSPACE_ENV] || undefined
|
|
336
341
|
if (outer !== undefined && runLockPath(outer, opts.dir) === lockDir) {
|
|
337
342
|
throw new UserError(
|
|
338
343
|
`this vx runs inside task ${process.env[VX_RUN_TASK_ENV] ?? '<unknown>'} of a vx run on this workspace, which holds its run lock until it ends — run it before or after that run, not from one of its tasks`,
|
|
@@ -363,12 +368,13 @@ export async function acquireRunLock(
|
|
|
363
368
|
if (takers.get(lockDir) === taker) takers.delete(lockDir)
|
|
364
369
|
}
|
|
365
370
|
}
|
|
371
|
+
const held: RunLockRelease = Object.assign(release, { path: lockDir })
|
|
366
372
|
for (;;) {
|
|
367
|
-
if (opts.signal?.aborted === true) return async () => {}
|
|
373
|
+
if (opts.signal?.aborted === true) return Object.assign(async () => {}, { path: lockDir })
|
|
368
374
|
const here = heldHere.get(lockDir) ?? 0
|
|
369
375
|
if (here > 0) {
|
|
370
376
|
heldHere.set(lockDir, here + 1)
|
|
371
|
-
return
|
|
377
|
+
return held
|
|
372
378
|
}
|
|
373
379
|
let h: Holder | 'free' | null
|
|
374
380
|
try {
|
|
@@ -379,7 +385,7 @@ export async function acquireRunLock(
|
|
|
379
385
|
heldHere.set(lockDir, 1)
|
|
380
386
|
takers.set(lockDir, entry)
|
|
381
387
|
releaseOnExit()
|
|
382
|
-
return
|
|
388
|
+
return held
|
|
383
389
|
}
|
|
384
390
|
h = await holder(lockDir)
|
|
385
391
|
if (h === 'free') continue
|
|
@@ -402,7 +408,7 @@ export async function acquireRunLock(
|
|
|
402
408
|
opts.log(
|
|
403
409
|
`[vx] no run lock for this workspace (${(err as Error).message}${hint}) — another vx run on it at the same time may race this one`,
|
|
404
410
|
)
|
|
405
|
-
return async () => {}
|
|
411
|
+
return Object.assign(async () => {}, { path: lockDir })
|
|
406
412
|
}
|
|
407
413
|
if (h !== null && !said && Date.now() - started >= SAY_AFTER_MS) {
|
|
408
414
|
said = true
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// tests/run-records.test.ts holds to this one.
|
|
8
8
|
|
|
9
9
|
import type { InvocationRecord, RunRecord } from '../cache/index.js'
|
|
10
|
-
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
10
|
+
import { isGroupTask, taskDefinition, type TaskOutcome } from '../graph/index.js'
|
|
11
11
|
import { VERSION } from '../version.js'
|
|
12
12
|
import type { CiContext, GitContext, HostContext } from './run-context.js'
|
|
13
13
|
import { taskTelemetryOf, type TaskTelemetry } from './telemetry.js'
|
|
@@ -83,18 +83,15 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
83
83
|
if (input.withTelemetry) telemetryTasks.push(taskTelemetryOf(o))
|
|
84
84
|
const blockedBy = o.blockedBy ?? input.tainted?.get(o.node.id)
|
|
85
85
|
const remoteError = remoteErrorOf(input.remoteTraffic, o.hash)
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Built field by field: a spread per optional field was ~2 µs of each
|
|
87
|
+
// of a 1,200-hit warm run's rows.
|
|
88
|
+
const hit = isCacheHit(o.status)
|
|
89
|
+
const r: RunRecord = {
|
|
88
90
|
project: o.node.projectName,
|
|
89
91
|
task: o.node.taskName,
|
|
90
92
|
status: o.status,
|
|
91
93
|
exitCode: o.exitCode,
|
|
92
94
|
durationMs: o.durationMs,
|
|
93
|
-
// A run forwards its args to the tasks it was asked for, never to a
|
|
94
|
-
// dependency: the row says which ran with them.
|
|
95
|
-
...(input.forwardArgs !== undefined && input.forwardArgs.length > 0 && o.node.requested
|
|
96
|
-
? { forwardArgs: input.forwardArgs }
|
|
97
|
-
: {}),
|
|
98
95
|
// Anchor to the REAL per-task wall-clock window: run-start wall time +
|
|
99
96
|
// the task's ns offset (captured for hits and executed tasks alike).
|
|
100
97
|
// The `end - duration` fallback applies to outcomes without an offset —
|
|
@@ -111,28 +108,37 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
111
108
|
? startedAtMs + Math.round(Number(o.wallclockEndNs) / 1e6)
|
|
112
109
|
: endedAtMs,
|
|
113
110
|
runId,
|
|
114
|
-
|
|
115
|
-
...(o.peakRssBytes !== undefined ? { peakRssBytes: o.peakRssBytes } : {}),
|
|
116
|
-
...(o.peakCores !== undefined ? { peakCores: o.peakCores } : {}),
|
|
117
|
-
...(o.avgRssBytes !== undefined ? { avgRssBytes: o.avgRssBytes } : {}),
|
|
118
|
-
...(o.where !== undefined ? { placement: o.where } : {}),
|
|
119
|
-
...(o.whereCores !== undefined ? { placementCores: o.whereCores } : {}),
|
|
120
|
-
...(o.whereMemoryBytes !== undefined ? { placementMemoryBytes: o.whereMemoryBytes } : {}),
|
|
121
|
-
...(o.wallclockStartNs !== undefined ? { wallclockStartNs: o.wallclockStartNs } : {}),
|
|
122
|
-
...(o.wallclockEndNs !== undefined ? { wallclockEndNs: o.wallclockEndNs } : {}),
|
|
123
|
-
cacheHit: isCacheHit(o.status),
|
|
124
|
-
...(isCacheHit(o.status) ? { restored: o.restored === true } : {}),
|
|
125
|
-
...(o.attempts !== undefined ? { attempts: o.attempts } : {}),
|
|
111
|
+
cacheHit: hit,
|
|
126
112
|
cached: o.node.config.cache !== undefined,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
113
|
+
}
|
|
114
|
+
if (o.hash !== undefined) r.hash = o.hash
|
|
115
|
+
// A run forwards its args to the tasks it was asked for, never to a
|
|
116
|
+
// dependency: the row says which ran with them.
|
|
117
|
+
if (input.forwardArgs !== undefined && input.forwardArgs.length > 0 && o.node.requested)
|
|
118
|
+
r.forwardArgs = input.forwardArgs
|
|
119
|
+
if (o.cpuMs !== undefined) r.cpuMs = o.cpuMs
|
|
120
|
+
if (o.peakRssBytes !== undefined) r.peakRssBytes = o.peakRssBytes
|
|
121
|
+
if (o.peakCores !== undefined) r.peakCores = o.peakCores
|
|
122
|
+
if (o.avgRssBytes !== undefined) r.avgRssBytes = o.avgRssBytes
|
|
123
|
+
if (o.where !== undefined) r.placement = o.where
|
|
124
|
+
if (o.whereCores !== undefined) r.placementCores = o.whereCores
|
|
125
|
+
if (o.whereMemoryBytes !== undefined) r.placementMemoryBytes = o.whereMemoryBytes
|
|
126
|
+
if (o.cpuShare !== undefined) r.cpuShare = o.cpuShare
|
|
127
|
+
if (o.wallclockStartNs !== undefined) r.wallclockStartNs = o.wallclockStartNs
|
|
128
|
+
if (o.wallclockEndNs !== undefined) r.wallclockEndNs = o.wallclockEndNs
|
|
129
|
+
if (hit) r.restored = o.restored === true
|
|
130
|
+
if (o.attempts !== undefined) r.attempts = o.attempts
|
|
131
|
+
// A task that ran behind a failure names it too, so the flakiness
|
|
132
|
+
// projection counts its outcome on no key (failure-mode.ts).
|
|
133
|
+
if (blockedBy !== undefined) r.blockedBy = blockedBy
|
|
134
|
+
if (o.timedOut === true) r.timedOut = true
|
|
135
|
+
if (o.sandboxViolations !== undefined) r.sandboxViolations = o.sandboxViolations
|
|
136
|
+
if (o.notReady !== undefined) r.notReady = o.notReady
|
|
137
|
+
if (remoteError !== undefined) r.remoteError = remoteError
|
|
138
|
+
if (o.keylessBy !== undefined) r.keylessBy = o.keylessBy
|
|
139
|
+
const def = taskDefinition(o.node)
|
|
140
|
+
if (def !== undefined) r.def = def
|
|
141
|
+
runs.push(r)
|
|
136
142
|
if (o.status === 'failed') failedCount++
|
|
137
143
|
if (o.status === 'cache-hit') hitLocalCount++
|
|
138
144
|
if (o.status === 'cache-hit-remote') hitRemoteCount++
|