@llblab/pi-actors 0.23.0 → 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.
Files changed (161) hide show
  1. package/AGENTS.md +125 -60
  2. package/BACKLOG.md +93 -188
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +13 -0
  5. package/dist/fixtures/protocol/actor-message-branch.json +13 -0
  6. package/dist/fixtures/protocol/artifact-manifest.json +9 -0
  7. package/dist/fixtures/protocol/mailbox-contract.json +15 -0
  8. package/dist/fixtures/protocol/recipe-summary.json +16 -0
  9. package/dist/fixtures/protocol/room-message.json +11 -0
  10. package/dist/fixtures/protocol/room-roster.json +11 -0
  11. package/dist/fixtures/protocol/run-inbox-message.json +9 -0
  12. package/dist/fixtures/protocol/run-outbox-event.json +9 -0
  13. package/dist/fixtures/protocol/run-state.json +10 -0
  14. package/dist/index.js +3 -2
  15. package/dist/lib/actor-inspector-tui.js +5 -32
  16. package/dist/lib/actor-rooms.d.ts +8 -0
  17. package/dist/lib/actor-rooms.js +64 -20
  18. package/dist/lib/actor-worker.d.ts +13 -0
  19. package/dist/lib/actor-worker.js +86 -0
  20. package/dist/lib/async-runner.d.ts +5 -0
  21. package/dist/lib/async-runner.js +134 -0
  22. package/dist/lib/async-runs.js +9 -28
  23. package/dist/lib/build-dist.d.ts +5 -0
  24. package/dist/lib/build-dist.js +24 -0
  25. package/dist/lib/conformance.d.ts +12 -0
  26. package/dist/lib/conformance.js +28 -0
  27. package/dist/lib/coordinator.d.ts +5 -0
  28. package/dist/lib/coordinator.js +557 -0
  29. package/dist/lib/locker.d.ts +5 -0
  30. package/dist/lib/locker.js +310 -0
  31. package/dist/lib/mailbox-loop.d.ts +41 -0
  32. package/dist/lib/mailbox-loop.js +62 -0
  33. package/dist/lib/observability.d.ts +2 -2
  34. package/dist/lib/observability.js +51 -53
  35. package/dist/lib/prompts.d.ts +1 -1
  36. package/dist/lib/prompts.js +1 -1
  37. package/dist/lib/recipe-references.js +25 -2
  38. package/dist/lib/recipe-utils.d.ts +5 -0
  39. package/dist/lib/recipe-utils.js +385 -0
  40. package/dist/lib/runtime-notifier.js +3 -7
  41. package/dist/lib/state-readers.d.ts +21 -0
  42. package/dist/lib/state-readers.js +74 -0
  43. package/dist/lib/tools.js +1 -1
  44. package/dist/lib/validate-recipe.d.ts +6 -0
  45. package/dist/lib/validate-recipe.js +104 -0
  46. package/dist/recipes/actor-worker.json +35 -0
  47. package/dist/recipes/coordinator-locker.json +45 -0
  48. package/dist/recipes/lens-swarm.json +66 -0
  49. package/dist/recipes/locker.json +45 -0
  50. package/dist/recipes/music-player.json +38 -0
  51. package/dist/recipes/pipeline-architect-coordinator.json +95 -0
  52. package/dist/recipes/pipeline-artifact-bundle.json +100 -0
  53. package/dist/recipes/pipeline-artifact-report.json +58 -0
  54. package/dist/recipes/pipeline-artifact-write.json +72 -0
  55. package/dist/recipes/pipeline-async-run-ops.json +70 -0
  56. package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
  57. package/dist/recipes/pipeline-development-tasking.json +81 -0
  58. package/dist/recipes/pipeline-docs-maintenance.json +80 -0
  59. package/dist/recipes/pipeline-media-library.json +59 -0
  60. package/dist/recipes/pipeline-quorum-review.json +79 -0
  61. package/dist/recipes/pipeline-release-readiness.json +110 -0
  62. package/dist/recipes/pipeline-release-summary.json +88 -0
  63. package/dist/recipes/pipeline-repo-health.json +89 -0
  64. package/dist/recipes/pipeline-research-synthesis.json +94 -0
  65. package/dist/recipes/pipeline-review-readiness.json +54 -0
  66. package/dist/recipes/pipeline-room-swarm.json +50 -0
  67. package/dist/recipes/subagent-artifact.json +32 -0
  68. package/dist/recipes/subagent-checkpoint.json +33 -0
  69. package/dist/recipes/subagent-conflict-report.json +32 -0
  70. package/dist/recipes/subagent-contradiction-map.json +33 -0
  71. package/dist/recipes/subagent-critic.json +35 -0
  72. package/dist/recipes/subagent-evidence-map.json +33 -0
  73. package/dist/recipes/subagent-followup.json +33 -0
  74. package/dist/recipes/subagent-judge.json +33 -0
  75. package/dist/recipes/subagent-merge.json +33 -0
  76. package/dist/recipes/subagent-message.json +34 -0
  77. package/dist/recipes/subagent-normalize.json +31 -0
  78. package/dist/recipes/subagent-plan.json +33 -0
  79. package/dist/recipes/subagent-prompt.json +28 -0
  80. package/dist/recipes/subagent-quorum.json +43 -0
  81. package/dist/recipes/subagent-review-coordinator.json +114 -0
  82. package/dist/recipes/subagent-review.json +37 -0
  83. package/dist/recipes/subagent-task-card.json +35 -0
  84. package/dist/recipes/subagent-tools.json +27 -0
  85. package/dist/recipes/subagent-verify.json +34 -0
  86. package/dist/recipes/subagents-prompts.json +51 -0
  87. package/dist/recipes/utility-actor-message.json +23 -0
  88. package/dist/recipes/utility-artifact-manifest.json +16 -0
  89. package/dist/recipes/utility-artifact-write.json +16 -0
  90. package/dist/recipes/utility-changelog-head.json +11 -0
  91. package/dist/recipes/utility-changelog-section.json +13 -0
  92. package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
  93. package/dist/recipes/utility-git-log.json +11 -0
  94. package/dist/recipes/utility-git-status.json +9 -0
  95. package/dist/recipes/utility-jsonl-tail.json +10 -0
  96. package/dist/recipes/utility-markdown-index.json +14 -0
  97. package/dist/recipes/utility-package-summary.json +11 -0
  98. package/dist/recipes/utility-playlist-build.json +17 -0
  99. package/dist/recipes/utility-playlist-scan.json +11 -0
  100. package/dist/recipes/utility-run-ops-snapshot.json +17 -0
  101. package/dist/recipes/utility-run-state-files.json +13 -0
  102. package/dist/recipes/utility-run-summary.json +11 -0
  103. package/dist/recipes/utility-skill-summary.json +13 -0
  104. package/dist/recipes/utility-validate-recipe.json +13 -0
  105. package/dist/recipes/utility-validation-wrapper.json +13 -0
  106. package/dist/scripts/actor-worker.mjs +31 -0
  107. package/dist/scripts/async-runner.mjs +31 -0
  108. package/dist/scripts/build-dist.mjs +25 -0
  109. package/dist/scripts/conformance.mjs +33 -0
  110. package/dist/scripts/coordinator.mjs +31 -0
  111. package/dist/scripts/locker.mjs +33 -0
  112. package/dist/scripts/music-player.mjs +964 -0
  113. package/dist/scripts/recipe-utils.mjs +31 -0
  114. package/dist/scripts/validate-recipe.mjs +34 -0
  115. package/dist/skills/actors/SKILL.md +375 -0
  116. package/dist/skills/swarm/SKILL.md +467 -0
  117. package/dist/skills/swarm/references/development-swarm.md +596 -0
  118. package/docs/actor-messages.md +2 -2
  119. package/docs/async-runs.md +11 -0
  120. package/docs/template-recipes.md +1 -1
  121. package/fixtures/protocol/actor-message-branch.json +13 -0
  122. package/fixtures/protocol/artifact-manifest.json +9 -0
  123. package/fixtures/protocol/mailbox-contract.json +15 -0
  124. package/fixtures/protocol/recipe-summary.json +16 -0
  125. package/fixtures/protocol/room-message.json +11 -0
  126. package/fixtures/protocol/room-roster.json +11 -0
  127. package/fixtures/protocol/run-inbox-message.json +9 -0
  128. package/fixtures/protocol/run-outbox-event.json +9 -0
  129. package/fixtures/protocol/run-state.json +10 -0
  130. package/index.ts +3 -0
  131. package/lib/actor-inspector-tui.ts +11 -34
  132. package/lib/actor-rooms.ts +88 -18
  133. package/lib/actor-worker.ts +118 -0
  134. package/lib/async-runner.ts +173 -0
  135. package/lib/async-runs.ts +12 -22
  136. package/lib/build-dist.ts +30 -0
  137. package/lib/conformance.ts +46 -0
  138. package/lib/coordinator.ts +649 -0
  139. package/lib/locker.ts +340 -0
  140. package/lib/mailbox-loop.ts +148 -0
  141. package/lib/observability.ts +24 -19
  142. package/lib/prompts.ts +1 -1
  143. package/lib/recipe-references.ts +37 -2
  144. package/lib/recipe-utils.ts +486 -0
  145. package/lib/runtime-notifier.ts +4 -6
  146. package/lib/state-readers.ts +93 -0
  147. package/lib/tools.ts +1 -1
  148. package/lib/validate-recipe.ts +110 -0
  149. package/package.json +10 -2
  150. package/recipes/actor-worker.json +35 -0
  151. package/recipes/pipeline-quorum-review.json +12 -7
  152. package/scripts/actor-worker.mjs +31 -0
  153. package/scripts/async-runner.mjs +11 -201
  154. package/scripts/build-dist.mjs +25 -0
  155. package/scripts/conformance.mjs +21 -35
  156. package/scripts/coordinator.mjs +15 -625
  157. package/scripts/locker.mjs +20 -332
  158. package/scripts/recipe-utils.mjs +17 -477
  159. package/scripts/validate-recipe.mjs +18 -121
  160. package/skills/actors/SKILL.md +5 -2
  161. package/skills/swarm/SKILL.md +3 -1
package/README.md CHANGED
@@ -43,6 +43,8 @@ Or from git:
43
43
  pi install git:github.com/llblab/pi-actors
44
44
  ```
45
45
 
46
+ The npm package is dist-first for JavaScript-only runtimes: default Pi metadata points at compiled `dist/` entrypoints and mirrored runtime assets. Source TypeScript and source skills remain in the package for TypeScript-native runtimes through optional source metadata.
47
+
46
48
  ## Address Surface
47
49
 
48
50
  Actors and coordination endpoints are addressed with compact route strings:
@@ -255,6 +257,17 @@ Use mailbox declarations when an actor has a stable conversational surface.
255
257
 
256
258
  Core actor state, inspection, foreground tools, and basic async runs are portable Node.js behavior. Run-local messaging and stop/kill use a platform adapter under the same `message` API: Unix-compatible recipes can use their existing local control endpoint, while native Windows recipes can expose a Windows-native endpoint in run state. Some packaged scripts still depend on Unix tools and are WSL/Linux/macOS-only until migrated; their public recipe surface should stay `spawn` / `message` / `inspect` either way.
257
259
 
260
+ | Surface | Linux/macOS/WSL | Native Windows |
261
+ | --- | --- | --- |
262
+ | Foreground tools, recipe discovery, inspect | Supported | Supported |
263
+ | Async runs and file-backed state | Supported | Supported |
264
+ | Mailbox-only actors and worker recipe | Supported | Supported |
265
+ | FIFO control endpoints | Supported | Not supported; use mailbox or named pipe |
266
+ | Named-pipe control endpoints | Not needed | Supported when recipe exposes one |
267
+ | Process cancel/kill | Process group signal with pid fallback | Windows process-tree adapter |
268
+
269
+ Packaged recipes should prefer mailbox/wake behavior for portable control. Recipes that require FIFO, Unix shell tools, or platform-specific media backends should make that limitation visible in docs or diagnostics before launch.
270
+
258
271
  ## Safety Boundary
259
272
 
260
273
  `pi-actors` is local-first, not sandbox-first.
@@ -0,0 +1,13 @@
1
+ {
2
+ "to": "branch:demo/reviewer",
3
+ "from": "run:demo",
4
+ "type": "task.assign",
5
+ "summary": "Review the current slice",
6
+ "body": {
7
+ "task": "Check mailbox loop semantics"
8
+ },
9
+ "correlation_id": "task-001",
10
+ "metadata": {
11
+ "requires_response": true
12
+ }
13
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "report": {
3
+ "path": "{state_dir}/report.md",
4
+ "kind": "markdown",
5
+ "media_type": "text/markdown",
6
+ "required": true
7
+ },
8
+ "journal": "{state_dir}/journal.jsonl"
9
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "accepts": [
3
+ "task.assign",
4
+ {
5
+ "type": "control.stop",
6
+ "description": "Request graceful worker shutdown"
7
+ }
8
+ ],
9
+ "emits": [
10
+ "task.claim",
11
+ "task.result",
12
+ "awaiting_assignment",
13
+ "actor.leave"
14
+ ]
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "actor-worker",
3
+ "path": "recipes/actor-worker.json",
4
+ "location": "packaged",
5
+ "active": true,
6
+ "args": [
7
+ "run",
8
+ "branch",
9
+ "poll_ms",
10
+ "state_dir"
11
+ ],
12
+ "mailbox": {
13
+ "kind": "branch",
14
+ "accepts": ["task.assign", "control.stop"]
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "to": "room:demo",
3
+ "from": "branch:demo/worker",
4
+ "type": "task.result",
5
+ "summary": "Worker completed task",
6
+ "body": {
7
+ "id": "inbox-001",
8
+ "result": "ok"
9
+ },
10
+ "received_at": "2026-05-26T00:00:00.000Z"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "room": "main",
3
+ "members": {
4
+ "branch:demo/worker": {
5
+ "display": "worker",
6
+ "role": "worker",
7
+ "status": "present",
8
+ "last_seen": "2026-05-26T00:00:00.000Z"
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "inbox-001",
3
+ "status": "queued",
4
+ "queued_at": "2026-05-26T00:00:00.000Z",
5
+ "to": "run:demo",
6
+ "from": "coordinator",
7
+ "type": "control.continue",
8
+ "body": "continue"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "event-001",
3
+ "type": "task.result",
4
+ "summary": "Worker completed task",
5
+ "body": {
6
+ "result": "ok"
7
+ },
8
+ "created_at": "2026-05-26T00:00:00.000Z"
9
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "run": "demo",
3
+ "state_dir": "{state_dir}",
4
+ "status": "running",
5
+ "template": "echo demo",
6
+ "values": {
7
+ "run_id": "demo",
8
+ "state_dir": "{state_dir}"
9
+ }
10
+ }
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ export default function toolRegistryExtension(pi) {
39
39
  const runDirWatchers = new Map();
40
40
  const observedRuns = new Map();
41
41
  const observedRunEventLines = new Map();
42
+ const observedRunOutboxEventIds = new Map();
42
43
  const retirementAttempts = new Set();
43
44
  let runStatusFrame = 0;
44
45
  let communicationWidgetVisible = false;
@@ -102,7 +103,7 @@ export default function toolRegistryExtension(pi) {
102
103
  }
103
104
  : undefined, { placement: "belowEditor" });
104
105
  const transitions = Observability.detectRunTransitions(observedRuns, summary);
105
- const outboxEvents = Observability.detectRunOutboxEvents(observedRunEventLines, summary);
106
+ const outboxEvents = Observability.detectRunOutboxEvents(observedRunEventLines, summary, observedRunOutboxEventIds);
106
107
  if (!notify)
107
108
  return;
108
109
  retireCandidateRuns(ctx, summary);
@@ -121,7 +122,7 @@ export default function toolRegistryExtension(pi) {
121
122
  details: transition,
122
123
  }, { deliverAs: "followUp", triggerTurn: true });
123
124
  }
124
- Observability.pruneRunObservationState(observedRuns, observedRunEventLines, summary, transitions.map((transition) => transition.stateDir ?? transition.run));
125
+ Observability.pruneRunObservationState(observedRuns, observedRunEventLines, summary, transitions.map((transition) => transition.stateDir ?? transition.run), observedRunOutboxEventIds);
125
126
  for (const event of outboxEvents) {
126
127
  if (!Observability.shouldNotifyRunOutboxEvent(event))
127
128
  continue;
@@ -7,31 +7,14 @@ import * as path from "node:path";
7
7
  import { visibleWidth } from "@earendil-works/pi-tui";
8
8
  import * as Limits from "./limits.js";
9
9
  import * as Paths from "./paths.js";
10
+ import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.js";
10
11
  function asRecord(value) {
11
12
  return value && typeof value === "object" && !Array.isArray(value)
12
13
  ? value
13
14
  : {};
14
15
  }
15
16
  function readJsonLines(file) {
16
- try {
17
- return fs
18
- .readFileSync(file, "utf8")
19
- .split("\n")
20
- .filter(Boolean)
21
- .flatMap((line) => {
22
- try {
23
- return [JSON.parse(line)];
24
- }
25
- catch {
26
- return [];
27
- }
28
- });
29
- }
30
- catch (error) {
31
- if (error.code === "ENOENT")
32
- return [];
33
- return [];
34
- }
17
+ return readJsonlFileResilient(file).records;
35
18
  }
36
19
  function previewValue(value, maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
37
20
  if (value === undefined)
@@ -85,12 +68,7 @@ function previewFromMessage(run, message, timestamp, displayNames = {}) {
85
68
  };
86
69
  }
87
70
  function readRoomRosterRecords(stateDir, room) {
88
- try {
89
- return JSON.parse(fs.readFileSync(path.join(stateDir, "rooms", room, "roster.json"), "utf8"));
90
- }
91
- catch {
92
- return {};
93
- }
71
+ return readJsonFileResilient(path.join(stateDir, "rooms", room, "roster.json"), {}).value;
94
72
  }
95
73
  function memberDisplay(_address, member) {
96
74
  const display = typeof member.display === "string" ? member.display.trim() : "";
@@ -170,13 +148,8 @@ function readOutboxPreviews(run, stateDir) {
170
148
  .filter((preview) => Boolean(preview));
171
149
  }
172
150
  function getRunOwnerId(stateDir) {
173
- try {
174
- const meta = JSON.parse(fs.readFileSync(path.join(stateDir, "run.json"), "utf8"));
175
- return typeof meta.ownerId === "string" ? meta.ownerId : undefined;
176
- }
177
- catch {
178
- return undefined;
179
- }
151
+ const meta = readJsonFileResilient(path.join(stateDir, "run.json"), undefined).value;
152
+ return typeof meta?.ownerId === "string" ? meta.ownerId : undefined;
180
153
  }
181
154
  function matchesOwner(stateDir, ownerId) {
182
155
  return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
@@ -44,6 +44,8 @@ export interface RoomStatus {
44
44
  message_count: number;
45
45
  roster_count: number;
46
46
  compaction?: RoomCompactionInfo;
47
+ diagnostics?: string[];
48
+ diagnostics_count?: number;
47
49
  last_message_at?: string;
48
50
  last_message_from?: string;
49
51
  last_message_summary?: string;
@@ -71,6 +73,11 @@ export interface ActorCommunicationSnapshot {
71
73
  }
72
74
  export declare function readRoomRoster(stateDir: string, room: string): Record<string, RoomMember>;
73
75
  export interface BranchInboxRecord extends ActorMessage {
76
+ claimed_at?: string;
77
+ claimed_by?: string;
78
+ error?: string;
79
+ failed_at?: string;
80
+ handled_at?: string;
74
81
  id?: string;
75
82
  queued_at?: string;
76
83
  status?: string;
@@ -83,6 +90,7 @@ export declare function readBranchInboxMessages(stateDir: string, run: string, a
83
90
  export declare function readBranchInboxDiagnostics(stateDir: string, run: string, address: string, limit?: number): BranchInboxReadResult;
84
91
  export declare function getBranchInboxTerminalRetainLimit(): number;
85
92
  export declare function appendBranchInboxMessage(stateDir: string, run: string, address: string, message: ActorMessage): void;
93
+ export declare function claimBranchInboxMessage(stateDir: string, run: string, address: string, owner?: string, statuses?: string[]): BranchInboxRecord | undefined;
86
94
  export declare function updateBranchInboxMessageStatus(stateDir: string, run: string, address: string, id: string, status: "claimed" | "handled" | "failed", metadata?: Record<string, unknown>): boolean;
87
95
  export declare function appendRoomMessage(stateDir: string, room: string, message: ActorMessage): RoomAppendResult;
88
96
  export declare function readRoomMessages(stateDir: string, room: string, limit?: number): RoomTimelineEntry[];
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto";
8
8
  import * as path from "node:path";
9
9
  import * as Limits from "./limits.js";
10
10
  import { notifyRuntimeWake } from "./runtime-notifier.js";
11
+ import { formatStateReadDiagnostics, readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
11
12
  const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
12
13
  const STATE_LOCK_TIMEOUT_MS = 5000;
13
14
  const DEFAULT_ROOM_MAX_MESSAGES = 10000;
@@ -87,14 +88,7 @@ function runFromRoomAddress(address) {
87
88
  return match?.[1];
88
89
  }
89
90
  function readJsonFile(file, fallback) {
90
- try {
91
- return JSON.parse(fs.readFileSync(file, "utf8"));
92
- }
93
- catch (error) {
94
- if (error.code === "ENOENT")
95
- return fallback;
96
- throw error;
97
- }
91
+ return readJsonFileResilient(file, fallback).value;
98
92
  }
99
93
  function writeJsonFile(file, value) {
100
94
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -394,6 +388,43 @@ export function appendBranchInboxMessage(stateDir, run, address, message) {
394
388
  releaseLock();
395
389
  }
396
390
  }
391
+ export function claimBranchInboxMessage(stateDir, run, address, owner = "runtime", statuses = ["queued"]) {
392
+ const branch = branchIdFromAddress(address, run);
393
+ if (!branch)
394
+ throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
395
+ const releaseLock = acquireBranchInboxLock(stateDir, branch);
396
+ try {
397
+ const file = branchInboxFile(stateDir, branch);
398
+ const records = readAllBranchInboxLines(stateDir, run, address);
399
+ let claimed;
400
+ const now = new Date().toISOString();
401
+ const updated = records.map((record) => {
402
+ if (claimed || !("message" in record))
403
+ return record;
404
+ const status = record.message.status ?? "queued";
405
+ if (!statuses.includes(status))
406
+ return record;
407
+ claimed = {
408
+ ...record.message,
409
+ claimed_at: now,
410
+ claimed_by: owner,
411
+ status: "claimed",
412
+ };
413
+ return { message: claimed };
414
+ });
415
+ if (!claimed)
416
+ return undefined;
417
+ fs.writeFileSync(file, `${updated.map((record) => ("raw" in record ? record.raw : JSON.stringify(record.message))).join("\n")}\n`);
418
+ notifyActorWake(stateDir, address, "branch.inbox.claim", {
419
+ id: claimed.id,
420
+ owner,
421
+ });
422
+ return claimed;
423
+ }
424
+ finally {
425
+ releaseLock();
426
+ }
427
+ }
397
428
  export function updateBranchInboxMessageStatus(stateDir, run, address, id, status, metadata = {}) {
398
429
  const branch = branchIdFromAddress(address, run);
399
430
  if (!branch)
@@ -469,7 +500,14 @@ export function appendRoomMessage(stateDir, room, message) {
469
500
  export function readRoomMessages(stateDir, room, limit = 40) {
470
501
  try {
471
502
  const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
472
- return lines.map((line) => JSON.parse(line));
503
+ return lines.flatMap((line) => {
504
+ try {
505
+ return [JSON.parse(line)];
506
+ }
507
+ catch {
508
+ return [];
509
+ }
510
+ });
473
511
  }
474
512
  catch (error) {
475
513
  if (error.code === "ENOENT")
@@ -503,7 +541,14 @@ export function readRoomMessagePreviews(stateDir, room, limit = 40) {
503
541
  export function getRoomStatus(stateDir, room) {
504
542
  const messageCount = readRoomMessageCount(stateDir, room);
505
543
  const [last] = readRoomMessages(stateDir, room, 1);
506
- const compaction = readJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), undefined);
544
+ const compactionRead = readJsonFileResilient(path.join(roomDir(stateDir, room), "compaction.json"), undefined);
545
+ const rosterRead = readJsonFileResilient(rosterFile(stateDir, room), {});
546
+ const messageRead = readJsonlFileResilient(messagesFile(stateDir, room));
547
+ const diagnostics = [
548
+ ...messageRead.diagnostics,
549
+ ...rosterRead.diagnostics,
550
+ ...compactionRead.diagnostics,
551
+ ];
507
552
  return {
508
553
  ...(last
509
554
  ? {
@@ -513,10 +558,16 @@ export function getRoomStatus(stateDir, room) {
513
558
  last_message_type: last.type,
514
559
  }
515
560
  : {}),
516
- ...(compaction ? { compaction } : {}),
561
+ ...(compactionRead.value ? { compaction: compactionRead.value } : {}),
562
+ ...(diagnostics.length
563
+ ? {
564
+ diagnostics: formatStateReadDiagnostics(diagnostics),
565
+ diagnostics_count: diagnostics.length,
566
+ }
567
+ : {}),
517
568
  message_count: messageCount,
518
569
  room,
519
- roster_count: Object.keys(readRoomRoster(stateDir, room)).length,
570
+ roster_count: Object.keys(rosterRead.value).length,
520
571
  };
521
572
  }
522
573
  export function ensureRoomMember(stateDir, run, room, address, body, summary) {
@@ -541,14 +592,7 @@ export function ensureDefaultRoom(stateDir, run) {
541
592
  return ensureRoomMember(stateDir, run, "main", `run:${run}`, { role: "run", status: "present" }, "Run joined default room");
542
593
  }
543
594
  export function readCommunicationSnapshot(stateDir) {
544
- try {
545
- return JSON.parse(fs.readFileSync(snapshotFile(stateDir), "utf8"));
546
- }
547
- catch (error) {
548
- if (error.code === "ENOENT")
549
- return undefined;
550
- throw error;
551
- }
595
+ return readJsonFileResilient(snapshotFile(stateDir), undefined).value;
552
596
  }
553
597
  export function readRoomContacts(stateDir, room, self) {
554
598
  return Object.values(readRoomRoster(stateDir, room))
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Canonical mailbox-backed actor worker entrypoint logic.
3
+ * Zones: worker demo, mailbox loop recipe runtime
4
+ */
5
+ interface ActorWorkerArgs {
6
+ branch?: string;
7
+ poll_ms?: string | number;
8
+ run?: string;
9
+ state_dir?: string;
10
+ }
11
+ export declare function parseActorWorkerArgs(argv: string[]): ActorWorkerArgs;
12
+ export declare function runActorWorker(argv?: string[]): Promise<void>;
13
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Canonical mailbox-backed actor worker entrypoint logic.
3
+ * Zones: worker demo, mailbox loop recipe runtime
4
+ */
5
+ import { appendFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { appendRoomMessage, ensureRoomMember } from "./actor-rooms.js";
8
+ import { handleMailboxLoopOnce, isMailboxLoopStopMessage } from "./mailbox-loop.js";
9
+ export function parseActorWorkerArgs(argv) {
10
+ const args = {};
11
+ for (let index = 0; index < argv.length; index += 1) {
12
+ const token = argv[index];
13
+ if (!token.startsWith("--"))
14
+ continue;
15
+ const key = token.slice(2).replaceAll("-", "_");
16
+ const next = argv[index + 1];
17
+ if (!next || next.startsWith("--")) {
18
+ args[key] = true;
19
+ continue;
20
+ }
21
+ args[key] = next;
22
+ index += 1;
23
+ }
24
+ return args;
25
+ }
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
29
+ function text(value) {
30
+ return typeof value === "string" ? value : JSON.stringify(value);
31
+ }
32
+ export async function runActorWorker(argv = process.argv.slice(2)) {
33
+ const args = parseActorWorkerArgs(argv);
34
+ const stateDir = String(args.state_dir ?? "");
35
+ const run = String(args.run ?? "");
36
+ const branch = String(args.branch ?? "worker");
37
+ const pollMs = Math.max(50, Number(args.poll_ms ?? 1000));
38
+ if (!stateDir || !run) {
39
+ throw new Error("usage: actor-worker.mjs --state-dir <dir> --run <id> [--branch worker] [--poll-ms 1000]");
40
+ }
41
+ const branchAddress = `branch:${run}/${branch}`;
42
+ const roomAddress = `room:${run}`;
43
+ const journalPath = join(stateDir, "worker-events.jsonl");
44
+ function journal(event, data = {}) {
45
+ appendFileSync(journalPath, `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`);
46
+ }
47
+ function room(type, summary, body = {}) {
48
+ appendRoomMessage(stateDir, "main", {
49
+ body,
50
+ from: branchAddress,
51
+ summary,
52
+ to: roomAddress,
53
+ type,
54
+ });
55
+ }
56
+ ensureRoomMember(stateDir, run, "main", branchAddress, { display: branch, role: "worker", status: "present" }, `${branch} joined as mailbox worker`);
57
+ room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
58
+ journal("worker.started", { branch, run });
59
+ let stopping = false;
60
+ while (!stopping) {
61
+ const result = await handleMailboxLoopOnce({ address: branchAddress, kind: "branch", run, stateDir }, (message) => {
62
+ if (isMailboxLoopStopMessage(message)) {
63
+ stopping = true;
64
+ room("actor.leave", `${branch} stopping`, { branch, reason: message.type });
65
+ journal("worker.stopping", { id: message.id, type: message.type });
66
+ return;
67
+ }
68
+ room("task.claim", `${branch} claimed ${message.type}`, {
69
+ branch,
70
+ id: message.id,
71
+ type: message.type,
72
+ });
73
+ room("task.result", `${branch} handled ${message.type}`, {
74
+ branch,
75
+ id: message.id,
76
+ result: text(message.body),
77
+ type: message.type,
78
+ });
79
+ room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
80
+ journal("task.handled", { id: message.id, type: message.type });
81
+ }, { owner: branchAddress });
82
+ if (!result.handled)
83
+ await sleep(pollMs);
84
+ }
85
+ journal("worker.done", { branch, run });
86
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Detached async-runner entrypoint logic.
3
+ * Zones: async run process, command-template execution telemetry
4
+ */
5
+ export declare function runAsyncRunner(stateDir?: string): Promise<void>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Detached async-runner entrypoint logic.
3
+ * Zones: async run process, command-template execution telemetry
4
+ */
5
+ import { appendFileSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { appendRecipeContextToPiArgs } from "./actor-recipe-context.js";
8
+ import { execCommandTemplate } from "./command-templates.js";
9
+ import { executeRegisteredTool } from "./execution.js";
10
+ import { writeJsonAtomic } from "./file-state.js";
11
+ function quoteCommandDetailPart(value) {
12
+ if (value === "")
13
+ return "''";
14
+ if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value))
15
+ return value;
16
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
17
+ }
18
+ function formatCommandDetail(command, args) {
19
+ return [command, ...args].map(quoteCommandDetailPart).join(" ");
20
+ }
21
+ function summarizeCommandDetail(commandDetail) {
22
+ return commandDetail.length > 160
23
+ ? `${commandDetail.slice(0, 157)}...`
24
+ : commandDetail;
25
+ }
26
+ export async function runAsyncRunner(stateDir = process.argv[2]) {
27
+ if (!stateDir)
28
+ throw new Error("missing state dir");
29
+ const runPath = join(stateDir, "run.json");
30
+ const progressPath = join(stateDir, "progress.json");
31
+ const resultPath = join(stateDir, "result.json");
32
+ const eventsPath = join(stateDir, "events.jsonl");
33
+ const outboxPath = join(stateDir, "outbox.jsonl");
34
+ const stdoutPath = join(stateDir, "stdout.log");
35
+ const stderrPath = join(stateDir, "stderr.log");
36
+ const meta = JSON.parse(readFileSync(runPath, "utf8"));
37
+ function event(name, data = {}) {
38
+ appendFileSync(eventsPath, `${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`);
39
+ }
40
+ function outbox(name, summary, data = {}, delivery = "log", level = "info") {
41
+ appendFileSync(outboxPath, `${JSON.stringify({ body: data, data, delivery, event: name, from: `run:${meta.run}`, level, summary, to: "coordinator", ts: new Date().toISOString(), type: name })}\n`);
42
+ }
43
+ function progress(phase, extra = {}) {
44
+ writeJsonAtomic(progressPath, {
45
+ phase,
46
+ updatedAt: new Date().toISOString(),
47
+ ...extra,
48
+ });
49
+ }
50
+ let activeSubagents = 0;
51
+ let completedSubagents = 0;
52
+ const subagentFailures = [];
53
+ function getCommandDoneDelivery(result) {
54
+ return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
55
+ }
56
+ function progressRunning() {
57
+ progress("running", {
58
+ activeSubagents,
59
+ completed: completedSubagents,
60
+ failures: subagentFailures,
61
+ });
62
+ }
63
+ async function observedExec(command, args, options) {
64
+ const commandDetail = formatCommandDetail(command, args);
65
+ const execArgs = appendRecipeContextToPiArgs(command, args, meta.recipe_context_records, options?.actorRecipeContext);
66
+ activeSubagents += 1;
67
+ event("command.start", { activeSubagents, command: commandDetail });
68
+ progressRunning();
69
+ const result = await execCommandTemplate(command, execArgs, options);
70
+ activeSubagents = Math.max(0, activeSubagents - 1);
71
+ completedSubagents += 1;
72
+ if (result.code !== 0) {
73
+ subagentFailures.push({
74
+ code: result.code,
75
+ command: commandDetail,
76
+ killed: result.killed,
77
+ });
78
+ }
79
+ event("command.done", {
80
+ activeSubagents,
81
+ code: result.code,
82
+ command: commandDetail,
83
+ killed: result.killed,
84
+ });
85
+ outbox("command.done", `Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`, {
86
+ activeSubagents,
87
+ ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
88
+ run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath],
89
+ code: result.code,
90
+ command: commandDetail,
91
+ killed: result.killed,
92
+ }, getCommandDoneDelivery(result), result.code === 0 ? "info" : "error");
93
+ progressRunning();
94
+ return result;
95
+ }
96
+ try {
97
+ event("run.runner.start", { pid: process.pid });
98
+ progressRunning();
99
+ const result = await executeRegisteredTool({
100
+ name: "run_actor",
101
+ description: "Detached command-template run actor",
102
+ template: meta.template,
103
+ args: [],
104
+ defaults: {},
105
+ }, meta.values || {}, observedExec, meta.cwd);
106
+ const text = result.content?.[0]?.text || "";
107
+ appendFileSync(stdoutPath, text);
108
+ writeJsonAtomic(resultPath, {
109
+ code: result.details.code,
110
+ command: result.details.command,
111
+ killed: result.details.killed,
112
+ truncated: result.details.truncated,
113
+ completedAt: new Date().toISOString(),
114
+ });
115
+ progress("done", {
116
+ completed: 1,
117
+ failures: result.details.nonCriticalFailures || [],
118
+ });
119
+ event("run.done", { code: result.details.code });
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ appendFileSync(stderrPath, `${message}\n`);
124
+ writeJsonAtomic(resultPath, {
125
+ code: 1,
126
+ error: message,
127
+ killed: false,
128
+ completedAt: new Date().toISOString(),
129
+ });
130
+ progress("failed", { completed: 0, failures: [{ message }] });
131
+ event("run.failed", { error: message });
132
+ throw error;
133
+ }
134
+ }