@llblab/pi-actors 0.23.0 → 0.24.1

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 (158) hide show
  1. package/AGENTS.md +125 -60
  2. package/BACKLOG.md +133 -176
  3. package/CHANGELOG.md +27 -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/conformance.d.ts +12 -0
  24. package/dist/lib/conformance.js +28 -0
  25. package/dist/lib/coordinator.d.ts +5 -0
  26. package/dist/lib/coordinator.js +574 -0
  27. package/dist/lib/locker.d.ts +5 -0
  28. package/dist/lib/locker.js +310 -0
  29. package/dist/lib/mailbox-loop.d.ts +41 -0
  30. package/dist/lib/mailbox-loop.js +62 -0
  31. package/dist/lib/observability.d.ts +2 -2
  32. package/dist/lib/observability.js +51 -53
  33. package/dist/lib/prompts.d.ts +1 -1
  34. package/dist/lib/prompts.js +1 -1
  35. package/dist/lib/recipe-references.js +25 -2
  36. package/dist/lib/recipe-utils.d.ts +5 -0
  37. package/dist/lib/recipe-utils.js +385 -0
  38. package/dist/lib/runtime-notifier.js +3 -7
  39. package/dist/lib/state-readers.d.ts +21 -0
  40. package/dist/lib/state-readers.js +74 -0
  41. package/dist/lib/tools.js +1 -1
  42. package/dist/lib/validate-recipe.d.ts +6 -0
  43. package/dist/lib/validate-recipe.js +104 -0
  44. package/dist/recipes/actor-worker.json +35 -0
  45. package/dist/recipes/coordinator-locker.json +45 -0
  46. package/dist/recipes/lens-swarm.json +66 -0
  47. package/dist/recipes/locker.json +45 -0
  48. package/dist/recipes/music-player.json +38 -0
  49. package/dist/recipes/pipeline-architect-coordinator.json +95 -0
  50. package/dist/recipes/pipeline-artifact-bundle.json +100 -0
  51. package/dist/recipes/pipeline-artifact-report.json +58 -0
  52. package/dist/recipes/pipeline-artifact-write.json +72 -0
  53. package/dist/recipes/pipeline-async-run-ops.json +70 -0
  54. package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
  55. package/dist/recipes/pipeline-development-tasking.json +81 -0
  56. package/dist/recipes/pipeline-docs-maintenance.json +80 -0
  57. package/dist/recipes/pipeline-media-library.json +59 -0
  58. package/dist/recipes/pipeline-quorum-review.json +79 -0
  59. package/dist/recipes/pipeline-release-readiness.json +110 -0
  60. package/dist/recipes/pipeline-release-summary.json +88 -0
  61. package/dist/recipes/pipeline-repo-health.json +89 -0
  62. package/dist/recipes/pipeline-research-synthesis.json +94 -0
  63. package/dist/recipes/pipeline-review-readiness.json +54 -0
  64. package/dist/recipes/pipeline-room-swarm.json +50 -0
  65. package/dist/recipes/subagent-artifact.json +32 -0
  66. package/dist/recipes/subagent-checkpoint.json +33 -0
  67. package/dist/recipes/subagent-conflict-report.json +32 -0
  68. package/dist/recipes/subagent-contradiction-map.json +33 -0
  69. package/dist/recipes/subagent-critic.json +35 -0
  70. package/dist/recipes/subagent-evidence-map.json +33 -0
  71. package/dist/recipes/subagent-followup.json +33 -0
  72. package/dist/recipes/subagent-judge.json +33 -0
  73. package/dist/recipes/subagent-merge.json +33 -0
  74. package/dist/recipes/subagent-message.json +34 -0
  75. package/dist/recipes/subagent-normalize.json +31 -0
  76. package/dist/recipes/subagent-plan.json +33 -0
  77. package/dist/recipes/subagent-prompt.json +28 -0
  78. package/dist/recipes/subagent-quorum.json +43 -0
  79. package/dist/recipes/subagent-review-coordinator.json +114 -0
  80. package/dist/recipes/subagent-review.json +37 -0
  81. package/dist/recipes/subagent-task-card.json +35 -0
  82. package/dist/recipes/subagent-tools.json +27 -0
  83. package/dist/recipes/subagent-verify.json +34 -0
  84. package/dist/recipes/subagents-prompts.json +51 -0
  85. package/dist/recipes/utility-actor-message.json +23 -0
  86. package/dist/recipes/utility-artifact-manifest.json +16 -0
  87. package/dist/recipes/utility-artifact-write.json +16 -0
  88. package/dist/recipes/utility-changelog-head.json +11 -0
  89. package/dist/recipes/utility-changelog-section.json +13 -0
  90. package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
  91. package/dist/recipes/utility-git-log.json +11 -0
  92. package/dist/recipes/utility-git-status.json +9 -0
  93. package/dist/recipes/utility-jsonl-tail.json +10 -0
  94. package/dist/recipes/utility-markdown-index.json +14 -0
  95. package/dist/recipes/utility-package-summary.json +11 -0
  96. package/dist/recipes/utility-playlist-build.json +17 -0
  97. package/dist/recipes/utility-playlist-scan.json +11 -0
  98. package/dist/recipes/utility-run-ops-snapshot.json +17 -0
  99. package/dist/recipes/utility-run-state-files.json +13 -0
  100. package/dist/recipes/utility-run-summary.json +11 -0
  101. package/dist/recipes/utility-skill-summary.json +13 -0
  102. package/dist/recipes/utility-validate-recipe.json +13 -0
  103. package/dist/recipes/utility-validation-wrapper.json +13 -0
  104. package/dist/scripts/actor-worker.mjs +31 -0
  105. package/dist/scripts/async-runner.mjs +31 -0
  106. package/dist/scripts/build-dist.mjs +33 -0
  107. package/dist/scripts/conformance.mjs +33 -0
  108. package/dist/scripts/coordinator.mjs +31 -0
  109. package/dist/scripts/locker.mjs +33 -0
  110. package/dist/scripts/music-player.mjs +964 -0
  111. package/dist/scripts/recipe-utils.mjs +31 -0
  112. package/dist/scripts/validate-recipe.mjs +34 -0
  113. package/dist/skills/actors/SKILL.md +377 -0
  114. package/dist/skills/swarm/SKILL.md +467 -0
  115. package/dist/skills/swarm/references/development-swarm.md +596 -0
  116. package/docs/actor-messages.md +2 -2
  117. package/docs/async-runs.md +11 -0
  118. package/docs/template-recipes.md +1 -1
  119. package/fixtures/protocol/actor-message-branch.json +13 -0
  120. package/fixtures/protocol/artifact-manifest.json +9 -0
  121. package/fixtures/protocol/mailbox-contract.json +15 -0
  122. package/fixtures/protocol/recipe-summary.json +16 -0
  123. package/fixtures/protocol/room-message.json +11 -0
  124. package/fixtures/protocol/room-roster.json +11 -0
  125. package/fixtures/protocol/run-inbox-message.json +9 -0
  126. package/fixtures/protocol/run-outbox-event.json +9 -0
  127. package/fixtures/protocol/run-state.json +10 -0
  128. package/index.ts +3 -0
  129. package/lib/actor-inspector-tui.ts +11 -34
  130. package/lib/actor-rooms.ts +88 -18
  131. package/lib/actor-worker.ts +118 -0
  132. package/lib/async-runner.ts +173 -0
  133. package/lib/async-runs.ts +12 -22
  134. package/lib/conformance.ts +46 -0
  135. package/lib/coordinator.ts +664 -0
  136. package/lib/locker.ts +340 -0
  137. package/lib/mailbox-loop.ts +148 -0
  138. package/lib/observability.ts +24 -19
  139. package/lib/prompts.ts +1 -1
  140. package/lib/recipe-references.ts +37 -2
  141. package/lib/recipe-utils.ts +486 -0
  142. package/lib/runtime-notifier.ts +4 -6
  143. package/lib/state-readers.ts +93 -0
  144. package/lib/tools.ts +1 -1
  145. package/lib/validate-recipe.ts +110 -0
  146. package/package.json +10 -2
  147. package/recipes/actor-worker.json +35 -0
  148. package/recipes/pipeline-quorum-review.json +12 -7
  149. package/scripts/actor-worker.mjs +31 -0
  150. package/scripts/async-runner.mjs +11 -201
  151. package/scripts/build-dist.mjs +33 -0
  152. package/scripts/conformance.mjs +21 -35
  153. package/scripts/coordinator.mjs +15 -625
  154. package/scripts/locker.mjs +20 -332
  155. package/scripts/recipe-utils.mjs +17 -477
  156. package/scripts/validate-recipe.mjs +18 -121
  157. package/skills/actors/SKILL.md +8 -3
  158. package/skills/swarm/SKILL.md +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.24.1: Build Script Package Hygiene Hotfix
6
+
7
+ - `[Backlog]` Added the next focused backlog set for recipe doctor remediation UX, actor worker v2, and dist package contract hardening.
8
+ - `[Skills]` Clarified agent-governed promotion of successful transient actor patterns into durable local tools via `register_tool`, without UI buttons or automatic registration.
9
+
10
+ ## 0.24.0: Reliability, Mailbox Workers, and Dist-First Packaging
11
+
12
+ - `[Prompts]` Clarified that recipe registry warnings are actionable maintenance: invalid or blocking recipes should be fixed, removed, or disabled rather than ignored.
13
+ - `[Backlog]` Pruned and refocused the backlog around reliability, mailbox-loop consolidation, protocol fixtures, follow-up deduplication, and portability reality checks.
14
+ - `[State]` Added resilient JSON/JSONL state reader helpers and routed room, inspector, runtime wake, run inbox, and observability outbox reads through them so malformed state records degrade instead of breaking previews; room status now reports state diagnostic counts and reader degradation behavior has direct regression coverage.
15
+ - `[Observability]` Deduplicated run outbox events by stable event id so line-counter resets do not replay already-seen follow-ups; stale dedupe state is pruned with terminal and missing runs.
16
+ - `[Mailbox Loop]` Added initial run/branch mailbox claim-and-handle helpers plus branch inbox claiming support, failed-handler transitions, standard stop-message detection, bounded message drains, duplicate-claim coverage, and a packaged `actor-worker` demo recipe for canonical mailbox loops.
17
+ - `[Scripts]` Added installed-package coverage proving the packaged `actor-worker` script uses compiled `dist` runtime modules instead of importing TypeScript from `node_modules`; `npm run build` now cleans stale `dist` output, mirrors packaged `scripts/`, `recipes/`, and `fixtures/` into `dist/`, and syntax-checks the built script entrypoints. The `actor-worker`, `async-runner`, `validate-recipe`, and `conformance` executables are now thin shims over compiled TypeScript entrypoint logic in `lib/actor-worker.ts`, `lib/async-runner.ts`, `lib/validate-recipe.ts`, and `lib/conformance.ts`, with build-output regressions for the compiled shim modules.
18
+ - `[Packaging]` Exposed optional `pi.sourceExtensions` metadata pointing at the root TypeScript entrypoint while keeping Node-compatible `pi.extensions` on compiled `dist` output.
19
+ - `[Context]` Clarified the project frame as an experimental self-evolution membrane for local agent capabilities, grounded in explicit actors, recipes, fixtures, skills, and inspectable state.
20
+ - `[Protocol]` Clarified dotted `channel.action` message types as the minimal action surface: scripts can often dispatch from `type` alone while agents may use `body` for free-form prompts.
21
+ - `[Recipes]` Fixed `pipeline-quorum-review` registry loading by inlining the quorum fanout over its `models` array instead of importing a nested repeated recipe with unresolved runtime values.
22
+ - `[Recipes]` Made Markdown recipe frontmatter more forgiving: `args` can be a comma-separated scalar and `defaults` can be a list of `key: value` entries, both normalizing to the canonical JSON recipe shape.
23
+ - `[Packaging]` Build output now mirrors packaged `skills/` into `dist/` alongside scripts, recipes, and fixtures so the JS-only distributive tree carries the project skills; package skill metadata now points at `dist/skills` with `pi.sourceSkills` preserving root TypeScript/source-tree paths, and README now documents the dist-first/source-optional package shape. The dist build pipeline now lives in `scripts/build-dist.mjs` instead of an inline package script, completing the compiled script entrypoint backlog slice.
24
+ - `[Docs]` Added a platform support matrix for mailbox-only, FIFO, named-pipe, and process-control behavior across Linux/macOS/WSL and native Windows, with regressions proving native Windows FIFO limits remain visible and the canonical worker recipe stays mailbox-only.
25
+ - `[Backlog]` Marked the reliability, mailbox loop, protocol fixture, portability, and compiled-entrypoint milestone set complete; future backlog additions should come from concrete actor workflow evidence.
26
+ - `[Scripts]` Migrated `recipe-utils`, `locker`, `coordinator`, and `validate-recipe` command logic behind compiled TypeScript domain modules while preserving the stable `scripts/*.mjs` shim paths; project guidance frames this as deliberate standard-library growth with clear domain boundaries while keeping self-contained application/build scripts such as `music-player.mjs` and `build-dist.mjs` standalone.
27
+ - `[Protocol]` Added compact protocol fixtures for actor messages, mailbox contracts, run inbox/outbox records, room messages/rosters, run state, recipe summaries, and artifact manifests with regression coverage.
28
+ - `[Skills]` Documented the passive-active skill evolution discipline: `actors` tracks extension mechanics while `swarm` tracks orchestration standards and lessons.
29
+
3
30
  ## 0.23.0: Actor Manifests, Inspection, and Runtime Hygiene
4
31
 
5
32
  - `[Tools]` Unified branch-envelope routing for direct branch messages and selected-recipient room multicast so both paths persist the same branch-local inbox shape before dispatching through the parent run mailbox.
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>;