@llblab/pi-actors 0.41.0 → 0.42.0
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 +13 -8
- package/BACKLOG.md +2 -81
- package/CHANGELOG.md +22 -0
- package/README.md +22 -4
- package/dist/index.js +29 -82
- package/dist/lib/async-runs.d.ts +25 -5
- package/dist/lib/async-runs.js +136 -47
- package/dist/lib/automatic-review-runtime.d.ts +18 -0
- package/dist/lib/automatic-review-runtime.js +96 -0
- 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 +29 -1
- package/dist/lib/inspector-overlay.js +537 -90
- package/dist/lib/inspector.d.ts +9 -0
- package/dist/lib/inspector.js +52 -1
- package/dist/lib/observability.d.ts +39 -3
- package/dist/lib/observability.js +162 -34
- 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 +8 -2
- package/dist/lib/runs-control.js +23 -6
- 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 +1 -1
- 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 +1 -1
- 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 +6 -48
- package/dist/lib/runs-status.d.ts +5 -3
- package/dist/lib/runs-status.js +4 -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 +26 -3
- package/dist/lib/tools-local.js +4 -2
- package/dist/lib/tools-message.d.ts +1 -0
- package/dist/lib/tools-message.js +29 -17
- package/dist/lib/tools-response.d.ts +1 -1
- package/dist/lib/tools-response.js +5 -7
- package/dist/lib/tools-spawn.js +4 -1
- 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/release-gates.mjs +165 -0
- package/dist/skills/actors/SKILL.md +11 -14
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +32 -18
- package/docs/async-runs.md +10 -2
- package/docs/recipe-library.md +7 -3
- package/docs/template-recipes.md +4 -11
- package/docs/tool-registry.md +11 -4
- package/index.ts +29 -103
- package/lib/async-runs.ts +218 -62
- package/lib/automatic-review-runtime.ts +135 -0
- 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 +675 -105
- package/lib/inspector.ts +78 -3
- package/lib/observability.ts +219 -40
- 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 +49 -5
- 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 +1 -1
- package/lib/runs-parent-teardown.ts +257 -0
- package/lib/runs-process.ts +1 -1
- package/lib/runs-retention.ts +1 -1
- package/lib/runs-start.ts +12 -68
- package/lib/runs-status.ts +12 -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 +33 -3
- package/lib/tools-local.ts +8 -2
- package/lib/tools-message.ts +45 -30
- package/lib/tools-response.ts +5 -6
- package/lib/tools-spawn.ts +8 -1
- package/lib/tools.ts +5 -0
- package/package.json +3 -2
- package/recipes/draft-review.json +24 -0
- package/recipes/tool-review.json +24 -0
- package/scripts/release-gates.mjs +165 -0
- package/skills/actors/SKILL.md +11 -14
- 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,11 +39,36 @@ 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
|
+
verifyIdentity?: (
|
|
46
|
+
pid: number,
|
|
47
|
+
expected: RunProcessIdentity,
|
|
48
|
+
runtimePlatform: NodeJS.Platform,
|
|
49
|
+
) => RunProcessIdentityResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
export function signalOwnedRunProcess(
|
|
38
53
|
pid: number,
|
|
39
54
|
signal: NodeJS.Signals,
|
|
55
|
+
expectedIdentity?: RunProcessIdentity,
|
|
56
|
+
deps: RunProcessSignalDeps = {},
|
|
40
57
|
): RunProcessSignalPlan {
|
|
41
|
-
const
|
|
58
|
+
const runtimePlatform = deps.runtimePlatform ?? process.platform;
|
|
59
|
+
if (expectedIdentity) {
|
|
60
|
+
const proof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
|
|
61
|
+
pid,
|
|
62
|
+
expectedIdentity,
|
|
63
|
+
runtimePlatform,
|
|
64
|
+
);
|
|
65
|
+
if (!proof.valid) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Run process identity changed before signaling: ${proof.status.replaceAll("_", " ")}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
|
|
42
72
|
if (plan.command && plan.args) {
|
|
43
73
|
const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
|
|
44
74
|
if (result.status !== 0) {
|
|
@@ -50,11 +80,25 @@ export function signalOwnedRunProcess(
|
|
|
50
80
|
}
|
|
51
81
|
return plan;
|
|
52
82
|
}
|
|
83
|
+
const killProcess = deps.killProcess ?? process.kill.bind(process);
|
|
53
84
|
try {
|
|
54
|
-
|
|
85
|
+
killProcess(-pid, signal);
|
|
55
86
|
return { signalTarget: "processGroup" };
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
89
|
+
if (expectedIdentity) {
|
|
90
|
+
const fallbackProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
|
|
91
|
+
pid,
|
|
92
|
+
expectedIdentity,
|
|
93
|
+
runtimePlatform,
|
|
94
|
+
);
|
|
95
|
+
if (!fallbackProof.valid) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Run process identity changed before pid fallback: ${fallbackProof.status.replaceAll("_", " ")}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
killProcess(pid, signal);
|
|
58
102
|
return { signalTarget: "process" };
|
|
59
103
|
}
|
|
60
104
|
}
|
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
|
@@ -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
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(
|
package/lib/runs-status.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async run status and log readers.
|
|
3
|
-
* Owns status derivation from run state files plus bounded log/event tails.
|
|
3
|
+
* Owns: status derivation from run state files plus bounded log/event tails.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
-
import type { AsyncRunStatus } from "./async-runs.ts";
|
|
10
9
|
import {
|
|
11
|
-
isWithinRunnerIdentityGrace,
|
|
12
10
|
verifyRunProcessIdentity,
|
|
13
11
|
type RunProcessIdentity,
|
|
14
12
|
} from "./runs-process.ts";
|
|
15
13
|
import { readJsonlFileResilient } from "./state-readers.ts";
|
|
16
14
|
|
|
17
|
-
export type
|
|
15
|
+
export type AsyncRunStatus =
|
|
16
|
+
| "running"
|
|
17
|
+
| "done"
|
|
18
|
+
| "failed"
|
|
19
|
+
| "exited"
|
|
20
|
+
| "cancelled"
|
|
21
|
+
| "killed";
|
|
22
|
+
|
|
23
|
+
type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
18
24
|
|
|
19
25
|
function getInterruptedRunStatus(
|
|
20
26
|
stateDir: string,
|
|
@@ -35,7 +41,7 @@ export function buildRunStatus(
|
|
|
35
41
|
meta: Record<string, unknown>,
|
|
36
42
|
readJson: RunJsonReader,
|
|
37
43
|
_runnerPath: string,
|
|
38
|
-
|
|
44
|
+
_runnerIdentityGraceMs: number,
|
|
39
45
|
): Record<string, unknown> {
|
|
40
46
|
const result = readJson(join(stateDir, "result.json"));
|
|
41
47
|
const pid = Number(meta.pid || 0);
|
|
@@ -45,9 +51,7 @@ export function buildRunStatus(
|
|
|
45
51
|
);
|
|
46
52
|
const aliveOwnedRunner = Boolean(
|
|
47
53
|
pid &&
|
|
48
|
-
(processIdentity.valid ||
|
|
49
|
-
(processIdentity.status === "unsupported_proof" &&
|
|
50
|
-
isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs))),
|
|
54
|
+
(processIdentity.valid || processIdentity.status === "unsupported_proof"),
|
|
51
55
|
);
|
|
52
56
|
const status: AsyncRunStatus = result
|
|
53
57
|
? Number(result.code ?? 0) === 0
|