@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/dist/lib/async-runs.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
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
|
-
import { spawn
|
|
7
|
-
import {
|
|
8
|
-
import { closeSync, constants, existsSync, mkdirSync, openSync, readdirSync, readFileSync, cpSync, readlinkSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
|
|
9
|
-
import { createConnection } from "node:net";
|
|
10
|
-
import { platform } from "node:os";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { closeSync, existsSync, mkdirSync, openSync, writeFileSync, } from "node:fs";
|
|
11
7
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
12
8
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { substituteCommandTemplateToken } from "./command-templates.js";
|
|
14
9
|
import { writeJsonAtomic } from "./file-state.js";
|
|
15
|
-
import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.js";
|
|
16
10
|
import * as Paths from "./paths.js";
|
|
17
|
-
import * as
|
|
18
|
-
import * as
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
import * as RecipesReferences from "./recipes-references.js";
|
|
12
|
+
import * as RecipesUsage from "./recipes-usage.js";
|
|
13
|
+
import { resolveArtifactPaths, } from "./runs-artifacts.js";
|
|
14
|
+
import { safeRunId } from "./runs-identity.js";
|
|
15
|
+
import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
|
|
16
|
+
import { buildRunOutboxEventPayload, parseRunOutboxEventLine, } from "./runs-outbox.js";
|
|
17
|
+
import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
|
|
18
|
+
import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun, } from "./runs-process.js";
|
|
19
|
+
import * as RunsStart from "./runs-start.js";
|
|
20
|
+
import * as RunsIndex from "./runs-index.js";
|
|
21
|
+
import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
|
|
22
|
+
import { deliverRunMessage, } from "./runs-messages.js";
|
|
23
|
+
import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
|
|
24
|
+
import { readJsonFileResilient } from "./state-readers.js";
|
|
22
25
|
const RUNNER_IDENTITY_GRACE_MS = 5000;
|
|
23
26
|
const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
|
|
24
27
|
const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
|
|
@@ -37,73 +40,8 @@ function asyncRunnerArgv(stateDir) {
|
|
|
37
40
|
? [RUNNER_PATH, stateDir]
|
|
38
41
|
: ["--experimental-strip-types", RUNNER_PATH, stateDir];
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(run))
|
|
43
|
-
throw new Error("Run id may contain only letters, numbers, dot, underscore, and dash.");
|
|
44
|
-
return run;
|
|
45
|
-
}
|
|
46
|
-
function resolveArtifactPaths(artifacts, values) {
|
|
47
|
-
if (!artifacts)
|
|
48
|
-
return undefined;
|
|
49
|
-
const resolved = {};
|
|
50
|
-
for (const [key, value] of Object.entries(artifacts)) {
|
|
51
|
-
if (!key.trim())
|
|
52
|
-
continue;
|
|
53
|
-
if (typeof value === "string") {
|
|
54
|
-
resolved[key] = substituteCommandTemplateToken(value, values, `recipe artifacts.${key}`);
|
|
55
|
-
}
|
|
56
|
-
else if (value &&
|
|
57
|
-
typeof value === "object" &&
|
|
58
|
-
typeof value.path === "string") {
|
|
59
|
-
resolved[key] = {
|
|
60
|
-
...value,
|
|
61
|
-
path: substituteCommandTemplateToken(value.path, values, `recipe artifacts.${key}.path`),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
66
|
-
}
|
|
67
|
-
export function resolveArtifactManifest(artifacts) {
|
|
68
|
-
if (!artifacts)
|
|
69
|
-
return undefined;
|
|
70
|
-
const manifest = {};
|
|
71
|
-
for (const [name, artifact] of Object.entries(artifacts)) {
|
|
72
|
-
const declaration = typeof artifact === "string" ? { path: artifact } : artifact;
|
|
73
|
-
if (!declaration?.path)
|
|
74
|
-
continue;
|
|
75
|
-
try {
|
|
76
|
-
const content = readFileSync(declaration.path);
|
|
77
|
-
manifest[name] = {
|
|
78
|
-
exists: true,
|
|
79
|
-
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
80
|
-
...(declaration.media_type
|
|
81
|
-
? { media_type: declaration.media_type }
|
|
82
|
-
: {}),
|
|
83
|
-
path: declaration.path,
|
|
84
|
-
...(declaration.required !== undefined
|
|
85
|
-
? { required: declaration.required }
|
|
86
|
-
: {}),
|
|
87
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
88
|
-
size: content.byteLength,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
manifest[name] = {
|
|
93
|
-
exists: false,
|
|
94
|
-
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
95
|
-
...(declaration.media_type
|
|
96
|
-
? { media_type: declaration.media_type }
|
|
97
|
-
: {}),
|
|
98
|
-
path: declaration.path,
|
|
99
|
-
...(declaration.required !== undefined
|
|
100
|
-
? { required: declaration.required }
|
|
101
|
-
: {}),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return Object.keys(manifest).length > 0 ? manifest : undefined;
|
|
106
|
-
}
|
|
43
|
+
export { safeRunId } from "./runs-identity.js";
|
|
44
|
+
export { resolveArtifactManifest } from "./runs-artifacts.js";
|
|
107
45
|
function resolveRunTemplate(params) {
|
|
108
46
|
if (!params.template)
|
|
109
47
|
throw new Error("spawn requires file or template.");
|
|
@@ -136,18 +74,11 @@ function resolveStateDir(params, run) {
|
|
|
136
74
|
return resolve(params.state_dir || join(DEFAULT_STATE_ROOT, run));
|
|
137
75
|
}
|
|
138
76
|
function assertNoActiveRunState(stateDir) {
|
|
139
|
-
|
|
140
|
-
if (!meta)
|
|
141
|
-
return;
|
|
142
|
-
const pid = Number(meta.pid || 0);
|
|
143
|
-
const cwd = String(meta.cwd ?? "");
|
|
144
|
-
if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir))
|
|
145
|
-
return;
|
|
146
|
-
throw new Error(`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`);
|
|
77
|
+
RunsStart.assertNoActiveRunState(stateDir, readJson, RUNNER_PATH);
|
|
147
78
|
}
|
|
148
79
|
function resolveRecipeFile(file) {
|
|
149
|
-
return (
|
|
150
|
-
|
|
80
|
+
return (RecipesReferences.getRecipePath(file, DEFAULT_RECIPE_ROOT) ??
|
|
81
|
+
RecipesReferences.resolveRecipePath(file, DEFAULT_RECIPE_ROOT));
|
|
151
82
|
}
|
|
152
83
|
function isMutableUsageRecipeFile(file) {
|
|
153
84
|
const userRoot = resolve(DEFAULT_RECIPE_ROOT);
|
|
@@ -156,9 +87,9 @@ function isMutableUsageRecipeFile(file) {
|
|
|
156
87
|
}
|
|
157
88
|
function readRecipeFile(file) {
|
|
158
89
|
const path = resolveRecipeFile(file);
|
|
159
|
-
const raw =
|
|
90
|
+
const raw = RecipesReferences.readRawRecipeConfig(path);
|
|
160
91
|
const includeActorRecipeContext = raw?.actor_context !== false && raw?.actor_context !== "off";
|
|
161
|
-
const config =
|
|
92
|
+
const config = RecipesReferences.readResolvedRecipeConfig(path, [], {
|
|
162
93
|
includeActorRecipeContext,
|
|
163
94
|
});
|
|
164
95
|
if (!config) {
|
|
@@ -196,104 +127,11 @@ function resolveStartParams(params) {
|
|
|
196
127
|
function readJson(path) {
|
|
197
128
|
return readJsonFileResilient(path, undefined).value;
|
|
198
129
|
}
|
|
199
|
-
function isAlive(pid) {
|
|
200
|
-
try {
|
|
201
|
-
process.kill(pid, 0);
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function pidMatchesRun(pid, cwd, stateDir) {
|
|
209
|
-
if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
|
|
210
|
-
return isAlive(pid);
|
|
211
|
-
try {
|
|
212
|
-
const procCwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
213
|
-
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
214
|
-
return (procCwd === resolve(cwd) &&
|
|
215
|
-
cmdline.includes(RUNNER_PATH) &&
|
|
216
|
-
cmdline.includes(stateDir));
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function isWithinRunnerIdentityGrace(meta) {
|
|
223
|
-
const createdAt = typeof meta.createdAt === "string" ? Date.parse(meta.createdAt) : NaN;
|
|
224
|
-
return (Number.isFinite(createdAt) &&
|
|
225
|
-
Date.now() - createdAt >= 0 &&
|
|
226
|
-
Date.now() - createdAt <= RUNNER_IDENTITY_GRACE_MS);
|
|
227
|
-
}
|
|
228
|
-
function tailFile(path, lines) {
|
|
229
|
-
if (!existsSync(path))
|
|
230
|
-
return "";
|
|
231
|
-
const content = readFileSync(path, "utf8").trimEnd();
|
|
232
|
-
if (!content)
|
|
233
|
-
return "";
|
|
234
|
-
return content.split("\n").slice(-lines).join("\n");
|
|
235
|
-
}
|
|
236
|
-
function tailLines(path, lines) {
|
|
237
|
-
const content = tailFile(path, lines);
|
|
238
|
-
return content ? content.split("\n") : [];
|
|
239
|
-
}
|
|
240
|
-
function getInterruptedRunStatus(stateDir) {
|
|
241
|
-
const events = readJsonlFileResilient(join(stateDir, "events.jsonl")).records.slice(-200);
|
|
242
|
-
for (const event of events.reverse()) {
|
|
243
|
-
if (event.event === "run.kill")
|
|
244
|
-
return "killed";
|
|
245
|
-
if (event.event === "run.cancel")
|
|
246
|
-
return "cancelled";
|
|
247
|
-
}
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
130
|
function acquireStateStartLock(stateDir) {
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
mkdirSync(lockDir);
|
|
254
|
-
writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`, "utf8");
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
try {
|
|
258
|
-
const stat = statSync(lockDir);
|
|
259
|
-
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
|
|
260
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
261
|
-
mkdirSync(lockDir);
|
|
262
|
-
writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`, "utf8");
|
|
263
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
// Keep the original lock acquisition error below.
|
|
268
|
-
}
|
|
269
|
-
throw new Error(`Run state is already being started: ${stateDir}. Retry after the current start finishes.`, { cause: error });
|
|
270
|
-
}
|
|
271
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
131
|
+
return RunsStart.acquireStateStartLock(stateDir);
|
|
272
132
|
}
|
|
273
133
|
function prepareStateDirForStart(stateDir) {
|
|
274
|
-
|
|
275
|
-
const existingPid = Number(existing?.pid || 0);
|
|
276
|
-
const existingCwd = typeof existing?.cwd === "string" ? existing.cwd : undefined;
|
|
277
|
-
const existingResult = readJson(join(stateDir, "result.json"));
|
|
278
|
-
if (!existingResult &&
|
|
279
|
-
existingPid &&
|
|
280
|
-
existingCwd &&
|
|
281
|
-
isAlive(existingPid) &&
|
|
282
|
-
pidMatchesRun(existingPid, existingCwd, stateDir)) {
|
|
283
|
-
throw new Error(`Run is already running: ${String(existing?.run ?? stateDir)}`);
|
|
284
|
-
}
|
|
285
|
-
for (const file of [
|
|
286
|
-
"events.jsonl",
|
|
287
|
-
"inbox.jsonl",
|
|
288
|
-
"outbox.jsonl",
|
|
289
|
-
"progress.json",
|
|
290
|
-
"result.json",
|
|
291
|
-
"stderr.log",
|
|
292
|
-
"stdout.log",
|
|
293
|
-
"terminal-handled.json",
|
|
294
|
-
]) {
|
|
295
|
-
rmSync(join(stateDir, file), { force: true });
|
|
296
|
-
}
|
|
134
|
+
RunsStart.prepareStateDirForStart(stateDir, readJson, RUNNER_PATH);
|
|
297
135
|
}
|
|
298
136
|
export function startRun(params, cwd) {
|
|
299
137
|
const startParams = resolveStartParams(params);
|
|
@@ -315,10 +153,10 @@ export function startRun(params, cwd) {
|
|
|
315
153
|
const includeActorRecipeContext = startParams.actor_context !== false &&
|
|
316
154
|
startParams.actor_context !== "off";
|
|
317
155
|
const recipeContextRecords = recipeFile && includeActorRecipeContext
|
|
318
|
-
?
|
|
156
|
+
? RecipesReferences.buildRecipeContextRecords(recipeFile)
|
|
319
157
|
: undefined;
|
|
320
158
|
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
321
|
-
|
|
159
|
+
RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn");
|
|
322
160
|
}
|
|
323
161
|
const outFd = openSync(stdout, "a");
|
|
324
162
|
const errFd = openSync(stderr, "a");
|
|
@@ -387,63 +225,7 @@ export function startRun(params, cwd) {
|
|
|
387
225
|
releaseStartLock();
|
|
388
226
|
}
|
|
389
227
|
}
|
|
390
|
-
|
|
391
|
-
return value === "notify" || value === "followup" ? value : "log";
|
|
392
|
-
}
|
|
393
|
-
function normalizeRunOutboxLevel(value) {
|
|
394
|
-
return value === "warning" || value === "error" ? value : "info";
|
|
395
|
-
}
|
|
396
|
-
function normalizeRunOutboxEvent(raw, run, stateDir, index) {
|
|
397
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
398
|
-
return undefined;
|
|
399
|
-
const record = raw;
|
|
400
|
-
const event = typeof record.event === "string" && record.event.trim()
|
|
401
|
-
? record.event.trim()
|
|
402
|
-
: "run.event";
|
|
403
|
-
const summary = typeof record.summary === "string" && record.summary.trim()
|
|
404
|
-
? record.summary.trim()
|
|
405
|
-
: event;
|
|
406
|
-
const ts = typeof record.ts === "string" && record.ts.trim()
|
|
407
|
-
? record.ts.trim()
|
|
408
|
-
: new Date(0).toISOString();
|
|
409
|
-
const id = typeof record.id === "string" && record.id.trim()
|
|
410
|
-
? record.id.trim()
|
|
411
|
-
: `${run}:${index}`;
|
|
412
|
-
return {
|
|
413
|
-
...(record.body !== undefined ? { body: record.body } : {}),
|
|
414
|
-
...(typeof record.correlation_id === "string"
|
|
415
|
-
? { correlation_id: record.correlation_id }
|
|
416
|
-
: {}),
|
|
417
|
-
...(record.data !== undefined ? { data: record.data } : {}),
|
|
418
|
-
delivery: normalizeRunOutboxDelivery(record.delivery),
|
|
419
|
-
...(record.metadata &&
|
|
420
|
-
typeof record.metadata === "object" &&
|
|
421
|
-
!Array.isArray(record.metadata)
|
|
422
|
-
? { metadata: record.metadata }
|
|
423
|
-
: {}),
|
|
424
|
-
event,
|
|
425
|
-
...(typeof record.from === "string" ? { from: record.from } : {}),
|
|
426
|
-
id,
|
|
427
|
-
level: normalizeRunOutboxLevel(record.level),
|
|
428
|
-
...(typeof record.reply_to === "string"
|
|
429
|
-
? { reply_to: record.reply_to }
|
|
430
|
-
: {}),
|
|
431
|
-
run,
|
|
432
|
-
state_dir: stateDir,
|
|
433
|
-
summary,
|
|
434
|
-
...(typeof record.to === "string" ? { to: record.to } : {}),
|
|
435
|
-
ts,
|
|
436
|
-
...(typeof record.type === "string" ? { type: record.type } : {}),
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
export function parseRunOutboxEventLine(line, run, stateDir, index) {
|
|
440
|
-
try {
|
|
441
|
-
return normalizeRunOutboxEvent(JSON.parse(line), run, stateDir, index);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return undefined;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
228
|
+
export { parseRunOutboxEventLine } from "./runs-outbox.js";
|
|
447
229
|
export function getRunStatus(runOrDir) {
|
|
448
230
|
const stateDir = resolve(runOrDir.includes("/")
|
|
449
231
|
? runOrDir
|
|
@@ -451,153 +233,19 @@ export function getRunStatus(runOrDir) {
|
|
|
451
233
|
const meta = readJson(join(stateDir, "run.json"));
|
|
452
234
|
if (!meta)
|
|
453
235
|
throw new Error(`Run not found: ${runOrDir}`);
|
|
454
|
-
|
|
455
|
-
const pid = Number(meta.pid || 0);
|
|
456
|
-
const aliveOwnedRunner = Boolean(pid &&
|
|
457
|
-
isAlive(pid) &&
|
|
458
|
-
(!Array.isArray(meta.argv) ||
|
|
459
|
-
pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir) ||
|
|
460
|
-
isWithinRunnerIdentityGrace(meta)));
|
|
461
|
-
const status = result
|
|
462
|
-
? Number(result.code ?? 0) === 0
|
|
463
|
-
? "done"
|
|
464
|
-
: "failed"
|
|
465
|
-
: aliveOwnedRunner
|
|
466
|
-
? "running"
|
|
467
|
-
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
468
|
-
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
469
|
-
return {
|
|
470
|
-
...meta,
|
|
471
|
-
eventsFile: join(stateDir, "events.jsonl"),
|
|
472
|
-
inboxFile: join(stateDir, "inbox.jsonl"),
|
|
473
|
-
outboxFile: join(stateDir, "outbox.jsonl"),
|
|
474
|
-
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
475
|
-
result: result || null,
|
|
476
|
-
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
477
|
-
state_dir: String(meta.state_dir ?? stateDir),
|
|
478
|
-
stderrLog: join(stateDir, "stderr.log"),
|
|
479
|
-
stdoutLog: join(stateDir, "stdout.log"),
|
|
480
|
-
status,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function matchesStatusFilter(status, filter) {
|
|
484
|
-
if (!filter || filter === "all")
|
|
485
|
-
return true;
|
|
486
|
-
if (filter === "active")
|
|
487
|
-
return status === "running";
|
|
488
|
-
if (filter === "terminal")
|
|
489
|
-
return status !== "running";
|
|
490
|
-
return status === filter;
|
|
236
|
+
return buildRunStatus(stateDir, runOrDir, meta, readJson, RUNNER_PATH, RUNNER_IDENTITY_GRACE_MS);
|
|
491
237
|
}
|
|
492
238
|
export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT, depth = 0, seen = new Set()) {
|
|
493
|
-
|
|
494
|
-
return [];
|
|
495
|
-
seen.add(resolve(stateRoot));
|
|
496
|
-
let entries;
|
|
497
|
-
try {
|
|
498
|
-
entries = readdirSync(stateRoot, { withFileTypes: true });
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
return [];
|
|
502
|
-
}
|
|
503
|
-
const result = [];
|
|
504
|
-
for (const entry of entries) {
|
|
505
|
-
if (!entry.isDirectory())
|
|
506
|
-
continue;
|
|
507
|
-
const child = join(stateRoot, entry.name);
|
|
508
|
-
if (existsSync(join(child, "run.json")))
|
|
509
|
-
result.push(child);
|
|
510
|
-
if (depth + 1 < 8)
|
|
511
|
-
result.push(...listRunStateDirs(child, depth + 1, seen));
|
|
512
|
-
}
|
|
513
|
-
return result;
|
|
514
|
-
}
|
|
515
|
-
function runIndexPath(stateRoot) {
|
|
516
|
-
return join(stateRoot, "index.json");
|
|
517
|
-
}
|
|
518
|
-
function indexEntryFromStatus(status) {
|
|
519
|
-
const progress = status.progress && typeof status.progress === "object"
|
|
520
|
-
? status.progress
|
|
521
|
-
: {};
|
|
522
|
-
return {
|
|
523
|
-
...(typeof status.ownerId === "string" ? { ownerId: status.ownerId } : {}),
|
|
524
|
-
...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
|
|
525
|
-
run: String(status.run),
|
|
526
|
-
state_dir: String(status.state_dir),
|
|
527
|
-
status: status.status,
|
|
528
|
-
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
529
|
-
updated_at: typeof progress.updatedAt === "string"
|
|
530
|
-
? progress.updatedAt
|
|
531
|
-
: typeof status.createdAt === "string"
|
|
532
|
-
? status.createdAt
|
|
533
|
-
: new Date(0).toISOString(),
|
|
534
|
-
};
|
|
239
|
+
return RunsIndex.listRunStateDirs(stateRoot, depth, seen);
|
|
535
240
|
}
|
|
536
241
|
export function rebuildRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
|
|
537
|
-
|
|
538
|
-
const entries = listRunStateDirs(stateRoot)
|
|
539
|
-
.flatMap((stateDir) => {
|
|
540
|
-
try {
|
|
541
|
-
return [indexEntryFromStatus(getRunStatus(stateDir))];
|
|
542
|
-
}
|
|
543
|
-
catch {
|
|
544
|
-
return [];
|
|
545
|
-
}
|
|
546
|
-
})
|
|
547
|
-
.sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
|
|
548
|
-
writeJsonAtomic(runIndexPath(stateRoot), {
|
|
549
|
-
entries,
|
|
550
|
-
rebuilt_at: new Date().toISOString(),
|
|
551
|
-
});
|
|
552
|
-
return entries;
|
|
242
|
+
return RunsIndex.rebuildRunStateIndex(stateRoot, getRunStatus);
|
|
553
243
|
}
|
|
554
244
|
export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
|
|
555
|
-
|
|
556
|
-
if (!index || typeof index !== "object")
|
|
557
|
-
return undefined;
|
|
558
|
-
const entries = index.entries;
|
|
559
|
-
if (!Array.isArray(entries))
|
|
560
|
-
return undefined;
|
|
561
|
-
const valid = entries.filter((entry) => {
|
|
562
|
-
const record = entry && typeof entry === "object"
|
|
563
|
-
? entry
|
|
564
|
-
: {};
|
|
565
|
-
return (typeof record.run === "string" &&
|
|
566
|
-
typeof record.state_dir === "string" &&
|
|
567
|
-
typeof record.status === "string");
|
|
568
|
-
});
|
|
569
|
-
if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
|
|
570
|
-
return undefined;
|
|
571
|
-
const indexedDirs = new Set(valid.map((entry) => resolve(entry.state_dir)));
|
|
572
|
-
const stateDirs = listRunStateDirs(stateRoot).map((stateDir) => resolve(stateDir));
|
|
573
|
-
if (stateDirs.some((stateDir) => !indexedDirs.has(stateDir)))
|
|
574
|
-
return undefined;
|
|
575
|
-
return valid;
|
|
245
|
+
return RunsIndex.readRunStateIndex(stateRoot, readJson);
|
|
576
246
|
}
|
|
577
247
|
export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
|
|
578
|
-
|
|
579
|
-
return [];
|
|
580
|
-
const indexed = readRunStateIndex(stateRoot);
|
|
581
|
-
if (indexed) {
|
|
582
|
-
return indexed
|
|
583
|
-
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
584
|
-
.map((entry) => ({
|
|
585
|
-
run: entry.run,
|
|
586
|
-
state_dir: entry.state_dir,
|
|
587
|
-
status: entry.status,
|
|
588
|
-
...(entry.tool ? { tool: entry.tool } : {}),
|
|
589
|
-
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
590
|
-
}));
|
|
591
|
-
}
|
|
592
|
-
return rebuildRunStateIndex(stateRoot)
|
|
593
|
-
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
594
|
-
.map((entry) => ({
|
|
595
|
-
run: entry.run,
|
|
596
|
-
state_dir: entry.state_dir,
|
|
597
|
-
status: entry.status,
|
|
598
|
-
...(entry.tool ? { tool: entry.tool } : {}),
|
|
599
|
-
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
600
|
-
}));
|
|
248
|
+
return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
|
|
601
249
|
}
|
|
602
250
|
export function tailRun(runOrDir, lines = 40) {
|
|
603
251
|
const status = getRunStatus(runOrDir);
|
|
@@ -616,55 +264,6 @@ export function readRunEvents(runOrDir, lines = 40) {
|
|
|
616
264
|
.map((line, index) => parseRunOutboxEventLine(line, run, stateDir, index))
|
|
617
265
|
.filter((event) => Boolean(event));
|
|
618
266
|
}
|
|
619
|
-
function runInboxFile(stateDir) {
|
|
620
|
-
return join(stateDir, "inbox.jsonl");
|
|
621
|
-
}
|
|
622
|
-
function sleepSync(ms) {
|
|
623
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
624
|
-
}
|
|
625
|
-
function acquireRunInboxLock(stateDir) {
|
|
626
|
-
const lockDir = join(stateDir, ".inbox.lock");
|
|
627
|
-
const started = Date.now();
|
|
628
|
-
while (true) {
|
|
629
|
-
try {
|
|
630
|
-
mkdirSync(lockDir, { recursive: false });
|
|
631
|
-
writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`, "utf8");
|
|
632
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
633
|
-
}
|
|
634
|
-
catch (error) {
|
|
635
|
-
try {
|
|
636
|
-
const stat = statSync(lockDir);
|
|
637
|
-
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
|
|
638
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
catch {
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
if (Date.now() - started > RUN_INBOX_LOCK_TIMEOUT_MS) {
|
|
646
|
-
throw new Error("Run inbox lock timed out.", { cause: error });
|
|
647
|
-
}
|
|
648
|
-
sleepSync(10);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
function parseRunInboxLine(line) {
|
|
653
|
-
try {
|
|
654
|
-
return JSON.parse(line);
|
|
655
|
-
}
|
|
656
|
-
catch {
|
|
657
|
-
return undefined;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
function readRunInboxMessagesFromStateDir(stateDir) {
|
|
661
|
-
return readJsonlFileResilient(runInboxFile(stateDir)).records;
|
|
662
|
-
}
|
|
663
|
-
function writeRunInboxMessages(stateDir, messages) {
|
|
664
|
-
writeFileSync(runInboxFile(stateDir), messages.length
|
|
665
|
-
? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n`
|
|
666
|
-
: "", "utf8");
|
|
667
|
-
}
|
|
668
267
|
export function readRunInboxMessages(runOrDir, lines = 40) {
|
|
669
268
|
const status = getRunStatus(runOrDir);
|
|
670
269
|
const stateDir = String(status.state_dir);
|
|
@@ -675,109 +274,22 @@ export function readRunInboxMessages(runOrDir, lines = 40) {
|
|
|
675
274
|
export function updateRunInboxMessageStatus(runOrDir, id, nextStatus, metadata = {}) {
|
|
676
275
|
const status = getRunStatus(runOrDir);
|
|
677
276
|
const stateDir = String(status.state_dir);
|
|
678
|
-
|
|
679
|
-
try {
|
|
680
|
-
const messages = readRunInboxMessagesFromStateDir(stateDir);
|
|
681
|
-
const timestampKey = `${nextStatus}_at`;
|
|
682
|
-
let changed = false;
|
|
683
|
-
const updated = messages.map((message) => {
|
|
684
|
-
if (message.id !== id)
|
|
685
|
-
return message;
|
|
686
|
-
changed = true;
|
|
687
|
-
return {
|
|
688
|
-
...message,
|
|
689
|
-
...metadata,
|
|
690
|
-
[timestampKey]: new Date().toISOString(),
|
|
691
|
-
status: nextStatus,
|
|
692
|
-
};
|
|
693
|
-
});
|
|
694
|
-
if (changed)
|
|
695
|
-
writeRunInboxMessages(stateDir, updated);
|
|
696
|
-
return changed;
|
|
697
|
-
}
|
|
698
|
-
finally {
|
|
699
|
-
releaseLock();
|
|
700
|
-
}
|
|
277
|
+
return updateRunInboxMessageStatusInStateDir(stateDir, id, nextStatus, metadata);
|
|
701
278
|
}
|
|
702
279
|
export function claimRunInboxMessage(runOrDir, owner = "runtime", statuses = ["queued"]) {
|
|
703
280
|
const status = getRunStatus(runOrDir);
|
|
704
281
|
const stateDir = String(status.state_dir);
|
|
705
|
-
|
|
706
|
-
try {
|
|
707
|
-
const messages = readRunInboxMessagesFromStateDir(stateDir);
|
|
708
|
-
const index = messages.findIndex((message) => statuses.includes(String(message.status ?? "queued")));
|
|
709
|
-
if (index < 0)
|
|
710
|
-
return undefined;
|
|
711
|
-
const claimed = {
|
|
712
|
-
...messages[index],
|
|
713
|
-
claimed_at: new Date().toISOString(),
|
|
714
|
-
claimed_by: owner,
|
|
715
|
-
id: typeof messages[index].id === "string"
|
|
716
|
-
? messages[index].id
|
|
717
|
-
: randomUUID(),
|
|
718
|
-
status: "claimed",
|
|
719
|
-
};
|
|
720
|
-
messages[index] = claimed;
|
|
721
|
-
writeRunInboxMessages(stateDir, messages);
|
|
722
|
-
return claimed;
|
|
723
|
-
}
|
|
724
|
-
finally {
|
|
725
|
-
releaseLock();
|
|
726
|
-
}
|
|
282
|
+
return claimRunInboxMessageInStateDir(stateDir, owner, statuses);
|
|
727
283
|
}
|
|
728
284
|
export async function processRunInboxMessages(runOrDir, handler, options = {}) {
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
const owner = options.owner ?? "runtime";
|
|
732
|
-
for (let index = 0; index < limit; index += 1) {
|
|
733
|
-
const message = claimRunInboxMessage(runOrDir, owner, options.statuses);
|
|
734
|
-
if (!message?.id)
|
|
735
|
-
break;
|
|
736
|
-
result.claimed += 1;
|
|
737
|
-
try {
|
|
738
|
-
await handler(message);
|
|
739
|
-
if (updateRunInboxMessageStatus(runOrDir, message.id, "handled")) {
|
|
740
|
-
result.handled += 1;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
catch (error) {
|
|
744
|
-
if (updateRunInboxMessageStatus(runOrDir, message.id, "failed", {
|
|
745
|
-
error: error instanceof Error ? error.message : String(error),
|
|
746
|
-
})) {
|
|
747
|
-
result.failed += 1;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
return result;
|
|
285
|
+
const status = getRunStatus(runOrDir);
|
|
286
|
+
return processRunInboxMessagesInStateDir(String(status.state_dir), handler, options);
|
|
752
287
|
}
|
|
753
288
|
export function appendRunOutboxEvent(runOrDir, event) {
|
|
754
289
|
const status = getRunStatus(runOrDir);
|
|
755
290
|
const stateDir = String(status.state_dir);
|
|
756
291
|
const run = String(status.run ?? runOrDir);
|
|
757
|
-
const
|
|
758
|
-
const to = event.to || "coordinator";
|
|
759
|
-
const metadata = event.metadata ?? {};
|
|
760
|
-
const requiresResponse = metadata.requires_response === true;
|
|
761
|
-
const payload = {
|
|
762
|
-
...(event.body !== undefined ? { body: event.body } : {}),
|
|
763
|
-
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
764
|
-
...(event.data !== undefined ? { data: event.data } : {}),
|
|
765
|
-
delivery: normalizeRunOutboxDelivery(event.delivery ??
|
|
766
|
-
(requiresResponse
|
|
767
|
-
? "followup"
|
|
768
|
-
: to === "coordinator"
|
|
769
|
-
? "notify"
|
|
770
|
-
: "log")),
|
|
771
|
-
event: type,
|
|
772
|
-
from: event.from || `run:${run}`,
|
|
773
|
-
level: normalizeRunOutboxLevel(event.level),
|
|
774
|
-
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
775
|
-
...(event.reply_to ? { reply_to: event.reply_to } : {}),
|
|
776
|
-
summary: event.summary || type,
|
|
777
|
-
to,
|
|
778
|
-
ts: new Date().toISOString(),
|
|
779
|
-
type,
|
|
780
|
-
};
|
|
292
|
+
const payload = buildRunOutboxEventPayload(run, event);
|
|
781
293
|
const line = `${JSON.stringify(payload)}\n`;
|
|
782
294
|
writeFileSync(join(stateDir, "outbox.jsonl"), line, { flag: "a" });
|
|
783
295
|
return {
|
|
@@ -788,115 +300,6 @@ export function appendRunOutboxEvent(runOrDir, event) {
|
|
|
788
300
|
state_dir: stateDir,
|
|
789
301
|
};
|
|
790
302
|
}
|
|
791
|
-
function getRunControlEndpoint(status, stateDir) {
|
|
792
|
-
const control = status.control;
|
|
793
|
-
if (control && typeof control === "object" && !Array.isArray(control)) {
|
|
794
|
-
const record = control;
|
|
795
|
-
if ((record.type === "fifo" ||
|
|
796
|
-
record.type === "mailbox" ||
|
|
797
|
-
record.type === "named-pipe") &&
|
|
798
|
-
typeof record.path === "string" &&
|
|
799
|
-
record.path.trim()) {
|
|
800
|
-
return { path: record.path, type: record.type };
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return { path: join(stateDir, "control.fifo"), type: "fifo" };
|
|
804
|
-
}
|
|
805
|
-
function appendRunInboxMessage(stateDir, message) {
|
|
806
|
-
const id = randomUUID();
|
|
807
|
-
const ts = new Date().toISOString();
|
|
808
|
-
let record;
|
|
809
|
-
try {
|
|
810
|
-
const parsed = JSON.parse(message);
|
|
811
|
-
record =
|
|
812
|
-
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
813
|
-
? parsed
|
|
814
|
-
: { body: parsed, type: "run.message" };
|
|
815
|
-
}
|
|
816
|
-
catch {
|
|
817
|
-
record = { body: message, type: "run.message" };
|
|
818
|
-
}
|
|
819
|
-
const releaseLock = acquireRunInboxLock(stateDir);
|
|
820
|
-
try {
|
|
821
|
-
writeFileSync(runInboxFile(stateDir), `${JSON.stringify({ ...record, id, queued_at: ts, received_at: ts, status: "queued" })}\n`, { flag: "a" });
|
|
822
|
-
}
|
|
823
|
-
finally {
|
|
824
|
-
releaseLock();
|
|
825
|
-
}
|
|
826
|
-
return id;
|
|
827
|
-
}
|
|
828
|
-
function writeRunMessageReceipt(stateDir, message, bytes, inboxId) {
|
|
829
|
-
const trimmedMessage = message.trim().toLowerCase();
|
|
830
|
-
const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(trimmedMessage);
|
|
831
|
-
const ts = new Date().toISOString();
|
|
832
|
-
writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ bytes, event: "run.message", inbox_id: inboxId, terminal: terminalMessage || undefined, ts })}\n`, { flag: "a" });
|
|
833
|
-
updateRunInboxMessageStatus(stateDir, inboxId, "sent", { bytes });
|
|
834
|
-
if (terminalMessage) {
|
|
835
|
-
markTerminalHandled(stateDir, {
|
|
836
|
-
event: "run.message",
|
|
837
|
-
message: trimmedMessage,
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
function sendRunMessageToFifo(endpoint, payload) {
|
|
842
|
-
if (!existsSync(endpoint.path))
|
|
843
|
-
throw new Error(`Run control FIFO not found: ${endpoint.path}`);
|
|
844
|
-
const stat = statSync(endpoint.path);
|
|
845
|
-
if ((stat.mode & constants.S_IFMT) !== constants.S_IFIFO) {
|
|
846
|
-
throw new Error(`Run control endpoint is not a FIFO: ${endpoint.path}`);
|
|
847
|
-
}
|
|
848
|
-
let fd;
|
|
849
|
-
try {
|
|
850
|
-
fd = openSync(endpoint.path, constants.O_WRONLY | constants.O_NONBLOCK);
|
|
851
|
-
return writeSync(fd, payload);
|
|
852
|
-
}
|
|
853
|
-
finally {
|
|
854
|
-
if (fd !== undefined)
|
|
855
|
-
closeSync(fd);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function notifyRunMessageWake(stateDir, run, bytes, endpoint, inboxId) {
|
|
859
|
-
try {
|
|
860
|
-
const event = notifyRuntimeWake(stateDir, {
|
|
861
|
-
actor: `run:${run}`,
|
|
862
|
-
metadata: { bytes, control_type: endpoint.type, inbox_id: inboxId },
|
|
863
|
-
reason: "run.message",
|
|
864
|
-
});
|
|
865
|
-
return { wake: "wake.jsonl", wake_id: event.id };
|
|
866
|
-
}
|
|
867
|
-
catch {
|
|
868
|
-
return undefined;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
function sendRunMessageToNamedPipe(endpoint, payload, send) {
|
|
872
|
-
if (send)
|
|
873
|
-
return send(endpoint.path, payload);
|
|
874
|
-
return new Promise((resolve, reject) => {
|
|
875
|
-
const socket = createConnection(endpoint.path);
|
|
876
|
-
let settled = false;
|
|
877
|
-
const timeout = setTimeout(() => {
|
|
878
|
-
if (settled)
|
|
879
|
-
return;
|
|
880
|
-
settled = true;
|
|
881
|
-
socket.destroy();
|
|
882
|
-
reject(new Error("named pipe connection timed out"));
|
|
883
|
-
}, 5000);
|
|
884
|
-
const finish = (error) => {
|
|
885
|
-
if (settled)
|
|
886
|
-
return;
|
|
887
|
-
settled = true;
|
|
888
|
-
clearTimeout(timeout);
|
|
889
|
-
if (error)
|
|
890
|
-
reject(error);
|
|
891
|
-
else
|
|
892
|
-
resolve(Buffer.byteLength(payload));
|
|
893
|
-
};
|
|
894
|
-
socket.on("error", finish);
|
|
895
|
-
socket.on("connect", () => {
|
|
896
|
-
socket.end(payload, () => finish());
|
|
897
|
-
});
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
303
|
export async function sendRunMessage(runOrDir, message, options = {}) {
|
|
901
304
|
const status = getRunStatus(runOrDir);
|
|
902
305
|
const stateDir = String(status.state_dir);
|
|
@@ -906,130 +309,18 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
|
|
|
906
309
|
const pid = Number(status.pid || 0);
|
|
907
310
|
if (!pid || !isAlive(pid))
|
|
908
311
|
throw new Error(`Run pid is not alive: ${run}`);
|
|
909
|
-
if (!pidMatchesRun(pid, String(status.cwd), stateDir) &&
|
|
910
|
-
!isWithinRunnerIdentityGrace(status))
|
|
312
|
+
if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH) &&
|
|
313
|
+
!isWithinRunnerIdentityGrace(status, RUNNER_IDENTITY_GRACE_MS))
|
|
911
314
|
throw new Error(`Run pid owner mismatch: ${run}`);
|
|
912
|
-
|
|
913
|
-
const payload = message.endsWith("\n") ? message : `${message}\n`;
|
|
914
|
-
const payloadBytes = Buffer.byteLength(payload);
|
|
915
|
-
const inboxId = appendRunInboxMessage(stateDir, message);
|
|
916
|
-
const wake = notifyRunMessageWake(stateDir, run, payloadBytes, endpoint, inboxId);
|
|
917
|
-
const runtimePlatform = options.platform ?? process.platform;
|
|
918
|
-
if (endpoint.type === "mailbox") {
|
|
919
|
-
return {
|
|
920
|
-
...(wake ?? {}),
|
|
921
|
-
bytes: payloadBytes,
|
|
922
|
-
control: "inbox.jsonl",
|
|
923
|
-
control_path: endpoint.path,
|
|
924
|
-
control_type: endpoint.type,
|
|
925
|
-
inbox_id: inboxId,
|
|
926
|
-
queued: true,
|
|
927
|
-
run,
|
|
928
|
-
sent: true,
|
|
929
|
-
state_dir: stateDir,
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
try {
|
|
933
|
-
if (endpoint.type === "fifo") {
|
|
934
|
-
if (runtimePlatform === "win32") {
|
|
935
|
-
throw new Error("run actor messages on native Windows require a named-pipe control endpoint; this recipe still exposes Unix FIFO control.");
|
|
936
|
-
}
|
|
937
|
-
const bytes = sendRunMessageToFifo(endpoint, payload);
|
|
938
|
-
writeRunMessageReceipt(stateDir, message, bytes, inboxId);
|
|
939
|
-
return {
|
|
940
|
-
...(wake ?? {}),
|
|
941
|
-
bytes,
|
|
942
|
-
control: "control.fifo",
|
|
943
|
-
control_path: endpoint.path,
|
|
944
|
-
control_type: endpoint.type,
|
|
945
|
-
inbox_id: inboxId,
|
|
946
|
-
run,
|
|
947
|
-
sent: true,
|
|
948
|
-
state_dir: stateDir,
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
const bytes = await sendRunMessageToNamedPipe(endpoint, payload, options.namedPipeSend);
|
|
952
|
-
writeRunMessageReceipt(stateDir, message, bytes, inboxId);
|
|
953
|
-
return {
|
|
954
|
-
...(wake ?? {}),
|
|
955
|
-
bytes,
|
|
956
|
-
control: endpoint.path,
|
|
957
|
-
control_path: endpoint.path,
|
|
958
|
-
control_type: endpoint.type,
|
|
959
|
-
inbox_id: inboxId,
|
|
960
|
-
run,
|
|
961
|
-
sent: true,
|
|
962
|
-
state_dir: stateDir,
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
catch (error) {
|
|
966
|
-
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
967
|
-
throw Object.assign(new Error(`Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`), {
|
|
968
|
-
control_path: endpoint.path,
|
|
969
|
-
control_type: endpoint.type,
|
|
970
|
-
delivery_error: deliveryError,
|
|
971
|
-
inbox_id: inboxId,
|
|
972
|
-
queued: true,
|
|
973
|
-
run,
|
|
974
|
-
sent: false,
|
|
975
|
-
state_dir: stateDir,
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
export function getRunProcessSignalPlan(pid, signal, runtimePlatform = process.platform) {
|
|
980
|
-
if (runtimePlatform === "win32") {
|
|
981
|
-
return {
|
|
982
|
-
args: [
|
|
983
|
-
"/PID",
|
|
984
|
-
String(pid),
|
|
985
|
-
"/T",
|
|
986
|
-
...(signal === "SIGKILL" ? ["/F"] : []),
|
|
987
|
-
],
|
|
988
|
-
command: "taskkill",
|
|
989
|
-
signalTarget: "processTree",
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
return { signalTarget: "processGroup" };
|
|
993
|
-
}
|
|
994
|
-
function signalOwnedRunProcess(pid, signal) {
|
|
995
|
-
const plan = getRunProcessSignalPlan(pid, signal);
|
|
996
|
-
if (plan.command && plan.args) {
|
|
997
|
-
const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
|
|
998
|
-
if (result.status !== 0) {
|
|
999
|
-
throw new Error(result.stderr?.trim() ||
|
|
1000
|
-
result.stdout?.trim() ||
|
|
1001
|
-
`${plan.command} failed`);
|
|
1002
|
-
}
|
|
1003
|
-
return plan;
|
|
1004
|
-
}
|
|
1005
|
-
try {
|
|
1006
|
-
process.kill(-pid, signal);
|
|
1007
|
-
return { signalTarget: "processGroup" };
|
|
1008
|
-
}
|
|
1009
|
-
catch {
|
|
1010
|
-
process.kill(pid, signal);
|
|
1011
|
-
return { signalTarget: "process" };
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
function markTerminalHandled(stateDir, details) {
|
|
1015
|
-
writeJsonAtomic(join(stateDir, "terminal-handled.json"), {
|
|
1016
|
-
...details,
|
|
1017
|
-
ts: new Date().toISOString(),
|
|
1018
|
-
});
|
|
315
|
+
return deliverRunMessage(status, run, stateDir, message, options);
|
|
1019
316
|
}
|
|
317
|
+
export { getRunProcessSignalPlan } from "./runs-control.js";
|
|
1020
318
|
function markTerminalProgress(stateDir, phase) {
|
|
1021
319
|
const existing = readJson(join(stateDir, "progress.json"));
|
|
1022
320
|
const progress = existing && typeof existing === "object" && !Array.isArray(existing)
|
|
1023
321
|
? existing
|
|
1024
|
-
:
|
|
1025
|
-
|
|
1026
|
-
writeJsonAtomic(join(stateDir, "progress.json"), {
|
|
1027
|
-
...rest,
|
|
1028
|
-
completed: typeof progress.completed === "number" ? progress.completed : 0,
|
|
1029
|
-
failures: Array.isArray(progress.failures) ? progress.failures : [],
|
|
1030
|
-
phase,
|
|
1031
|
-
updatedAt: new Date().toISOString(),
|
|
1032
|
-
});
|
|
322
|
+
: undefined;
|
|
323
|
+
writeJsonAtomic(join(stateDir, "progress.json"), buildTerminalProgress(progress, phase));
|
|
1033
324
|
}
|
|
1034
325
|
function stopRun(runOrDir, signal, event) {
|
|
1035
326
|
const status = getRunStatus(runOrDir);
|
|
@@ -1039,7 +330,7 @@ function stopRun(runOrDir, signal, event) {
|
|
|
1039
330
|
return { stopped: false, reason: "not running", status };
|
|
1040
331
|
if (!pid || !isAlive(pid))
|
|
1041
332
|
return { stopped: false, reason: "pid not alive", status };
|
|
1042
|
-
if (!pidMatchesRun(pid, String(status.cwd), stateDir)) {
|
|
333
|
+
if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH)) {
|
|
1043
334
|
return { stopped: false, reason: "pid owner mismatch", status };
|
|
1044
335
|
}
|
|
1045
336
|
const signalResult = signalOwnedRunProcess(pid, signal);
|
|
@@ -1064,54 +355,11 @@ function assertTerminalRun(runOrDir) {
|
|
|
1064
355
|
}
|
|
1065
356
|
return status;
|
|
1066
357
|
}
|
|
1067
|
-
function archivePathFor(run, stateDir) {
|
|
1068
|
-
const archiveRoot = join(dirname(stateDir), "archived");
|
|
1069
|
-
mkdirSync(archiveRoot, { recursive: true });
|
|
1070
|
-
return join(archiveRoot, `${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
1071
|
-
}
|
|
1072
358
|
export function archiveRun(runOrDir) {
|
|
1073
|
-
|
|
1074
|
-
const stateDir = String(status.state_dir);
|
|
1075
|
-
const run = String(status.run ?? basename(stateDir));
|
|
1076
|
-
const archiveDir = archivePathFor(run, stateDir);
|
|
1077
|
-
renameSync(stateDir, archiveDir);
|
|
1078
|
-
mkdirSync(stateDir, { recursive: true });
|
|
1079
|
-
const tombstone = {
|
|
1080
|
-
archived: true,
|
|
1081
|
-
archive_dir: archiveDir,
|
|
1082
|
-
original_state_dir: stateDir,
|
|
1083
|
-
run,
|
|
1084
|
-
status: status.status,
|
|
1085
|
-
ts: new Date().toISOString(),
|
|
1086
|
-
};
|
|
1087
|
-
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
1088
|
-
return tombstone;
|
|
359
|
+
return archiveTerminalRun(assertTerminalRun(runOrDir));
|
|
1089
360
|
}
|
|
1090
361
|
export function pruneRun(runOrDir, options = {}) {
|
|
1091
|
-
|
|
1092
|
-
const stateDir = String(status.state_dir);
|
|
1093
|
-
const run = String(status.run ?? basename(stateDir));
|
|
1094
|
-
const manifest = resolveArtifactManifest(status.artifacts);
|
|
1095
|
-
const preserved = {};
|
|
1096
|
-
if (options.preserveArtifacts && manifest) {
|
|
1097
|
-
const preserveRoot = join(dirname(stateDir), "preserved-artifacts", safeRunId(run));
|
|
1098
|
-
mkdirSync(preserveRoot, { recursive: true });
|
|
1099
|
-
for (const [name, artifact] of Object.entries(manifest)) {
|
|
1100
|
-
if (!artifact.exists)
|
|
1101
|
-
continue;
|
|
1102
|
-
const target = join(preserveRoot, basename(artifact.path));
|
|
1103
|
-
cpSync(artifact.path, target, { force: true });
|
|
1104
|
-
preserved[name] = target;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
rmSync(stateDir, { recursive: true, force: true });
|
|
1108
|
-
return {
|
|
1109
|
-
pruned: true,
|
|
1110
|
-
preserved_artifacts: preserved,
|
|
1111
|
-
run,
|
|
1112
|
-
state_dir: stateDir,
|
|
1113
|
-
ts: new Date().toISOString(),
|
|
1114
|
-
};
|
|
362
|
+
return pruneTerminalRun(assertTerminalRun(runOrDir), options);
|
|
1115
363
|
}
|
|
1116
364
|
export function killRun(runOrDir) {
|
|
1117
365
|
const result = stopRun(runOrDir, "SIGKILL", "run.kill");
|