@vzn/vx-dte 0.0.0-stage → 0.0.649
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/LICENSE +21 -0
- package/README.md +924 -2
- package/index.ts +4 -0
- package/package.json +42 -4
- package/src/cpu-account.ts +134 -0
- package/src/dispatch.ts +268 -0
- package/src/docker.ts +156 -0
- package/src/forecast.ts +68 -0
- package/src/github.ts +523 -0
- package/src/gitlab.ts +130 -0
- package/src/index.ts +12 -0
- package/src/init.ts +522 -0
- package/src/local.ts +256 -0
- package/src/messages.ts +219 -0
- package/src/noise.ts +248 -0
- package/src/orchestrator.ts +2359 -0
- package/src/outputs.ts +117 -0
- package/src/planner.ts +988 -0
- package/src/plugin.ts +1020 -0
- package/src/printable.ts +11 -0
- package/src/seal.ts +509 -0
- package/src/strategy.ts +50 -0
- package/src/trace.ts +288 -0
- package/src/tree.ts +72 -0
- package/src/usage.ts +135 -0
- package/src/warm-ups.ts +85 -0
- package/src/wire.ts +1060 -0
- package/src/worker.ts +1038 -0
package/src/local.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// `local()`: workers as processes on this machine, each in its own git
|
|
2
|
+
// worktree of the working tree's tracked state, and a relay of its own on
|
|
3
|
+
// loopback. For tests, demos
|
|
4
|
+
// and trying a distributed run before a CI starts the workers.
|
|
5
|
+
|
|
6
|
+
import { mkdtemp, writeFile } from 'node:fs/promises'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { refuseUnknownOptions, type PluginOptionKinds } from '@vzn/vx'
|
|
10
|
+
import type { DteAdapter } from './plugin.js'
|
|
11
|
+
|
|
12
|
+
export interface LocalOptions {
|
|
13
|
+
/** A shell line each worktree runs before its worker starts, such as `bun install`. */
|
|
14
|
+
readonly prepare?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const KEYS: PluginOptionKinds<LocalOptions> = { prepare: 'string' }
|
|
18
|
+
|
|
19
|
+
export function local(options: LocalOptions = {}): DteAdapter {
|
|
20
|
+
refuseUnknownOptions('local()', options, KEYS)
|
|
21
|
+
let relay: { url: string; stop(): void } | undefined
|
|
22
|
+
let base: Promise<string> | undefined
|
|
23
|
+
let closed = false
|
|
24
|
+
const procs = new Set<Bun.Subprocess>()
|
|
25
|
+
/** A worktree add or a prepare: what close stops at once. */
|
|
26
|
+
const helpers = new Set<Bun.Subprocess>()
|
|
27
|
+
const trees: Array<{ root: string; dir: string }> = []
|
|
28
|
+
const pending = new Set<Promise<unknown>>()
|
|
29
|
+
|
|
30
|
+
// Async throughout: a run starts its workers while it shows its first rows.
|
|
31
|
+
const start = <P extends Bun.Subprocess>(p: P): P => {
|
|
32
|
+
procs.add(p)
|
|
33
|
+
void p.exited.then(() => procs.delete(p))
|
|
34
|
+
return p
|
|
35
|
+
}
|
|
36
|
+
const run = async (
|
|
37
|
+
argv: string[],
|
|
38
|
+
cwd: string,
|
|
39
|
+
env?: Record<string, string | undefined>,
|
|
40
|
+
helper = true,
|
|
41
|
+
// A prepare's reason can be on stdout: pnpm prints its install errors there.
|
|
42
|
+
stdout = false,
|
|
43
|
+
) => {
|
|
44
|
+
const p = start(
|
|
45
|
+
Bun.spawn(argv, {
|
|
46
|
+
cwd,
|
|
47
|
+
env: env ?? process.env,
|
|
48
|
+
stdin: 'ignore',
|
|
49
|
+
stdout: stdout ? 'pipe' : 'ignore',
|
|
50
|
+
stderr: 'pipe',
|
|
51
|
+
// Its own group, which close signals whole: SIGTERM to the shell
|
|
52
|
+
// alone left the install it started writing into a removed tree.
|
|
53
|
+
detached: helper,
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
if (helper) {
|
|
57
|
+
helpers.add(p)
|
|
58
|
+
void p.exited.then(() => helpers.delete(p))
|
|
59
|
+
}
|
|
60
|
+
const chunks: Uint8Array[] = []
|
|
61
|
+
const read = async (from: ReadableStream<Uint8Array>) => {
|
|
62
|
+
for await (const c of from) chunks.push(c)
|
|
63
|
+
}
|
|
64
|
+
const reading = Promise.all([
|
|
65
|
+
read(p.stderr),
|
|
66
|
+
p.stdout instanceof ReadableStream ? read(p.stdout) : undefined,
|
|
67
|
+
])
|
|
68
|
+
const code = await p.exited
|
|
69
|
+
// A grandchild of `sh -c` that outlives it keeps the pipe open; what it
|
|
70
|
+
// said by then is enough.
|
|
71
|
+
await Promise.race([reading, Bun.sleep(200)])
|
|
72
|
+
return { code, err: Buffer.concat(chunks).toString() }
|
|
73
|
+
}
|
|
74
|
+
const why = reasons()
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
// Each run asks for one (`vx watch` runs again): a new run, on the tree as it is now.
|
|
78
|
+
async relay() {
|
|
79
|
+
closed = false
|
|
80
|
+
relay = await loopbackRelay()
|
|
81
|
+
return relay.url
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async spawn(a) {
|
|
85
|
+
const work = (async () => {
|
|
86
|
+
base ??= mkdtemp(path.join(os.tmpdir(), 'vx-dte-'))
|
|
87
|
+
const dir = path.join(await base, String(a.index))
|
|
88
|
+
if (closed) return
|
|
89
|
+
// The run's own snapshot: one taken here, after a hit restored a
|
|
90
|
+
// committed file, was another tree, and every worker was refused
|
|
91
|
+
// (2026-10-25).
|
|
92
|
+
const add = await run(
|
|
93
|
+
['git', 'worktree', 'add', '-q', '--detach', dir, a.commit],
|
|
94
|
+
a.workspaceRoot,
|
|
95
|
+
{ ...process.env, GIT_INDEX_FILE: undefined },
|
|
96
|
+
)
|
|
97
|
+
if (add.code !== 0) throw new Error(`git worktree add: ${errorLine(add.err)}`)
|
|
98
|
+
trees.push({ root: a.workspaceRoot, dir })
|
|
99
|
+
// The workspace's own place in the checkout: from the worktree's
|
|
100
|
+
// root, a workspace in a subdirectory of its repository had no
|
|
101
|
+
// `.vx/workspace` above, and every worker exited (2026-10-25).
|
|
102
|
+
const prefix = Bun.spawn(['git', 'rev-parse', '--show-prefix'], {
|
|
103
|
+
cwd: a.workspaceRoot,
|
|
104
|
+
stdout: 'pipe',
|
|
105
|
+
stderr: 'ignore',
|
|
106
|
+
})
|
|
107
|
+
const at = path.join(dir, (await new Response(prefix.stdout).text()).trim())
|
|
108
|
+
const env: Record<string, string | undefined> = { ...process.env, ...a.env }
|
|
109
|
+
// The worktree is a repository of its own: a hook's index or
|
|
110
|
+
// repository would stand in for it.
|
|
111
|
+
for (const name of GIT_REPO_ENV) delete env[name]
|
|
112
|
+
// A cache dir the workspace names, relative, would name each
|
|
113
|
+
// worktree's own store, where the run cannot read what it saved.
|
|
114
|
+
if (a.cacheDir !== undefined) env['VX_CACHE_DIR'] = a.cacheDir
|
|
115
|
+
if (options.prepare !== undefined && !closed) {
|
|
116
|
+
const p = await run(['sh', '-c', options.prepare], at, env, true, true)
|
|
117
|
+
if (p.code !== 0 && !closed)
|
|
118
|
+
throw new Error(await why(`prepare exited ${p.code}`, a.id, p.err))
|
|
119
|
+
}
|
|
120
|
+
if (closed) return
|
|
121
|
+
void run([...vx(), 'dte', 'worker'], at, env, false).then(async (r) => {
|
|
122
|
+
if (!closed) a.exited(await why(`exited ${r.code}`, a.id, r.err))
|
|
123
|
+
})
|
|
124
|
+
})()
|
|
125
|
+
pending.add(work)
|
|
126
|
+
try {
|
|
127
|
+
await work
|
|
128
|
+
} finally {
|
|
129
|
+
pending.delete(work)
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async close() {
|
|
134
|
+
closed = true
|
|
135
|
+
// The relay closing the run tells each worker to leave; a prepare
|
|
136
|
+
// still running is stopped.
|
|
137
|
+
relay?.stop()
|
|
138
|
+
relay = undefined
|
|
139
|
+
for (const p of helpers) group(p, 'SIGTERM')
|
|
140
|
+
const grace = setTimeout(() => {
|
|
141
|
+
for (const p of procs) {
|
|
142
|
+
if (helpers.has(p)) group(p, 'SIGKILL')
|
|
143
|
+
else p.kill('SIGKILL')
|
|
144
|
+
}
|
|
145
|
+
}, 5_000)
|
|
146
|
+
await Promise.allSettled([...pending])
|
|
147
|
+
await Promise.all([...procs].map((p) => p.exited))
|
|
148
|
+
clearTimeout(grace)
|
|
149
|
+
// A detached shell removes the worktrees after vx exits: removing ones
|
|
150
|
+
// an install filled took seconds after a Ctrl-C (2026-10-21), and a
|
|
151
|
+
// 300k-file tree 2.7 s. `prune` then drops git's record of each.
|
|
152
|
+
const roots = [...new Set(trees.splice(0).map((t) => t.root))]
|
|
153
|
+
if (base !== undefined) {
|
|
154
|
+
const dir = await base
|
|
155
|
+
base = undefined
|
|
156
|
+
Bun.spawn(
|
|
157
|
+
[
|
|
158
|
+
'sh',
|
|
159
|
+
'-c',
|
|
160
|
+
'rm -rf "$1"; shift; for r; do git -C "$r" worktree prune; done',
|
|
161
|
+
'sh',
|
|
162
|
+
dir,
|
|
163
|
+
...roots,
|
|
164
|
+
],
|
|
165
|
+
{ stdio: ['ignore', 'ignore', 'ignore'], detached: true },
|
|
166
|
+
).unref()
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Signal `p`'s process group: a helper leads its own (`detached`). */
|
|
173
|
+
function group(p: Bun.Subprocess, signal: NodeJS.Signals): void {
|
|
174
|
+
try {
|
|
175
|
+
process.kill(-p.pid, signal)
|
|
176
|
+
} catch {
|
|
177
|
+
// quiet: the group is gone
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `<what>: <its error line> (log: <path>)`, a worker's whole stderr kept at
|
|
183
|
+
* that path, which outlives the run.
|
|
184
|
+
*/
|
|
185
|
+
export function reasons(): (what: string, id: string, err: string) => Promise<string> {
|
|
186
|
+
let logs: Promise<string> | undefined
|
|
187
|
+
return async (what, id, err) => {
|
|
188
|
+
const line = errorLine(err)
|
|
189
|
+
if (line === '') return what
|
|
190
|
+
logs ??= mkdtemp(path.join(os.tmpdir(), 'vx-dte-logs-'))
|
|
191
|
+
const file = path.join(await logs, `${id}.log`)
|
|
192
|
+
await writeFile(file, err)
|
|
193
|
+
return `${what}: ${line} (log: ${file})`
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The line of a process's stderr that says what went wrong: vx's own `✗`
|
|
199
|
+
* line, else the first naming an error or git's `fatal:`, else the last. The last alone was
|
|
200
|
+
* often a hint (`fix vx run …`) that hid the error above it.
|
|
201
|
+
*/
|
|
202
|
+
export function errorLine(text: string): string {
|
|
203
|
+
const lines = Bun.stripANSI(text)
|
|
204
|
+
.split('\n')
|
|
205
|
+
.map((l) => l.trim())
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
return (
|
|
208
|
+
lines.find((l) => l.startsWith('✗')) ??
|
|
209
|
+
lines.find((l) => /\berror\b|^fatal:/i.test(l)) ??
|
|
210
|
+
lines.at(-1) ??
|
|
211
|
+
''
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A relay of the run's own on loopback, with no auth: only this machine reaches it. */
|
|
216
|
+
export async function loopbackRelay(): Promise<{ url: string; stop(): void }> {
|
|
217
|
+
const { serveRelay } = await import('@vzn/vx-dte-server')
|
|
218
|
+
const server = serveRelay({ hostname: '127.0.0.1', port: 0 })
|
|
219
|
+
return {
|
|
220
|
+
url: server.url,
|
|
221
|
+
stop() {
|
|
222
|
+
void server.stop()
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* What `git rev-parse --local-env-vars` names: variables that tie a
|
|
229
|
+
* process to one repository and its index. A git hook sets
|
|
230
|
+
* `GIT_INDEX_FILE` (and, in a linked worktree, `GIT_DIR`); a worktree add
|
|
231
|
+
* under them wrote its checkout to the hook's index, or failed on a
|
|
232
|
+
* relative one, and a worker read the hook's repository.
|
|
233
|
+
*/
|
|
234
|
+
const GIT_REPO_ENV = [
|
|
235
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
236
|
+
'GIT_CONFIG',
|
|
237
|
+
'GIT_CONFIG_PARAMETERS',
|
|
238
|
+
'GIT_CONFIG_COUNT',
|
|
239
|
+
'GIT_OBJECT_DIRECTORY',
|
|
240
|
+
'GIT_DIR',
|
|
241
|
+
'GIT_WORK_TREE',
|
|
242
|
+
'GIT_IMPLICIT_WORK_TREE',
|
|
243
|
+
'GIT_GRAFT_FILE',
|
|
244
|
+
'GIT_INDEX_FILE',
|
|
245
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
246
|
+
'GIT_REPLACE_REF_BASE',
|
|
247
|
+
'GIT_PREFIX',
|
|
248
|
+
'GIT_SHALLOW_FILE',
|
|
249
|
+
'GIT_COMMON_DIR',
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
/** The argv that runs this vx: the binary when compiled, else Bun and its entry. */
|
|
253
|
+
function vx(): string[] {
|
|
254
|
+
const compiled = Bun.main.startsWith('/$bunfs/') || Bun.main.startsWith('B:/~BUN/')
|
|
255
|
+
return compiled ? [process.execPath] : [process.execPath, Bun.main]
|
|
256
|
+
}
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// What the orchestrator and its workers say to each other through the relay
|
|
2
|
+
// (the relay never reads it): orders and status only. Every task's files
|
|
3
|
+
// and output go through the workspace's cache, by the keys these name.
|
|
4
|
+
|
|
5
|
+
import type { CacheFacts, CapturedChunk, ExecuteResult, SandboxConfig } from '@vzn/vx'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A worker says what it holds every this many heartbeats, and a task's
|
|
9
|
+
* lease is three of them. Every heartbeat (2026-10-25: the fastest, not
|
|
10
|
+
* a free plan's message count): a task its worker dropped while alive
|
|
11
|
+
* goes back in 15 s, not 90.
|
|
12
|
+
*/
|
|
13
|
+
export const HELD_EVERY = 1
|
|
14
|
+
|
|
15
|
+
/** A worker's `join` info. */
|
|
16
|
+
export interface WorkerInfo {
|
|
17
|
+
readonly v: 1
|
|
18
|
+
/** `WorkerSpawn.id` (`VX_DTE_WORKER`), for a worker the run started. */
|
|
19
|
+
readonly id?: string
|
|
20
|
+
/** What its machine has (core's `machineParallelism`, `machineMemoryBytes`), not what its size says. */
|
|
21
|
+
readonly cores: number
|
|
22
|
+
readonly memory: number
|
|
23
|
+
/** Its machine's platform, `os-arch` (`linux-x64`): a task naming `exec.platforms` goes only to a worker of its platform. */
|
|
24
|
+
readonly platform?: string
|
|
25
|
+
/** Its checkout's tree: a worker with other tracked files than the run is refused. */
|
|
26
|
+
readonly tree: string
|
|
27
|
+
/** When its `vx` started, wall-clock ms: the run's trace splits its warm-up there. */
|
|
28
|
+
readonly booted?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A task whose files the worker restores before it runs one that needs them. */
|
|
32
|
+
export interface DepOrder {
|
|
33
|
+
task: string
|
|
34
|
+
/** Where its entry is: its cache key, or the run key it was saved under. */
|
|
35
|
+
key: string
|
|
36
|
+
/** Workspace-relative. */
|
|
37
|
+
project: string
|
|
38
|
+
outputs: { files: string[]; workspaceFiles: string[] }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One task a worker is sent. */
|
|
42
|
+
export interface RunOrder {
|
|
43
|
+
task: string
|
|
44
|
+
/** Its cache key, which a success with no violation is saved under; absent, uncached. */
|
|
45
|
+
key?: string
|
|
46
|
+
/** What anything else is saved under: no hit ever serves it. */
|
|
47
|
+
runKey: string
|
|
48
|
+
/**
|
|
49
|
+
* The run reads no cache (`--force`): a success is saved under `key`, which
|
|
50
|
+
* keeps an entry it holds, and under `runKey`, whose fresh bytes this run's
|
|
51
|
+
* machines restore.
|
|
52
|
+
*/
|
|
53
|
+
fresh?: true
|
|
54
|
+
/** The run's probe found `key` in no cache: its upload asks the remote nothing first. */
|
|
55
|
+
absent?: true
|
|
56
|
+
/** The tasks it depends on, transitively, whose files the worker does not hold yet. */
|
|
57
|
+
deps: DepOrder[]
|
|
58
|
+
/** Workspace-relative projects inside `project`, whose files are not its outputs. */
|
|
59
|
+
nested: string[]
|
|
60
|
+
command: string
|
|
61
|
+
/** Workspace-relative, as `project` is. */
|
|
62
|
+
cwd: string
|
|
63
|
+
project: string
|
|
64
|
+
env: Record<string, string>
|
|
65
|
+
/** Its `exec.env.secret`: the names in `env` whose values its saved output masks. */
|
|
66
|
+
secret?: string[]
|
|
67
|
+
timeoutMs?: number
|
|
68
|
+
outputs: { files: string[]; workspaceFiles: string[] }
|
|
69
|
+
/** Its recorded peak: the worker starts it when that much is free. */
|
|
70
|
+
cores: number
|
|
71
|
+
memory: number
|
|
72
|
+
/**
|
|
73
|
+
* Its `exec.sandbox`, and for a cached task the projects its key answers
|
|
74
|
+
* for (workspace-relative): the worker builds the request on its own
|
|
75
|
+
* paths and OS (`sandboxRequestFor`), as its orchestrator may run another.
|
|
76
|
+
*/
|
|
77
|
+
sandbox?: { config: SandboxConfig; keyed?: string[] }
|
|
78
|
+
/** Its persistent dependencies (`ServerSpec`), `cwd` workspace-relative: the worker starts each once. */
|
|
79
|
+
servers?: ServerOrder[]
|
|
80
|
+
/** The runtime probes its key includes, as the orchestrator answered them: the worker's must answer the same. */
|
|
81
|
+
runtime?: RuntimeOrder[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RuntimeOrder {
|
|
85
|
+
command: string
|
|
86
|
+
output: string
|
|
87
|
+
/** A `workspaceRuntime` probe, run at the root; else in `project`. */
|
|
88
|
+
workspace?: true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ServerOrder {
|
|
92
|
+
task: string
|
|
93
|
+
command: string
|
|
94
|
+
cwd: string
|
|
95
|
+
env: Record<string, string>
|
|
96
|
+
readyWhen?: string
|
|
97
|
+
timeoutMs?: number
|
|
98
|
+
/** Its `exec.sandbox` and the projects nested in its own, workspace-relative: it starts inside it. */
|
|
99
|
+
sandbox?: { config: SandboxConfig; nested: string[] }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ToWorker =
|
|
103
|
+
/** Tasks that fit what it has free, each begun at once; `root` is swapped for the worker's in `env`. */
|
|
104
|
+
| {
|
|
105
|
+
type: 'batch'
|
|
106
|
+
root: string
|
|
107
|
+
/** `root`'s real path, as sandbox paths spell it. */
|
|
108
|
+
realRoot: string
|
|
109
|
+
tasks: RunOrder[]
|
|
110
|
+
/** The run's sandboxed tasks' `exec.sandbox`, in a worker's first batch that has a sandboxed task. */
|
|
111
|
+
sandboxes?: SandboxConfig[]
|
|
112
|
+
}
|
|
113
|
+
/** Part of an order's `deps`, ahead of the batch whose order was too long for one frame. */
|
|
114
|
+
| { type: 'deps'; task: string; deps: DepOrder[] }
|
|
115
|
+
| { type: 'cancel'; task: string }
|
|
116
|
+
/**
|
|
117
|
+
* It was taken into the run, under the name the run's output gives it,
|
|
118
|
+
* and reads the cache as the run does: its refs, and the access the
|
|
119
|
+
* run's remotes lend, for one with none of its own.
|
|
120
|
+
*/
|
|
121
|
+
| {
|
|
122
|
+
type: 'welcome'
|
|
123
|
+
name: string
|
|
124
|
+
cache: CacheFacts
|
|
125
|
+
}
|
|
126
|
+
/** The run's remotes lent again, before the last tokens end. */
|
|
127
|
+
| { type: 'cache'; cache: CacheFacts }
|
|
128
|
+
| { type: 'refused'; reason: string }
|
|
129
|
+
/** The run needs it no more: it leaves. */
|
|
130
|
+
| { type: 'exit' }
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What its machine uses now: cores busy over the last second and bytes of
|
|
134
|
+
* memory in use. Each frame a worker sends carries it, so the run places
|
|
135
|
+
* by what is free, not only by what history booked (2026-10-23: "you
|
|
136
|
+
* watch instruments and play most efficient as they go").
|
|
137
|
+
*/
|
|
138
|
+
export interface Load {
|
|
139
|
+
readonly cpu: number
|
|
140
|
+
readonly memory: number
|
|
141
|
+
/** Cores its running tasks' process trees used over the last look: the rest of `cpu` is no task's. */
|
|
142
|
+
readonly tasks?: number
|
|
143
|
+
/**
|
|
144
|
+
* Cores busy on average since the worker started: the summary's
|
|
145
|
+
* `machine avg`. A worker sends few frames, so the run cannot average
|
|
146
|
+
* `cpu` (one that sent 3, each as a task started, read 0.6 of 2.0 used).
|
|
147
|
+
*/
|
|
148
|
+
readonly avg?: number
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type ToOrchestrator =
|
|
152
|
+
/** It began the task. */
|
|
153
|
+
| { type: 'start'; task: string; load?: Load }
|
|
154
|
+
/** Its command is done; its outputs save now, its cores are free. */
|
|
155
|
+
| { type: 'ran'; task: string; load?: Load }
|
|
156
|
+
/** Each heartbeat while it runs any: what it runs, which renews its lease on each. */
|
|
157
|
+
| { type: 'held'; running: string[]; load?: Load }
|
|
158
|
+
/** It could not run a task it began, not a non-zero exit: a full disk, a failed unpack. */
|
|
159
|
+
| { type: 'fault'; task: string; reason: string }
|
|
160
|
+
/** It ran the task and saved it under `key`; its output is in that entry. */
|
|
161
|
+
| { type: 'end'; task: string; key: string; result: WiredResult; load?: Load }
|
|
162
|
+
/**
|
|
163
|
+
* Ahead of an `end` too long for one relay frame, some of its lists:
|
|
164
|
+
* the `end` that follows adds its own to them. One `end` with a sandboxed
|
|
165
|
+
* task's 9000 violations was over the relay's 1 MiB, and the run waited
|
|
166
|
+
* for it (2026-10-25).
|
|
167
|
+
*/
|
|
168
|
+
| ({ type: 'part'; task: string } & Partial<Pick<WiredResult, PartList>>)
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A message's JSON in UTF-8 bytes, what a relay frame holds: counted in
|
|
172
|
+
* UTF-16 units, a part of CJK output sealed past the relay's 1 MiB and its
|
|
173
|
+
* result never arrived (2026-10-25).
|
|
174
|
+
*/
|
|
175
|
+
export const bytesOf = (body: unknown): number => Buffer.byteLength(JSON.stringify(body))
|
|
176
|
+
|
|
177
|
+
/** The lists a long result sends ahead of its `end`, in parts. */
|
|
178
|
+
export const PART_LISTS = ['violations', 'changed', 'removed', 'output'] as const
|
|
179
|
+
export type PartList = (typeof PART_LISTS)[number]
|
|
180
|
+
|
|
181
|
+
/** A result as JSON carries it: its output stays in the cache entry, and a violation's time is a string. */
|
|
182
|
+
export type WiredResult = Omit<ExecuteResult, 'violations' | 'outputs' | 'stdout' | 'stderr'> & {
|
|
183
|
+
violations?: (Omit<ExecuteResult['violations'][number], 'timestamp'> & { timestamp: string })[]
|
|
184
|
+
/** The share of a core each runnable thread got on the worker while it ran (`ExecuteResult.machine.cpuShare`). */
|
|
185
|
+
cpuShare?: number
|
|
186
|
+
/** Workspace-relative upstream outputs the task removed: this machine removes them too. */
|
|
187
|
+
removed?: string[]
|
|
188
|
+
/** Dependency keys whose remote copy did not read back: this machine sends its own over them. */
|
|
189
|
+
unread?: string[]
|
|
190
|
+
/**
|
|
191
|
+
* No remote took its entry's upload (too big, a 429): its files are on
|
|
192
|
+
* that worker alone, and its output comes here in `output`.
|
|
193
|
+
*/
|
|
194
|
+
unshared?: true
|
|
195
|
+
/** Its output, both streams in the order written, when `unshared`. */
|
|
196
|
+
output?: CapturedChunk[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Names whose value is the worker's own: the orchestrator sends one only
|
|
201
|
+
* when a task's `env.define` sets it, a value its key includes.
|
|
202
|
+
*/
|
|
203
|
+
export const HOST_ENV = [
|
|
204
|
+
'HOME',
|
|
205
|
+
'SHELL',
|
|
206
|
+
'USER',
|
|
207
|
+
'LOGNAME',
|
|
208
|
+
'TMPDIR',
|
|
209
|
+
'TEMP',
|
|
210
|
+
'TMP',
|
|
211
|
+
'npm_execpath',
|
|
212
|
+
'PNPM_HOME',
|
|
213
|
+
'COREPACK_HOME',
|
|
214
|
+
'XDG_CONFIG_HOME',
|
|
215
|
+
'XDG_DATA_HOME',
|
|
216
|
+
'XDG_CACHE_HOME',
|
|
217
|
+
'XDG_STATE_HOME',
|
|
218
|
+
'XDG_RUNTIME_DIR',
|
|
219
|
+
]
|