@llblab/pi-actors 0.41.1 → 0.42.1
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/AGENTS.md +11 -6
- package/BACKLOG.md +1 -65
- package/CHANGELOG.md +25 -0
- package/README.md +24 -4
- package/dist/index.js +25 -121
- package/dist/lib/async-runs.d.ts +37 -5
- package/dist/lib/async-runs.js +181 -51
- package/dist/lib/automatic-review-runtime.d.ts +18 -0
- package/dist/lib/automatic-review-runtime.js +96 -0
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
- package/dist/lib/draft-consolidation-transaction.js +610 -0
- package/dist/lib/draft-consolidation.d.ts +35 -0
- package/dist/lib/draft-consolidation.js +126 -0
- package/dist/lib/draft-review.d.ts +56 -0
- package/dist/lib/draft-review.js +254 -0
- package/dist/lib/draft-sleep.d.ts +65 -0
- package/dist/lib/draft-sleep.js +468 -0
- package/dist/lib/file-state.d.ts +8 -1
- package/dist/lib/file-state.js +115 -18
- package/dist/lib/inspector-actions.d.ts +16 -0
- package/dist/lib/inspector-actions.js +57 -0
- package/dist/lib/inspector-command.d.ts +7 -0
- package/dist/lib/inspector-command.js +37 -0
- package/dist/lib/inspector-overlay.d.ts +23 -3
- package/dist/lib/inspector-overlay.js +327 -70
- package/dist/lib/inspector.d.ts +9 -0
- package/dist/lib/inspector.js +49 -1
- package/dist/lib/observability.d.ts +13 -0
- package/dist/lib/observability.js +160 -6
- package/dist/lib/paths.d.ts +7 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/recipes-discovery.js +32 -24
- package/dist/lib/recipes-usage.d.ts +18 -6
- package/dist/lib/recipes-usage.js +445 -34
- package/dist/lib/review-control.d.ts +14 -0
- package/dist/lib/review-control.js +111 -0
- package/dist/lib/review-diagnostics.d.ts +11 -0
- package/dist/lib/review-diagnostics.js +148 -0
- package/dist/lib/review-projection.d.ts +14 -0
- package/dist/lib/review-projection.js +170 -0
- package/dist/lib/run-ui-runtime.d.ts +18 -0
- package/dist/lib/run-ui-runtime.js +123 -0
- package/dist/lib/runs-artifacts.d.ts +1 -1
- package/dist/lib/runs-artifacts.js +1 -1
- package/dist/lib/runs-control.d.ts +10 -2
- package/dist/lib/runs-control.js +37 -7
- package/dist/lib/runs-identity.d.ts +1 -1
- package/dist/lib/runs-identity.js +1 -1
- package/dist/lib/runs-index.d.ts +11 -2
- package/dist/lib/runs-index.js +46 -23
- package/dist/lib/runs-mailbox.d.ts +1 -1
- package/dist/lib/runs-mailbox.js +1 -1
- package/dist/lib/runs-messages.d.ts +1 -1
- package/dist/lib/runs-messages.js +1 -1
- package/dist/lib/runs-outbox.d.ts +1 -1
- package/dist/lib/runs-outbox.js +1 -1
- package/dist/lib/runs-ownership.d.ts +1 -1
- package/dist/lib/runs-ownership.js +18 -4
- package/dist/lib/runs-parent-teardown.d.ts +51 -0
- package/dist/lib/runs-parent-teardown.js +172 -0
- package/dist/lib/runs-process.d.ts +1 -1
- package/dist/lib/runs-process.js +5 -4
- package/dist/lib/runs-retention.d.ts +1 -1
- package/dist/lib/runs-retention.js +1 -1
- package/dist/lib/runs-start.d.ts +5 -3
- package/dist/lib/runs-start.js +7 -48
- package/dist/lib/runs-status.d.ts +5 -3
- package/dist/lib/runs-status.js +7 -6
- package/dist/lib/runtime.d.ts +7 -1
- package/dist/lib/runtime.js +32 -18
- package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
- package/dist/lib/tool-review-lineage-transaction.js +597 -0
- package/dist/lib/tool-review-lineage.d.ts +24 -0
- package/dist/lib/tool-review-lineage.js +98 -0
- package/dist/lib/tool-review-scheduler.d.ts +80 -0
- package/dist/lib/tool-review-scheduler.js +494 -0
- package/dist/lib/tool-review-transaction.d.ts +50 -0
- package/dist/lib/tool-review-transaction.js +362 -0
- package/dist/lib/tool-review.d.ts +56 -0
- package/dist/lib/tool-review.js +197 -0
- package/dist/lib/tools-inspect.js +28 -4
- package/dist/lib/tools-local.js +21 -4
- package/dist/lib/tools-message.d.ts +1 -0
- package/dist/lib/tools-message.js +29 -17
- package/dist/lib/tools-spawn.js +14 -2
- package/dist/lib/tools.d.ts +1 -0
- package/dist/lib/tools.js +1 -0
- package/dist/recipes/draft-review.json +24 -0
- package/dist/recipes/tool-review.json +24 -0
- package/dist/scripts/async-runner.mjs +9 -9
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/scripts/release-gates.mjs +165 -0
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +20 -11
- package/docs/async-runs.md +16 -2
- package/docs/recipe-library.md +7 -3
- package/docs/template-recipes.md +4 -11
- package/docs/tool-registry.md +13 -4
- package/index.ts +27 -142
- package/lib/async-runs.ts +283 -67
- package/lib/automatic-review-runtime.ts +135 -0
- package/lib/command-templates.ts +1 -1
- package/lib/draft-consolidation-transaction.ts +821 -0
- package/lib/draft-consolidation.ts +181 -0
- package/lib/draft-review.ts +325 -0
- package/lib/draft-sleep.ts +576 -0
- package/lib/file-state.ts +143 -19
- package/lib/inspector-actions.ts +79 -0
- package/lib/inspector-command.ts +54 -0
- package/lib/inspector-overlay.ts +377 -91
- package/lib/inspector.ts +73 -1
- package/lib/observability.ts +194 -5
- package/lib/paths.ts +43 -0
- package/lib/recipes-discovery.ts +34 -26
- package/lib/recipes-usage.ts +569 -40
- package/lib/review-control.ts +137 -0
- package/lib/review-diagnostics.ts +164 -0
- package/lib/review-projection.ts +200 -0
- package/lib/run-ui-runtime.ts +153 -0
- package/lib/runs-artifacts.ts +1 -1
- package/lib/runs-control.ts +69 -6
- package/lib/runs-identity.ts +1 -1
- package/lib/runs-index.ts +57 -21
- package/lib/runs-mailbox.ts +1 -1
- package/lib/runs-messages.ts +1 -1
- package/lib/runs-outbox.ts +1 -1
- package/lib/runs-ownership.ts +23 -4
- package/lib/runs-parent-teardown.ts +257 -0
- package/lib/runs-process.ts +5 -4
- package/lib/runs-retention.ts +1 -1
- package/lib/runs-start.ts +13 -68
- package/lib/runs-status.ts +17 -8
- package/lib/runtime.ts +34 -17
- package/lib/tool-review-lineage-transaction.ts +881 -0
- package/lib/tool-review-lineage.ts +145 -0
- package/lib/tool-review-scheduler.ts +635 -0
- package/lib/tool-review-transaction.ts +563 -0
- package/lib/tool-review.ts +270 -0
- package/lib/tools-inspect.ts +35 -4
- package/lib/tools-local.ts +29 -4
- package/lib/tools-message.ts +45 -30
- package/lib/tools-spawn.ts +22 -2
- package/lib/tools.ts +5 -0
- package/package.json +5 -3
- package/recipes/draft-review.json +24 -0
- package/recipes/tool-review.json +24 -0
- package/scripts/async-runner.mjs +9 -9
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/scripts/release-gates.mjs +165 -0
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +1 -1
package/lib/runs-control.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async run process control primitives.
|
|
3
|
-
* Owns platform signal planning, owned-process signalling, and terminal control markers.
|
|
3
|
+
* Owns: platform signal planning, owned-process signalling, and terminal control markers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
10
|
+
import {
|
|
11
|
+
verifyRunProcessIdentity,
|
|
12
|
+
type RunProcessIdentity,
|
|
13
|
+
type RunProcessIdentityResult,
|
|
14
|
+
} from "./runs-process.ts";
|
|
10
15
|
|
|
11
16
|
export interface RunProcessSignalPlan {
|
|
12
17
|
args?: string[];
|
|
@@ -34,14 +39,58 @@ export function getRunProcessSignalPlan(
|
|
|
34
39
|
return { signalTarget: "processGroup" };
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
export interface RunProcessSignalDeps {
|
|
43
|
+
killProcess?: typeof process.kill;
|
|
44
|
+
runtimePlatform?: NodeJS.Platform;
|
|
45
|
+
spawnProcess?: typeof spawnSync;
|
|
46
|
+
verifyIdentity?: (
|
|
47
|
+
pid: number,
|
|
48
|
+
expected: RunProcessIdentity,
|
|
49
|
+
runtimePlatform: NodeJS.Platform,
|
|
50
|
+
) => RunProcessIdentityResult;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
export function signalOwnedRunProcess(
|
|
38
54
|
pid: number,
|
|
39
55
|
signal: NodeJS.Signals,
|
|
56
|
+
expectedIdentity?: RunProcessIdentity,
|
|
57
|
+
deps: RunProcessSignalDeps = {},
|
|
40
58
|
): RunProcessSignalPlan {
|
|
41
|
-
const
|
|
59
|
+
const runtimePlatform = deps.runtimePlatform ?? process.platform;
|
|
60
|
+
if (expectedIdentity) {
|
|
61
|
+
const proof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
|
|
62
|
+
pid,
|
|
63
|
+
expectedIdentity,
|
|
64
|
+
runtimePlatform,
|
|
65
|
+
);
|
|
66
|
+
if (!proof.valid) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Run process identity changed before signaling: ${proof.status.replaceAll("_", " ")}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
|
|
42
73
|
if (plan.command && plan.args) {
|
|
43
|
-
const
|
|
74
|
+
const spawnProcess = deps.spawnProcess ?? spawnSync;
|
|
75
|
+
let result = spawnProcess(plan.command, plan.args, { encoding: "utf8" });
|
|
76
|
+
if (
|
|
77
|
+
runtimePlatform === "win32" &&
|
|
78
|
+
result.status !== 0 &&
|
|
79
|
+
!plan.args.includes("/F")
|
|
80
|
+
) {
|
|
81
|
+
result = spawnProcess(plan.command, [...plan.args, "/F"], {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
44
85
|
if (result.status !== 0) {
|
|
86
|
+
if (expectedIdentity) {
|
|
87
|
+
const finalProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
|
|
88
|
+
pid,
|
|
89
|
+
expectedIdentity,
|
|
90
|
+
runtimePlatform,
|
|
91
|
+
);
|
|
92
|
+
if (finalProof.status === "dead_pid") return plan;
|
|
93
|
+
}
|
|
45
94
|
throw new Error(
|
|
46
95
|
result.stderr?.trim() ||
|
|
47
96
|
result.stdout?.trim() ||
|
|
@@ -50,11 +99,25 @@ export function signalOwnedRunProcess(
|
|
|
50
99
|
}
|
|
51
100
|
return plan;
|
|
52
101
|
}
|
|
102
|
+
const killProcess = deps.killProcess ?? process.kill.bind(process);
|
|
53
103
|
try {
|
|
54
|
-
|
|
104
|
+
killProcess(-pid, signal);
|
|
55
105
|
return { signalTarget: "processGroup" };
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
108
|
+
if (expectedIdentity) {
|
|
109
|
+
const fallbackProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
|
|
110
|
+
pid,
|
|
111
|
+
expectedIdentity,
|
|
112
|
+
runtimePlatform,
|
|
113
|
+
);
|
|
114
|
+
if (!fallbackProof.valid) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Run process identity changed before pid fallback: ${fallbackProof.status.replaceAll("_", " ")}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
killProcess(pid, signal);
|
|
58
121
|
return { signalTarget: "process" };
|
|
59
122
|
}
|
|
60
123
|
}
|
package/lib/runs-identity.ts
CHANGED
package/lib/runs-index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async run state index.
|
|
3
|
-
* Owns recursive run-state discovery, index rebuild/read, and status filtering.
|
|
3
|
+
* Owns: recursive run-state discovery, index rebuild/read, and status filtering.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
@@ -30,27 +30,63 @@ function matchesStatusFilter(
|
|
|
30
30
|
return status === filter;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export
|
|
33
|
+
export interface RunStateDiscoveryIssue {
|
|
34
|
+
path: string;
|
|
35
|
+
reason: "depth_truncated" | "unreadable";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RunStateDiscoveryResult {
|
|
39
|
+
issues: RunStateDiscoveryIssue[];
|
|
40
|
+
stateDirs: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function discoverRunStateDirs(
|
|
34
44
|
stateRoot: string,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
seen
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
maxDepth = 8,
|
|
46
|
+
): RunStateDiscoveryResult {
|
|
47
|
+
const issues: RunStateDiscoveryIssue[] = [];
|
|
48
|
+
const stateDirs: string[] = [];
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
const visit = (directory: string, depth: number): void => {
|
|
51
|
+
const canonical = resolve(directory);
|
|
52
|
+
if (!existsSync(directory) || seen.has(canonical)) return;
|
|
53
|
+
seen.add(canonical);
|
|
54
|
+
let entries: import("node:fs").Dirent[];
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
issues.push({ path: directory, reason: "unreadable" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.isDirectory()) continue;
|
|
63
|
+
const child = join(directory, entry.name);
|
|
64
|
+
if (existsSync(join(child, "run.json"))) stateDirs.push(child);
|
|
65
|
+
if (depth + 1 < maxDepth) visit(child, depth + 1);
|
|
66
|
+
else {
|
|
67
|
+
try {
|
|
68
|
+
if (
|
|
69
|
+
readdirSync(child, { withFileTypes: true }).some((item) =>
|
|
70
|
+
item.isDirectory(),
|
|
71
|
+
)
|
|
72
|
+
) {
|
|
73
|
+
issues.push({ path: child, reason: "depth_truncated" });
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
issues.push({ path: child, reason: "unreadable" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
visit(stateRoot, 0);
|
|
82
|
+
return {
|
|
83
|
+
issues: issues.sort((left, right) => left.path.localeCompare(right.path)),
|
|
84
|
+
stateDirs: [...new Set(stateDirs)].sort(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function listRunStateDirs(stateRoot: string): string[] {
|
|
89
|
+
return discoverRunStateDirs(stateRoot).stateDirs;
|
|
54
90
|
}
|
|
55
91
|
|
|
56
92
|
function runIndexPath(stateRoot: string): string {
|
package/lib/runs-mailbox.ts
CHANGED
package/lib/runs-messages.ts
CHANGED
package/lib/runs-outbox.ts
CHANGED
package/lib/runs-ownership.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async run state-directory ownership.
|
|
3
|
-
* Owns the marker proof required before launch and destructive retention.
|
|
3
|
+
* Owns: the marker proof required before launch and destructive retention.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
realpathSync,
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
|
-
import {
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
16
17
|
|
|
17
18
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
18
19
|
|
|
@@ -35,13 +36,31 @@ function comparablePath(path: string): string {
|
|
|
35
36
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function isSystemTempRootAlias(resolved: string, canonical: string): boolean {
|
|
40
|
+
const tempRoot = resolve(tmpdir());
|
|
41
|
+
const relativeStateDir = relative(tempRoot, resolved);
|
|
42
|
+
if (
|
|
43
|
+
relativeStateDir === ".." ||
|
|
44
|
+
relativeStateDir.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
|
|
45
|
+
isAbsolute(relativeStateDir)
|
|
46
|
+
) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const canonicalTempRoot = realpathSync.native(tempRoot);
|
|
50
|
+
const expectedCanonical = resolve(canonicalTempRoot, relativeStateDir);
|
|
51
|
+
return comparablePath(canonical) === comparablePath(expectedCanonical);
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
function assertCanonicalDirectory(stateDir: string): string {
|
|
39
55
|
const resolved = resolve(stateDir);
|
|
40
56
|
if (lstatSync(resolved).isSymbolicLink()) {
|
|
41
57
|
throw new Error(`Run state directory cannot be a symlink: ${resolved}`);
|
|
42
58
|
}
|
|
43
|
-
const canonical = realpathSync(resolved);
|
|
44
|
-
if (
|
|
59
|
+
const canonical = realpathSync.native(resolved);
|
|
60
|
+
if (
|
|
61
|
+
comparablePath(canonical) !== comparablePath(resolved) &&
|
|
62
|
+
!isSystemTempRootAlias(resolved, canonical)
|
|
63
|
+
) {
|
|
45
64
|
throw new Error(`Run state directory has an ambiguous symlink alias: ${resolved}`);
|
|
46
65
|
}
|
|
47
66
|
return resolved;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-session actor teardown.
|
|
3
|
+
* Owns: exact-owner running-run selection, kill revalidation, bounded outcomes, and partial-failure continuation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ParentRunTeardownCandidate {
|
|
7
|
+
ownerId: string;
|
|
8
|
+
run: string;
|
|
9
|
+
runInstanceId?: string;
|
|
10
|
+
stateDir: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ParentRunTeardownOutcome =
|
|
14
|
+
| "killed"
|
|
15
|
+
| "skipped"
|
|
16
|
+
| "failed";
|
|
17
|
+
|
|
18
|
+
export interface ParentRunTeardownAttempt extends ParentRunTeardownCandidate {
|
|
19
|
+
outcome: ParentRunTeardownOutcome;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParentRunTeardownDiscoveryFailure {
|
|
24
|
+
path: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ParentRunTeardownResult {
|
|
29
|
+
attempted: number;
|
|
30
|
+
discoveryFailed: number;
|
|
31
|
+
discoveryFailures: ParentRunTeardownDiscoveryFailure[];
|
|
32
|
+
failed: number;
|
|
33
|
+
killed: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
attempts: ParentRunTeardownAttempt[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ParentRunTeardownSummary extends ParentRunTeardownResult {
|
|
39
|
+
attemptsOmitted: number;
|
|
40
|
+
discoveryFailuresOmitted: number;
|
|
41
|
+
ownerId: string;
|
|
42
|
+
trigger: string;
|
|
43
|
+
ts: string;
|
|
44
|
+
version: 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SUMMARY_ENTRY_LIMIT = 200;
|
|
48
|
+
const SUMMARY_TEXT_LIMIT = 500;
|
|
49
|
+
|
|
50
|
+
function boundedSummaryText(value: string): string {
|
|
51
|
+
return value.length > SUMMARY_TEXT_LIMIT
|
|
52
|
+
? `${value.slice(0, SUMMARY_TEXT_LIMIT - 1)}…`
|
|
53
|
+
: value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildBoundedParentTeardownSummary(
|
|
57
|
+
result: ParentRunTeardownResult,
|
|
58
|
+
ownerId: string,
|
|
59
|
+
trigger: string,
|
|
60
|
+
ts: string,
|
|
61
|
+
): ParentRunTeardownSummary {
|
|
62
|
+
const attempts = result.attempts.slice(0, SUMMARY_ENTRY_LIMIT).map((attempt) => ({
|
|
63
|
+
ownerId: boundedSummaryText(attempt.ownerId),
|
|
64
|
+
outcome: attempt.outcome,
|
|
65
|
+
...(attempt.reason ? { reason: boundedSummaryText(attempt.reason) } : {}),
|
|
66
|
+
run: boundedSummaryText(attempt.run),
|
|
67
|
+
...(attempt.runInstanceId
|
|
68
|
+
? { runInstanceId: boundedSummaryText(attempt.runInstanceId) }
|
|
69
|
+
: {}),
|
|
70
|
+
stateDir: boundedSummaryText(attempt.stateDir),
|
|
71
|
+
}));
|
|
72
|
+
const discoveryFailures = result.discoveryFailures
|
|
73
|
+
.slice(0, SUMMARY_ENTRY_LIMIT)
|
|
74
|
+
.map((failure) => ({
|
|
75
|
+
path: boundedSummaryText(failure.path),
|
|
76
|
+
reason: boundedSummaryText(failure.reason),
|
|
77
|
+
}));
|
|
78
|
+
return {
|
|
79
|
+
attempted: result.attempted,
|
|
80
|
+
attempts,
|
|
81
|
+
attemptsOmitted: Math.max(0, result.attempts.length - attempts.length),
|
|
82
|
+
discoveryFailed: result.discoveryFailed,
|
|
83
|
+
discoveryFailures,
|
|
84
|
+
discoveryFailuresOmitted: Math.max(
|
|
85
|
+
0,
|
|
86
|
+
result.discoveryFailures.length - discoveryFailures.length,
|
|
87
|
+
),
|
|
88
|
+
failed: result.failed,
|
|
89
|
+
killed: result.killed,
|
|
90
|
+
ownerId: boundedSummaryText(ownerId),
|
|
91
|
+
skipped: result.skipped,
|
|
92
|
+
trigger: boundedSummaryText(trigger),
|
|
93
|
+
ts: boundedSummaryText(ts),
|
|
94
|
+
version: 1,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ParentRunTeardownDeps {
|
|
99
|
+
getRunStatus: (stateDir: string) => Record<string, unknown>;
|
|
100
|
+
killRun: (
|
|
101
|
+
stateDir: string,
|
|
102
|
+
expected: { ownerId: string; runInstanceId: string },
|
|
103
|
+
) => Record<string, unknown>;
|
|
104
|
+
listRunStatuses: () =>
|
|
105
|
+
| Array<Record<string, unknown>>
|
|
106
|
+
| {
|
|
107
|
+
failures: ParentRunTeardownDiscoveryFailure[];
|
|
108
|
+
statuses: Array<Record<string, unknown>>;
|
|
109
|
+
};
|
|
110
|
+
recordAttempt: (attempt: ParentRunTeardownAttempt) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function errorMessage(error: unknown): string {
|
|
114
|
+
return error instanceof Error ? error.message : String(error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function candidateFrom(
|
|
118
|
+
ownerId: string,
|
|
119
|
+
status: Record<string, unknown>,
|
|
120
|
+
): ParentRunTeardownCandidate | undefined {
|
|
121
|
+
if (
|
|
122
|
+
status.ownerId !== ownerId ||
|
|
123
|
+
status.status !== "running" ||
|
|
124
|
+
typeof status.run !== "string" ||
|
|
125
|
+
typeof status.state_dir !== "string"
|
|
126
|
+
) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
ownerId,
|
|
131
|
+
run: status.run,
|
|
132
|
+
...(typeof status.run_instance_id === "string"
|
|
133
|
+
? { runInstanceId: status.run_instance_id }
|
|
134
|
+
: {}),
|
|
135
|
+
stateDir: status.state_dir,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function selectParentRunTeardownCandidates(
|
|
140
|
+
ownerId: string | undefined,
|
|
141
|
+
statuses: Array<Record<string, unknown>>,
|
|
142
|
+
): ParentRunTeardownCandidate[] {
|
|
143
|
+
if (!ownerId) return [];
|
|
144
|
+
const byStateDir = new Map<string, ParentRunTeardownCandidate>();
|
|
145
|
+
for (const status of statuses) {
|
|
146
|
+
const candidate = candidateFrom(ownerId, status);
|
|
147
|
+
if (candidate && !byStateDir.has(candidate.stateDir)) {
|
|
148
|
+
byStateDir.set(candidate.stateDir, candidate);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [...byStateDir.values()].sort((left, right) =>
|
|
152
|
+
left.run.localeCompare(right.run),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function recordAttempt(
|
|
157
|
+
deps: ParentRunTeardownDeps,
|
|
158
|
+
attempt: ParentRunTeardownAttempt,
|
|
159
|
+
): ParentRunTeardownAttempt {
|
|
160
|
+
try {
|
|
161
|
+
deps.recordAttempt(attempt);
|
|
162
|
+
return attempt;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
...attempt,
|
|
166
|
+
outcome: "failed",
|
|
167
|
+
reason: `${attempt.reason ? `${attempt.reason}; ` : ""}evidence: ${errorMessage(error)}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function teardownParentRuns(
|
|
173
|
+
ownerId: string | undefined,
|
|
174
|
+
deps: ParentRunTeardownDeps,
|
|
175
|
+
): ParentRunTeardownResult {
|
|
176
|
+
let discovery: ReturnType<ParentRunTeardownDeps["listRunStatuses"]>;
|
|
177
|
+
try {
|
|
178
|
+
const listed = deps.listRunStatuses();
|
|
179
|
+
discovery = Array.isArray(listed)
|
|
180
|
+
? { failures: [], statuses: listed }
|
|
181
|
+
: listed;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
discovery = {
|
|
184
|
+
failures: [{ path: "<state-root>", reason: errorMessage(error) }],
|
|
185
|
+
statuses: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const candidates = selectParentRunTeardownCandidates(
|
|
189
|
+
ownerId,
|
|
190
|
+
discovery.statuses,
|
|
191
|
+
);
|
|
192
|
+
const attempts: ParentRunTeardownAttempt[] = [];
|
|
193
|
+
for (const candidate of candidates) {
|
|
194
|
+
let attempt: ParentRunTeardownAttempt;
|
|
195
|
+
try {
|
|
196
|
+
const current = deps.getRunStatus(candidate.stateDir);
|
|
197
|
+
if (!candidate.runInstanceId) {
|
|
198
|
+
attempt = {
|
|
199
|
+
...candidate,
|
|
200
|
+
outcome: "failed",
|
|
201
|
+
reason: "run generation unavailable",
|
|
202
|
+
};
|
|
203
|
+
} else if (current.ownerId !== candidate.ownerId) {
|
|
204
|
+
attempt = {
|
|
205
|
+
...candidate,
|
|
206
|
+
outcome: "skipped",
|
|
207
|
+
reason: "ownership changed",
|
|
208
|
+
};
|
|
209
|
+
} else if (current.run_instance_id !== candidate.runInstanceId) {
|
|
210
|
+
attempt = {
|
|
211
|
+
...candidate,
|
|
212
|
+
outcome: "skipped",
|
|
213
|
+
reason: "run generation changed",
|
|
214
|
+
};
|
|
215
|
+
} else if (current.status !== "running") {
|
|
216
|
+
attempt = {
|
|
217
|
+
...candidate,
|
|
218
|
+
outcome: "skipped",
|
|
219
|
+
reason: "already terminal",
|
|
220
|
+
};
|
|
221
|
+
} else {
|
|
222
|
+
const killed = deps.killRun(candidate.stateDir, {
|
|
223
|
+
ownerId: candidate.ownerId,
|
|
224
|
+
runInstanceId: candidate.runInstanceId,
|
|
225
|
+
});
|
|
226
|
+
attempt = killed.killed === true
|
|
227
|
+
? { ...candidate, outcome: "killed" }
|
|
228
|
+
: {
|
|
229
|
+
...candidate,
|
|
230
|
+
outcome: "failed",
|
|
231
|
+
reason:
|
|
232
|
+
typeof killed.reason === "string"
|
|
233
|
+
? killed.reason
|
|
234
|
+
: "kill rejected",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
attempt = {
|
|
239
|
+
...candidate,
|
|
240
|
+
outcome: "failed",
|
|
241
|
+
reason: errorMessage(error),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
attempts.push(recordAttempt(deps, attempt));
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
attempted: candidates.length,
|
|
248
|
+
discoveryFailed: discovery.failures.length,
|
|
249
|
+
discoveryFailures: discovery.failures,
|
|
250
|
+
failed:
|
|
251
|
+
attempts.filter((attempt) => attempt.outcome === "failed").length +
|
|
252
|
+
discovery.failures.length,
|
|
253
|
+
killed: attempts.filter((attempt) => attempt.outcome === "killed").length,
|
|
254
|
+
skipped: attempts.filter((attempt) => attempt.outcome === "skipped").length,
|
|
255
|
+
attempts,
|
|
256
|
+
};
|
|
257
|
+
}
|
package/lib/runs-process.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async run process identity helpers.
|
|
3
|
-
* Owns cross-platform liveness and stable runner process identity proofs.
|
|
3
|
+
* Owns: cross-platform liveness and stable runner process identity proofs.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
8
8
|
import { platform } from "node:os";
|
|
9
|
-
import {
|
|
9
|
+
import { posix, win32 } from "node:path";
|
|
10
10
|
|
|
11
11
|
export interface RunProcessIdentity {
|
|
12
12
|
command: string;
|
|
@@ -125,8 +125,9 @@ export function captureRunProcessIdentity(
|
|
|
125
125
|
if (!identity.command.includes(runnerPath) || !identity.command.includes(stateDir)) {
|
|
126
126
|
return undefined;
|
|
127
127
|
}
|
|
128
|
-
const
|
|
129
|
-
const
|
|
128
|
+
const pathApi = runtimePlatform === "win32" ? win32 : posix;
|
|
129
|
+
const resolvedCwd = pathApi.resolve(cwd);
|
|
130
|
+
const canonicalCwd = runtimePlatform === platform() && existsSync(resolvedCwd)
|
|
130
131
|
? realpathSync.native(resolvedCwd)
|
|
131
132
|
: resolvedCwd;
|
|
132
133
|
const expectedCwd =
|
package/lib/runs-retention.ts
CHANGED
package/lib/runs-start.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async run start guards.
|
|
3
|
-
* Owns active-run reuse checks, start lock acquisition, and safe state-dir
|
|
3
|
+
* Owns: active-run reuse checks, start lock acquisition, and safe state-dir
|
|
4
4
|
* preparation before a new runner process is spawned.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readFileSync,
|
|
10
|
-
rmSync,
|
|
11
|
-
statSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
7
|
+
import { rmSync } from "node:fs";
|
|
14
8
|
import { join } from "node:path";
|
|
15
9
|
|
|
10
|
+
import {
|
|
11
|
+
acquireFileMutationLock,
|
|
12
|
+
type FileMutationLockOptions,
|
|
13
|
+
} from "./file-state.ts";
|
|
16
14
|
import {
|
|
17
15
|
isAlive,
|
|
18
|
-
readProcessIdentity,
|
|
19
16
|
verifyRunProcessIdentity,
|
|
20
17
|
type RunProcessIdentity,
|
|
21
18
|
} from "./runs-process.ts";
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
20
|
+
type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
26
21
|
|
|
27
22
|
export function assertNoActiveRunState(
|
|
28
23
|
stateDir: string,
|
|
@@ -55,62 +50,11 @@ export function assertNoActiveRunState(
|
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
pid: process.pid,
|
|
64
|
-
createdAt: new Date().toISOString(),
|
|
65
|
-
...(processIdentity ? { process_identity: processIdentity } : {}),
|
|
66
|
-
...(recovered ? { recovered: true } : {}),
|
|
67
|
-
})}\n`,
|
|
68
|
-
"utf8",
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isStartLockOwnerProvenDead(lockDir: string): boolean {
|
|
73
|
-
try {
|
|
74
|
-
const owner = JSON.parse(
|
|
75
|
-
readFileSync(join(lockDir, "owner.json"), "utf8"),
|
|
76
|
-
) as Record<string, unknown>;
|
|
77
|
-
const pid = Number(owner.pid || 0);
|
|
78
|
-
if (!pid) return false;
|
|
79
|
-
return verifyRunProcessIdentity(
|
|
80
|
-
pid,
|
|
81
|
-
owner.process_identity as RunProcessIdentity | undefined,
|
|
82
|
-
).status === "dead_pid";
|
|
83
|
-
} catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function acquireStateStartLock(stateDir: string): () => void {
|
|
89
|
-
const lockDir = join(stateDir, ".start.lock");
|
|
90
|
-
try {
|
|
91
|
-
mkdirSync(lockDir);
|
|
92
|
-
writeStartLockOwner(lockDir);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
try {
|
|
95
|
-
const stat = statSync(lockDir);
|
|
96
|
-
if (
|
|
97
|
-
Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS &&
|
|
98
|
-
isStartLockOwnerProvenDead(lockDir)
|
|
99
|
-
) {
|
|
100
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
101
|
-
mkdirSync(lockDir);
|
|
102
|
-
writeStartLockOwner(lockDir, true);
|
|
103
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
// Keep the original lock acquisition error below.
|
|
107
|
-
}
|
|
108
|
-
throw new Error(
|
|
109
|
-
`Run state is already being started: ${stateDir}. Retry after the current start finishes.`,
|
|
110
|
-
{ cause: error },
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
53
|
+
export function acquireStateStartLock(
|
|
54
|
+
stateDir: string,
|
|
55
|
+
options: FileMutationLockOptions = {},
|
|
56
|
+
): () => void {
|
|
57
|
+
return acquireFileMutationLock(join(stateDir, ".lifecycle"), options);
|
|
114
58
|
}
|
|
115
59
|
|
|
116
60
|
export function prepareStateDirForStart(
|
|
@@ -149,6 +93,7 @@ export function prepareStateDirForStart(
|
|
|
149
93
|
"result.json",
|
|
150
94
|
"stderr.log",
|
|
151
95
|
"stdout.log",
|
|
96
|
+
"terminal-delivery-failure.json",
|
|
152
97
|
"terminal-handled.json",
|
|
153
98
|
]) {
|
|
154
99
|
rmSync(join(stateDir, file), { force: true });
|