@llblab/pi-actors 0.19.10 → 0.20.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 +1 -1
- package/BACKLOG.md +40 -1
- package/CHANGELOG.md +15 -0
- package/dist/lib/actor-inspector-tui.d.ts +55 -0
- package/dist/lib/actor-inspector-tui.js +559 -0
- package/dist/lib/actor-messages.d.ts +25 -0
- package/dist/lib/actor-messages.js +122 -0
- package/dist/lib/actor-recipe-context.d.ts +14 -0
- package/dist/lib/actor-recipe-context.js +79 -0
- package/dist/lib/actor-rooms.d.ts +81 -0
- package/dist/lib/actor-rooms.js +468 -0
- package/dist/lib/async-runs.d.ts +101 -0
- package/dist/lib/async-runs.js +612 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +592 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +226 -0
- package/dist/lib/execution.d.ts +63 -0
- package/dist/lib/execution.js +450 -0
- package/dist/lib/file-state.d.ts +6 -0
- package/dist/lib/file-state.js +25 -0
- package/dist/lib/identity.d.ts +9 -0
- package/dist/lib/identity.js +27 -0
- package/dist/lib/observability.d.ts +86 -0
- package/dist/lib/observability.js +534 -0
- package/dist/lib/output.d.ts +25 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/paths.d.ts +11 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/prompts.d.ts +23 -0
- package/dist/lib/prompts.js +50 -0
- package/dist/lib/recipe-discovery.d.ts +50 -0
- package/dist/lib/recipe-discovery.js +317 -0
- package/dist/lib/recipe-migration.d.ts +21 -0
- package/dist/lib/recipe-migration.js +90 -0
- package/dist/lib/recipe-references.d.ts +67 -0
- package/dist/lib/recipe-references.js +542 -0
- package/dist/lib/recipe-usage.d.ts +6 -0
- package/dist/lib/recipe-usage.js +57 -0
- package/dist/lib/registry.d.ts +47 -0
- package/dist/lib/registry.js +222 -0
- package/dist/lib/runtime.d.ts +36 -0
- package/dist/lib/runtime.js +126 -0
- package/dist/lib/schema.d.ts +48 -0
- package/dist/lib/schema.js +355 -0
- package/dist/lib/temp.d.ts +10 -0
- package/dist/lib/temp.js +90 -0
- package/dist/lib/tools.d.ts +39 -0
- package/dist/lib/tools.js +982 -0
- package/lib/async-runs.ts +20 -4
- package/package.json +6 -3
- package/scripts/async-runner.mjs +26 -6
- package/scripts/validate-recipe.mjs +19 -2
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor room persistence helpers.
|
|
3
|
+
* Zones: room timelines, room rosters, cross-branch discovery state
|
|
4
|
+
* Owns small file-backed room state; routing policy stays in tools/runtime adapters.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
10
|
+
const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
11
|
+
const DEFAULT_ROOM_MAX_MESSAGES = 10000;
|
|
12
|
+
const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
13
|
+
function roomDir(stateDir, room) {
|
|
14
|
+
return path.join(stateDir, "rooms", room);
|
|
15
|
+
}
|
|
16
|
+
function messagesFile(stateDir, room) {
|
|
17
|
+
return path.join(roomDir(stateDir, room), "messages.jsonl");
|
|
18
|
+
}
|
|
19
|
+
function rosterFile(stateDir, room) {
|
|
20
|
+
return path.join(roomDir(stateDir, room), "roster.json");
|
|
21
|
+
}
|
|
22
|
+
function snapshotFile(stateDir) {
|
|
23
|
+
return path.join(stateDir, "communication.json");
|
|
24
|
+
}
|
|
25
|
+
function branchSnapshotFile(stateDir, branch) {
|
|
26
|
+
return path.join(stateDir, "branches", branch, "communication.json");
|
|
27
|
+
}
|
|
28
|
+
function branchInboxFile(stateDir, branch) {
|
|
29
|
+
return path.join(stateDir, "branches", branch, "inbox.jsonl");
|
|
30
|
+
}
|
|
31
|
+
function branchIdFromAddress(address, run) {
|
|
32
|
+
if (!address)
|
|
33
|
+
return undefined;
|
|
34
|
+
const match = new RegExp(`^branch:${run}/(.+)$`).exec(address);
|
|
35
|
+
return match?.[1];
|
|
36
|
+
}
|
|
37
|
+
function ensureRoomDir(stateDir, room) {
|
|
38
|
+
fs.mkdirSync(roomDir(stateDir, room), { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
function sleepSync(ms) {
|
|
41
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
42
|
+
}
|
|
43
|
+
function acquireStateLock(parentDir, name, label) {
|
|
44
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
45
|
+
const lockDir = path.join(parentDir, name);
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
while (true) {
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(lockDir);
|
|
50
|
+
fs.writeFileSync(path.join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`);
|
|
51
|
+
return () => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.statSync(lockDir);
|
|
56
|
+
if (Date.now() - stat.mtimeMs > STATE_LOCK_MAX_AGE_MS) {
|
|
57
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Date.now() - started > STATE_LOCK_TIMEOUT_MS) {
|
|
65
|
+
throw new Error(`${label} lock timed out.`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
sleepSync(10);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function acquireRoomLock(stateDir, room) {
|
|
72
|
+
return acquireStateLock(roomDir(stateDir, room), ".append.lock", `Room append ${room}`);
|
|
73
|
+
}
|
|
74
|
+
function acquireBranchInboxLock(stateDir, branch) {
|
|
75
|
+
return acquireStateLock(path.dirname(branchInboxFile(stateDir, branch)), ".inbox.lock", `Branch inbox ${branch}`);
|
|
76
|
+
}
|
|
77
|
+
function asRecord(value) {
|
|
78
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
79
|
+
? value
|
|
80
|
+
: {};
|
|
81
|
+
}
|
|
82
|
+
function runFromRoomAddress(address) {
|
|
83
|
+
const match = /^room:([^/]+)(?:\/main)?$/.exec(address);
|
|
84
|
+
return match?.[1];
|
|
85
|
+
}
|
|
86
|
+
function readJsonFile(file, fallback) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === "ENOENT")
|
|
92
|
+
return fallback;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function writeJsonFile(file, value) {
|
|
97
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
99
|
+
}
|
|
100
|
+
function positiveEnvInt(name, fallback) {
|
|
101
|
+
const value = Number(process.env[name] ?? fallback);
|
|
102
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
103
|
+
}
|
|
104
|
+
function roomMaxMessages() {
|
|
105
|
+
return positiveEnvInt("PI_ACTORS_ROOM_MAX_MESSAGES", DEFAULT_ROOM_MAX_MESSAGES);
|
|
106
|
+
}
|
|
107
|
+
function snapshotMinIntervalMs() {
|
|
108
|
+
return positiveEnvInt("PI_ACTORS_COMMUNICATION_SNAPSHOT_MIN_MS", DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
109
|
+
}
|
|
110
|
+
function rosterMinIntervalMs() {
|
|
111
|
+
return positiveEnvInt("PI_ACTORS_ROOM_ROSTER_MIN_MS", DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
112
|
+
}
|
|
113
|
+
function compactRoomMessages(stateDir, room) {
|
|
114
|
+
const maxMessages = roomMaxMessages();
|
|
115
|
+
const file = messagesFile(stateDir, room);
|
|
116
|
+
const lines = readJsonlTailLines(file, maxMessages + 1);
|
|
117
|
+
if (lines.length <= maxMessages)
|
|
118
|
+
return;
|
|
119
|
+
const kept = lines.slice(-maxMessages);
|
|
120
|
+
fs.writeFileSync(file, `${kept.join("\n")}\n`);
|
|
121
|
+
writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
|
|
122
|
+
compacted_at: new Date().toISOString(),
|
|
123
|
+
max_messages: maxMessages,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function readJsonlLineCount(file) {
|
|
127
|
+
const stat = fs.statSync(file);
|
|
128
|
+
if (stat.size === 0)
|
|
129
|
+
return 0;
|
|
130
|
+
const fd = fs.openSync(file, "r");
|
|
131
|
+
try {
|
|
132
|
+
const chunkSize = 64 * 1024;
|
|
133
|
+
const chunk = Buffer.allocUnsafe(chunkSize);
|
|
134
|
+
let position = 0;
|
|
135
|
+
let count = 0;
|
|
136
|
+
let lastByte;
|
|
137
|
+
while (position < stat.size) {
|
|
138
|
+
const bytesRead = fs.readSync(fd, chunk, 0, Math.min(chunkSize, stat.size - position), position);
|
|
139
|
+
if (bytesRead <= 0)
|
|
140
|
+
break;
|
|
141
|
+
position += bytesRead;
|
|
142
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
143
|
+
if (chunk[index] === 10)
|
|
144
|
+
count += 1;
|
|
145
|
+
}
|
|
146
|
+
lastByte = chunk[bytesRead - 1];
|
|
147
|
+
}
|
|
148
|
+
return lastByte === 10 ? count : count + 1;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
fs.closeSync(fd);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function readJsonlTailLines(file, limit) {
|
|
155
|
+
const lineLimit = Math.max(1, limit);
|
|
156
|
+
const stat = fs.statSync(file);
|
|
157
|
+
if (stat.size === 0)
|
|
158
|
+
return [];
|
|
159
|
+
const fd = fs.openSync(file, "r");
|
|
160
|
+
try {
|
|
161
|
+
const chunkSize = 64 * 1024;
|
|
162
|
+
const chunks = [];
|
|
163
|
+
let position = stat.size;
|
|
164
|
+
let newlines = 0;
|
|
165
|
+
while (position > 0 && newlines <= lineLimit) {
|
|
166
|
+
const size = Math.min(chunkSize, position);
|
|
167
|
+
position -= size;
|
|
168
|
+
const chunk = Buffer.allocUnsafe(size);
|
|
169
|
+
fs.readSync(fd, chunk, 0, size, position);
|
|
170
|
+
chunks.unshift(chunk);
|
|
171
|
+
for (let index = size - 1; index >= 0; index -= 1) {
|
|
172
|
+
if (chunk[index] === 10)
|
|
173
|
+
newlines += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return Buffer.concat(chunks)
|
|
177
|
+
.toString("utf8")
|
|
178
|
+
.split("\n")
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.slice(-lineLimit);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
fs.closeSync(fd);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export function readRoomRoster(stateDir, room) {
|
|
187
|
+
return readJsonFile(rosterFile(stateDir, room), {});
|
|
188
|
+
}
|
|
189
|
+
function writeRoomRoster(stateDir, room, roster) {
|
|
190
|
+
ensureRoomDir(stateDir, room);
|
|
191
|
+
writeJsonFile(rosterFile(stateDir, room), roster);
|
|
192
|
+
}
|
|
193
|
+
function shouldDebounceFile(file, minIntervalMs) {
|
|
194
|
+
try {
|
|
195
|
+
return Date.now() - fs.statSync(file).mtimeMs < minIntervalMs;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error.code === "ENOENT")
|
|
199
|
+
return false;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function shouldDebounceSnapshot(file) {
|
|
204
|
+
return shouldDebounceFile(file, snapshotMinIntervalMs());
|
|
205
|
+
}
|
|
206
|
+
function comparableRosterMember(member) {
|
|
207
|
+
if (!member)
|
|
208
|
+
return "";
|
|
209
|
+
const { last_seen: _lastSeen, ...semantic } = member;
|
|
210
|
+
return JSON.stringify(semantic);
|
|
211
|
+
}
|
|
212
|
+
function shouldWriteRoomRosterMember(stateDir, room, before, after) {
|
|
213
|
+
if (!before)
|
|
214
|
+
return true;
|
|
215
|
+
if (comparableRosterMember(before) !== comparableRosterMember(after)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return !shouldDebounceFile(rosterFile(stateDir, room), rosterMinIntervalMs());
|
|
219
|
+
}
|
|
220
|
+
function updateRosterForMessage(stateDir, room, message, receivedAt) {
|
|
221
|
+
const roster = readRoomRoster(stateDir, room);
|
|
222
|
+
if (!message.from)
|
|
223
|
+
return roster;
|
|
224
|
+
const body = asRecord(message.body);
|
|
225
|
+
const current = roster[message.from];
|
|
226
|
+
const next = message.type === "actor.leave"
|
|
227
|
+
? {
|
|
228
|
+
address: message.from,
|
|
229
|
+
joined_at: current?.joined_at ?? receivedAt,
|
|
230
|
+
last_seen: receivedAt,
|
|
231
|
+
...(current?.caps !== undefined ? { caps: current.caps } : {}),
|
|
232
|
+
...(current?.claim !== undefined ? { claim: current.claim } : {}),
|
|
233
|
+
...(current?.display !== undefined ? { display: current.display } : {}),
|
|
234
|
+
...(current?.parent !== undefined ? { parent: current.parent } : {}),
|
|
235
|
+
...(current?.role !== undefined ? { role: current.role } : { role: "actor" }),
|
|
236
|
+
status: String(body.status ?? "left"),
|
|
237
|
+
}
|
|
238
|
+
: {
|
|
239
|
+
address: message.from,
|
|
240
|
+
joined_at: current?.joined_at ?? receivedAt,
|
|
241
|
+
last_seen: receivedAt,
|
|
242
|
+
...(body.caps !== undefined ? { caps: body.caps } : current?.caps !== undefined ? { caps: current.caps } : {}),
|
|
243
|
+
...(body.claim !== undefined ? { claim: body.claim } : current?.claim !== undefined ? { claim: current.claim } : {}),
|
|
244
|
+
...(body.display !== undefined ? { display: body.display } : current?.display !== undefined ? { display: current.display } : {}),
|
|
245
|
+
...(body.parent !== undefined ? { parent: body.parent } : current?.parent !== undefined ? { parent: current.parent } : {}),
|
|
246
|
+
...(body.role !== undefined ? { role: body.role } : current?.role !== undefined ? { role: current.role } : { role: "actor" }),
|
|
247
|
+
status: String(body.status ?? current?.status ?? "present"),
|
|
248
|
+
};
|
|
249
|
+
roster[message.from] = next;
|
|
250
|
+
if (shouldWriteRoomRosterMember(stateDir, room, current, next)) {
|
|
251
|
+
writeRoomRoster(stateDir, room, roster);
|
|
252
|
+
}
|
|
253
|
+
return roster;
|
|
254
|
+
}
|
|
255
|
+
export function readBranchInboxMessages(stateDir, run, address, limit = 40) {
|
|
256
|
+
const branch = branchIdFromAddress(address, run);
|
|
257
|
+
if (!branch)
|
|
258
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
259
|
+
try {
|
|
260
|
+
return readJsonlTailLines(branchInboxFile(stateDir, branch), limit).map((line) => JSON.parse(line));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error.code === "ENOENT")
|
|
264
|
+
return [];
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export function appendBranchInboxMessage(stateDir, run, address, message) {
|
|
269
|
+
const branch = branchIdFromAddress(address, run);
|
|
270
|
+
if (!branch)
|
|
271
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
272
|
+
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
273
|
+
try {
|
|
274
|
+
fs.writeFileSync(branchInboxFile(stateDir, branch), `${JSON.stringify({ ...message, id: randomUUID(), queued_at: new Date().toISOString(), status: "queued" })}\n`, { flag: "a" });
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
releaseLock();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export function updateBranchInboxMessageStatus(stateDir, run, address, id, status, metadata = {}) {
|
|
281
|
+
const branch = branchIdFromAddress(address, run);
|
|
282
|
+
if (!branch)
|
|
283
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
284
|
+
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
285
|
+
try {
|
|
286
|
+
const file = branchInboxFile(stateDir, branch);
|
|
287
|
+
const messages = readBranchInboxMessages(stateDir, run, address, Number.MAX_SAFE_INTEGER);
|
|
288
|
+
let changed = false;
|
|
289
|
+
const timestampKey = `${status}_at`;
|
|
290
|
+
const updated = messages.map((message) => {
|
|
291
|
+
if (message.id !== id)
|
|
292
|
+
return message;
|
|
293
|
+
changed = true;
|
|
294
|
+
return { ...message, ...metadata, [timestampKey]: new Date().toISOString(), status };
|
|
295
|
+
});
|
|
296
|
+
if (!changed)
|
|
297
|
+
return false;
|
|
298
|
+
fs.writeFileSync(file, `${updated.map((message) => JSON.stringify(message)).join("\n")}\n`);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
releaseLock();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function appendRoomMessage(stateDir, room, message) {
|
|
306
|
+
const releaseLock = acquireRoomLock(stateDir, room);
|
|
307
|
+
try {
|
|
308
|
+
const receivedAt = new Date().toISOString();
|
|
309
|
+
const entry = { ...message, received_at: receivedAt };
|
|
310
|
+
fs.appendFileSync(messagesFile(stateDir, room), `${JSON.stringify(entry)}\n`);
|
|
311
|
+
compactRoomMessages(stateDir, room);
|
|
312
|
+
const roster = updateRosterForMessage(stateDir, room, message, receivedAt);
|
|
313
|
+
const run = runFromRoomAddress(message.to);
|
|
314
|
+
if (run) {
|
|
315
|
+
writeCommunicationSnapshot(stateDir, run);
|
|
316
|
+
if (message.from && branchIdFromAddress(message.from, run)) {
|
|
317
|
+
writeBranchCommunicationSnapshotDebounced(stateDir, run, message.from);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
message_count: readRoomMessages(stateDir, room).length,
|
|
322
|
+
room,
|
|
323
|
+
roster_count: Object.keys(roster).length,
|
|
324
|
+
sent: true,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
releaseLock();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
export function readRoomMessages(stateDir, room, limit = 40) {
|
|
332
|
+
try {
|
|
333
|
+
const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
|
|
334
|
+
return lines.map((line) => JSON.parse(line));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error.code === "ENOENT")
|
|
338
|
+
return [];
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function previewValue(value, maxLength = 120) {
|
|
343
|
+
if (value === undefined)
|
|
344
|
+
return undefined;
|
|
345
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
346
|
+
const compact = text.replaceAll(/\s+/g, " ").trim();
|
|
347
|
+
if (!compact)
|
|
348
|
+
return undefined;
|
|
349
|
+
return compact.length > maxLength
|
|
350
|
+
? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
|
|
351
|
+
: compact;
|
|
352
|
+
}
|
|
353
|
+
export function readRoomMessagePreviews(stateDir, room, limit = 40) {
|
|
354
|
+
return readRoomMessages(stateDir, room, limit).map((message) => ({
|
|
355
|
+
...(previewValue(message.body) ? { body_preview: previewValue(message.body) } : {}),
|
|
356
|
+
...(message.from ? { from: message.from } : {}),
|
|
357
|
+
...(message.summary ? { summary: message.summary } : {}),
|
|
358
|
+
timestamp: message.received_at,
|
|
359
|
+
to: message.to,
|
|
360
|
+
type: message.type,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
export function getRoomStatus(stateDir, room) {
|
|
364
|
+
let messageCount = 0;
|
|
365
|
+
try {
|
|
366
|
+
messageCount = readJsonlLineCount(messagesFile(stateDir, room));
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
if (error.code !== "ENOENT")
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
const [last] = readRoomMessages(stateDir, room, 1);
|
|
373
|
+
return {
|
|
374
|
+
...(last
|
|
375
|
+
? {
|
|
376
|
+
last_message_at: last.received_at,
|
|
377
|
+
...(last.from ? { last_message_from: last.from } : {}),
|
|
378
|
+
...(last.summary ? { last_message_summary: last.summary } : {}),
|
|
379
|
+
last_message_type: last.type,
|
|
380
|
+
}
|
|
381
|
+
: {}),
|
|
382
|
+
message_count: messageCount,
|
|
383
|
+
room,
|
|
384
|
+
roster_count: Object.keys(readRoomRoster(stateDir, room)).length,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
export function ensureRoomMember(stateDir, run, room, address, body, summary) {
|
|
388
|
+
const roster = readRoomRoster(stateDir, room);
|
|
389
|
+
if (roster[address]) {
|
|
390
|
+
return {
|
|
391
|
+
message_count: readRoomMessages(stateDir, room).length,
|
|
392
|
+
room,
|
|
393
|
+
roster_count: Object.keys(roster).length,
|
|
394
|
+
sent: true,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return appendRoomMessage(stateDir, room, {
|
|
398
|
+
body,
|
|
399
|
+
from: address,
|
|
400
|
+
summary,
|
|
401
|
+
to: `room:${run}`,
|
|
402
|
+
type: "actor.join",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
export function ensureDefaultRoom(stateDir, run) {
|
|
406
|
+
return ensureRoomMember(stateDir, run, "main", `run:${run}`, { role: "run", status: "present" }, "Run joined default room");
|
|
407
|
+
}
|
|
408
|
+
export function readCommunicationSnapshot(stateDir) {
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(fs.readFileSync(snapshotFile(stateDir), "utf8"));
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (error.code === "ENOENT")
|
|
414
|
+
return undefined;
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
export function readRoomContacts(stateDir, room, self) {
|
|
419
|
+
return Object.values(readRoomRoster(stateDir, room))
|
|
420
|
+
.filter((member) => member.address !== self)
|
|
421
|
+
.map((member) => ({
|
|
422
|
+
address: member.address,
|
|
423
|
+
...(member.caps !== undefined ? { caps: member.caps } : {}),
|
|
424
|
+
...(member.claim !== undefined ? { claim: member.claim } : {}),
|
|
425
|
+
...(member.parent !== undefined ? { parent: member.parent } : {}),
|
|
426
|
+
...(member.role !== undefined ? { role: member.role } : {}),
|
|
427
|
+
...(member.status !== undefined ? { status: member.status } : {}),
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
function buildCommunicationSnapshot(stateDir, run, self) {
|
|
431
|
+
const mainRoster = Object.values(readRoomRoster(stateDir, "main"));
|
|
432
|
+
const contacts = readRoomContacts(stateDir, "main", self);
|
|
433
|
+
return {
|
|
434
|
+
...(contacts.length > 0 ? { contacts } : {}),
|
|
435
|
+
...(self !== `run:${run}` ? { parent: `run:${run}` } : {}),
|
|
436
|
+
root: `run:${run}`,
|
|
437
|
+
self,
|
|
438
|
+
rooms: [
|
|
439
|
+
{
|
|
440
|
+
address: `room:${run}`,
|
|
441
|
+
...(mainRoster.length > 0 ? { members: mainRoster } : {}),
|
|
442
|
+
name: "main",
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
updated_at: new Date().toISOString(),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
export function writeCommunicationSnapshot(stateDir, run) {
|
|
449
|
+
const snapshot = buildCommunicationSnapshot(stateDir, run, `run:${run}`);
|
|
450
|
+
writeJsonFile(snapshotFile(stateDir), snapshot);
|
|
451
|
+
return snapshot;
|
|
452
|
+
}
|
|
453
|
+
export function writeBranchCommunicationSnapshot(stateDir, run, self) {
|
|
454
|
+
const branch = branchIdFromAddress(self, run);
|
|
455
|
+
if (!branch)
|
|
456
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
|
|
457
|
+
const snapshot = buildCommunicationSnapshot(stateDir, run, self);
|
|
458
|
+
writeJsonFile(branchSnapshotFile(stateDir, branch), snapshot);
|
|
459
|
+
return snapshot;
|
|
460
|
+
}
|
|
461
|
+
function writeBranchCommunicationSnapshotDebounced(stateDir, run, self) {
|
|
462
|
+
const branch = branchIdFromAddress(self, run);
|
|
463
|
+
if (!branch)
|
|
464
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
|
|
465
|
+
if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
|
|
466
|
+
return undefined;
|
|
467
|
+
return writeBranchCommunicationSnapshot(stateDir, run, self);
|
|
468
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-template async run primitives
|
|
3
|
+
* Zones: async runtime, lifecycle, state files
|
|
4
|
+
* Owns detached run state, observation, log tailing, listing, and cancellation safety
|
|
5
|
+
*/
|
|
6
|
+
import type { CommandTemplateFailureScope, CommandTemplateValue } from "./command-templates.ts";
|
|
7
|
+
import * as RecipeReferences from "./recipe-references.ts";
|
|
8
|
+
export type AsyncRunLaunchSource = "spawn" | "tool";
|
|
9
|
+
export interface AsyncRunStartParams {
|
|
10
|
+
async?: boolean;
|
|
11
|
+
file?: string;
|
|
12
|
+
launch_source?: AsyncRunLaunchSource;
|
|
13
|
+
name?: string;
|
|
14
|
+
ownerId?: string;
|
|
15
|
+
run_id?: string;
|
|
16
|
+
state_dir?: string;
|
|
17
|
+
tool?: string;
|
|
18
|
+
template?: CommandTemplateValue;
|
|
19
|
+
args?: string[];
|
|
20
|
+
defaults?: Record<string, unknown>;
|
|
21
|
+
parallel?: boolean;
|
|
22
|
+
label?: string;
|
|
23
|
+
when?: boolean | string;
|
|
24
|
+
timeout?: number | string;
|
|
25
|
+
delay?: number | string;
|
|
26
|
+
output?: string;
|
|
27
|
+
artifacts?: Record<string, string>;
|
|
28
|
+
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
29
|
+
retire_when?: "children_terminal";
|
|
30
|
+
retry?: number | string;
|
|
31
|
+
failure?: CommandTemplateFailureScope;
|
|
32
|
+
recover?: CommandTemplateValue;
|
|
33
|
+
repeat?: number;
|
|
34
|
+
values?: Record<string, unknown>;
|
|
35
|
+
actor_context?: boolean | string;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
}
|
|
38
|
+
export type AsyncRunStatus = "running" | "done" | "failed" | "exited" | "cancelled" | "killed";
|
|
39
|
+
export type RunOutboxDelivery = "log" | "notify" | "followup";
|
|
40
|
+
export type RunOutboxLevel = "info" | "warning" | "error";
|
|
41
|
+
export interface RunOutboxEvent {
|
|
42
|
+
body?: unknown;
|
|
43
|
+
correlation_id?: string;
|
|
44
|
+
data?: unknown;
|
|
45
|
+
delivery: RunOutboxDelivery;
|
|
46
|
+
event: string;
|
|
47
|
+
from?: string;
|
|
48
|
+
id: string;
|
|
49
|
+
level: RunOutboxLevel;
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
reply_to?: string;
|
|
52
|
+
run: string;
|
|
53
|
+
state_dir: string;
|
|
54
|
+
summary: string;
|
|
55
|
+
to?: string;
|
|
56
|
+
ts: string;
|
|
57
|
+
type?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface AsyncRunMeta {
|
|
60
|
+
argv: string[];
|
|
61
|
+
createdAt: string;
|
|
62
|
+
cwd: string;
|
|
63
|
+
launch_source?: AsyncRunLaunchSource;
|
|
64
|
+
ownerId?: string;
|
|
65
|
+
pid: number;
|
|
66
|
+
recipe?: string;
|
|
67
|
+
recipe_file?: string;
|
|
68
|
+
run: string;
|
|
69
|
+
state_dir: string;
|
|
70
|
+
status: AsyncRunStatus;
|
|
71
|
+
tool?: string;
|
|
72
|
+
template: CommandTemplateValue;
|
|
73
|
+
values: Record<string, unknown>;
|
|
74
|
+
artifacts?: Record<string, string>;
|
|
75
|
+
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
76
|
+
recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
|
|
77
|
+
retire_when?: "children_terminal";
|
|
78
|
+
}
|
|
79
|
+
export declare function startRun(params: AsyncRunStartParams, cwd: string): AsyncRunMeta;
|
|
80
|
+
export declare function parseRunOutboxEventLine(line: string, run: string, stateDir: string, index: number): RunOutboxEvent | undefined;
|
|
81
|
+
export declare function getRunStatus(runOrDir: string): Record<string, unknown>;
|
|
82
|
+
export declare function listRuns(stateRoot?: string, statusFilter?: string): Array<Record<string, unknown>>;
|
|
83
|
+
export declare function tailRun(runOrDir: string, lines?: number): string;
|
|
84
|
+
export declare function readRunEvents(runOrDir: string, lines?: number): RunOutboxEvent[];
|
|
85
|
+
export declare function appendRunOutboxEvent(runOrDir: string, event: {
|
|
86
|
+
body?: unknown;
|
|
87
|
+
correlation_id?: string;
|
|
88
|
+
data?: unknown;
|
|
89
|
+
delivery?: string;
|
|
90
|
+
event?: string;
|
|
91
|
+
from?: string;
|
|
92
|
+
level?: string;
|
|
93
|
+
metadata?: Record<string, unknown>;
|
|
94
|
+
reply_to?: string;
|
|
95
|
+
summary?: string;
|
|
96
|
+
to?: string;
|
|
97
|
+
type?: string;
|
|
98
|
+
}): Record<string, unknown>;
|
|
99
|
+
export declare function sendRunMessage(runOrDir: string, message: string): Record<string, unknown>;
|
|
100
|
+
export declare function cancelRun(runOrDir: string): Record<string, unknown>;
|
|
101
|
+
export declare function killRun(runOrDir: string): Record<string, unknown>;
|