@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.
- package/AGENTS.md +125 -60
- package/BACKLOG.md +133 -176
- package/CHANGELOG.md +27 -0
- package/README.md +13 -0
- 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 +3 -2
- package/dist/lib/actor-inspector-tui.js +5 -32
- package/dist/lib/actor-rooms.d.ts +8 -0
- package/dist/lib/actor-rooms.js +64 -20
- 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.js +9 -28
- 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 +574 -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 +51 -53
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-references.js +25 -2
- 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/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +1 -1
- 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 +33 -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 +377 -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 +11 -0
- package/docs/template-recipes.md +1 -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 +3 -0
- package/lib/actor-inspector-tui.ts +11 -34
- package/lib/actor-rooms.ts +88 -18
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +12 -22
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +664 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +24 -19
- package/lib/prompts.ts +1 -1
- package/lib/recipe-references.ts +37 -2
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +1 -1
- package/lib/validate-recipe.ts +110 -0
- package/package.json +10 -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 -201
- package/scripts/build-dist.mjs +33 -0
- package/scripts/conformance.mjs +21 -35
- package/scripts/coordinator.mjs +15 -625
- package/scripts/locker.mjs +20 -332
- package/scripts/recipe-utils.mjs +17 -477
- package/scripts/validate-recipe.mjs +18 -121
- package/skills/actors/SKILL.md +8 -3
- 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,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
|
+
}
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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[];
|
package/dist/lib/actor-rooms.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
...(
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|