@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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run message delivery.
|
|
3
|
+
* Owns run control endpoint resolution, inbox enqueueing, wake notification,
|
|
4
|
+
* transport writes, and delivery receipts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
constants,
|
|
10
|
+
existsSync,
|
|
11
|
+
openSync,
|
|
12
|
+
statSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
writeSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { createConnection } from "node:net";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { markTerminalHandled } from "./runs-control.ts";
|
|
20
|
+
import {
|
|
21
|
+
appendRunInboxMessageInStateDir,
|
|
22
|
+
updateRunInboxMessageStatusInStateDir,
|
|
23
|
+
} from "./runs-mailbox.ts";
|
|
24
|
+
import { notifyRuntimeWake } from "./runtime-notifier.ts";
|
|
25
|
+
|
|
26
|
+
export interface RunControlEndpoint {
|
|
27
|
+
path: string;
|
|
28
|
+
type: "fifo" | "mailbox" | "named-pipe";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SendRunMessageOptions {
|
|
32
|
+
namedPipeSend?: (path: string, payload: string) => Promise<number>;
|
|
33
|
+
platform?: NodeJS.Platform;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getRunControlEndpoint(
|
|
37
|
+
status: Record<string, unknown>,
|
|
38
|
+
stateDir: string,
|
|
39
|
+
): RunControlEndpoint {
|
|
40
|
+
const control = status.control;
|
|
41
|
+
if (control && typeof control === "object" && !Array.isArray(control)) {
|
|
42
|
+
const record = control as Record<string, unknown>;
|
|
43
|
+
if (
|
|
44
|
+
(record.type === "fifo" ||
|
|
45
|
+
record.type === "mailbox" ||
|
|
46
|
+
record.type === "named-pipe") &&
|
|
47
|
+
typeof record.path === "string" &&
|
|
48
|
+
record.path.trim()
|
|
49
|
+
) {
|
|
50
|
+
return { path: record.path, type: record.type };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { path: join(stateDir, "control.fifo"), type: "fifo" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function appendRunInboxMessage(stateDir: string, message: string): string {
|
|
57
|
+
let record: Record<string, unknown>;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(message) as unknown;
|
|
60
|
+
record =
|
|
61
|
+
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
62
|
+
? (parsed as Record<string, unknown>)
|
|
63
|
+
: { body: parsed, type: "run.message" };
|
|
64
|
+
} catch {
|
|
65
|
+
record = { body: message, type: "run.message" };
|
|
66
|
+
}
|
|
67
|
+
return appendRunInboxMessageInStateDir(stateDir, record);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeRunMessageReceipt(
|
|
71
|
+
stateDir: string,
|
|
72
|
+
message: string,
|
|
73
|
+
bytes: number,
|
|
74
|
+
inboxId: string,
|
|
75
|
+
): void {
|
|
76
|
+
const trimmedMessage = message.trim().toLowerCase();
|
|
77
|
+
const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(
|
|
78
|
+
trimmedMessage,
|
|
79
|
+
);
|
|
80
|
+
const ts = new Date().toISOString();
|
|
81
|
+
writeFileSync(
|
|
82
|
+
join(stateDir, "events.jsonl"),
|
|
83
|
+
`${JSON.stringify({ bytes, event: "run.message", inbox_id: inboxId, terminal: terminalMessage || undefined, ts })}\n`,
|
|
84
|
+
{ flag: "a" },
|
|
85
|
+
);
|
|
86
|
+
updateRunInboxMessageStatusInStateDir(stateDir, inboxId, "sent", { bytes });
|
|
87
|
+
if (terminalMessage) {
|
|
88
|
+
markTerminalHandled(stateDir, {
|
|
89
|
+
event: "run.message",
|
|
90
|
+
message: trimmedMessage,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sendRunMessageToFifo(
|
|
96
|
+
endpoint: RunControlEndpoint,
|
|
97
|
+
payload: string,
|
|
98
|
+
): number {
|
|
99
|
+
if (!existsSync(endpoint.path))
|
|
100
|
+
throw new Error(`Run control FIFO not found: ${endpoint.path}`);
|
|
101
|
+
const stat = statSync(endpoint.path);
|
|
102
|
+
if ((stat.mode & constants.S_IFMT) !== constants.S_IFIFO) {
|
|
103
|
+
throw new Error(`Run control endpoint is not a FIFO: ${endpoint.path}`);
|
|
104
|
+
}
|
|
105
|
+
let fd: number | undefined;
|
|
106
|
+
try {
|
|
107
|
+
fd = openSync(endpoint.path, constants.O_WRONLY | constants.O_NONBLOCK);
|
|
108
|
+
return writeSync(fd, payload);
|
|
109
|
+
} finally {
|
|
110
|
+
if (fd !== undefined) closeSync(fd);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function notifyRunMessageWake(
|
|
115
|
+
stateDir: string,
|
|
116
|
+
run: string,
|
|
117
|
+
bytes: number,
|
|
118
|
+
endpoint: RunControlEndpoint,
|
|
119
|
+
inboxId: string,
|
|
120
|
+
): Record<string, unknown> | undefined {
|
|
121
|
+
try {
|
|
122
|
+
const event = notifyRuntimeWake(stateDir, {
|
|
123
|
+
actor: `run:${run}`,
|
|
124
|
+
metadata: { bytes, control_type: endpoint.type, inbox_id: inboxId },
|
|
125
|
+
reason: "run.message",
|
|
126
|
+
});
|
|
127
|
+
return { wake: "wake.jsonl", wake_id: event.id };
|
|
128
|
+
} catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sendRunMessageToNamedPipe(
|
|
134
|
+
endpoint: RunControlEndpoint,
|
|
135
|
+
payload: string,
|
|
136
|
+
send?: (path: string, payload: string) => Promise<number>,
|
|
137
|
+
): Promise<number> {
|
|
138
|
+
if (send) return send(endpoint.path, payload);
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const socket = createConnection(endpoint.path);
|
|
141
|
+
let settled = false;
|
|
142
|
+
const timeout = setTimeout(() => {
|
|
143
|
+
if (settled) return;
|
|
144
|
+
settled = true;
|
|
145
|
+
socket.destroy();
|
|
146
|
+
reject(new Error("named pipe connection timed out"));
|
|
147
|
+
}, 5000);
|
|
148
|
+
const finish = (error?: Error): void => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
settled = true;
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
if (error) reject(error);
|
|
153
|
+
else resolve(Buffer.byteLength(payload));
|
|
154
|
+
};
|
|
155
|
+
socket.on("error", finish);
|
|
156
|
+
socket.on("connect", () => {
|
|
157
|
+
socket.end(payload, () => finish());
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function deliverRunMessage(
|
|
163
|
+
status: Record<string, unknown>,
|
|
164
|
+
run: string,
|
|
165
|
+
stateDir: string,
|
|
166
|
+
message: string,
|
|
167
|
+
options: SendRunMessageOptions = {},
|
|
168
|
+
): Promise<Record<string, unknown>> {
|
|
169
|
+
const endpoint = getRunControlEndpoint(status, stateDir);
|
|
170
|
+
const payload = message.endsWith("\n") ? message : `${message}\n`;
|
|
171
|
+
const payloadBytes = Buffer.byteLength(payload);
|
|
172
|
+
const inboxId = appendRunInboxMessage(stateDir, message);
|
|
173
|
+
const wake = notifyRunMessageWake(
|
|
174
|
+
stateDir,
|
|
175
|
+
run,
|
|
176
|
+
payloadBytes,
|
|
177
|
+
endpoint,
|
|
178
|
+
inboxId,
|
|
179
|
+
);
|
|
180
|
+
const runtimePlatform = options.platform ?? process.platform;
|
|
181
|
+
if (endpoint.type === "mailbox") {
|
|
182
|
+
return {
|
|
183
|
+
...(wake ?? {}),
|
|
184
|
+
bytes: payloadBytes,
|
|
185
|
+
control: "inbox.jsonl",
|
|
186
|
+
control_path: endpoint.path,
|
|
187
|
+
control_type: endpoint.type,
|
|
188
|
+
inbox_id: inboxId,
|
|
189
|
+
queued: true,
|
|
190
|
+
run,
|
|
191
|
+
sent: true,
|
|
192
|
+
state_dir: stateDir,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
if (endpoint.type === "fifo") {
|
|
197
|
+
if (runtimePlatform === "win32") {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"run actor messages on native Windows require a named-pipe control endpoint; this recipe still exposes Unix FIFO control.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const bytes = sendRunMessageToFifo(endpoint, payload);
|
|
203
|
+
writeRunMessageReceipt(stateDir, message, bytes, inboxId);
|
|
204
|
+
return {
|
|
205
|
+
...(wake ?? {}),
|
|
206
|
+
bytes,
|
|
207
|
+
control: "control.fifo",
|
|
208
|
+
control_path: endpoint.path,
|
|
209
|
+
control_type: endpoint.type,
|
|
210
|
+
inbox_id: inboxId,
|
|
211
|
+
run,
|
|
212
|
+
sent: true,
|
|
213
|
+
state_dir: stateDir,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const bytes = await sendRunMessageToNamedPipe(
|
|
217
|
+
endpoint,
|
|
218
|
+
payload,
|
|
219
|
+
options.namedPipeSend,
|
|
220
|
+
);
|
|
221
|
+
writeRunMessageReceipt(stateDir, message, bytes, inboxId);
|
|
222
|
+
return {
|
|
223
|
+
...(wake ?? {}),
|
|
224
|
+
bytes,
|
|
225
|
+
control: endpoint.path,
|
|
226
|
+
control_path: endpoint.path,
|
|
227
|
+
control_type: endpoint.type,
|
|
228
|
+
inbox_id: inboxId,
|
|
229
|
+
run,
|
|
230
|
+
sent: true,
|
|
231
|
+
state_dir: stateDir,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const deliveryError =
|
|
235
|
+
error instanceof Error ? error.message : String(error);
|
|
236
|
+
throw Object.assign(
|
|
237
|
+
new Error(
|
|
238
|
+
`Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`,
|
|
239
|
+
),
|
|
240
|
+
{
|
|
241
|
+
control_path: endpoint.path,
|
|
242
|
+
control_type: endpoint.type,
|
|
243
|
+
delivery_error: deliveryError,
|
|
244
|
+
inbox_id: inboxId,
|
|
245
|
+
queued: true,
|
|
246
|
+
run,
|
|
247
|
+
sent: false,
|
|
248
|
+
state_dir: stateDir,
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run outbox events.
|
|
3
|
+
* Owns outbox event normalization, parsing, and append payload formatting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type RunOutboxDelivery = "log" | "notify" | "followup";
|
|
7
|
+
export type RunOutboxLevel = "info" | "warning" | "error";
|
|
8
|
+
|
|
9
|
+
export interface RunOutboxEvent {
|
|
10
|
+
body?: unknown;
|
|
11
|
+
correlation_id?: string;
|
|
12
|
+
data?: unknown;
|
|
13
|
+
delivery: RunOutboxDelivery;
|
|
14
|
+
event: string;
|
|
15
|
+
from?: string;
|
|
16
|
+
id: string;
|
|
17
|
+
level: RunOutboxLevel;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
reply_to?: string;
|
|
20
|
+
run: string;
|
|
21
|
+
state_dir: string;
|
|
22
|
+
summary: string;
|
|
23
|
+
to?: string;
|
|
24
|
+
ts: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeRunOutboxDelivery(value: unknown): RunOutboxDelivery {
|
|
29
|
+
return value === "notify" || value === "followup" ? value : "log";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeRunOutboxLevel(value: unknown): RunOutboxLevel {
|
|
33
|
+
return value === "warning" || value === "error" ? value : "info";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeRunOutboxEvent(
|
|
37
|
+
raw: unknown,
|
|
38
|
+
run: string,
|
|
39
|
+
stateDir: string,
|
|
40
|
+
index: number,
|
|
41
|
+
): RunOutboxEvent | undefined {
|
|
42
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
43
|
+
const record = raw as Record<string, unknown>;
|
|
44
|
+
const event =
|
|
45
|
+
typeof record.event === "string" && record.event.trim()
|
|
46
|
+
? record.event.trim()
|
|
47
|
+
: "run.event";
|
|
48
|
+
const summary =
|
|
49
|
+
typeof record.summary === "string" && record.summary.trim()
|
|
50
|
+
? record.summary.trim()
|
|
51
|
+
: event;
|
|
52
|
+
const ts =
|
|
53
|
+
typeof record.ts === "string" && record.ts.trim()
|
|
54
|
+
? record.ts.trim()
|
|
55
|
+
: new Date(0).toISOString();
|
|
56
|
+
const id =
|
|
57
|
+
typeof record.id === "string" && record.id.trim()
|
|
58
|
+
? record.id.trim()
|
|
59
|
+
: `${run}:${index}`;
|
|
60
|
+
return {
|
|
61
|
+
...(record.body !== undefined ? { body: record.body } : {}),
|
|
62
|
+
...(typeof record.correlation_id === "string"
|
|
63
|
+
? { correlation_id: record.correlation_id }
|
|
64
|
+
: {}),
|
|
65
|
+
...(record.data !== undefined ? { data: record.data } : {}),
|
|
66
|
+
delivery: normalizeRunOutboxDelivery(record.delivery),
|
|
67
|
+
...(record.metadata &&
|
|
68
|
+
typeof record.metadata === "object" &&
|
|
69
|
+
!Array.isArray(record.metadata)
|
|
70
|
+
? { metadata: record.metadata as Record<string, unknown> }
|
|
71
|
+
: {}),
|
|
72
|
+
event,
|
|
73
|
+
...(typeof record.from === "string" ? { from: record.from } : {}),
|
|
74
|
+
id,
|
|
75
|
+
level: normalizeRunOutboxLevel(record.level),
|
|
76
|
+
...(typeof record.reply_to === "string"
|
|
77
|
+
? { reply_to: record.reply_to }
|
|
78
|
+
: {}),
|
|
79
|
+
run,
|
|
80
|
+
state_dir: stateDir,
|
|
81
|
+
summary,
|
|
82
|
+
...(typeof record.to === "string" ? { to: record.to } : {}),
|
|
83
|
+
ts,
|
|
84
|
+
...(typeof record.type === "string" ? { type: record.type } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseRunOutboxEventLine(
|
|
89
|
+
line: string,
|
|
90
|
+
run: string,
|
|
91
|
+
stateDir: string,
|
|
92
|
+
index: number,
|
|
93
|
+
): RunOutboxEvent | undefined {
|
|
94
|
+
try {
|
|
95
|
+
return normalizeRunOutboxEvent(JSON.parse(line), run, stateDir, index);
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildRunOutboxEventPayload(
|
|
102
|
+
run: string,
|
|
103
|
+
event: {
|
|
104
|
+
body?: unknown;
|
|
105
|
+
correlation_id?: string;
|
|
106
|
+
data?: unknown;
|
|
107
|
+
delivery?: string;
|
|
108
|
+
event?: string;
|
|
109
|
+
from?: string;
|
|
110
|
+
level?: string;
|
|
111
|
+
metadata?: Record<string, unknown>;
|
|
112
|
+
reply_to?: string;
|
|
113
|
+
summary?: string;
|
|
114
|
+
to?: string;
|
|
115
|
+
type?: string;
|
|
116
|
+
},
|
|
117
|
+
): Record<string, unknown> {
|
|
118
|
+
const type = event.type || event.event || "run.message";
|
|
119
|
+
const to = event.to || "coordinator";
|
|
120
|
+
const metadata = event.metadata ?? {};
|
|
121
|
+
const requiresResponse = metadata.requires_response === true;
|
|
122
|
+
return {
|
|
123
|
+
...(event.body !== undefined ? { body: event.body } : {}),
|
|
124
|
+
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
125
|
+
...(event.data !== undefined ? { data: event.data } : {}),
|
|
126
|
+
delivery: normalizeRunOutboxDelivery(
|
|
127
|
+
event.delivery ??
|
|
128
|
+
(requiresResponse
|
|
129
|
+
? "followup"
|
|
130
|
+
: to === "coordinator"
|
|
131
|
+
? "notify"
|
|
132
|
+
: "log"),
|
|
133
|
+
),
|
|
134
|
+
event: type,
|
|
135
|
+
from: event.from || `run:${run}`,
|
|
136
|
+
level: normalizeRunOutboxLevel(event.level),
|
|
137
|
+
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
138
|
+
...(event.reply_to ? { reply_to: event.reply_to } : {}),
|
|
139
|
+
summary: event.summary || type,
|
|
140
|
+
to,
|
|
141
|
+
ts: new Date().toISOString(),
|
|
142
|
+
type,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run process identity helpers.
|
|
3
|
+
* Owns liveness checks and Linux runner identity matching for run-owned processes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readlinkSync } from "node:fs";
|
|
7
|
+
import { platform } from "node:os";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
export function isAlive(pid: number): boolean {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pidMatchesRun(
|
|
20
|
+
pid: number,
|
|
21
|
+
cwd: string,
|
|
22
|
+
stateDir: string,
|
|
23
|
+
runnerPath: string,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
|
|
26
|
+
return isAlive(pid);
|
|
27
|
+
try {
|
|
28
|
+
const procCwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
29
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
30
|
+
return (
|
|
31
|
+
procCwd === resolve(cwd) &&
|
|
32
|
+
cmdline.includes(runnerPath) &&
|
|
33
|
+
cmdline.includes(stateDir)
|
|
34
|
+
);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isWithinRunnerIdentityGrace(
|
|
41
|
+
meta: Record<string, unknown>,
|
|
42
|
+
graceMs: number,
|
|
43
|
+
): boolean {
|
|
44
|
+
const createdAt =
|
|
45
|
+
typeof meta.createdAt === "string" ? Date.parse(meta.createdAt) : NaN;
|
|
46
|
+
return (
|
|
47
|
+
Number.isFinite(createdAt) &&
|
|
48
|
+
Date.now() - createdAt >= 0 &&
|
|
49
|
+
Date.now() - createdAt <= graceMs
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run retention operations.
|
|
3
|
+
* Owns terminal-run archive and prune filesystem behavior.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { writeJsonAtomic } from "./file-state.ts";
|
|
10
|
+
import {
|
|
11
|
+
resolveArtifactManifest,
|
|
12
|
+
type RunArtifactDeclaration,
|
|
13
|
+
} from "./runs-artifacts.ts";
|
|
14
|
+
import { safeRunId } from "./runs-identity.ts";
|
|
15
|
+
|
|
16
|
+
function archivePathFor(run: string, stateDir: string): string {
|
|
17
|
+
const archiveRoot = join(dirname(stateDir), "archived");
|
|
18
|
+
mkdirSync(archiveRoot, { recursive: true });
|
|
19
|
+
return join(
|
|
20
|
+
archiveRoot,
|
|
21
|
+
`${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function archiveTerminalRun(
|
|
26
|
+
status: Record<string, unknown>,
|
|
27
|
+
): Record<string, unknown> {
|
|
28
|
+
const stateDir = String(status.state_dir);
|
|
29
|
+
const run = String(status.run ?? basename(stateDir));
|
|
30
|
+
const archiveDir = archivePathFor(run, stateDir);
|
|
31
|
+
renameSync(stateDir, archiveDir);
|
|
32
|
+
mkdirSync(stateDir, { recursive: true });
|
|
33
|
+
const tombstone = {
|
|
34
|
+
archived: true,
|
|
35
|
+
archive_dir: archiveDir,
|
|
36
|
+
original_state_dir: stateDir,
|
|
37
|
+
run,
|
|
38
|
+
status: status.status,
|
|
39
|
+
ts: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
42
|
+
return tombstone;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function pruneTerminalRun(
|
|
46
|
+
status: Record<string, unknown>,
|
|
47
|
+
options: { preserveArtifacts?: boolean } = {},
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
const stateDir = String(status.state_dir);
|
|
50
|
+
const run = String(status.run ?? basename(stateDir));
|
|
51
|
+
const manifest = resolveArtifactManifest(
|
|
52
|
+
status.artifacts as Record<string, RunArtifactDeclaration> | undefined,
|
|
53
|
+
);
|
|
54
|
+
const preserved: Record<string, string> = {};
|
|
55
|
+
if (options.preserveArtifacts && manifest) {
|
|
56
|
+
const preserveRoot = join(
|
|
57
|
+
dirname(stateDir),
|
|
58
|
+
"preserved-artifacts",
|
|
59
|
+
safeRunId(run),
|
|
60
|
+
);
|
|
61
|
+
mkdirSync(preserveRoot, { recursive: true });
|
|
62
|
+
for (const [name, artifact] of Object.entries(manifest)) {
|
|
63
|
+
if (!artifact.exists) continue;
|
|
64
|
+
const target = join(preserveRoot, basename(artifact.path));
|
|
65
|
+
cpSync(artifact.path, target, { force: true });
|
|
66
|
+
preserved[name] = target;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
70
|
+
return {
|
|
71
|
+
pruned: true,
|
|
72
|
+
preserved_artifacts: preserved,
|
|
73
|
+
run,
|
|
74
|
+
state_dir: stateDir,
|
|
75
|
+
ts: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run start guards.
|
|
3
|
+
* Owns active-run reuse checks, start lock acquisition, and safe state-dir
|
|
4
|
+
* preparation before a new runner process is spawned.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { isAlive, pidMatchesRun } from "./runs-process.ts";
|
|
11
|
+
|
|
12
|
+
const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
15
|
+
|
|
16
|
+
export function assertNoActiveRunState(
|
|
17
|
+
stateDir: string,
|
|
18
|
+
readJson: RunJsonReader,
|
|
19
|
+
runnerPath: string,
|
|
20
|
+
): void {
|
|
21
|
+
const meta = readJson(join(stateDir, "run.json"));
|
|
22
|
+
if (!meta) return;
|
|
23
|
+
const pid = Number(meta.pid || 0);
|
|
24
|
+
const cwd = String(meta.cwd ?? "");
|
|
25
|
+
if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir, runnerPath))
|
|
26
|
+
return;
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function acquireStateStartLock(stateDir: string): () => void {
|
|
33
|
+
const lockDir = join(stateDir, ".start.lock");
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(lockDir);
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(lockDir, "owner.json"),
|
|
38
|
+
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
|
|
39
|
+
"utf8",
|
|
40
|
+
);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(lockDir);
|
|
44
|
+
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
|
|
45
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
46
|
+
mkdirSync(lockDir);
|
|
47
|
+
writeFileSync(
|
|
48
|
+
join(lockDir, "owner.json"),
|
|
49
|
+
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`,
|
|
50
|
+
"utf8",
|
|
51
|
+
);
|
|
52
|
+
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Keep the original lock acquisition error below.
|
|
56
|
+
}
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Run state is already being started: ${stateDir}. Retry after the current start finishes.`,
|
|
59
|
+
{ cause: error },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function prepareStateDirForStart(
|
|
66
|
+
stateDir: string,
|
|
67
|
+
readJson: RunJsonReader,
|
|
68
|
+
runnerPath: string,
|
|
69
|
+
): void {
|
|
70
|
+
const existing = readJson(join(stateDir, "run.json"));
|
|
71
|
+
const existingPid = Number(existing?.pid || 0);
|
|
72
|
+
const existingCwd =
|
|
73
|
+
typeof existing?.cwd === "string" ? existing.cwd : undefined;
|
|
74
|
+
const existingResult = readJson(join(stateDir, "result.json"));
|
|
75
|
+
if (
|
|
76
|
+
!existingResult &&
|
|
77
|
+
existingPid &&
|
|
78
|
+
existingCwd &&
|
|
79
|
+
isAlive(existingPid) &&
|
|
80
|
+
pidMatchesRun(existingPid, existingCwd, stateDir, runnerPath)
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
for (const file of [
|
|
87
|
+
"events.jsonl",
|
|
88
|
+
"inbox.jsonl",
|
|
89
|
+
"outbox.jsonl",
|
|
90
|
+
"progress.json",
|
|
91
|
+
"result.json",
|
|
92
|
+
"stderr.log",
|
|
93
|
+
"stdout.log",
|
|
94
|
+
"terminal-handled.json",
|
|
95
|
+
]) {
|
|
96
|
+
rmSync(join(stateDir, file), { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run status and log readers.
|
|
3
|
+
* Owns status derivation from run state files plus bounded log/event tails.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import type { AsyncRunStatus } from "./async-runs.ts";
|
|
10
|
+
import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun } from "./runs-process.ts";
|
|
11
|
+
import { readJsonlFileResilient } from "./state-readers.ts";
|
|
12
|
+
|
|
13
|
+
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
14
|
+
|
|
15
|
+
function getInterruptedRunStatus(
|
|
16
|
+
stateDir: string,
|
|
17
|
+
): "cancelled" | "killed" | undefined {
|
|
18
|
+
const events = readJsonlFileResilient<Record<string, unknown>>(
|
|
19
|
+
join(stateDir, "events.jsonl"),
|
|
20
|
+
).records.slice(-200);
|
|
21
|
+
for (const event of events.reverse()) {
|
|
22
|
+
if (event.event === "run.kill") return "killed";
|
|
23
|
+
if (event.event === "run.cancel") return "cancelled";
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildRunStatus(
|
|
29
|
+
stateDir: string,
|
|
30
|
+
runOrDir: string,
|
|
31
|
+
meta: Record<string, unknown>,
|
|
32
|
+
readJson: RunJsonReader,
|
|
33
|
+
runnerPath: string,
|
|
34
|
+
runnerIdentityGraceMs: number,
|
|
35
|
+
): Record<string, unknown> {
|
|
36
|
+
const result = readJson(join(stateDir, "result.json"));
|
|
37
|
+
const pid = Number(meta.pid || 0);
|
|
38
|
+
const aliveOwnedRunner = Boolean(
|
|
39
|
+
pid &&
|
|
40
|
+
isAlive(pid) &&
|
|
41
|
+
(!Array.isArray(meta.argv) ||
|
|
42
|
+
pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir, runnerPath) ||
|
|
43
|
+
isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs)),
|
|
44
|
+
);
|
|
45
|
+
const status: AsyncRunStatus = result
|
|
46
|
+
? Number(result.code ?? 0) === 0
|
|
47
|
+
? "done"
|
|
48
|
+
: "failed"
|
|
49
|
+
: aliveOwnedRunner
|
|
50
|
+
? "running"
|
|
51
|
+
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
52
|
+
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
53
|
+
return {
|
|
54
|
+
...meta,
|
|
55
|
+
eventsFile: join(stateDir, "events.jsonl"),
|
|
56
|
+
inboxFile: join(stateDir, "inbox.jsonl"),
|
|
57
|
+
outboxFile: join(stateDir, "outbox.jsonl"),
|
|
58
|
+
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
59
|
+
result: result || null,
|
|
60
|
+
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
61
|
+
state_dir: String(meta.state_dir ?? stateDir),
|
|
62
|
+
stderrLog: join(stateDir, "stderr.log"),
|
|
63
|
+
stdoutLog: join(stateDir, "stdout.log"),
|
|
64
|
+
status,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function tailFile(path: string, lines: number): string {
|
|
69
|
+
if (!existsSync(path)) return "";
|
|
70
|
+
const content = readFileSync(path, "utf8").trimEnd();
|
|
71
|
+
if (!content) return "";
|
|
72
|
+
return content.split("\n").slice(-lines).join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function tailLines(path: string, lines: number): string[] {
|
|
76
|
+
const content = tailFile(path, lines);
|
|
77
|
+
return content ? content.split("\n") : [];
|
|
78
|
+
}
|