@llblab/pi-actors 0.32.0 → 0.34.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 +23 -11
- package/BACKLOG.md +2 -30
- package/CHANGELOG.md +30 -1
- package/dist/index.js +10 -10
- package/dist/lib/async-runs.d.ts +23 -75
- package/dist/lib/async-runs.js +49 -801
- package/dist/lib/command-templates.d.ts +3 -3
- package/dist/lib/command-templates.js +3 -3
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.js +5 -5
- package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
- package/dist/lib/{output.js → execution-output.js} +3 -3
- package/dist/lib/execution.js +1 -1
- package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
- package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
- package/dist/lib/mailbox-loop.d.ts +3 -3
- package/dist/lib/mailbox-loop.js +3 -3
- package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
- package/dist/lib/{actor-messages.js → messages.js} +3 -3
- package/dist/lib/paths.d.ts +1 -1
- package/dist/lib/paths.js +3 -3
- package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
- package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
- package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +9 -9
- package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +17 -17
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +22 -22
- package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
- package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
- package/dist/lib/runs-artifacts.d.ts +21 -0
- package/dist/lib/runs-artifacts.js +68 -0
- package/dist/lib/runs-control.d.ts +13 -0
- package/dist/lib/runs-control.js +59 -0
- package/dist/lib/runs-identity.d.ts +5 -0
- package/dist/lib/runs-identity.js +11 -0
- package/dist/lib/runs-index.d.ts +18 -0
- package/dist/lib/runs-index.js +119 -0
- package/dist/lib/runs-mailbox.d.ts +25 -0
- package/dist/lib/runs-mailbox.js +146 -0
- package/dist/lib/runs-messages.d.ts +15 -0
- package/dist/lib/runs-messages.js +179 -0
- package/dist/lib/runs-outbox.d.ts +41 -0
- package/dist/lib/runs-outbox.js +87 -0
- package/dist/lib/runs-process.d.ts +7 -0
- package/dist/lib/runs-process.js +36 -0
- package/dist/lib/runs-retention.d.ts +8 -0
- package/dist/lib/runs-retention.js +56 -0
- package/dist/lib/runs-start.d.ts +9 -0
- package/dist/lib/runs-start.js +67 -0
- package/dist/lib/runs-status.d.ts +8 -0
- package/dist/lib/runs-status.js +60 -0
- package/dist/lib/runtime.d.ts +2 -2
- package/dist/lib/runtime.js +5 -5
- package/dist/lib/schema.d.ts +9 -0
- package/dist/lib/schema.js +40 -0
- package/dist/lib/tools-access.d.ts +19 -0
- package/dist/lib/tools-access.js +44 -0
- package/dist/lib/tools-inspect.d.ts +11 -0
- package/dist/lib/tools-inspect.js +534 -0
- package/dist/lib/tools-local.d.ts +14 -0
- package/dist/lib/tools-local.js +156 -0
- package/dist/lib/tools-mailbox.d.ts +8 -0
- package/dist/lib/tools-mailbox.js +48 -0
- package/dist/lib/tools-message.d.ts +9 -0
- package/dist/lib/tools-message.js +346 -0
- package/dist/lib/tools-register.d.ts +18 -0
- package/dist/lib/tools-register.js +40 -0
- package/dist/lib/tools-response.d.ts +21 -0
- package/dist/lib/tools-response.js +219 -0
- package/dist/lib/tools-spawn.d.ts +12 -0
- package/dist/lib/tools-spawn.js +159 -0
- package/dist/lib/tools.d.ts +13 -39
- package/dist/lib/tools.js +11 -1497
- package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
- package/dist/scripts/actor-worker.mjs +186 -7
- package/dist/scripts/async-runner.mjs +171 -8
- package/dist/scripts/build-dist.mjs +1 -1
- package/dist/scripts/conformance.mjs +34 -18
- package/dist/scripts/coordinator.mjs +733 -11
- package/dist/scripts/locker.mjs +345 -16
- package/dist/scripts/recipe-utils.mjs +478 -14
- package/dist/scripts/validate-recipe.mjs +110 -10
- package/dist/skills/actors/SKILL.md +2 -2
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/recipe-library.md +1 -1
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +3 -3
- package/index.ts +12 -19
- package/lib/async-runs.ts +124 -1016
- package/lib/command-templates.ts +3 -3
- package/lib/config.ts +7 -7
- package/lib/{output.ts → execution-output.ts} +3 -3
- package/lib/execution.ts +5 -1
- package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
- package/lib/mailbox-loop.ts +3 -3
- package/lib/{actor-messages.ts → messages.ts} +3 -3
- package/lib/paths.ts +3 -3
- package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
- package/lib/{recipe-discovery.ts → recipes-discovery.ts} +30 -30
- package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
- package/lib/registry.ts +37 -24
- package/lib/{actor-rooms.ts → rooms.ts} +4 -4
- package/lib/runs-artifacts.ts +96 -0
- package/lib/runs-control.ts +85 -0
- package/lib/runs-identity.ts +14 -0
- package/lib/runs-index.ts +156 -0
- package/lib/runs-mailbox.ts +208 -0
- package/lib/runs-messages.ts +252 -0
- package/lib/runs-outbox.ts +144 -0
- package/lib/runs-process.ts +51 -0
- package/lib/runs-retention.ts +77 -0
- package/lib/runs-start.ts +98 -0
- package/lib/runs-status.ts +78 -0
- package/lib/runtime.ts +6 -6
- package/lib/schema.ts +55 -0
- package/lib/tools-access.ts +68 -0
- package/lib/tools-inspect.ts +735 -0
- package/lib/tools-local.ts +234 -0
- package/lib/tools-mailbox.ts +56 -0
- package/lib/tools-message.ts +483 -0
- package/lib/tools-register.ts +64 -0
- package/lib/tools-response.ts +267 -0
- package/lib/tools-spawn.ts +240 -0
- package/lib/tools.ts +26 -2051
- package/package.json +1 -1
- package/recipes/utility-coordinator-lock-snapshot.json +1 -1
- package/scripts/actor-worker.mjs +186 -7
- package/scripts/async-runner.mjs +171 -8
- package/scripts/build-dist.mjs +1 -1
- package/scripts/conformance.mjs +34 -18
- package/scripts/coordinator.mjs +733 -11
- package/scripts/locker.mjs +345 -16
- package/scripts/recipe-utils.mjs +478 -14
- package/scripts/validate-recipe.mjs +110 -10
- package/skills/actors/SKILL.md +2 -2
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/actor-worker.d.ts +0 -16
- package/dist/lib/actor-worker.js +0 -134
- package/dist/lib/async-runner.d.ts +0 -5
- package/dist/lib/async-runner.js +0 -134
- package/dist/lib/conformance.d.ts +0 -12
- package/dist/lib/conformance.js +0 -28
- package/dist/lib/coordinator.d.ts +0 -5
- package/dist/lib/coordinator.js +0 -660
- package/dist/lib/locker.d.ts +0 -5
- package/dist/lib/locker.js +0 -310
- package/dist/lib/recipe-utils.d.ts +0 -5
- package/dist/lib/recipe-utils.js +0 -385
- package/dist/lib/validate-recipe.d.ts +0 -6
- package/dist/lib/validate-recipe.js +0 -104
- package/lib/actor-worker.ts +0 -174
- package/lib/async-runner.ts +0 -173
- package/lib/conformance.ts +0 -46
- package/lib/coordinator.ts +0 -742
- package/lib/locker.ts +0 -340
- package/lib/recipe-utils.ts +0 -486
- package/lib/validate-recipe.ts +0 -110
- /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
- /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
- /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
- /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
- /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
package/lib/async-runs.ts
CHANGED
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Command-template async run
|
|
3
|
-
*
|
|
4
|
-
* Owns detached run state, observation, log tailing, listing, and cancellation safety
|
|
2
|
+
* Command-template async run lifecycle facade.
|
|
3
|
+
* Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import { spawn
|
|
8
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
9
7
|
import {
|
|
10
8
|
closeSync,
|
|
11
|
-
constants,
|
|
12
9
|
existsSync,
|
|
13
10
|
mkdirSync,
|
|
14
11
|
openSync,
|
|
15
|
-
readdirSync,
|
|
16
12
|
readFileSync,
|
|
17
|
-
cpSync,
|
|
18
|
-
readlinkSync,
|
|
19
|
-
renameSync,
|
|
20
13
|
rmSync,
|
|
21
14
|
statSync,
|
|
22
15
|
writeFileSync,
|
|
23
|
-
writeSync,
|
|
24
16
|
} from "node:fs";
|
|
25
|
-
import { createConnection } from "node:net";
|
|
26
|
-
import { platform } from "node:os";
|
|
27
17
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
28
18
|
import { fileURLToPath } from "node:url";
|
|
29
19
|
|
|
@@ -31,16 +21,59 @@ import type {
|
|
|
31
21
|
CommandTemplateFailureScope,
|
|
32
22
|
CommandTemplateValue,
|
|
33
23
|
} from "./command-templates.ts";
|
|
34
|
-
import { substituteCommandTemplateToken } from "./command-templates.ts";
|
|
35
24
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
36
|
-
import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.ts";
|
|
37
25
|
import * as Paths from "./paths.ts";
|
|
38
|
-
import * as
|
|
39
|
-
import * as
|
|
40
|
-
import {
|
|
26
|
+
import * as RecipesReferences from "./recipes-references.ts";
|
|
27
|
+
import * as RecipesUsage from "./recipes-usage.ts";
|
|
28
|
+
import {
|
|
29
|
+
resolveArtifactManifest,
|
|
30
|
+
resolveArtifactPaths,
|
|
31
|
+
type RunArtifactDeclaration,
|
|
32
|
+
type RunArtifactManifestEntry,
|
|
33
|
+
} from "./runs-artifacts.ts";
|
|
34
|
+
import { safeRunId } from "./runs-identity.ts";
|
|
35
|
+
import {
|
|
36
|
+
buildTerminalProgress,
|
|
37
|
+
getRunProcessSignalPlan,
|
|
38
|
+
markTerminalHandled,
|
|
39
|
+
signalOwnedRunProcess,
|
|
40
|
+
type RunProcessSignalPlan,
|
|
41
|
+
} from "./runs-control.ts";
|
|
42
|
+
import {
|
|
43
|
+
buildRunOutboxEventPayload,
|
|
44
|
+
parseRunOutboxEventLine,
|
|
45
|
+
type RunOutboxEvent,
|
|
46
|
+
} from "./runs-outbox.ts";
|
|
47
|
+
import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.ts";
|
|
48
|
+
import {
|
|
49
|
+
isAlive,
|
|
50
|
+
isWithinRunnerIdentityGrace,
|
|
51
|
+
pidMatchesRun,
|
|
52
|
+
} from "./runs-process.ts";
|
|
53
|
+
import * as RunsStart from "./runs-start.ts";
|
|
54
|
+
import * as RunsIndex from "./runs-index.ts";
|
|
55
|
+
import {
|
|
56
|
+
claimRunInboxMessageInStateDir,
|
|
57
|
+
parseRunInboxLine,
|
|
58
|
+
processRunInboxMessagesInStateDir,
|
|
59
|
+
readRunInboxMessagesFromStateDir,
|
|
60
|
+
runInboxFile,
|
|
61
|
+
type ProcessRunInboxResult,
|
|
62
|
+
type RunInboxMessage,
|
|
63
|
+
type RunInboxStatus,
|
|
64
|
+
updateRunInboxMessageStatusInStateDir,
|
|
65
|
+
} from "./runs-mailbox.ts";
|
|
66
|
+
import {
|
|
67
|
+
deliverRunMessage,
|
|
68
|
+
type SendRunMessageOptions,
|
|
69
|
+
} from "./runs-messages.ts";
|
|
70
|
+
import {
|
|
71
|
+
buildRunStatus,
|
|
72
|
+
tailFile,
|
|
73
|
+
tailLines,
|
|
74
|
+
} from "./runs-status.ts";
|
|
75
|
+
import { readJsonFileResilient } from "./state-readers.ts";
|
|
41
76
|
|
|
42
|
-
const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
43
|
-
const RUN_INBOX_LOCK_TIMEOUT_MS = 5000;
|
|
44
77
|
const RUNNER_IDENTITY_GRACE_MS = 5000;
|
|
45
78
|
|
|
46
79
|
export type AsyncRunLaunchSource = "spawn" | "tool";
|
|
@@ -70,7 +103,7 @@ export interface AsyncRunStartParams {
|
|
|
70
103
|
delay?: number | string;
|
|
71
104
|
output?: string;
|
|
72
105
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
73
|
-
mailbox?:
|
|
106
|
+
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
74
107
|
retire_when?: "children_terminal";
|
|
75
108
|
retry?: number | string;
|
|
76
109
|
failure?: CommandTemplateFailureScope;
|
|
@@ -88,27 +121,6 @@ export type AsyncRunStatus =
|
|
|
88
121
|
| "exited"
|
|
89
122
|
| "cancelled"
|
|
90
123
|
| "killed";
|
|
91
|
-
export type RunOutboxDelivery = "log" | "notify" | "followup";
|
|
92
|
-
export type RunOutboxLevel = "info" | "warning" | "error";
|
|
93
|
-
|
|
94
|
-
export interface RunOutboxEvent {
|
|
95
|
-
body?: unknown;
|
|
96
|
-
correlation_id?: string;
|
|
97
|
-
data?: unknown;
|
|
98
|
-
delivery: RunOutboxDelivery;
|
|
99
|
-
event: string;
|
|
100
|
-
from?: string;
|
|
101
|
-
id: string;
|
|
102
|
-
level: RunOutboxLevel;
|
|
103
|
-
metadata?: Record<string, unknown>;
|
|
104
|
-
reply_to?: string;
|
|
105
|
-
run: string;
|
|
106
|
-
state_dir: string;
|
|
107
|
-
summary: string;
|
|
108
|
-
to?: string;
|
|
109
|
-
ts: string;
|
|
110
|
-
type?: string;
|
|
111
|
-
}
|
|
112
124
|
|
|
113
125
|
export interface AsyncRunMeta {
|
|
114
126
|
argv: string[];
|
|
@@ -127,8 +139,8 @@ export interface AsyncRunMeta {
|
|
|
127
139
|
values: Record<string, unknown>;
|
|
128
140
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
129
141
|
control?: AsyncRunControlEndpoint;
|
|
130
|
-
mailbox?:
|
|
131
|
-
recipe_context_records?:
|
|
142
|
+
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
143
|
+
recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
|
|
132
144
|
retire_when?: "children_terminal";
|
|
133
145
|
}
|
|
134
146
|
|
|
@@ -155,101 +167,13 @@ function asyncRunnerArgv(stateDir: string): string[] {
|
|
|
155
167
|
: ["--experimental-strip-types", RUNNER_PATH, stateDir];
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
|
|
159
|
-
const run = (value || `run-${Date.now()}`).trim();
|
|
160
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(run))
|
|
161
|
-
throw new Error(
|
|
162
|
-
"Run id may contain only letters, numbers, dot, underscore, and dash.",
|
|
163
|
-
);
|
|
164
|
-
return run;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export type RunArtifactDeclaration =
|
|
168
|
-
| string
|
|
169
|
-
| { path: string; kind?: string; media_type?: string; required?: boolean };
|
|
170
|
+
export { safeRunId } from "./runs-identity.ts";
|
|
170
171
|
|
|
171
|
-
export
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
required?: boolean;
|
|
177
|
-
sha256?: string;
|
|
178
|
-
size?: number;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function resolveArtifactPaths(
|
|
182
|
-
artifacts: Record<string, RunArtifactDeclaration> | undefined,
|
|
183
|
-
values: Record<string, unknown>,
|
|
184
|
-
): Record<string, RunArtifactDeclaration> | undefined {
|
|
185
|
-
if (!artifacts) return undefined;
|
|
186
|
-
const resolved: Record<string, RunArtifactDeclaration> = {};
|
|
187
|
-
for (const [key, value] of Object.entries(artifacts)) {
|
|
188
|
-
if (!key.trim()) continue;
|
|
189
|
-
if (typeof value === "string") {
|
|
190
|
-
resolved[key] = substituteCommandTemplateToken(
|
|
191
|
-
value,
|
|
192
|
-
values,
|
|
193
|
-
`recipe artifacts.${key}`,
|
|
194
|
-
);
|
|
195
|
-
} else if (
|
|
196
|
-
value &&
|
|
197
|
-
typeof value === "object" &&
|
|
198
|
-
typeof value.path === "string"
|
|
199
|
-
) {
|
|
200
|
-
resolved[key] = {
|
|
201
|
-
...value,
|
|
202
|
-
path: substituteCommandTemplateToken(
|
|
203
|
-
value.path,
|
|
204
|
-
values,
|
|
205
|
-
`recipe artifacts.${key}.path`,
|
|
206
|
-
),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function resolveArtifactManifest(
|
|
214
|
-
artifacts: Record<string, RunArtifactDeclaration> | undefined,
|
|
215
|
-
): Record<string, RunArtifactManifestEntry> | undefined {
|
|
216
|
-
if (!artifacts) return undefined;
|
|
217
|
-
const manifest: Record<string, RunArtifactManifestEntry> = {};
|
|
218
|
-
for (const [name, artifact] of Object.entries(artifacts)) {
|
|
219
|
-
const declaration =
|
|
220
|
-
typeof artifact === "string" ? { path: artifact } : artifact;
|
|
221
|
-
if (!declaration?.path) continue;
|
|
222
|
-
try {
|
|
223
|
-
const content = readFileSync(declaration.path);
|
|
224
|
-
manifest[name] = {
|
|
225
|
-
exists: true,
|
|
226
|
-
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
227
|
-
...(declaration.media_type
|
|
228
|
-
? { media_type: declaration.media_type }
|
|
229
|
-
: {}),
|
|
230
|
-
path: declaration.path,
|
|
231
|
-
...(declaration.required !== undefined
|
|
232
|
-
? { required: declaration.required }
|
|
233
|
-
: {}),
|
|
234
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
235
|
-
size: content.byteLength,
|
|
236
|
-
};
|
|
237
|
-
} catch {
|
|
238
|
-
manifest[name] = {
|
|
239
|
-
exists: false,
|
|
240
|
-
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
241
|
-
...(declaration.media_type
|
|
242
|
-
? { media_type: declaration.media_type }
|
|
243
|
-
: {}),
|
|
244
|
-
path: declaration.path,
|
|
245
|
-
...(declaration.required !== undefined
|
|
246
|
-
? { required: declaration.required }
|
|
247
|
-
: {}),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return Object.keys(manifest).length > 0 ? manifest : undefined;
|
|
252
|
-
}
|
|
172
|
+
export { resolveArtifactManifest } from "./runs-artifacts.ts";
|
|
173
|
+
export type {
|
|
174
|
+
RunArtifactDeclaration,
|
|
175
|
+
RunArtifactManifestEntry,
|
|
176
|
+
} from "./runs-artifacts.ts";
|
|
253
177
|
|
|
254
178
|
function resolveRunTemplate(params: AsyncRunStartParams): {
|
|
255
179
|
template: CommandTemplateValue;
|
|
@@ -284,20 +208,13 @@ function resolveStateDir(params: AsyncRunStartParams, run: string): string {
|
|
|
284
208
|
}
|
|
285
209
|
|
|
286
210
|
function assertNoActiveRunState(stateDir: string): void {
|
|
287
|
-
|
|
288
|
-
if (!meta) return;
|
|
289
|
-
const pid = Number(meta.pid || 0);
|
|
290
|
-
const cwd = String(meta.cwd ?? "");
|
|
291
|
-
if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir)) return;
|
|
292
|
-
throw new Error(
|
|
293
|
-
`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`,
|
|
294
|
-
);
|
|
211
|
+
RunsStart.assertNoActiveRunState(stateDir, readJson, RUNNER_PATH);
|
|
295
212
|
}
|
|
296
213
|
|
|
297
214
|
function resolveRecipeFile(file: string): string {
|
|
298
215
|
return (
|
|
299
|
-
|
|
300
|
-
|
|
216
|
+
RecipesReferences.getRecipePath(file, DEFAULT_RECIPE_ROOT) ??
|
|
217
|
+
RecipesReferences.resolveRecipePath(file, DEFAULT_RECIPE_ROOT)
|
|
301
218
|
);
|
|
302
219
|
}
|
|
303
220
|
|
|
@@ -309,10 +226,10 @@ function isMutableUsageRecipeFile(file: string): boolean {
|
|
|
309
226
|
|
|
310
227
|
function readRecipeFile(file: string): AsyncRunStartParams {
|
|
311
228
|
const path = resolveRecipeFile(file);
|
|
312
|
-
const raw =
|
|
229
|
+
const raw = RecipesReferences.readRawRecipeConfig(path);
|
|
313
230
|
const includeActorRecipeContext =
|
|
314
231
|
raw?.actor_context !== false && raw?.actor_context !== "off";
|
|
315
|
-
const config =
|
|
232
|
+
const config = RecipesReferences.readResolvedRecipeConfig(path, [], {
|
|
316
233
|
includeActorRecipeContext,
|
|
317
234
|
});
|
|
318
235
|
if (!config) {
|
|
@@ -356,128 +273,12 @@ function readJson(path: string): Record<string, unknown> | undefined {
|
|
|
356
273
|
).value;
|
|
357
274
|
}
|
|
358
275
|
|
|
359
|
-
function isAlive(pid: number): boolean {
|
|
360
|
-
try {
|
|
361
|
-
process.kill(pid, 0);
|
|
362
|
-
return true;
|
|
363
|
-
} catch {
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function pidMatchesRun(pid: number, cwd: string, stateDir: string): boolean {
|
|
369
|
-
if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
|
|
370
|
-
return isAlive(pid);
|
|
371
|
-
try {
|
|
372
|
-
const procCwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
373
|
-
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
374
|
-
return (
|
|
375
|
-
procCwd === resolve(cwd) &&
|
|
376
|
-
cmdline.includes(RUNNER_PATH) &&
|
|
377
|
-
cmdline.includes(stateDir)
|
|
378
|
-
);
|
|
379
|
-
} catch {
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function isWithinRunnerIdentityGrace(meta: Record<string, unknown>): boolean {
|
|
385
|
-
const createdAt =
|
|
386
|
-
typeof meta.createdAt === "string" ? Date.parse(meta.createdAt) : NaN;
|
|
387
|
-
return (
|
|
388
|
-
Number.isFinite(createdAt) &&
|
|
389
|
-
Date.now() - createdAt >= 0 &&
|
|
390
|
-
Date.now() - createdAt <= RUNNER_IDENTITY_GRACE_MS
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function tailFile(path: string, lines: number): string {
|
|
395
|
-
if (!existsSync(path)) return "";
|
|
396
|
-
const content = readFileSync(path, "utf8").trimEnd();
|
|
397
|
-
if (!content) return "";
|
|
398
|
-
return content.split("\n").slice(-lines).join("\n");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function tailLines(path: string, lines: number): string[] {
|
|
402
|
-
const content = tailFile(path, lines);
|
|
403
|
-
return content ? content.split("\n") : [];
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function getInterruptedRunStatus(
|
|
407
|
-
stateDir: string,
|
|
408
|
-
): "cancelled" | "killed" | undefined {
|
|
409
|
-
const events = readJsonlFileResilient<Record<string, unknown>>(
|
|
410
|
-
join(stateDir, "events.jsonl"),
|
|
411
|
-
).records.slice(-200);
|
|
412
|
-
for (const event of events.reverse()) {
|
|
413
|
-
if (event.event === "run.kill") return "killed";
|
|
414
|
-
if (event.event === "run.cancel") return "cancelled";
|
|
415
|
-
}
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
276
|
function acquireStateStartLock(stateDir: string): () => void {
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
mkdirSync(lockDir);
|
|
423
|
-
writeFileSync(
|
|
424
|
-
join(lockDir, "owner.json"),
|
|
425
|
-
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
|
|
426
|
-
"utf8",
|
|
427
|
-
);
|
|
428
|
-
} catch (error) {
|
|
429
|
-
try {
|
|
430
|
-
const stat = statSync(lockDir);
|
|
431
|
-
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
|
|
432
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
433
|
-
mkdirSync(lockDir);
|
|
434
|
-
writeFileSync(
|
|
435
|
-
join(lockDir, "owner.json"),
|
|
436
|
-
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`,
|
|
437
|
-
"utf8",
|
|
438
|
-
);
|
|
439
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
440
|
-
}
|
|
441
|
-
} catch {
|
|
442
|
-
// Keep the original lock acquisition error below.
|
|
443
|
-
}
|
|
444
|
-
throw new Error(
|
|
445
|
-
`Run state is already being started: ${stateDir}. Retry after the current start finishes.`,
|
|
446
|
-
{ cause: error },
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
277
|
+
return RunsStart.acquireStateStartLock(stateDir);
|
|
450
278
|
}
|
|
451
279
|
|
|
452
280
|
function prepareStateDirForStart(stateDir: string): void {
|
|
453
|
-
|
|
454
|
-
const existingPid = Number(existing?.pid || 0);
|
|
455
|
-
const existingCwd =
|
|
456
|
-
typeof existing?.cwd === "string" ? existing.cwd : undefined;
|
|
457
|
-
const existingResult = readJson(join(stateDir, "result.json"));
|
|
458
|
-
if (
|
|
459
|
-
!existingResult &&
|
|
460
|
-
existingPid &&
|
|
461
|
-
existingCwd &&
|
|
462
|
-
isAlive(existingPid) &&
|
|
463
|
-
pidMatchesRun(existingPid, existingCwd, stateDir)
|
|
464
|
-
) {
|
|
465
|
-
throw new Error(
|
|
466
|
-
`Run is already running: ${String(existing?.run ?? stateDir)}`,
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
for (const file of [
|
|
470
|
-
"events.jsonl",
|
|
471
|
-
"inbox.jsonl",
|
|
472
|
-
"outbox.jsonl",
|
|
473
|
-
"progress.json",
|
|
474
|
-
"result.json",
|
|
475
|
-
"stderr.log",
|
|
476
|
-
"stdout.log",
|
|
477
|
-
"terminal-handled.json",
|
|
478
|
-
]) {
|
|
479
|
-
rmSync(join(stateDir, file), { force: true });
|
|
480
|
-
}
|
|
281
|
+
RunsStart.prepareStateDirForStart(stateDir, readJson, RUNNER_PATH);
|
|
481
282
|
}
|
|
482
283
|
|
|
483
284
|
export function startRun(
|
|
@@ -505,10 +306,10 @@ export function startRun(
|
|
|
505
306
|
startParams.actor_context !== "off";
|
|
506
307
|
const recipeContextRecords =
|
|
507
308
|
recipeFile && includeActorRecipeContext
|
|
508
|
-
?
|
|
309
|
+
? RecipesReferences.buildRecipeContextRecords(recipeFile)
|
|
509
310
|
: undefined;
|
|
510
311
|
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
511
|
-
|
|
312
|
+
RecipesUsage.recordRecipeLaunch(
|
|
512
313
|
recipeFile,
|
|
513
314
|
new Date(),
|
|
514
315
|
startParams.launch_source === "tool" ? "tool" : "spawn",
|
|
@@ -585,78 +386,12 @@ export function startRun(
|
|
|
585
386
|
}
|
|
586
387
|
}
|
|
587
388
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function normalizeRunOutboxEvent(
|
|
597
|
-
raw: unknown,
|
|
598
|
-
run: string,
|
|
599
|
-
stateDir: string,
|
|
600
|
-
index: number,
|
|
601
|
-
): RunOutboxEvent | undefined {
|
|
602
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
603
|
-
const record = raw as Record<string, unknown>;
|
|
604
|
-
const event =
|
|
605
|
-
typeof record.event === "string" && record.event.trim()
|
|
606
|
-
? record.event.trim()
|
|
607
|
-
: "run.event";
|
|
608
|
-
const summary =
|
|
609
|
-
typeof record.summary === "string" && record.summary.trim()
|
|
610
|
-
? record.summary.trim()
|
|
611
|
-
: event;
|
|
612
|
-
const ts =
|
|
613
|
-
typeof record.ts === "string" && record.ts.trim()
|
|
614
|
-
? record.ts.trim()
|
|
615
|
-
: new Date(0).toISOString();
|
|
616
|
-
const id =
|
|
617
|
-
typeof record.id === "string" && record.id.trim()
|
|
618
|
-
? record.id.trim()
|
|
619
|
-
: `${run}:${index}`;
|
|
620
|
-
return {
|
|
621
|
-
...(record.body !== undefined ? { body: record.body } : {}),
|
|
622
|
-
...(typeof record.correlation_id === "string"
|
|
623
|
-
? { correlation_id: record.correlation_id }
|
|
624
|
-
: {}),
|
|
625
|
-
...(record.data !== undefined ? { data: record.data } : {}),
|
|
626
|
-
delivery: normalizeRunOutboxDelivery(record.delivery),
|
|
627
|
-
...(record.metadata &&
|
|
628
|
-
typeof record.metadata === "object" &&
|
|
629
|
-
!Array.isArray(record.metadata)
|
|
630
|
-
? { metadata: record.metadata as Record<string, unknown> }
|
|
631
|
-
: {}),
|
|
632
|
-
event,
|
|
633
|
-
...(typeof record.from === "string" ? { from: record.from } : {}),
|
|
634
|
-
id,
|
|
635
|
-
level: normalizeRunOutboxLevel(record.level),
|
|
636
|
-
...(typeof record.reply_to === "string"
|
|
637
|
-
? { reply_to: record.reply_to }
|
|
638
|
-
: {}),
|
|
639
|
-
run,
|
|
640
|
-
state_dir: stateDir,
|
|
641
|
-
summary,
|
|
642
|
-
...(typeof record.to === "string" ? { to: record.to } : {}),
|
|
643
|
-
ts,
|
|
644
|
-
...(typeof record.type === "string" ? { type: record.type } : {}),
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
export function parseRunOutboxEventLine(
|
|
649
|
-
line: string,
|
|
650
|
-
run: string,
|
|
651
|
-
stateDir: string,
|
|
652
|
-
index: number,
|
|
653
|
-
): RunOutboxEvent | undefined {
|
|
654
|
-
try {
|
|
655
|
-
return normalizeRunOutboxEvent(JSON.parse(line), run, stateDir, index);
|
|
656
|
-
} catch {
|
|
657
|
-
return undefined;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
389
|
+
export { parseRunOutboxEventLine } from "./runs-outbox.ts";
|
|
390
|
+
export type {
|
|
391
|
+
RunOutboxDelivery,
|
|
392
|
+
RunOutboxEvent,
|
|
393
|
+
RunOutboxLevel,
|
|
394
|
+
} from "./runs-outbox.ts";
|
|
660
395
|
|
|
661
396
|
export function getRunStatus(runOrDir: string): Record<string, unknown> {
|
|
662
397
|
const stateDir = resolve(
|
|
@@ -666,180 +401,43 @@ export function getRunStatus(runOrDir: string): Record<string, unknown> {
|
|
|
666
401
|
);
|
|
667
402
|
const meta = readJson(join(stateDir, "run.json"));
|
|
668
403
|
if (!meta) throw new Error(`Run not found: ${runOrDir}`);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
isWithinRunnerIdentityGrace(meta)),
|
|
404
|
+
return buildRunStatus(
|
|
405
|
+
stateDir,
|
|
406
|
+
runOrDir,
|
|
407
|
+
meta,
|
|
408
|
+
readJson,
|
|
409
|
+
RUNNER_PATH,
|
|
410
|
+
RUNNER_IDENTITY_GRACE_MS,
|
|
677
411
|
);
|
|
678
|
-
const status: AsyncRunStatus = result
|
|
679
|
-
? Number(result.code ?? 0) === 0
|
|
680
|
-
? "done"
|
|
681
|
-
: "failed"
|
|
682
|
-
: aliveOwnedRunner
|
|
683
|
-
? "running"
|
|
684
|
-
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
685
|
-
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
686
|
-
return {
|
|
687
|
-
...meta,
|
|
688
|
-
eventsFile: join(stateDir, "events.jsonl"),
|
|
689
|
-
inboxFile: join(stateDir, "inbox.jsonl"),
|
|
690
|
-
outboxFile: join(stateDir, "outbox.jsonl"),
|
|
691
|
-
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
692
|
-
result: result || null,
|
|
693
|
-
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
694
|
-
state_dir: String(meta.state_dir ?? stateDir),
|
|
695
|
-
stderrLog: join(stateDir, "stderr.log"),
|
|
696
|
-
stdoutLog: join(stateDir, "stdout.log"),
|
|
697
|
-
status,
|
|
698
|
-
};
|
|
699
412
|
}
|
|
700
413
|
|
|
701
|
-
export
|
|
702
|
-
ownerId?: string;
|
|
703
|
-
recipe?: string;
|
|
704
|
-
run: string;
|
|
705
|
-
state_dir: string;
|
|
706
|
-
status: AsyncRunStatus;
|
|
707
|
-
tool?: string;
|
|
708
|
-
updated_at?: string;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function matchesStatusFilter(
|
|
712
|
-
status: unknown,
|
|
713
|
-
filter: string | undefined,
|
|
714
|
-
): boolean {
|
|
715
|
-
if (!filter || filter === "all") return true;
|
|
716
|
-
if (filter === "active") return status === "running";
|
|
717
|
-
if (filter === "terminal") return status !== "running";
|
|
718
|
-
return status === filter;
|
|
719
|
-
}
|
|
414
|
+
export type { RunStateIndexEntry } from "./runs-index.ts";
|
|
720
415
|
|
|
721
416
|
export function listRunStateDirs(
|
|
722
417
|
stateRoot = DEFAULT_STATE_ROOT,
|
|
723
418
|
depth = 0,
|
|
724
419
|
seen = new Set<string>(),
|
|
725
420
|
): string[] {
|
|
726
|
-
|
|
727
|
-
seen.add(resolve(stateRoot));
|
|
728
|
-
let entries: import("node:fs").Dirent[];
|
|
729
|
-
try {
|
|
730
|
-
entries = readdirSync(stateRoot, { withFileTypes: true });
|
|
731
|
-
} catch {
|
|
732
|
-
return [];
|
|
733
|
-
}
|
|
734
|
-
const result: string[] = [];
|
|
735
|
-
for (const entry of entries) {
|
|
736
|
-
if (!entry.isDirectory()) continue;
|
|
737
|
-
const child = join(stateRoot, entry.name);
|
|
738
|
-
if (existsSync(join(child, "run.json"))) result.push(child);
|
|
739
|
-
if (depth + 1 < 8) result.push(...listRunStateDirs(child, depth + 1, seen));
|
|
740
|
-
}
|
|
741
|
-
return result;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function runIndexPath(stateRoot: string): string {
|
|
745
|
-
return join(stateRoot, "index.json");
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function indexEntryFromStatus(
|
|
749
|
-
status: Record<string, unknown>,
|
|
750
|
-
): RunStateIndexEntry {
|
|
751
|
-
const progress =
|
|
752
|
-
status.progress && typeof status.progress === "object"
|
|
753
|
-
? (status.progress as Record<string, unknown>)
|
|
754
|
-
: {};
|
|
755
|
-
return {
|
|
756
|
-
...(typeof status.ownerId === "string" ? { ownerId: status.ownerId } : {}),
|
|
757
|
-
...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
|
|
758
|
-
run: String(status.run),
|
|
759
|
-
state_dir: String(status.state_dir),
|
|
760
|
-
status: status.status as AsyncRunStatus,
|
|
761
|
-
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
762
|
-
updated_at:
|
|
763
|
-
typeof progress.updatedAt === "string"
|
|
764
|
-
? progress.updatedAt
|
|
765
|
-
: typeof status.createdAt === "string"
|
|
766
|
-
? status.createdAt
|
|
767
|
-
: new Date(0).toISOString(),
|
|
768
|
-
};
|
|
421
|
+
return RunsIndex.listRunStateDirs(stateRoot, depth, seen);
|
|
769
422
|
}
|
|
770
423
|
|
|
771
424
|
export function rebuildRunStateIndex(
|
|
772
425
|
stateRoot = DEFAULT_STATE_ROOT,
|
|
773
|
-
): RunStateIndexEntry[] {
|
|
774
|
-
|
|
775
|
-
const entries = listRunStateDirs(stateRoot)
|
|
776
|
-
.flatMap((stateDir) => {
|
|
777
|
-
try {
|
|
778
|
-
return [indexEntryFromStatus(getRunStatus(stateDir))];
|
|
779
|
-
} catch {
|
|
780
|
-
return [];
|
|
781
|
-
}
|
|
782
|
-
})
|
|
783
|
-
.sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
|
|
784
|
-
writeJsonAtomic(runIndexPath(stateRoot), {
|
|
785
|
-
entries,
|
|
786
|
-
rebuilt_at: new Date().toISOString(),
|
|
787
|
-
});
|
|
788
|
-
return entries;
|
|
426
|
+
): RunsIndex.RunStateIndexEntry[] {
|
|
427
|
+
return RunsIndex.rebuildRunStateIndex(stateRoot, getRunStatus);
|
|
789
428
|
}
|
|
790
429
|
|
|
791
430
|
export function readRunStateIndex(
|
|
792
431
|
stateRoot = DEFAULT_STATE_ROOT,
|
|
793
|
-
): RunStateIndexEntry[] | undefined {
|
|
794
|
-
|
|
795
|
-
if (!index || typeof index !== "object") return undefined;
|
|
796
|
-
const entries = (index as Record<string, unknown>).entries;
|
|
797
|
-
if (!Array.isArray(entries)) return undefined;
|
|
798
|
-
const valid = entries.filter((entry): entry is RunStateIndexEntry => {
|
|
799
|
-
const record =
|
|
800
|
-
entry && typeof entry === "object"
|
|
801
|
-
? (entry as Record<string, unknown>)
|
|
802
|
-
: {};
|
|
803
|
-
return (
|
|
804
|
-
typeof record.run === "string" &&
|
|
805
|
-
typeof record.state_dir === "string" &&
|
|
806
|
-
typeof record.status === "string"
|
|
807
|
-
);
|
|
808
|
-
});
|
|
809
|
-
if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
|
|
810
|
-
return undefined;
|
|
811
|
-
const indexedDirs = new Set(valid.map((entry) => resolve(entry.state_dir)));
|
|
812
|
-
const stateDirs = listRunStateDirs(stateRoot).map((stateDir) => resolve(stateDir));
|
|
813
|
-
if (stateDirs.some((stateDir) => !indexedDirs.has(stateDir))) return undefined;
|
|
814
|
-
return valid;
|
|
432
|
+
): RunsIndex.RunStateIndexEntry[] | undefined {
|
|
433
|
+
return RunsIndex.readRunStateIndex(stateRoot, readJson);
|
|
815
434
|
}
|
|
816
435
|
|
|
817
436
|
export function listRuns(
|
|
818
437
|
stateRoot = DEFAULT_STATE_ROOT,
|
|
819
438
|
statusFilter?: string,
|
|
820
439
|
): Array<Record<string, unknown>> {
|
|
821
|
-
|
|
822
|
-
const indexed = readRunStateIndex(stateRoot);
|
|
823
|
-
if (indexed) {
|
|
824
|
-
return indexed
|
|
825
|
-
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
826
|
-
.map((entry) => ({
|
|
827
|
-
run: entry.run,
|
|
828
|
-
state_dir: entry.state_dir,
|
|
829
|
-
status: entry.status,
|
|
830
|
-
...(entry.tool ? { tool: entry.tool } : {}),
|
|
831
|
-
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
832
|
-
}));
|
|
833
|
-
}
|
|
834
|
-
return rebuildRunStateIndex(stateRoot)
|
|
835
|
-
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
836
|
-
.map((entry) => ({
|
|
837
|
-
run: entry.run,
|
|
838
|
-
state_dir: entry.state_dir,
|
|
839
|
-
status: entry.status,
|
|
840
|
-
...(entry.tool ? { tool: entry.tool } : {}),
|
|
841
|
-
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
842
|
-
}));
|
|
440
|
+
return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
|
|
843
441
|
}
|
|
844
442
|
|
|
845
443
|
export function tailRun(runOrDir: string, lines = 40): string {
|
|
@@ -862,86 +460,11 @@ export function readRunEvents(runOrDir: string, lines = 40): RunOutboxEvent[] {
|
|
|
862
460
|
.filter((event): event is RunOutboxEvent => Boolean(event));
|
|
863
461
|
}
|
|
864
462
|
|
|
865
|
-
export type
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
| "failed";
|
|
871
|
-
|
|
872
|
-
export type RunInboxMessage = Record<string, unknown> & {
|
|
873
|
-
id?: string;
|
|
874
|
-
status?: RunInboxStatus | string;
|
|
875
|
-
};
|
|
876
|
-
|
|
877
|
-
export interface ProcessRunInboxResult {
|
|
878
|
-
claimed: number;
|
|
879
|
-
failed: number;
|
|
880
|
-
handled: number;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function runInboxFile(stateDir: string): string {
|
|
884
|
-
return join(stateDir, "inbox.jsonl");
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function sleepSync(ms: number): void {
|
|
888
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function acquireRunInboxLock(stateDir: string): () => void {
|
|
892
|
-
const lockDir = join(stateDir, ".inbox.lock");
|
|
893
|
-
const started = Date.now();
|
|
894
|
-
while (true) {
|
|
895
|
-
try {
|
|
896
|
-
mkdirSync(lockDir, { recursive: false });
|
|
897
|
-
writeFileSync(
|
|
898
|
-
join(lockDir, "owner.json"),
|
|
899
|
-
`${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`,
|
|
900
|
-
"utf8",
|
|
901
|
-
);
|
|
902
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
903
|
-
} catch (error) {
|
|
904
|
-
try {
|
|
905
|
-
const stat = statSync(lockDir);
|
|
906
|
-
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
|
|
907
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
} catch {
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
if (Date.now() - started > RUN_INBOX_LOCK_TIMEOUT_MS) {
|
|
914
|
-
throw new Error("Run inbox lock timed out.", { cause: error });
|
|
915
|
-
}
|
|
916
|
-
sleepSync(10);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function parseRunInboxLine(line: string): RunInboxMessage | undefined {
|
|
922
|
-
try {
|
|
923
|
-
return JSON.parse(line) as RunInboxMessage;
|
|
924
|
-
} catch {
|
|
925
|
-
return undefined;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function readRunInboxMessagesFromStateDir(stateDir: string): RunInboxMessage[] {
|
|
930
|
-
return readJsonlFileResilient<RunInboxMessage>(runInboxFile(stateDir)).records;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function writeRunInboxMessages(
|
|
934
|
-
stateDir: string,
|
|
935
|
-
messages: RunInboxMessage[],
|
|
936
|
-
): void {
|
|
937
|
-
writeFileSync(
|
|
938
|
-
runInboxFile(stateDir),
|
|
939
|
-
messages.length
|
|
940
|
-
? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n`
|
|
941
|
-
: "",
|
|
942
|
-
"utf8",
|
|
943
|
-
);
|
|
944
|
-
}
|
|
463
|
+
export type {
|
|
464
|
+
ProcessRunInboxResult,
|
|
465
|
+
RunInboxMessage,
|
|
466
|
+
RunInboxStatus,
|
|
467
|
+
} from "./runs-mailbox.ts";
|
|
945
468
|
|
|
946
469
|
export function readRunInboxMessages(
|
|
947
470
|
runOrDir: string,
|
|
@@ -962,26 +485,12 @@ export function updateRunInboxMessageStatus(
|
|
|
962
485
|
): boolean {
|
|
963
486
|
const status = getRunStatus(runOrDir);
|
|
964
487
|
const stateDir = String(status.state_dir);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (message.id !== id) return message;
|
|
972
|
-
changed = true;
|
|
973
|
-
return {
|
|
974
|
-
...message,
|
|
975
|
-
...metadata,
|
|
976
|
-
[timestampKey]: new Date().toISOString(),
|
|
977
|
-
status: nextStatus,
|
|
978
|
-
};
|
|
979
|
-
});
|
|
980
|
-
if (changed) writeRunInboxMessages(stateDir, updated);
|
|
981
|
-
return changed;
|
|
982
|
-
} finally {
|
|
983
|
-
releaseLock();
|
|
984
|
-
}
|
|
488
|
+
return updateRunInboxMessageStatusInStateDir(
|
|
489
|
+
stateDir,
|
|
490
|
+
id,
|
|
491
|
+
nextStatus,
|
|
492
|
+
metadata,
|
|
493
|
+
);
|
|
985
494
|
}
|
|
986
495
|
|
|
987
496
|
export function claimRunInboxMessage(
|
|
@@ -991,29 +500,7 @@ export function claimRunInboxMessage(
|
|
|
991
500
|
): RunInboxMessage | undefined {
|
|
992
501
|
const status = getRunStatus(runOrDir);
|
|
993
502
|
const stateDir = String(status.state_dir);
|
|
994
|
-
|
|
995
|
-
try {
|
|
996
|
-
const messages = readRunInboxMessagesFromStateDir(stateDir);
|
|
997
|
-
const index = messages.findIndex((message) =>
|
|
998
|
-
statuses.includes(String(message.status ?? "queued")),
|
|
999
|
-
);
|
|
1000
|
-
if (index < 0) return undefined;
|
|
1001
|
-
const claimed = {
|
|
1002
|
-
...messages[index],
|
|
1003
|
-
claimed_at: new Date().toISOString(),
|
|
1004
|
-
claimed_by: owner,
|
|
1005
|
-
id:
|
|
1006
|
-
typeof messages[index].id === "string"
|
|
1007
|
-
? messages[index].id
|
|
1008
|
-
: randomUUID(),
|
|
1009
|
-
status: "claimed",
|
|
1010
|
-
} satisfies RunInboxMessage;
|
|
1011
|
-
messages[index] = claimed;
|
|
1012
|
-
writeRunInboxMessages(stateDir, messages);
|
|
1013
|
-
return claimed;
|
|
1014
|
-
} finally {
|
|
1015
|
-
releaseLock();
|
|
1016
|
-
}
|
|
503
|
+
return claimRunInboxMessageInStateDir(stateDir, owner, statuses);
|
|
1017
504
|
}
|
|
1018
505
|
|
|
1019
506
|
export async function processRunInboxMessages(
|
|
@@ -1021,29 +508,12 @@ export async function processRunInboxMessages(
|
|
|
1021
508
|
handler: (message: RunInboxMessage) => Promise<void> | void,
|
|
1022
509
|
options: { limit?: number; owner?: string; statuses?: string[] } = {},
|
|
1023
510
|
): Promise<ProcessRunInboxResult> {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
result.claimed += 1;
|
|
1031
|
-
try {
|
|
1032
|
-
await handler(message);
|
|
1033
|
-
if (updateRunInboxMessageStatus(runOrDir, message.id, "handled")) {
|
|
1034
|
-
result.handled += 1;
|
|
1035
|
-
}
|
|
1036
|
-
} catch (error) {
|
|
1037
|
-
if (
|
|
1038
|
-
updateRunInboxMessageStatus(runOrDir, message.id, "failed", {
|
|
1039
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1040
|
-
})
|
|
1041
|
-
) {
|
|
1042
|
-
result.failed += 1;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
return result;
|
|
511
|
+
const status = getRunStatus(runOrDir);
|
|
512
|
+
return processRunInboxMessagesInStateDir(
|
|
513
|
+
String(status.state_dir),
|
|
514
|
+
handler,
|
|
515
|
+
options,
|
|
516
|
+
);
|
|
1047
517
|
}
|
|
1048
518
|
|
|
1049
519
|
export function appendRunOutboxEvent(
|
|
@@ -1066,32 +536,7 @@ export function appendRunOutboxEvent(
|
|
|
1066
536
|
const status = getRunStatus(runOrDir);
|
|
1067
537
|
const stateDir = String(status.state_dir);
|
|
1068
538
|
const run = String(status.run ?? runOrDir);
|
|
1069
|
-
const
|
|
1070
|
-
const to = event.to || "coordinator";
|
|
1071
|
-
const metadata = event.metadata ?? {};
|
|
1072
|
-
const requiresResponse = metadata.requires_response === true;
|
|
1073
|
-
const payload = {
|
|
1074
|
-
...(event.body !== undefined ? { body: event.body } : {}),
|
|
1075
|
-
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
1076
|
-
...(event.data !== undefined ? { data: event.data } : {}),
|
|
1077
|
-
delivery: normalizeRunOutboxDelivery(
|
|
1078
|
-
event.delivery ??
|
|
1079
|
-
(requiresResponse
|
|
1080
|
-
? "followup"
|
|
1081
|
-
: to === "coordinator"
|
|
1082
|
-
? "notify"
|
|
1083
|
-
: "log"),
|
|
1084
|
-
),
|
|
1085
|
-
event: type,
|
|
1086
|
-
from: event.from || `run:${run}`,
|
|
1087
|
-
level: normalizeRunOutboxLevel(event.level),
|
|
1088
|
-
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
1089
|
-
...(event.reply_to ? { reply_to: event.reply_to } : {}),
|
|
1090
|
-
summary: event.summary || type,
|
|
1091
|
-
to,
|
|
1092
|
-
ts: new Date().toISOString(),
|
|
1093
|
-
type,
|
|
1094
|
-
};
|
|
539
|
+
const payload = buildRunOutboxEventPayload(run, event);
|
|
1095
540
|
const line = `${JSON.stringify(payload)}\n`;
|
|
1096
541
|
writeFileSync(join(stateDir, "outbox.jsonl"), line, { flag: "a" });
|
|
1097
542
|
return {
|
|
@@ -1103,148 +548,7 @@ export function appendRunOutboxEvent(
|
|
|
1103
548
|
};
|
|
1104
549
|
}
|
|
1105
550
|
|
|
1106
|
-
export
|
|
1107
|
-
namedPipeSend?: (path: string, payload: string) => Promise<number>;
|
|
1108
|
-
platform?: NodeJS.Platform;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function getRunControlEndpoint(
|
|
1112
|
-
status: Record<string, unknown>,
|
|
1113
|
-
stateDir: string,
|
|
1114
|
-
): AsyncRunControlEndpoint {
|
|
1115
|
-
const control = status.control;
|
|
1116
|
-
if (control && typeof control === "object" && !Array.isArray(control)) {
|
|
1117
|
-
const record = control as Record<string, unknown>;
|
|
1118
|
-
if (
|
|
1119
|
-
(record.type === "fifo" ||
|
|
1120
|
-
record.type === "mailbox" ||
|
|
1121
|
-
record.type === "named-pipe") &&
|
|
1122
|
-
typeof record.path === "string" &&
|
|
1123
|
-
record.path.trim()
|
|
1124
|
-
) {
|
|
1125
|
-
return { path: record.path, type: record.type };
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
return { path: join(stateDir, "control.fifo"), type: "fifo" };
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function appendRunInboxMessage(stateDir: string, message: string): string {
|
|
1132
|
-
const id = randomUUID();
|
|
1133
|
-
const ts = new Date().toISOString();
|
|
1134
|
-
let record: Record<string, unknown>;
|
|
1135
|
-
try {
|
|
1136
|
-
const parsed = JSON.parse(message) as unknown;
|
|
1137
|
-
record =
|
|
1138
|
-
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1139
|
-
? (parsed as Record<string, unknown>)
|
|
1140
|
-
: { body: parsed, type: "run.message" };
|
|
1141
|
-
} catch {
|
|
1142
|
-
record = { body: message, type: "run.message" };
|
|
1143
|
-
}
|
|
1144
|
-
const releaseLock = acquireRunInboxLock(stateDir);
|
|
1145
|
-
try {
|
|
1146
|
-
writeFileSync(
|
|
1147
|
-
runInboxFile(stateDir),
|
|
1148
|
-
`${JSON.stringify({ ...record, id, queued_at: ts, received_at: ts, status: "queued" })}\n`,
|
|
1149
|
-
{ flag: "a" },
|
|
1150
|
-
);
|
|
1151
|
-
} finally {
|
|
1152
|
-
releaseLock();
|
|
1153
|
-
}
|
|
1154
|
-
return id;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function writeRunMessageReceipt(
|
|
1158
|
-
stateDir: string,
|
|
1159
|
-
message: string,
|
|
1160
|
-
bytes: number,
|
|
1161
|
-
inboxId: string,
|
|
1162
|
-
): void {
|
|
1163
|
-
const trimmedMessage = message.trim().toLowerCase();
|
|
1164
|
-
const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(
|
|
1165
|
-
trimmedMessage,
|
|
1166
|
-
);
|
|
1167
|
-
const ts = new Date().toISOString();
|
|
1168
|
-
writeFileSync(
|
|
1169
|
-
join(stateDir, "events.jsonl"),
|
|
1170
|
-
`${JSON.stringify({ bytes, event: "run.message", inbox_id: inboxId, terminal: terminalMessage || undefined, ts })}\n`,
|
|
1171
|
-
{ flag: "a" },
|
|
1172
|
-
);
|
|
1173
|
-
updateRunInboxMessageStatus(stateDir, inboxId, "sent", { bytes });
|
|
1174
|
-
if (terminalMessage) {
|
|
1175
|
-
markTerminalHandled(stateDir, {
|
|
1176
|
-
event: "run.message",
|
|
1177
|
-
message: trimmedMessage,
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function sendRunMessageToFifo(
|
|
1183
|
-
endpoint: AsyncRunControlEndpoint,
|
|
1184
|
-
payload: string,
|
|
1185
|
-
): number {
|
|
1186
|
-
if (!existsSync(endpoint.path))
|
|
1187
|
-
throw new Error(`Run control FIFO not found: ${endpoint.path}`);
|
|
1188
|
-
const stat = statSync(endpoint.path);
|
|
1189
|
-
if ((stat.mode & constants.S_IFMT) !== constants.S_IFIFO) {
|
|
1190
|
-
throw new Error(`Run control endpoint is not a FIFO: ${endpoint.path}`);
|
|
1191
|
-
}
|
|
1192
|
-
let fd: number | undefined;
|
|
1193
|
-
try {
|
|
1194
|
-
fd = openSync(endpoint.path, constants.O_WRONLY | constants.O_NONBLOCK);
|
|
1195
|
-
return writeSync(fd, payload);
|
|
1196
|
-
} finally {
|
|
1197
|
-
if (fd !== undefined) closeSync(fd);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
function notifyRunMessageWake(
|
|
1202
|
-
stateDir: string,
|
|
1203
|
-
run: string,
|
|
1204
|
-
bytes: number,
|
|
1205
|
-
endpoint: AsyncRunControlEndpoint,
|
|
1206
|
-
inboxId: string,
|
|
1207
|
-
): Record<string, unknown> | undefined {
|
|
1208
|
-
try {
|
|
1209
|
-
const event = notifyRuntimeWake(stateDir, {
|
|
1210
|
-
actor: `run:${run}`,
|
|
1211
|
-
metadata: { bytes, control_type: endpoint.type, inbox_id: inboxId },
|
|
1212
|
-
reason: "run.message",
|
|
1213
|
-
});
|
|
1214
|
-
return { wake: "wake.jsonl", wake_id: event.id };
|
|
1215
|
-
} catch {
|
|
1216
|
-
return undefined;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
function sendRunMessageToNamedPipe(
|
|
1221
|
-
endpoint: AsyncRunControlEndpoint,
|
|
1222
|
-
payload: string,
|
|
1223
|
-
send?: (path: string, payload: string) => Promise<number>,
|
|
1224
|
-
): Promise<number> {
|
|
1225
|
-
if (send) return send(endpoint.path, payload);
|
|
1226
|
-
return new Promise((resolve, reject) => {
|
|
1227
|
-
const socket = createConnection(endpoint.path);
|
|
1228
|
-
let settled = false;
|
|
1229
|
-
const timeout = setTimeout(() => {
|
|
1230
|
-
if (settled) return;
|
|
1231
|
-
settled = true;
|
|
1232
|
-
socket.destroy();
|
|
1233
|
-
reject(new Error("named pipe connection timed out"));
|
|
1234
|
-
}, 5000);
|
|
1235
|
-
const finish = (error?: Error): void => {
|
|
1236
|
-
if (settled) return;
|
|
1237
|
-
settled = true;
|
|
1238
|
-
clearTimeout(timeout);
|
|
1239
|
-
if (error) reject(error);
|
|
1240
|
-
else resolve(Buffer.byteLength(payload));
|
|
1241
|
-
};
|
|
1242
|
-
socket.on("error", finish);
|
|
1243
|
-
socket.on("connect", () => {
|
|
1244
|
-
socket.end(payload, () => finish());
|
|
1245
|
-
});
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
551
|
+
export type { SendRunMessageOptions } from "./runs-messages.ts";
|
|
1248
552
|
|
|
1249
553
|
export async function sendRunMessage(
|
|
1250
554
|
runOrDir: string,
|
|
@@ -1259,155 +563,15 @@ export async function sendRunMessage(
|
|
|
1259
563
|
const pid = Number(status.pid || 0);
|
|
1260
564
|
if (!pid || !isAlive(pid)) throw new Error(`Run pid is not alive: ${run}`);
|
|
1261
565
|
if (
|
|
1262
|
-
!pidMatchesRun(pid, String(status.cwd), stateDir) &&
|
|
1263
|
-
!isWithinRunnerIdentityGrace(status)
|
|
566
|
+
!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH) &&
|
|
567
|
+
!isWithinRunnerIdentityGrace(status, RUNNER_IDENTITY_GRACE_MS)
|
|
1264
568
|
)
|
|
1265
569
|
throw new Error(`Run pid owner mismatch: ${run}`);
|
|
1266
|
-
|
|
1267
|
-
const payload = message.endsWith("\n") ? message : `${message}\n`;
|
|
1268
|
-
const payloadBytes = Buffer.byteLength(payload);
|
|
1269
|
-
const inboxId = appendRunInboxMessage(stateDir, message);
|
|
1270
|
-
const wake = notifyRunMessageWake(
|
|
1271
|
-
stateDir,
|
|
1272
|
-
run,
|
|
1273
|
-
payloadBytes,
|
|
1274
|
-
endpoint,
|
|
1275
|
-
inboxId,
|
|
1276
|
-
);
|
|
1277
|
-
const runtimePlatform = options.platform ?? process.platform;
|
|
1278
|
-
if (endpoint.type === "mailbox") {
|
|
1279
|
-
return {
|
|
1280
|
-
...(wake ?? {}),
|
|
1281
|
-
bytes: payloadBytes,
|
|
1282
|
-
control: "inbox.jsonl",
|
|
1283
|
-
control_path: endpoint.path,
|
|
1284
|
-
control_type: endpoint.type,
|
|
1285
|
-
inbox_id: inboxId,
|
|
1286
|
-
queued: true,
|
|
1287
|
-
run,
|
|
1288
|
-
sent: true,
|
|
1289
|
-
state_dir: stateDir,
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
try {
|
|
1293
|
-
if (endpoint.type === "fifo") {
|
|
1294
|
-
if (runtimePlatform === "win32") {
|
|
1295
|
-
throw new Error(
|
|
1296
|
-
"run actor messages on native Windows require a named-pipe control endpoint; this recipe still exposes Unix FIFO control.",
|
|
1297
|
-
);
|
|
1298
|
-
}
|
|
1299
|
-
const bytes = sendRunMessageToFifo(endpoint, payload);
|
|
1300
|
-
writeRunMessageReceipt(stateDir, message, bytes, inboxId);
|
|
1301
|
-
return {
|
|
1302
|
-
...(wake ?? {}),
|
|
1303
|
-
bytes,
|
|
1304
|
-
control: "control.fifo",
|
|
1305
|
-
control_path: endpoint.path,
|
|
1306
|
-
control_type: endpoint.type,
|
|
1307
|
-
inbox_id: inboxId,
|
|
1308
|
-
run,
|
|
1309
|
-
sent: true,
|
|
1310
|
-
state_dir: stateDir,
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
const bytes = await sendRunMessageToNamedPipe(
|
|
1314
|
-
endpoint,
|
|
1315
|
-
payload,
|
|
1316
|
-
options.namedPipeSend,
|
|
1317
|
-
);
|
|
1318
|
-
writeRunMessageReceipt(stateDir, message, bytes, inboxId);
|
|
1319
|
-
return {
|
|
1320
|
-
...(wake ?? {}),
|
|
1321
|
-
bytes,
|
|
1322
|
-
control: endpoint.path,
|
|
1323
|
-
control_path: endpoint.path,
|
|
1324
|
-
control_type: endpoint.type,
|
|
1325
|
-
inbox_id: inboxId,
|
|
1326
|
-
run,
|
|
1327
|
-
sent: true,
|
|
1328
|
-
state_dir: stateDir,
|
|
1329
|
-
};
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
const deliveryError =
|
|
1332
|
-
error instanceof Error ? error.message : String(error);
|
|
1333
|
-
throw Object.assign(
|
|
1334
|
-
new Error(
|
|
1335
|
-
`Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`,
|
|
1336
|
-
),
|
|
1337
|
-
{
|
|
1338
|
-
control_path: endpoint.path,
|
|
1339
|
-
control_type: endpoint.type,
|
|
1340
|
-
delivery_error: deliveryError,
|
|
1341
|
-
inbox_id: inboxId,
|
|
1342
|
-
queued: true,
|
|
1343
|
-
run,
|
|
1344
|
-
sent: false,
|
|
1345
|
-
state_dir: stateDir,
|
|
1346
|
-
},
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
export interface RunProcessSignalPlan {
|
|
1352
|
-
args?: string[];
|
|
1353
|
-
command?: string;
|
|
1354
|
-
signalTarget: "processGroup" | "process" | "processTree";
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
export function getRunProcessSignalPlan(
|
|
1358
|
-
pid: number,
|
|
1359
|
-
signal: NodeJS.Signals,
|
|
1360
|
-
runtimePlatform: NodeJS.Platform = process.platform,
|
|
1361
|
-
): RunProcessSignalPlan {
|
|
1362
|
-
if (runtimePlatform === "win32") {
|
|
1363
|
-
return {
|
|
1364
|
-
args: [
|
|
1365
|
-
"/PID",
|
|
1366
|
-
String(pid),
|
|
1367
|
-
"/T",
|
|
1368
|
-
...(signal === "SIGKILL" ? ["/F"] : []),
|
|
1369
|
-
],
|
|
1370
|
-
command: "taskkill",
|
|
1371
|
-
signalTarget: "processTree",
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
return { signalTarget: "processGroup" };
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function signalOwnedRunProcess(
|
|
1378
|
-
pid: number,
|
|
1379
|
-
signal: NodeJS.Signals,
|
|
1380
|
-
): RunProcessSignalPlan {
|
|
1381
|
-
const plan = getRunProcessSignalPlan(pid, signal);
|
|
1382
|
-
if (plan.command && plan.args) {
|
|
1383
|
-
const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
|
|
1384
|
-
if (result.status !== 0) {
|
|
1385
|
-
throw new Error(
|
|
1386
|
-
result.stderr?.trim() ||
|
|
1387
|
-
result.stdout?.trim() ||
|
|
1388
|
-
`${plan.command} failed`,
|
|
1389
|
-
);
|
|
1390
|
-
}
|
|
1391
|
-
return plan;
|
|
1392
|
-
}
|
|
1393
|
-
try {
|
|
1394
|
-
process.kill(-pid, signal);
|
|
1395
|
-
return { signalTarget: "processGroup" };
|
|
1396
|
-
} catch {
|
|
1397
|
-
process.kill(pid, signal);
|
|
1398
|
-
return { signalTarget: "process" };
|
|
1399
|
-
}
|
|
570
|
+
return deliverRunMessage(status, run, stateDir, message, options);
|
|
1400
571
|
}
|
|
1401
572
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
details: Record<string, unknown>,
|
|
1405
|
-
): void {
|
|
1406
|
-
writeJsonAtomic(join(stateDir, "terminal-handled.json"), {
|
|
1407
|
-
...details,
|
|
1408
|
-
ts: new Date().toISOString(),
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
573
|
+
export { getRunProcessSignalPlan } from "./runs-control.ts";
|
|
574
|
+
export type { RunProcessSignalPlan } from "./runs-control.ts";
|
|
1411
575
|
|
|
1412
576
|
function markTerminalProgress(
|
|
1413
577
|
stateDir: string,
|
|
@@ -1417,15 +581,11 @@ function markTerminalProgress(
|
|
|
1417
581
|
const progress =
|
|
1418
582
|
existing && typeof existing === "object" && !Array.isArray(existing)
|
|
1419
583
|
? (existing as Record<string, unknown>)
|
|
1420
|
-
:
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
failures: Array.isArray(progress.failures) ? progress.failures : [],
|
|
1426
|
-
phase,
|
|
1427
|
-
updatedAt: new Date().toISOString(),
|
|
1428
|
-
});
|
|
584
|
+
: undefined;
|
|
585
|
+
writeJsonAtomic(
|
|
586
|
+
join(stateDir, "progress.json"),
|
|
587
|
+
buildTerminalProgress(progress, phase),
|
|
588
|
+
);
|
|
1429
589
|
}
|
|
1430
590
|
|
|
1431
591
|
function stopRun(
|
|
@@ -1440,7 +600,7 @@ function stopRun(
|
|
|
1440
600
|
return { stopped: false, reason: "not running", status };
|
|
1441
601
|
if (!pid || !isAlive(pid))
|
|
1442
602
|
return { stopped: false, reason: "pid not alive", status };
|
|
1443
|
-
if (!pidMatchesRun(pid, String(status.cwd), stateDir)) {
|
|
603
|
+
if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH)) {
|
|
1444
604
|
return { stopped: false, reason: "pid owner mismatch", status };
|
|
1445
605
|
}
|
|
1446
606
|
const signalResult = signalOwnedRunProcess(pid, signal);
|
|
@@ -1470,67 +630,15 @@ function assertTerminalRun(runOrDir: string): Record<string, unknown> {
|
|
|
1470
630
|
return status;
|
|
1471
631
|
}
|
|
1472
632
|
|
|
1473
|
-
function archivePathFor(run: string, stateDir: string): string {
|
|
1474
|
-
const archiveRoot = join(dirname(stateDir), "archived");
|
|
1475
|
-
mkdirSync(archiveRoot, { recursive: true });
|
|
1476
|
-
return join(
|
|
1477
|
-
archiveRoot,
|
|
1478
|
-
`${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
633
|
export function archiveRun(runOrDir: string): Record<string, unknown> {
|
|
1483
|
-
|
|
1484
|
-
const stateDir = String(status.state_dir);
|
|
1485
|
-
const run = String(status.run ?? basename(stateDir));
|
|
1486
|
-
const archiveDir = archivePathFor(run, stateDir);
|
|
1487
|
-
renameSync(stateDir, archiveDir);
|
|
1488
|
-
mkdirSync(stateDir, { recursive: true });
|
|
1489
|
-
const tombstone = {
|
|
1490
|
-
archived: true,
|
|
1491
|
-
archive_dir: archiveDir,
|
|
1492
|
-
original_state_dir: stateDir,
|
|
1493
|
-
run,
|
|
1494
|
-
status: status.status,
|
|
1495
|
-
ts: new Date().toISOString(),
|
|
1496
|
-
};
|
|
1497
|
-
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
1498
|
-
return tombstone;
|
|
634
|
+
return archiveTerminalRun(assertTerminalRun(runOrDir));
|
|
1499
635
|
}
|
|
1500
636
|
|
|
1501
637
|
export function pruneRun(
|
|
1502
638
|
runOrDir: string,
|
|
1503
639
|
options: { preserveArtifacts?: boolean } = {},
|
|
1504
640
|
): Record<string, unknown> {
|
|
1505
|
-
|
|
1506
|
-
const stateDir = String(status.state_dir);
|
|
1507
|
-
const run = String(status.run ?? basename(stateDir));
|
|
1508
|
-
const manifest = resolveArtifactManifest(
|
|
1509
|
-
status.artifacts as Record<string, RunArtifactDeclaration> | undefined,
|
|
1510
|
-
);
|
|
1511
|
-
const preserved: Record<string, string> = {};
|
|
1512
|
-
if (options.preserveArtifacts && manifest) {
|
|
1513
|
-
const preserveRoot = join(
|
|
1514
|
-
dirname(stateDir),
|
|
1515
|
-
"preserved-artifacts",
|
|
1516
|
-
safeRunId(run),
|
|
1517
|
-
);
|
|
1518
|
-
mkdirSync(preserveRoot, { recursive: true });
|
|
1519
|
-
for (const [name, artifact] of Object.entries(manifest)) {
|
|
1520
|
-
if (!artifact.exists) continue;
|
|
1521
|
-
const target = join(preserveRoot, basename(artifact.path));
|
|
1522
|
-
cpSync(artifact.path, target, { force: true });
|
|
1523
|
-
preserved[name] = target;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
rmSync(stateDir, { recursive: true, force: true });
|
|
1527
|
-
return {
|
|
1528
|
-
pruned: true,
|
|
1529
|
-
preserved_artifacts: preserved,
|
|
1530
|
-
run,
|
|
1531
|
-
state_dir: stateDir,
|
|
1532
|
-
ts: new Date().toISOString(),
|
|
1533
|
-
};
|
|
641
|
+
return pruneTerminalRun(assertTerminalRun(runOrDir), options);
|
|
1534
642
|
}
|
|
1535
643
|
|
|
1536
644
|
export function killRun(runOrDir: string): Record<string, unknown> {
|