@llblab/pi-actors 0.22.5 → 0.24.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 +125 -59
- package/BACKLOG.md +88 -403
- package/CHANGELOG.md +39 -0
- package/README.md +14 -1
- package/dist/fixtures/protocol/actor-message-branch.json +13 -0
- package/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/dist/fixtures/protocol/mailbox-contract.json +15 -0
- package/dist/fixtures/protocol/recipe-summary.json +16 -0
- package/dist/fixtures/protocol/room-message.json +11 -0
- package/dist/fixtures/protocol/room-roster.json +11 -0
- package/dist/fixtures/protocol/run-inbox-message.json +9 -0
- package/dist/fixtures/protocol/run-outbox-event.json +9 -0
- package/dist/fixtures/protocol/run-state.json +10 -0
- package/dist/index.js +18 -2
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +58 -44
- package/dist/lib/actor-rooms.d.ts +16 -0
- package/dist/lib/actor-rooms.js +123 -28
- package/dist/lib/actor-worker.d.ts +13 -0
- package/dist/lib/actor-worker.js +86 -0
- package/dist/lib/async-runner.d.ts +5 -0
- package/dist/lib/async-runner.js +134 -0
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +224 -50
- package/dist/lib/build-dist.d.ts +5 -0
- package/dist/lib/build-dist.js +24 -0
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/conformance.d.ts +12 -0
- package/dist/lib/conformance.js +28 -0
- package/dist/lib/coordinator.d.ts +5 -0
- package/dist/lib/coordinator.js +557 -0
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/locker.d.ts +5 -0
- package/dist/lib/locker.js +310 -0
- package/dist/lib/mailbox-loop.d.ts +41 -0
- package/dist/lib/mailbox-loop.js +62 -0
- package/dist/lib/observability.d.ts +2 -2
- package/dist/lib/observability.js +57 -57
- package/dist/lib/output.js +4 -5
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +29 -4
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/recipe-utils.d.ts +5 -0
- package/dist/lib/recipe-utils.js +385 -0
- package/dist/lib/runtime-notifier.js +3 -7
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +198 -39
- package/dist/lib/validate-recipe.d.ts +6 -0
- package/dist/lib/validate-recipe.js +104 -0
- package/dist/recipes/actor-worker.json +35 -0
- package/dist/recipes/coordinator-locker.json +45 -0
- package/dist/recipes/lens-swarm.json +66 -0
- package/dist/recipes/locker.json +45 -0
- package/dist/recipes/music-player.json +38 -0
- package/dist/recipes/pipeline-architect-coordinator.json +95 -0
- package/dist/recipes/pipeline-artifact-bundle.json +100 -0
- package/dist/recipes/pipeline-artifact-report.json +58 -0
- package/dist/recipes/pipeline-artifact-write.json +72 -0
- package/dist/recipes/pipeline-async-run-ops.json +70 -0
- package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
- package/dist/recipes/pipeline-development-tasking.json +81 -0
- package/dist/recipes/pipeline-docs-maintenance.json +80 -0
- package/dist/recipes/pipeline-media-library.json +59 -0
- package/dist/recipes/pipeline-quorum-review.json +79 -0
- package/dist/recipes/pipeline-release-readiness.json +110 -0
- package/dist/recipes/pipeline-release-summary.json +88 -0
- package/dist/recipes/pipeline-repo-health.json +89 -0
- package/dist/recipes/pipeline-research-synthesis.json +94 -0
- package/dist/recipes/pipeline-review-readiness.json +54 -0
- package/dist/recipes/pipeline-room-swarm.json +50 -0
- package/dist/recipes/subagent-artifact.json +32 -0
- package/dist/recipes/subagent-checkpoint.json +33 -0
- package/dist/recipes/subagent-conflict-report.json +32 -0
- package/dist/recipes/subagent-contradiction-map.json +33 -0
- package/dist/recipes/subagent-critic.json +35 -0
- package/dist/recipes/subagent-evidence-map.json +33 -0
- package/dist/recipes/subagent-followup.json +33 -0
- package/dist/recipes/subagent-judge.json +33 -0
- package/dist/recipes/subagent-merge.json +33 -0
- package/dist/recipes/subagent-message.json +34 -0
- package/dist/recipes/subagent-normalize.json +31 -0
- package/dist/recipes/subagent-plan.json +33 -0
- package/dist/recipes/subagent-prompt.json +28 -0
- package/dist/recipes/subagent-quorum.json +43 -0
- package/dist/recipes/subagent-review-coordinator.json +114 -0
- package/dist/recipes/subagent-review.json +37 -0
- package/dist/recipes/subagent-task-card.json +35 -0
- package/dist/recipes/subagent-tools.json +27 -0
- package/dist/recipes/subagent-verify.json +34 -0
- package/dist/recipes/subagents-prompts.json +51 -0
- package/dist/recipes/utility-actor-message.json +23 -0
- package/dist/recipes/utility-artifact-manifest.json +16 -0
- package/dist/recipes/utility-artifact-write.json +16 -0
- package/dist/recipes/utility-changelog-head.json +11 -0
- package/dist/recipes/utility-changelog-section.json +13 -0
- package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
- package/dist/recipes/utility-git-log.json +11 -0
- package/dist/recipes/utility-git-status.json +9 -0
- package/dist/recipes/utility-jsonl-tail.json +10 -0
- package/dist/recipes/utility-markdown-index.json +14 -0
- package/dist/recipes/utility-package-summary.json +11 -0
- package/dist/recipes/utility-playlist-build.json +17 -0
- package/dist/recipes/utility-playlist-scan.json +11 -0
- package/dist/recipes/utility-run-ops-snapshot.json +17 -0
- package/dist/recipes/utility-run-state-files.json +13 -0
- package/dist/recipes/utility-run-summary.json +11 -0
- package/dist/recipes/utility-skill-summary.json +13 -0
- package/dist/recipes/utility-validate-recipe.json +13 -0
- package/dist/recipes/utility-validation-wrapper.json +13 -0
- package/dist/scripts/actor-worker.mjs +31 -0
- package/dist/scripts/async-runner.mjs +31 -0
- package/dist/scripts/build-dist.mjs +25 -0
- package/dist/scripts/conformance.mjs +33 -0
- package/dist/scripts/coordinator.mjs +31 -0
- package/dist/scripts/locker.mjs +33 -0
- package/dist/scripts/music-player.mjs +964 -0
- package/dist/scripts/recipe-utils.mjs +31 -0
- package/dist/scripts/validate-recipe.mjs +34 -0
- package/dist/skills/actors/SKILL.md +375 -0
- package/dist/skills/swarm/SKILL.md +467 -0
- package/dist/skills/swarm/references/development-swarm.md +596 -0
- package/docs/actor-messages.md +2 -2
- package/docs/async-runs.md +13 -1
- package/docs/template-recipes.md +2 -2
- package/docs/tool-registry.md +0 -1
- package/fixtures/protocol/actor-message-branch.json +13 -0
- package/fixtures/protocol/artifact-manifest.json +9 -0
- package/fixtures/protocol/mailbox-contract.json +15 -0
- package/fixtures/protocol/recipe-summary.json +16 -0
- package/fixtures/protocol/room-message.json +11 -0
- package/fixtures/protocol/room-roster.json +11 -0
- package/fixtures/protocol/run-inbox-message.json +9 -0
- package/fixtures/protocol/run-outbox-event.json +9 -0
- package/fixtures/protocol/run-state.json +10 -0
- package/index.ts +21 -0
- package/lib/actor-inspector-tui.ts +138 -59
- package/lib/actor-rooms.ts +241 -60
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +302 -53
- package/lib/build-dist.ts +30 -0
- package/lib/command-templates.ts +5 -5
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +649 -0
- package/lib/limits.ts +12 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +34 -23
- package/lib/output.ts +4 -6
- package/lib/prompts.ts +1 -1
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +57 -6
- package/lib/recipe-usage.ts +31 -4
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/runtime.ts +31 -7
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +297 -58
- package/lib/validate-recipe.ts +110 -0
- package/package.json +11 -2
- package/recipes/actor-worker.json +35 -0
- package/recipes/pipeline-quorum-review.json +12 -7
- package/scripts/actor-worker.mjs +31 -0
- package/scripts/async-runner.mjs +11 -195
- package/scripts/build-dist.mjs +25 -0
- package/scripts/conformance.mjs +33 -0
- package/scripts/coordinator.mjs +19 -616
- package/scripts/locker.mjs +23 -322
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +20 -467
- package/scripts/validate-recipe.mjs +23 -113
- package/skills/actors/SKILL.md +7 -4
- package/skills/swarm/SKILL.md +3 -1
package/lib/actor-rooms.ts
CHANGED
|
@@ -9,7 +9,13 @@ import { randomUUID } from "node:crypto";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
|
|
11
11
|
import type { ActorMessage } from "./actor-messages.ts";
|
|
12
|
+
import * as Limits from "./limits.ts";
|
|
12
13
|
import { notifyRuntimeWake } from "./runtime-notifier.ts";
|
|
14
|
+
import {
|
|
15
|
+
formatStateReadDiagnostics,
|
|
16
|
+
readJsonFileResilient,
|
|
17
|
+
readJsonlFileResilient,
|
|
18
|
+
} from "./state-readers.ts";
|
|
13
19
|
|
|
14
20
|
const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
15
21
|
const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
@@ -49,10 +55,21 @@ export interface RoomMessagePreview {
|
|
|
49
55
|
type: string;
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
export interface RoomCompactionInfo {
|
|
59
|
+
compacted_at: string;
|
|
60
|
+
dropped_count: number;
|
|
61
|
+
first_kept_at?: string;
|
|
62
|
+
last_kept_at?: string;
|
|
63
|
+
max_messages: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
export interface RoomStatus {
|
|
53
67
|
room: string;
|
|
54
68
|
message_count: number;
|
|
55
69
|
roster_count: number;
|
|
70
|
+
compaction?: RoomCompactionInfo;
|
|
71
|
+
diagnostics?: string[];
|
|
72
|
+
diagnostics_count?: number;
|
|
56
73
|
last_message_at?: string;
|
|
57
74
|
last_message_from?: string;
|
|
58
75
|
last_message_summary?: string;
|
|
@@ -105,7 +122,10 @@ function branchInboxFile(stateDir: string, branch: string): string {
|
|
|
105
122
|
return path.join(stateDir, "branches", branch, "inbox.jsonl");
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
function branchIdFromAddress(
|
|
125
|
+
function branchIdFromAddress(
|
|
126
|
+
address: string | undefined,
|
|
127
|
+
run: string,
|
|
128
|
+
): string | undefined {
|
|
109
129
|
if (!address) return undefined;
|
|
110
130
|
const match = new RegExp(`^branch:${run}/(.+)$`).exec(address);
|
|
111
131
|
return match?.[1];
|
|
@@ -119,7 +139,11 @@ function sleepSync(ms: number): void {
|
|
|
119
139
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
function acquireStateLock(
|
|
142
|
+
function acquireStateLock(
|
|
143
|
+
parentDir: string,
|
|
144
|
+
name: string,
|
|
145
|
+
label: string,
|
|
146
|
+
): () => void {
|
|
123
147
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
124
148
|
const lockDir = path.join(parentDir, name);
|
|
125
149
|
const started = Date.now();
|
|
@@ -150,11 +174,19 @@ function acquireStateLock(parentDir: string, name: string, label: string): () =>
|
|
|
150
174
|
}
|
|
151
175
|
|
|
152
176
|
function acquireRoomLock(stateDir: string, room: string): () => void {
|
|
153
|
-
return acquireStateLock(
|
|
177
|
+
return acquireStateLock(
|
|
178
|
+
roomDir(stateDir, room),
|
|
179
|
+
".append.lock",
|
|
180
|
+
`Room append ${room}`,
|
|
181
|
+
);
|
|
154
182
|
}
|
|
155
183
|
|
|
156
184
|
function acquireBranchInboxLock(stateDir: string, branch: string): () => void {
|
|
157
|
-
return acquireStateLock(
|
|
185
|
+
return acquireStateLock(
|
|
186
|
+
path.dirname(branchInboxFile(stateDir, branch)),
|
|
187
|
+
".inbox.lock",
|
|
188
|
+
`Branch inbox ${branch}`,
|
|
189
|
+
);
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
@@ -169,12 +201,7 @@ function runFromRoomAddress(address: string): string | undefined {
|
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
function readJsonFile<T>(file: string, fallback: T): T {
|
|
172
|
-
|
|
173
|
-
return JSON.parse(fs.readFileSync(file, "utf8")) as T;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return fallback;
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
204
|
+
return readJsonFileResilient<T>(file, fallback).value;
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
function writeJsonFile(file: string, value: unknown): void {
|
|
@@ -201,7 +228,10 @@ function positiveEnvInt(name: string, fallback: number): number {
|
|
|
201
228
|
}
|
|
202
229
|
|
|
203
230
|
function roomMaxMessages(): number {
|
|
204
|
-
return positiveEnvInt(
|
|
231
|
+
return positiveEnvInt(
|
|
232
|
+
"PI_ACTORS_ROOM_MAX_MESSAGES",
|
|
233
|
+
DEFAULT_ROOM_MAX_MESSAGES,
|
|
234
|
+
);
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
function snapshotMinIntervalMs(): number {
|
|
@@ -225,8 +255,22 @@ function compactRoomMessages(stateDir: string, room: string): void {
|
|
|
225
255
|
if (lines.length <= maxMessages) return;
|
|
226
256
|
const kept = lines.slice(-maxMessages);
|
|
227
257
|
fs.writeFileSync(file, `${kept.join("\n")}\n`);
|
|
258
|
+
const keptMessages = kept.flatMap((line) => {
|
|
259
|
+
try {
|
|
260
|
+
return [JSON.parse(line) as RoomTimelineEntry];
|
|
261
|
+
} catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
});
|
|
228
265
|
writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
|
|
229
266
|
compacted_at: new Date().toISOString(),
|
|
267
|
+
dropped_count: lines.length - kept.length,
|
|
268
|
+
...(keptMessages[0]?.received_at
|
|
269
|
+
? { first_kept_at: keptMessages[0].received_at }
|
|
270
|
+
: {}),
|
|
271
|
+
...(keptMessages.at(-1)?.received_at
|
|
272
|
+
? { last_kept_at: keptMessages.at(-1)?.received_at }
|
|
273
|
+
: {}),
|
|
230
274
|
max_messages: maxMessages,
|
|
231
275
|
});
|
|
232
276
|
}
|
|
@@ -242,7 +286,13 @@ function readJsonlLineCount(file: string): number {
|
|
|
242
286
|
let count = 0;
|
|
243
287
|
let lastByte: number | undefined;
|
|
244
288
|
while (position < stat.size) {
|
|
245
|
-
const bytesRead = fs.readSync(
|
|
289
|
+
const bytesRead = fs.readSync(
|
|
290
|
+
fd,
|
|
291
|
+
chunk,
|
|
292
|
+
0,
|
|
293
|
+
Math.min(chunkSize, stat.size - position),
|
|
294
|
+
position,
|
|
295
|
+
);
|
|
246
296
|
if (bytesRead <= 0) break;
|
|
247
297
|
position += bytesRead;
|
|
248
298
|
for (let index = 0; index < bytesRead; index += 1) {
|
|
@@ -299,7 +349,10 @@ export function readRoomRoster(
|
|
|
299
349
|
stateDir: string,
|
|
300
350
|
room: string,
|
|
301
351
|
): Record<string, RoomMember> {
|
|
302
|
-
return readJsonFile<Record<string, RoomMember>>(
|
|
352
|
+
return readJsonFile<Record<string, RoomMember>>(
|
|
353
|
+
rosterFile(stateDir, room),
|
|
354
|
+
{},
|
|
355
|
+
);
|
|
303
356
|
}
|
|
304
357
|
|
|
305
358
|
function writeRoomRoster(
|
|
@@ -353,29 +406,54 @@ function updateRosterForMessage(
|
|
|
353
406
|
if (!message.from) return roster;
|
|
354
407
|
const body = asRecord(message.body);
|
|
355
408
|
const current = roster[message.from];
|
|
356
|
-
const next =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
const next =
|
|
410
|
+
message.type === "actor.leave"
|
|
411
|
+
? {
|
|
412
|
+
address: message.from,
|
|
413
|
+
joined_at: current?.joined_at ?? receivedAt,
|
|
414
|
+
last_seen: receivedAt,
|
|
415
|
+
...(current?.caps !== undefined ? { caps: current.caps } : {}),
|
|
416
|
+
...(current?.claim !== undefined ? { claim: current.claim } : {}),
|
|
417
|
+
...(current?.display !== undefined
|
|
418
|
+
? { display: current.display }
|
|
419
|
+
: {}),
|
|
420
|
+
...(current?.parent !== undefined ? { parent: current.parent } : {}),
|
|
421
|
+
...(current?.role !== undefined
|
|
422
|
+
? { role: current.role }
|
|
423
|
+
: { role: "actor" }),
|
|
424
|
+
status: String(body.status ?? "left"),
|
|
425
|
+
}
|
|
426
|
+
: {
|
|
427
|
+
address: message.from,
|
|
428
|
+
joined_at: current?.joined_at ?? receivedAt,
|
|
429
|
+
last_seen: receivedAt,
|
|
430
|
+
...(body.caps !== undefined
|
|
431
|
+
? { caps: body.caps }
|
|
432
|
+
: current?.caps !== undefined
|
|
433
|
+
? { caps: current.caps }
|
|
434
|
+
: {}),
|
|
435
|
+
...(body.claim !== undefined
|
|
436
|
+
? { claim: body.claim }
|
|
437
|
+
: current?.claim !== undefined
|
|
438
|
+
? { claim: current.claim }
|
|
439
|
+
: {}),
|
|
440
|
+
...(body.display !== undefined
|
|
441
|
+
? { display: body.display }
|
|
442
|
+
: current?.display !== undefined
|
|
443
|
+
? { display: current.display }
|
|
444
|
+
: {}),
|
|
445
|
+
...(body.parent !== undefined
|
|
446
|
+
? { parent: body.parent }
|
|
447
|
+
: current?.parent !== undefined
|
|
448
|
+
? { parent: current.parent }
|
|
449
|
+
: {}),
|
|
450
|
+
...(body.role !== undefined
|
|
451
|
+
? { role: body.role }
|
|
452
|
+
: current?.role !== undefined
|
|
453
|
+
? { role: current.role }
|
|
454
|
+
: { role: "actor" }),
|
|
455
|
+
status: String(body.status ?? current?.status ?? "present"),
|
|
456
|
+
};
|
|
379
457
|
roster[message.from] = next;
|
|
380
458
|
if (shouldWriteRoomRosterMember(stateDir, room, current, next)) {
|
|
381
459
|
writeRoomRoster(stateDir, room, roster);
|
|
@@ -384,6 +462,11 @@ function updateRosterForMessage(
|
|
|
384
462
|
}
|
|
385
463
|
|
|
386
464
|
export interface BranchInboxRecord extends ActorMessage {
|
|
465
|
+
claimed_at?: string;
|
|
466
|
+
claimed_by?: string;
|
|
467
|
+
error?: string;
|
|
468
|
+
failed_at?: string;
|
|
469
|
+
handled_at?: string;
|
|
387
470
|
id?: string;
|
|
388
471
|
queued_at?: string;
|
|
389
472
|
status?: string;
|
|
@@ -401,7 +484,8 @@ function readBranchInboxFile(
|
|
|
401
484
|
limit = 40,
|
|
402
485
|
): BranchInboxReadResult {
|
|
403
486
|
const branch = branchIdFromAddress(address, run);
|
|
404
|
-
if (!branch)
|
|
487
|
+
if (!branch)
|
|
488
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
405
489
|
try {
|
|
406
490
|
const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
|
|
407
491
|
const messages: BranchInboxRecord[] = [];
|
|
@@ -415,7 +499,8 @@ function readBranchInboxFile(
|
|
|
415
499
|
}
|
|
416
500
|
return { corrupted, messages };
|
|
417
501
|
} catch (error) {
|
|
418
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
502
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
503
|
+
return { corrupted: 0, messages: [] };
|
|
419
504
|
throw error;
|
|
420
505
|
}
|
|
421
506
|
}
|
|
@@ -426,7 +511,8 @@ function readAllBranchInboxLines(
|
|
|
426
511
|
address: string,
|
|
427
512
|
): Array<{ raw: string } | { message: BranchInboxRecord }> {
|
|
428
513
|
const branch = branchIdFromAddress(address, run);
|
|
429
|
-
if (!branch)
|
|
514
|
+
if (!branch)
|
|
515
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
430
516
|
try {
|
|
431
517
|
const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
|
|
432
518
|
return content
|
|
@@ -464,7 +550,9 @@ export function readBranchInboxDiagnostics(
|
|
|
464
550
|
}
|
|
465
551
|
|
|
466
552
|
export function getBranchInboxTerminalRetainLimit(): number {
|
|
467
|
-
const value = Number(
|
|
553
|
+
const value = Number(
|
|
554
|
+
process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "",
|
|
555
|
+
);
|
|
468
556
|
return Number.isInteger(value) && value >= 0
|
|
469
557
|
? value
|
|
470
558
|
: DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED;
|
|
@@ -490,7 +578,8 @@ export function appendBranchInboxMessage(
|
|
|
490
578
|
message: ActorMessage,
|
|
491
579
|
): void {
|
|
492
580
|
const branch = branchIdFromAddress(address, run);
|
|
493
|
-
if (!branch)
|
|
581
|
+
if (!branch)
|
|
582
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
494
583
|
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
495
584
|
try {
|
|
496
585
|
fs.writeFileSync(
|
|
@@ -507,6 +596,49 @@ export function appendBranchInboxMessage(
|
|
|
507
596
|
}
|
|
508
597
|
}
|
|
509
598
|
|
|
599
|
+
export function claimBranchInboxMessage(
|
|
600
|
+
stateDir: string,
|
|
601
|
+
run: string,
|
|
602
|
+
address: string,
|
|
603
|
+
owner = "runtime",
|
|
604
|
+
statuses: string[] = ["queued"],
|
|
605
|
+
): BranchInboxRecord | undefined {
|
|
606
|
+
const branch = branchIdFromAddress(address, run);
|
|
607
|
+
if (!branch)
|
|
608
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
609
|
+
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
610
|
+
try {
|
|
611
|
+
const file = branchInboxFile(stateDir, branch);
|
|
612
|
+
const records = readAllBranchInboxLines(stateDir, run, address);
|
|
613
|
+
let claimed: BranchInboxRecord | undefined;
|
|
614
|
+
const now = new Date().toISOString();
|
|
615
|
+
const updated = records.map((record) => {
|
|
616
|
+
if (claimed || !("message" in record)) return record;
|
|
617
|
+
const status = record.message.status ?? "queued";
|
|
618
|
+
if (!statuses.includes(status)) return record;
|
|
619
|
+
claimed = {
|
|
620
|
+
...record.message,
|
|
621
|
+
claimed_at: now,
|
|
622
|
+
claimed_by: owner,
|
|
623
|
+
status: "claimed",
|
|
624
|
+
};
|
|
625
|
+
return { message: claimed };
|
|
626
|
+
});
|
|
627
|
+
if (!claimed) return undefined;
|
|
628
|
+
fs.writeFileSync(
|
|
629
|
+
file,
|
|
630
|
+
`${updated.map((record) => ("raw" in record ? record.raw : JSON.stringify(record.message))).join("\n")}\n`,
|
|
631
|
+
);
|
|
632
|
+
notifyActorWake(stateDir, address, "branch.inbox.claim", {
|
|
633
|
+
id: claimed.id,
|
|
634
|
+
owner,
|
|
635
|
+
});
|
|
636
|
+
return claimed;
|
|
637
|
+
} finally {
|
|
638
|
+
releaseLock();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
510
642
|
export function updateBranchInboxMessageStatus(
|
|
511
643
|
stateDir: string,
|
|
512
644
|
run: string,
|
|
@@ -516,7 +648,8 @@ export function updateBranchInboxMessageStatus(
|
|
|
516
648
|
metadata: Record<string, unknown> = {},
|
|
517
649
|
): boolean {
|
|
518
650
|
const branch = branchIdFromAddress(address, run);
|
|
519
|
-
if (!branch)
|
|
651
|
+
if (!branch)
|
|
652
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
520
653
|
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
521
654
|
try {
|
|
522
655
|
const file = branchInboxFile(stateDir, branch);
|
|
@@ -526,11 +659,22 @@ export function updateBranchInboxMessageStatus(
|
|
|
526
659
|
const updated = records.map((record) => {
|
|
527
660
|
if (!("message" in record) || record.message.id !== id) return record;
|
|
528
661
|
changed = true;
|
|
529
|
-
return {
|
|
662
|
+
return {
|
|
663
|
+
message: {
|
|
664
|
+
...record.message,
|
|
665
|
+
...metadata,
|
|
666
|
+
[timestampKey]: new Date().toISOString(),
|
|
667
|
+
status,
|
|
668
|
+
},
|
|
669
|
+
};
|
|
530
670
|
});
|
|
531
671
|
if (!changed) return false;
|
|
532
|
-
const validMessages = updated.flatMap((record) =>
|
|
533
|
-
|
|
672
|
+
const validMessages = updated.flatMap((record) =>
|
|
673
|
+
"message" in record ? [record.message] : [],
|
|
674
|
+
);
|
|
675
|
+
const compactedIds = new Set(
|
|
676
|
+
compactBranchInboxMessages(validMessages).map((message) => message.id),
|
|
677
|
+
);
|
|
534
678
|
const lines = updated.flatMap((record) => {
|
|
535
679
|
if ("raw" in record) return [record.raw];
|
|
536
680
|
if (record.message.id && !compactedIds.has(record.message.id)) return [];
|
|
@@ -553,7 +697,10 @@ export function appendRoomMessage(
|
|
|
553
697
|
try {
|
|
554
698
|
const receivedAt = new Date().toISOString();
|
|
555
699
|
const entry: RoomTimelineEntry = { ...message, received_at: receivedAt };
|
|
556
|
-
fs.appendFileSync(
|
|
700
|
+
fs.appendFileSync(
|
|
701
|
+
messagesFile(stateDir, room),
|
|
702
|
+
`${JSON.stringify(entry)}\n`,
|
|
703
|
+
);
|
|
557
704
|
compactRoomMessages(stateDir, room);
|
|
558
705
|
const roster = updateRosterForMessage(stateDir, room, message, receivedAt);
|
|
559
706
|
const run = runFromRoomAddress(message.to);
|
|
@@ -585,14 +732,23 @@ export function readRoomMessages(
|
|
|
585
732
|
): RoomTimelineEntry[] {
|
|
586
733
|
try {
|
|
587
734
|
const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
|
|
588
|
-
return lines.
|
|
735
|
+
return lines.flatMap((line) => {
|
|
736
|
+
try {
|
|
737
|
+
return [JSON.parse(line) as RoomTimelineEntry];
|
|
738
|
+
} catch {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
});
|
|
589
742
|
} catch (error) {
|
|
590
743
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
591
744
|
throw error;
|
|
592
745
|
}
|
|
593
746
|
}
|
|
594
747
|
|
|
595
|
-
function previewValue(
|
|
748
|
+
function previewValue(
|
|
749
|
+
value: unknown,
|
|
750
|
+
maxLength = Limits.ROOM_MESSAGE_PREVIEW_CHARS,
|
|
751
|
+
): string | undefined {
|
|
596
752
|
if (value === undefined) return undefined;
|
|
597
753
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
598
754
|
const compact = text.replaceAll(/\s+/g, " ").trim();
|
|
@@ -608,7 +764,9 @@ export function readRoomMessagePreviews(
|
|
|
608
764
|
limit = 40,
|
|
609
765
|
): RoomMessagePreview[] {
|
|
610
766
|
return readRoomMessages(stateDir, room, limit).map((message) => ({
|
|
611
|
-
...(previewValue(message.body)
|
|
767
|
+
...(previewValue(message.body)
|
|
768
|
+
? { body_preview: previewValue(message.body) }
|
|
769
|
+
: {}),
|
|
612
770
|
...(message.from ? { from: message.from } : {}),
|
|
613
771
|
...(message.summary ? { summary: message.summary } : {}),
|
|
614
772
|
timestamp: message.received_at,
|
|
@@ -620,6 +778,22 @@ export function readRoomMessagePreviews(
|
|
|
620
778
|
export function getRoomStatus(stateDir: string, room: string): RoomStatus {
|
|
621
779
|
const messageCount = readRoomMessageCount(stateDir, room);
|
|
622
780
|
const [last] = readRoomMessages(stateDir, room, 1);
|
|
781
|
+
const compactionRead = readJsonFileResilient<RoomCompactionInfo | undefined>(
|
|
782
|
+
path.join(roomDir(stateDir, room), "compaction.json"),
|
|
783
|
+
undefined,
|
|
784
|
+
);
|
|
785
|
+
const rosterRead = readJsonFileResilient<Record<string, RoomMember>>(
|
|
786
|
+
rosterFile(stateDir, room),
|
|
787
|
+
{},
|
|
788
|
+
);
|
|
789
|
+
const messageRead = readJsonlFileResilient<RoomTimelineEntry>(
|
|
790
|
+
messagesFile(stateDir, room),
|
|
791
|
+
);
|
|
792
|
+
const diagnostics = [
|
|
793
|
+
...messageRead.diagnostics,
|
|
794
|
+
...rosterRead.diagnostics,
|
|
795
|
+
...compactionRead.diagnostics,
|
|
796
|
+
];
|
|
623
797
|
return {
|
|
624
798
|
...(last
|
|
625
799
|
? {
|
|
@@ -629,9 +803,16 @@ export function getRoomStatus(stateDir: string, room: string): RoomStatus {
|
|
|
629
803
|
last_message_type: last.type,
|
|
630
804
|
}
|
|
631
805
|
: {}),
|
|
806
|
+
...(compactionRead.value ? { compaction: compactionRead.value } : {}),
|
|
807
|
+
...(diagnostics.length
|
|
808
|
+
? {
|
|
809
|
+
diagnostics: formatStateReadDiagnostics(diagnostics),
|
|
810
|
+
diagnostics_count: diagnostics.length,
|
|
811
|
+
}
|
|
812
|
+
: {}),
|
|
632
813
|
message_count: messageCount,
|
|
633
814
|
room,
|
|
634
|
-
roster_count: Object.keys(
|
|
815
|
+
roster_count: Object.keys(rosterRead.value).length,
|
|
635
816
|
};
|
|
636
817
|
}
|
|
637
818
|
|
|
@@ -661,7 +842,10 @@ export function ensureRoomMember(
|
|
|
661
842
|
});
|
|
662
843
|
}
|
|
663
844
|
|
|
664
|
-
export function ensureDefaultRoom(
|
|
845
|
+
export function ensureDefaultRoom(
|
|
846
|
+
stateDir: string,
|
|
847
|
+
run: string,
|
|
848
|
+
): RoomAppendResult {
|
|
665
849
|
return ensureRoomMember(
|
|
666
850
|
stateDir,
|
|
667
851
|
run,
|
|
@@ -675,14 +859,10 @@ export function ensureDefaultRoom(stateDir: string, run: string): RoomAppendResu
|
|
|
675
859
|
export function readCommunicationSnapshot(
|
|
676
860
|
stateDir: string,
|
|
677
861
|
): ActorCommunicationSnapshot | undefined {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
} catch (error) {
|
|
683
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
684
|
-
throw error;
|
|
685
|
-
}
|
|
862
|
+
return readJsonFileResilient<ActorCommunicationSnapshot | undefined>(
|
|
863
|
+
snapshotFile(stateDir),
|
|
864
|
+
undefined,
|
|
865
|
+
).value;
|
|
686
866
|
}
|
|
687
867
|
|
|
688
868
|
export function readRoomContacts(
|
|
@@ -753,6 +933,7 @@ function writeBranchCommunicationSnapshotDebounced(
|
|
|
753
933
|
): ActorCommunicationSnapshot | undefined {
|
|
754
934
|
const branch = branchIdFromAddress(self, run);
|
|
755
935
|
if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
|
|
756
|
-
if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
|
|
936
|
+
if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
|
|
937
|
+
return undefined;
|
|
757
938
|
return writeBranchCommunicationSnapshot(stateDir, run, self);
|
|
758
939
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mailbox-backed actor worker entrypoint logic.
|
|
3
|
+
* Zones: worker demo, mailbox loop recipe runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { appendFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { appendRoomMessage, ensureRoomMember } from "./actor-rooms.ts";
|
|
10
|
+
import { handleMailboxLoopOnce, isMailboxLoopStopMessage } from "./mailbox-loop.ts";
|
|
11
|
+
|
|
12
|
+
interface ActorWorkerArgs {
|
|
13
|
+
branch?: string;
|
|
14
|
+
poll_ms?: string | number;
|
|
15
|
+
run?: string;
|
|
16
|
+
state_dir?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseActorWorkerArgs(argv: string[]): ActorWorkerArgs {
|
|
20
|
+
const args: Record<string, string | true> = {};
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const token = argv[index];
|
|
23
|
+
if (!token.startsWith("--")) continue;
|
|
24
|
+
const key = token.slice(2).replaceAll("-", "_");
|
|
25
|
+
const next = argv[index + 1];
|
|
26
|
+
if (!next || next.startsWith("--")) {
|
|
27
|
+
args[key] = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
args[key] = next;
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
return args as ActorWorkerArgs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sleep(ms: number): Promise<void> {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function text(value: unknown): string {
|
|
41
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runActorWorker(argv = process.argv.slice(2)): Promise<void> {
|
|
45
|
+
const args = parseActorWorkerArgs(argv);
|
|
46
|
+
const stateDir = String(args.state_dir ?? "");
|
|
47
|
+
const run = String(args.run ?? "");
|
|
48
|
+
const branch = String(args.branch ?? "worker");
|
|
49
|
+
const pollMs = Math.max(50, Number(args.poll_ms ?? 1000));
|
|
50
|
+
|
|
51
|
+
if (!stateDir || !run) {
|
|
52
|
+
throw new Error("usage: actor-worker.mjs --state-dir <dir> --run <id> [--branch worker] [--poll-ms 1000]");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const branchAddress = `branch:${run}/${branch}`;
|
|
56
|
+
const roomAddress = `room:${run}`;
|
|
57
|
+
const journalPath = join(stateDir, "worker-events.jsonl");
|
|
58
|
+
|
|
59
|
+
function journal(event: string, data: Record<string, unknown> = {}): void {
|
|
60
|
+
appendFileSync(
|
|
61
|
+
journalPath,
|
|
62
|
+
`${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function room(type: string, summary: string, body: Record<string, unknown> = {}): void {
|
|
67
|
+
appendRoomMessage(stateDir, "main", {
|
|
68
|
+
body,
|
|
69
|
+
from: branchAddress,
|
|
70
|
+
summary,
|
|
71
|
+
to: roomAddress,
|
|
72
|
+
type,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ensureRoomMember(
|
|
77
|
+
stateDir,
|
|
78
|
+
run,
|
|
79
|
+
"main",
|
|
80
|
+
branchAddress,
|
|
81
|
+
{ display: branch, role: "worker", status: "present" },
|
|
82
|
+
`${branch} joined as mailbox worker`,
|
|
83
|
+
);
|
|
84
|
+
room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
|
|
85
|
+
journal("worker.started", { branch, run });
|
|
86
|
+
|
|
87
|
+
let stopping = false;
|
|
88
|
+
while (!stopping) {
|
|
89
|
+
const result = await handleMailboxLoopOnce(
|
|
90
|
+
{ address: branchAddress, kind: "branch", run, stateDir },
|
|
91
|
+
(message) => {
|
|
92
|
+
if (isMailboxLoopStopMessage(message)) {
|
|
93
|
+
stopping = true;
|
|
94
|
+
room("actor.leave", `${branch} stopping`, { branch, reason: message.type });
|
|
95
|
+
journal("worker.stopping", { id: message.id, type: message.type });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
room("task.claim", `${branch} claimed ${message.type}`, {
|
|
99
|
+
branch,
|
|
100
|
+
id: message.id,
|
|
101
|
+
type: message.type,
|
|
102
|
+
});
|
|
103
|
+
room("task.result", `${branch} handled ${message.type}`, {
|
|
104
|
+
branch,
|
|
105
|
+
id: message.id,
|
|
106
|
+
result: text(message.body),
|
|
107
|
+
type: message.type,
|
|
108
|
+
});
|
|
109
|
+
room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
|
|
110
|
+
journal("task.handled", { id: message.id, type: message.type });
|
|
111
|
+
},
|
|
112
|
+
{ owner: branchAddress },
|
|
113
|
+
);
|
|
114
|
+
if (!result.handled) await sleep(pollMs);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
journal("worker.done", { branch, run });
|
|
118
|
+
}
|