@vzn/vx 0.0.86 → 0.0.87
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.
|
|
3
|
+
"version": "0.0.87",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.87",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.87",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.87",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.87"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/exec/executor.ts
CHANGED
|
@@ -142,9 +142,11 @@ export interface ExecuteRequest {
|
|
|
142
142
|
/**
|
|
143
143
|
* Aborted when the run stops (Ctrl-C, SIGTERM, SIGHUP or an embedder's
|
|
144
144
|
* `RunOptions.signal`) or when `timeoutMs` elapses. An executor ends its
|
|
145
|
-
* work and returns
|
|
146
|
-
*
|
|
147
|
-
*
|
|
145
|
+
* work and returns: a child an executor spawned is not one core can
|
|
146
|
+
* reach (H-10, H-12). A non-zero exit after the timeout's abort is
|
|
147
|
+
* recorded `timedOut`. Core waits the kill grace (`VX_KILL_GRACE_MS`,
|
|
148
|
+
* 2 s) after the abort, then settles the attempt without `execute` and
|
|
149
|
+
* abandons it (H-14).
|
|
148
150
|
*/
|
|
149
151
|
readonly signal?: AbortSignal
|
|
150
152
|
/** See `RunOptions.liveChildren`: the run's SIGINT/SIGTERM registry. */
|
package/src/exec/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ export {
|
|
|
30
30
|
export { type DeniedCall } from './sandbox-violations.js'
|
|
31
31
|
export { isMountableLiteral, MOUNT_WILDCARDS, toRealPath } from './sandbox-paths.js'
|
|
32
32
|
export { bindableWrites, punchWalls } from './sandbox-binds.js'
|
|
33
|
-
export { localExecutor } from './local-executor.js'
|
|
33
|
+
export { isLocalExecutor, localExecutor } from './local-executor.js'
|
|
34
34
|
export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
|
|
35
35
|
export {
|
|
36
36
|
assertExecuteResult,
|
|
@@ -8,9 +8,20 @@ import { runCommand } from './runner.js'
|
|
|
8
8
|
import { runSandboxed } from './sandbox-runtime.js'
|
|
9
9
|
import type { TaskExecutor } from './executor.js'
|
|
10
10
|
|
|
11
|
+
const locals = new WeakSet<TaskExecutor>()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether `executor` is core's own. By identity: a plugin may name its
|
|
15
|
+
* executor 'local', and core bounds a plugin's `execute` after an abort,
|
|
16
|
+
* which the local one (it SIGKILLs its own group) never needs.
|
|
17
|
+
*/
|
|
18
|
+
export function isLocalExecutor(executor: TaskExecutor): boolean {
|
|
19
|
+
return locals.has(executor)
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/** The local executor. Accepts every task. */
|
|
12
23
|
export function localExecutor(): TaskExecutor {
|
|
13
|
-
|
|
24
|
+
const executor: TaskExecutor = {
|
|
14
25
|
name: 'local',
|
|
15
26
|
async execute(req) {
|
|
16
27
|
const common = {
|
|
@@ -38,4 +49,6 @@ export function localExecutor(): TaskExecutor {
|
|
|
38
49
|
})
|
|
39
50
|
},
|
|
40
51
|
}
|
|
52
|
+
locals.add(executor)
|
|
53
|
+
return executor
|
|
41
54
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
releaseBridges,
|
|
23
23
|
wrapSandboxedCommand,
|
|
24
24
|
signalExitCode,
|
|
25
|
+
isLocalExecutor,
|
|
25
26
|
type CaptureConfig,
|
|
26
27
|
type ExecuteRequest,
|
|
27
28
|
assertExecuteResult,
|
|
@@ -32,7 +33,9 @@ import {
|
|
|
32
33
|
PersistentReadyError,
|
|
33
34
|
} from '../exec/index.js'
|
|
34
35
|
import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
35
|
-
import { relPosix, span } from '../util/index.js'
|
|
36
|
+
import { killGraceMs, relPosix, span } from '../util/index.js'
|
|
37
|
+
import { SIGNAL_SHUTDOWN_GRACE_MS } from './signals.js'
|
|
38
|
+
import { executorLabel } from './plugin-host.js'
|
|
36
39
|
import {
|
|
37
40
|
mayWriteFingerprint,
|
|
38
41
|
type Placeholder,
|
|
@@ -493,6 +496,49 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
493
496
|
}
|
|
494
497
|
return stop.signal
|
|
495
498
|
}
|
|
499
|
+
// An executor that ignores `signal` held its task, and so the run, past
|
|
500
|
+
// the stop and past `exec.timeout` (H-14). Once the signal aborts it has
|
|
501
|
+
// the kill grace the local executor gives a process group; then core
|
|
502
|
+
// settles the attempt without it and says so. Its work is abandoned: core
|
|
503
|
+
// cannot reach what it started.
|
|
504
|
+
function boundAfterAbort(
|
|
505
|
+
running: Promise<unknown>,
|
|
506
|
+
signal: AbortSignal | undefined,
|
|
507
|
+
): Promise<unknown> {
|
|
508
|
+
if (signal === undefined || isLocalExecutor(args.executor)) return running
|
|
509
|
+
const started = Date.now()
|
|
510
|
+
let graceTimer: ReturnType<typeof setTimeout> | undefined
|
|
511
|
+
let onAbort: (() => void) | undefined
|
|
512
|
+
const abandoned = new Promise<ExecuteResult>((resolve) => {
|
|
513
|
+
onAbort = (): void => {
|
|
514
|
+
const graceMs = killGraceMs(SIGNAL_SHUTDOWN_GRACE_MS)
|
|
515
|
+
graceTimer = setTimeout(() => {
|
|
516
|
+
const why = timeoutFired ? `timeout (${effectiveTimeout}ms)` : 'stop'
|
|
517
|
+
log.taskStderr(
|
|
518
|
+
node,
|
|
519
|
+
`vx: ${executorLabel(args.executor)} did not return within ${graceMs}ms of the ${why}; abandoned\n`,
|
|
520
|
+
)
|
|
521
|
+
const reason: unknown = signal.reason
|
|
522
|
+
resolve({
|
|
523
|
+
exitCode: signalExitCode(reason === 'SIGINT' ? 'SIGINT' : 'SIGTERM'),
|
|
524
|
+
durationMs: Date.now() - started,
|
|
525
|
+
stdout: '',
|
|
526
|
+
stderr: '',
|
|
527
|
+
violations: [],
|
|
528
|
+
...(timeoutFired ? { timedOut: true } : {}),
|
|
529
|
+
})
|
|
530
|
+
}, graceMs)
|
|
531
|
+
}
|
|
532
|
+
if (signal.aborted) onAbort()
|
|
533
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
534
|
+
})
|
|
535
|
+
// The abandoned call may still settle, or reject, later: nobody awaits it.
|
|
536
|
+
running.catch(() => {})
|
|
537
|
+
return Promise.race([running, abandoned]).finally(() => {
|
|
538
|
+
clearTimeout(graceTimer)
|
|
539
|
+
if (onAbort !== undefined) signal.removeEventListener('abort', onAbort)
|
|
540
|
+
})
|
|
541
|
+
}
|
|
496
542
|
|
|
497
543
|
// When the task started, as a ns offset from run start — captured for
|
|
498
544
|
// EVERY outcome (hits included) so the run-detail timeline reflects when
|
|
@@ -722,8 +768,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
722
768
|
// no other trace. Rethrown unchanged — the scheduler still classifies it,
|
|
723
769
|
// and still prints it plainly for a UserError.
|
|
724
770
|
const endExec = span('miss: execute')
|
|
725
|
-
let res = await args.executor
|
|
726
|
-
.execute(req)
|
|
771
|
+
let res = await boundAfterAbort(args.executor.execute(req), req.signal)
|
|
727
772
|
.then((r: unknown) => {
|
|
728
773
|
assertExecuteResult(args.executor.name, node.id, r)
|
|
729
774
|
return r
|
|
@@ -199,7 +199,6 @@ function restoreTierExclusions(nodes: Map<string, TaskNode>, workspaceRoot: stri
|
|
|
199
199
|
if (prefixes.length === 0) return out
|
|
200
200
|
const reaches = (dir: string): boolean =>
|
|
201
201
|
dir === '' ||
|
|
202
|
-
dir === '.' ||
|
|
203
202
|
prefixes.some((p) => p === dir || p.startsWith(`${dir}/`) || dir.startsWith(`${p}/`))
|
|
204
203
|
const dependants = new Map<string, string[]>()
|
|
205
204
|
for (const node of nodes.values()) {
|
|
@@ -241,58 +241,24 @@ export function getInvocation(db: Database, runId: string): InvocationDetail | n
|
|
|
241
241
|
|
|
242
242
|
export interface ListInvocationsArgs {
|
|
243
243
|
limit?: number
|
|
244
|
-
branch?: string
|
|
245
|
-
ci?: boolean
|
|
246
|
-
tagKey?: string
|
|
247
|
-
tagValue?: string
|
|
248
244
|
}
|
|
249
245
|
|
|
250
246
|
/**
|
|
251
|
-
* List `vx run` invocations newest-first from the `invocations` header table
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* survives. Accepts a bare `number` for the limit (back-compat with the old
|
|
255
|
-
* `listInvocations(db, 50)` signature).
|
|
247
|
+
* List `vx run` invocations newest-first from the `invocations` header table.
|
|
248
|
+
* Reading the dedicated header table (vs the old `GROUP BY run_id` over
|
|
249
|
+
* `runs`) is lossless — git/ci/tag context survives.
|
|
256
250
|
*/
|
|
257
|
-
export function listInvocations(
|
|
258
|
-
|
|
259
|
-
args: ListInvocationsArgs | number = {},
|
|
260
|
-
): InvocationDetail[] {
|
|
261
|
-
const opts: ListInvocationsArgs = typeof args === 'number' ? { limit: args } : args
|
|
262
|
-
const limit = clampInt(opts.limit ?? 50, 1, 500)
|
|
263
|
-
const where: string[] = []
|
|
264
|
-
const params: (string | number)[] = []
|
|
265
|
-
if (opts.branch !== undefined) {
|
|
266
|
-
where.push('branch = ?')
|
|
267
|
-
params.push(opts.branch)
|
|
268
|
-
}
|
|
269
|
-
if (opts.ci !== undefined) {
|
|
270
|
-
where.push('ci = ?')
|
|
271
|
-
params.push(opts.ci ? 1 : 0)
|
|
272
|
-
}
|
|
273
|
-
if (opts.tagKey !== undefined && opts.tagValue !== undefined) {
|
|
274
|
-
// The tags column is a JSON object {"k":"v"}; a LIKE over the serialized
|
|
275
|
-
// pair is adequate at this table's scale (see the design doc).
|
|
276
|
-
where.push('tags LIKE ?')
|
|
277
|
-
params.push(`%${jsonPairFragment(opts.tagKey, opts.tagValue)}%`)
|
|
278
|
-
}
|
|
279
|
-
const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''
|
|
251
|
+
export function listInvocations(db: Database, args: ListInvocationsArgs = {}): InvocationDetail[] {
|
|
252
|
+
const limit = clampInt(args.limit ?? 50, 1, 500)
|
|
280
253
|
const rows = db
|
|
281
254
|
.query(
|
|
282
|
-
`SELECT ${INVOCATION_COLUMNS} FROM invocations
|
|
255
|
+
`SELECT ${INVOCATION_COLUMNS} FROM invocations
|
|
283
256
|
ORDER BY rowid DESC LIMIT ?`,
|
|
284
257
|
)
|
|
285
|
-
.all(
|
|
258
|
+
.all(limit) as InvocationRawRow[]
|
|
286
259
|
return rows.map(mapInvocation)
|
|
287
260
|
}
|
|
288
261
|
|
|
289
|
-
/** The `"key":"value"` fragment as it appears in `JSON.stringify({k:v})`. */
|
|
290
|
-
function jsonPairFragment(key: string, value: string): string {
|
|
291
|
-
const k = JSON.stringify(key).slice(1, -1)
|
|
292
|
-
const v = JSON.stringify(value).slice(1, -1)
|
|
293
|
-
return `"${k}":"${v}"`
|
|
294
|
-
}
|
|
295
|
-
|
|
296
262
|
export interface RunDetail {
|
|
297
263
|
runId: string
|
|
298
264
|
startedAt: number
|