@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/lib/locker.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local coordination locker service entrypoint logic.
|
|
3
|
+
* Zones: coordination locks, task leases, platform control endpoint
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// @ts-nocheck
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
appendFileSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { createServer } from "node:net";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import readline from "node:readline";
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const arg = argv[index];
|
|
26
|
+
if (arg === "serve" || arg === "snapshot") args.mode = arg;
|
|
27
|
+
else if (arg === "--state-dir") args.stateDir = argv[++index] ?? "";
|
|
28
|
+
else if (arg === "--lease-ms")
|
|
29
|
+
args.leaseMs = Number(argv[++index] ?? args.leaseMs);
|
|
30
|
+
else if (arg === "--lines")
|
|
31
|
+
args.lines = Number(argv[++index] ?? args.lines);
|
|
32
|
+
}
|
|
33
|
+
if (!args.stateDir) throw new Error("--state-dir is required");
|
|
34
|
+
if (!Number.isFinite(args.leaseMs) || args.leaseMs <= 0)
|
|
35
|
+
args.leaseMs = 600000;
|
|
36
|
+
if (!Number.isFinite(args.lines) || args.lines <= 0) args.lines = 20;
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runLocker(argv = process.argv.slice(2)) {
|
|
41
|
+
const { mode, stateDir, leaseMs, lines } = parseArgs(argv);
|
|
42
|
+
const queuePath = join(stateDir, "queue.json");
|
|
43
|
+
const locksPath = join(stateDir, "locks.json");
|
|
44
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
45
|
+
const outboxPath = join(stateDir, "outbox.jsonl");
|
|
46
|
+
const controlPath = join(stateDir, "control.fifo");
|
|
47
|
+
mkdirSync(stateDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
function readJson(path, fallback) {
|
|
50
|
+
if (!existsSync(path)) return fallback;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeJson(path, value) {
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
59
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
60
|
+
}
|
|
61
|
+
function getControlEndpoint() {
|
|
62
|
+
if (process.platform !== "win32") return { path: controlPath, type: "fifo" };
|
|
63
|
+
const hash = createHash("sha256")
|
|
64
|
+
.update(resolve(stateDir))
|
|
65
|
+
.digest("hex")
|
|
66
|
+
.slice(0, 20);
|
|
67
|
+
return { path: `\\\\.\\pipe\\pi-actors-locker-${hash}`, type: "named-pipe" };
|
|
68
|
+
}
|
|
69
|
+
function writeControlEndpoint(endpoint) {
|
|
70
|
+
writeJson(join(stateDir, "control.json"), endpoint);
|
|
71
|
+
const runPath = join(stateDir, "run.json");
|
|
72
|
+
const run = readJson(runPath, undefined);
|
|
73
|
+
if (run && typeof run === "object")
|
|
74
|
+
writeJson(runPath, { ...run, control: endpoint });
|
|
75
|
+
}
|
|
76
|
+
function journal(event, data = {}) {
|
|
77
|
+
appendFileSync(
|
|
78
|
+
journalPath,
|
|
79
|
+
`${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function outbox(type, summary, body = {}, level = "info") {
|
|
83
|
+
appendFileSync(
|
|
84
|
+
outboxPath,
|
|
85
|
+
`${JSON.stringify({ to: "coordinator", from: `run:${process.env.run_id ?? "locker"}`, type, event: type, summary, body, data: body, delivery: "followup", level, ts: new Date().toISOString() })}\n`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function now() {
|
|
89
|
+
return Date.now();
|
|
90
|
+
}
|
|
91
|
+
function cleanExpiredLocks(locks) {
|
|
92
|
+
const current = now();
|
|
93
|
+
const kept = {};
|
|
94
|
+
for (const [key, lock] of Object.entries(locks)) {
|
|
95
|
+
if (Number(lock.expiresAt) > current) kept[key] = lock;
|
|
96
|
+
else journal("lock.expired", { resource: key, owner: lock.owner });
|
|
97
|
+
}
|
|
98
|
+
return kept;
|
|
99
|
+
}
|
|
100
|
+
function normalizeMessage(line) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed) return undefined;
|
|
103
|
+
if (["stop", "quit", "exit", "cancel"].includes(trimmed.toLowerCase())) {
|
|
104
|
+
return { type: "control.stop", body: {} };
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(trimmed);
|
|
108
|
+
} catch {
|
|
109
|
+
return { type: "lock.enqueue", body: { task: trimmed } };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function tailJournal(count) {
|
|
113
|
+
if (!existsSync(journalPath)) return [];
|
|
114
|
+
return readFileSync(journalPath, "utf8")
|
|
115
|
+
.trimEnd()
|
|
116
|
+
.split("\n")
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.slice(-count)
|
|
119
|
+
.map((line) => {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(line);
|
|
122
|
+
} catch {
|
|
123
|
+
return { raw: line };
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function printSnapshot() {
|
|
128
|
+
const locks = cleanExpiredLocks(readJson(locksPath, {}));
|
|
129
|
+
writeJson(locksPath, locks);
|
|
130
|
+
const queue = readJson(queuePath, { items: [] });
|
|
131
|
+
console.log(
|
|
132
|
+
JSON.stringify(
|
|
133
|
+
{
|
|
134
|
+
queueDepth: Array.isArray(queue.items) ? queue.items.length : 0,
|
|
135
|
+
queue,
|
|
136
|
+
locks,
|
|
137
|
+
journal: tailJournal(lines),
|
|
138
|
+
},
|
|
139
|
+
null,
|
|
140
|
+
2,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function nextTask(queue, locks) {
|
|
145
|
+
const items = Array.isArray(queue.items) ? queue.items : [];
|
|
146
|
+
const index = items.findIndex((item) => {
|
|
147
|
+
const resources = Array.isArray(item.resources) ? item.resources : [];
|
|
148
|
+
return resources.every((resource) => !locks[resource]);
|
|
149
|
+
});
|
|
150
|
+
if (index < 0) return undefined;
|
|
151
|
+
return items.splice(index, 1)[0];
|
|
152
|
+
}
|
|
153
|
+
function handle(message) {
|
|
154
|
+
const type = message.type || message.event || "lock.enqueue";
|
|
155
|
+
const body =
|
|
156
|
+
message.body && typeof message.body === "object" ? message.body : message;
|
|
157
|
+
let queue = readJson(queuePath, { items: [] });
|
|
158
|
+
let locks = cleanExpiredLocks(readJson(locksPath, {}));
|
|
159
|
+
if (type === "control.stop" || type === "control.cancel") {
|
|
160
|
+
writeJson(locksPath, locks);
|
|
161
|
+
journal("control.stop", {});
|
|
162
|
+
outbox("lock.stopped", "Locker stopped", {
|
|
163
|
+
queueDepth: queue.items?.length ?? 0,
|
|
164
|
+
});
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
if (type === "lock.enqueue" || type === "coord.enqueue") {
|
|
168
|
+
const item = {
|
|
169
|
+
id: body.id || `task-${Date.now()}`,
|
|
170
|
+
task: body.task ?? body,
|
|
171
|
+
resources: body.resources ?? [],
|
|
172
|
+
enqueuedAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
queue.items = [...(queue.items ?? []), item];
|
|
175
|
+
writeJson(queuePath, queue);
|
|
176
|
+
writeJson(locksPath, locks);
|
|
177
|
+
journal("lock.enqueued", { id: item.id, resources: item.resources });
|
|
178
|
+
outbox("lock.enqueued", `Queued task ${item.id}`, {
|
|
179
|
+
id: item.id,
|
|
180
|
+
queueDepth: queue.items.length,
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (type === "lock.claim" || type === "coord.claim") {
|
|
185
|
+
const owner = body.owner || message.from || "worker";
|
|
186
|
+
const item = nextTask(queue, locks);
|
|
187
|
+
if (!item) {
|
|
188
|
+
writeJson(queuePath, queue);
|
|
189
|
+
writeJson(locksPath, locks);
|
|
190
|
+
outbox("lock.empty", "No claimable task", {
|
|
191
|
+
owner,
|
|
192
|
+
queueDepth: queue.items?.length ?? 0,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
for (const resource of item.resources ?? [])
|
|
197
|
+
locks[resource] = { owner, task: item.id, expiresAt: now() + leaseMs };
|
|
198
|
+
writeJson(queuePath, queue);
|
|
199
|
+
writeJson(locksPath, locks);
|
|
200
|
+
journal("lock.assigned", {
|
|
201
|
+
id: item.id,
|
|
202
|
+
owner,
|
|
203
|
+
resources: item.resources,
|
|
204
|
+
});
|
|
205
|
+
outbox("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (type === "lock.acquire") {
|
|
209
|
+
const resource = body.resource;
|
|
210
|
+
const owner = body.owner || message.from || "worker";
|
|
211
|
+
if (!resource) throw new Error("lock.acquire body.resource is required");
|
|
212
|
+
if (locks[resource])
|
|
213
|
+
outbox(
|
|
214
|
+
"lock.denied",
|
|
215
|
+
`Lock denied ${resource}`,
|
|
216
|
+
{ resource, owner, current: locks[resource] },
|
|
217
|
+
"warning",
|
|
218
|
+
);
|
|
219
|
+
else {
|
|
220
|
+
locks[resource] = { owner, expiresAt: now() + leaseMs };
|
|
221
|
+
outbox("lock.granted", `Lock granted ${resource}`, { resource, owner });
|
|
222
|
+
}
|
|
223
|
+
writeJson(locksPath, locks);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (type === "lock.renew") {
|
|
227
|
+
const resource = body.resource;
|
|
228
|
+
const owner = body.owner || message.from || "worker";
|
|
229
|
+
if (!resource) throw new Error("lock.renew body.resource is required");
|
|
230
|
+
const current = locks[resource];
|
|
231
|
+
if (!current) {
|
|
232
|
+
outbox(
|
|
233
|
+
"lock.denied",
|
|
234
|
+
`Lock renew denied ${resource}`,
|
|
235
|
+
{ resource, owner, reason: "missing" },
|
|
236
|
+
"warning",
|
|
237
|
+
);
|
|
238
|
+
} else if (current.owner !== owner) {
|
|
239
|
+
outbox(
|
|
240
|
+
"lock.denied",
|
|
241
|
+
`Lock renew denied ${resource}`,
|
|
242
|
+
{ resource, owner, current },
|
|
243
|
+
"warning",
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
locks[resource] = { ...current, expiresAt: now() + leaseMs };
|
|
247
|
+
outbox("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
|
|
248
|
+
}
|
|
249
|
+
writeJson(locksPath, locks);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (type === "lock.release") {
|
|
253
|
+
const resource = body.resource;
|
|
254
|
+
if (resource) delete locks[resource];
|
|
255
|
+
writeJson(locksPath, locks);
|
|
256
|
+
outbox("lock.released", `Lock released ${resource}`, { resource });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (
|
|
260
|
+
type === "lock.complete" ||
|
|
261
|
+
type === "lock.fail" ||
|
|
262
|
+
type === "coord.complete" ||
|
|
263
|
+
type === "coord.fail"
|
|
264
|
+
) {
|
|
265
|
+
const eventType = type.startsWith("coord.")
|
|
266
|
+
? type.replace("coord.", "lock.")
|
|
267
|
+
: type;
|
|
268
|
+
journal(eventType, body);
|
|
269
|
+
outbox(
|
|
270
|
+
eventType,
|
|
271
|
+
`${eventType} ${body.id ?? ""}`.trim(),
|
|
272
|
+
body,
|
|
273
|
+
eventType === "lock.fail" ? "error" : "info",
|
|
274
|
+
);
|
|
275
|
+
writeJson(locksPath, locks);
|
|
276
|
+
writeJson(queuePath, queue);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
journal("lock.unknown", { type, body });
|
|
280
|
+
outbox("lock.unknown", `Unknown message ${type}`, { type, body }, "warning");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (mode === "snapshot") {
|
|
284
|
+
printSnapshot();
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function handleLine(line) {
|
|
289
|
+
const message = normalizeMessage(line);
|
|
290
|
+
if (!message) return;
|
|
291
|
+
try {
|
|
292
|
+
handle(message);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
295
|
+
journal("lock.error", { error: text });
|
|
296
|
+
outbox("lock.error", text, { error: text }, "error");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function serveFifo(endpoint) {
|
|
301
|
+
if (!existsSync(endpoint.path)) {
|
|
302
|
+
const result = spawnSync("mkfifo", [endpoint.path]);
|
|
303
|
+
if (result.status !== 0)
|
|
304
|
+
throw new Error(`mkfifo failed: ${result.stderr?.toString?.() ?? ""}`);
|
|
305
|
+
}
|
|
306
|
+
writeControlEndpoint(endpoint);
|
|
307
|
+
while (true) {
|
|
308
|
+
const stream = await import("node:fs").then((fs) =>
|
|
309
|
+
fs.createReadStream(endpoint.path, { encoding: "utf8" }),
|
|
310
|
+
);
|
|
311
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
312
|
+
for await (const line of rl) handleLine(line);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function serveNamedPipe(endpoint) {
|
|
317
|
+
rmSync(endpoint.path, { force: true });
|
|
318
|
+
const server = createServer((socket) => {
|
|
319
|
+
const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
|
|
320
|
+
rl.on("line", handleLine);
|
|
321
|
+
});
|
|
322
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
323
|
+
server.once("error", rejectReady);
|
|
324
|
+
server.listen(endpoint.path, () => {
|
|
325
|
+
server.off("error", rejectReady);
|
|
326
|
+
writeControlEndpoint(endpoint);
|
|
327
|
+
resolveReady();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
await new Promise(() => {});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const endpoint = getControlEndpoint();
|
|
334
|
+
writeJson(queuePath, readJson(queuePath, { items: [] }));
|
|
335
|
+
writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
|
|
336
|
+
journal("lock.started", { leaseMs, control: endpoint.type });
|
|
337
|
+
outbox("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
|
|
338
|
+
if (endpoint.type === "named-pipe") await serveNamedPipe(endpoint);
|
|
339
|
+
else await serveFifo(endpoint);
|
|
340
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal mailbox loop helpers.
|
|
3
|
+
* Zones: mailbox-consuming actors, run/branch inbox claiming, handler status transitions
|
|
4
|
+
* Owns small reusable primitives for recipe authors; scheduling and task policy stay outside.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
claimBranchInboxMessage,
|
|
9
|
+
updateBranchInboxMessageStatus,
|
|
10
|
+
type BranchInboxRecord,
|
|
11
|
+
} from "./actor-rooms.ts";
|
|
12
|
+
import {
|
|
13
|
+
claimRunInboxMessage,
|
|
14
|
+
updateRunInboxMessageStatus,
|
|
15
|
+
type RunInboxMessage,
|
|
16
|
+
} from "./async-runs.ts";
|
|
17
|
+
|
|
18
|
+
export type MailboxLoopMessage = RunInboxMessage | BranchInboxRecord;
|
|
19
|
+
|
|
20
|
+
export type MailboxLoopTarget =
|
|
21
|
+
| {
|
|
22
|
+
kind: "run";
|
|
23
|
+
runOrDir: string;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
address: string;
|
|
27
|
+
kind: "branch";
|
|
28
|
+
run: string;
|
|
29
|
+
stateDir: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface MailboxLoopClaimOptions {
|
|
33
|
+
owner?: string;
|
|
34
|
+
statuses?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MailboxLoopHandleResult {
|
|
38
|
+
handled: boolean;
|
|
39
|
+
id?: string;
|
|
40
|
+
message?: MailboxLoopMessage;
|
|
41
|
+
target: MailboxLoopTarget;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MailboxLoopDrainOptions extends MailboxLoopClaimOptions {
|
|
45
|
+
maxMessages?: number;
|
|
46
|
+
stopOnControl?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MailboxLoopDrainResult {
|
|
50
|
+
handled: number;
|
|
51
|
+
stopped: boolean;
|
|
52
|
+
target: MailboxLoopTarget;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isMailboxLoopStopMessage(message: unknown): boolean {
|
|
56
|
+
const type =
|
|
57
|
+
message && typeof message === "object" && "type" in message
|
|
58
|
+
? (message as { type?: unknown }).type
|
|
59
|
+
: undefined;
|
|
60
|
+
return (
|
|
61
|
+
type === "control.stop" ||
|
|
62
|
+
type === "control.cancel" ||
|
|
63
|
+
type === "control.kill"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function messageId(
|
|
68
|
+
message: MailboxLoopMessage | undefined,
|
|
69
|
+
): string | undefined {
|
|
70
|
+
return typeof message?.id === "string" ? message.id : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function claimMailboxLoopMessage(
|
|
74
|
+
target: MailboxLoopTarget,
|
|
75
|
+
options: MailboxLoopClaimOptions = {},
|
|
76
|
+
): MailboxLoopMessage | undefined {
|
|
77
|
+
const owner = options.owner ?? "mailbox-loop";
|
|
78
|
+
const statuses = options.statuses ?? ["queued"];
|
|
79
|
+
return target.kind === "run"
|
|
80
|
+
? claimRunInboxMessage(target.runOrDir, owner, statuses)
|
|
81
|
+
: claimBranchInboxMessage(
|
|
82
|
+
target.stateDir,
|
|
83
|
+
target.run,
|
|
84
|
+
target.address,
|
|
85
|
+
owner,
|
|
86
|
+
statuses,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function updateMailboxLoopMessageStatus(
|
|
91
|
+
target: MailboxLoopTarget,
|
|
92
|
+
id: string,
|
|
93
|
+
status: "claimed" | "handled" | "failed",
|
|
94
|
+
metadata: Record<string, unknown> = {},
|
|
95
|
+
): boolean {
|
|
96
|
+
return target.kind === "run"
|
|
97
|
+
? updateRunInboxMessageStatus(target.runOrDir, id, status, metadata)
|
|
98
|
+
: updateBranchInboxMessageStatus(
|
|
99
|
+
target.stateDir,
|
|
100
|
+
target.run,
|
|
101
|
+
target.address,
|
|
102
|
+
id,
|
|
103
|
+
status,
|
|
104
|
+
metadata,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function handleMailboxLoopOnce(
|
|
109
|
+
target: MailboxLoopTarget,
|
|
110
|
+
handler: (message: MailboxLoopMessage) => Promise<void> | void,
|
|
111
|
+
options: MailboxLoopClaimOptions = {},
|
|
112
|
+
): Promise<MailboxLoopHandleResult> {
|
|
113
|
+
const message = claimMailboxLoopMessage(target, options);
|
|
114
|
+
const id = messageId(message);
|
|
115
|
+
if (!message || !id) return { handled: false, target };
|
|
116
|
+
try {
|
|
117
|
+
await handler(message);
|
|
118
|
+
updateMailboxLoopMessageStatus(target, id, "handled");
|
|
119
|
+
return { handled: true, id, message, target };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
updateMailboxLoopMessageStatus(target, id, "failed", {
|
|
122
|
+
error: error instanceof Error ? error.message : String(error),
|
|
123
|
+
});
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function drainMailboxLoopMessages(
|
|
129
|
+
target: MailboxLoopTarget,
|
|
130
|
+
handler: (message: MailboxLoopMessage) => Promise<void> | void,
|
|
131
|
+
options: MailboxLoopDrainOptions = {},
|
|
132
|
+
): Promise<MailboxLoopDrainResult> {
|
|
133
|
+
const maxMessages = Math.max(1, Math.floor(options.maxMessages ?? 100));
|
|
134
|
+
let handled = 0;
|
|
135
|
+
for (; handled < maxMessages; handled += 1) {
|
|
136
|
+
const result = await handleMailboxLoopOnce(target, handler, options);
|
|
137
|
+
if (!result.handled || !result.message) {
|
|
138
|
+
return { handled, stopped: false, target };
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
options.stopOnControl !== false &&
|
|
142
|
+
isMailboxLoopStopMessage(result.message)
|
|
143
|
+
) {
|
|
144
|
+
return { handled: handled + 1, stopped: true, target };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { handled, stopped: false, target };
|
|
148
|
+
}
|
package/lib/observability.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
|
|
17
17
|
import * as AsyncRuns from "./async-runs.ts";
|
|
18
18
|
import * as Paths from "./paths.ts";
|
|
19
|
+
import { readJsonlFileResilient } from "./state-readers.ts";
|
|
19
20
|
|
|
20
21
|
export type RunObservedStatus =
|
|
21
22
|
| "running"
|
|
@@ -605,15 +606,12 @@ function normalizeOutboxLevel(value: unknown): RunOutboxLevel {
|
|
|
605
606
|
return value === "warning" || value === "error" ? value : "info";
|
|
606
607
|
}
|
|
607
608
|
|
|
608
|
-
function
|
|
609
|
-
|
|
609
|
+
function parseOutboxRecord(
|
|
610
|
+
raw: Record<string, unknown>,
|
|
610
611
|
run: RunObservation,
|
|
611
612
|
index: number,
|
|
612
613
|
): RunOutboxEvent | undefined {
|
|
613
614
|
if (!run.stateDir) return undefined;
|
|
614
|
-
try {
|
|
615
|
-
const raw = JSON.parse(line) as Record<string, unknown>;
|
|
616
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
617
615
|
const event =
|
|
618
616
|
typeof raw.event === "string" && raw.event.trim()
|
|
619
617
|
? raw.event.trim()
|
|
@@ -647,17 +645,13 @@ function parseOutboxLine(
|
|
|
647
645
|
summary,
|
|
648
646
|
ts,
|
|
649
647
|
};
|
|
650
|
-
} catch {
|
|
651
|
-
return undefined;
|
|
652
|
-
}
|
|
653
648
|
}
|
|
654
649
|
|
|
655
|
-
function
|
|
650
|
+
function readOutboxRecords(run: RunObservation): Record<string, unknown>[] {
|
|
656
651
|
if (!run.stateDir) return [];
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return content ? content.split("\n") : [];
|
|
652
|
+
return readJsonlFileResilient<Record<string, unknown>>(
|
|
653
|
+
join(run.stateDir, "outbox.jsonl"),
|
|
654
|
+
).records;
|
|
661
655
|
}
|
|
662
656
|
|
|
663
657
|
export function pruneRunObservationState(
|
|
@@ -665,6 +659,7 @@ export function pruneRunObservationState(
|
|
|
665
659
|
previousLineCounts: Map<string, number>,
|
|
666
660
|
summary: RunSummary,
|
|
667
661
|
terminalRuns: Iterable<string> = [],
|
|
662
|
+
seenEventIds: Map<string, Set<string>> = new Map(),
|
|
668
663
|
): void {
|
|
669
664
|
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
670
665
|
const terminalRunSet = new Set(terminalRuns);
|
|
@@ -685,23 +680,33 @@ export function pruneRunObservationState(
|
|
|
685
680
|
previousLineCounts.delete(key);
|
|
686
681
|
}
|
|
687
682
|
}
|
|
683
|
+
for (const key of seenEventIds.keys()) {
|
|
684
|
+
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
|
|
685
|
+
seenEventIds.delete(key);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
export function detectRunOutboxEvents(
|
|
691
691
|
previousLineCounts: Map<string, number>,
|
|
692
692
|
summary: RunSummary,
|
|
693
|
+
seenEventIds: Map<string, Set<string>> = new Map(),
|
|
693
694
|
): RunOutboxEvent[] {
|
|
694
695
|
const events: RunOutboxEvent[] = [];
|
|
695
696
|
for (const run of summary.runs) {
|
|
696
697
|
const key = run.stateDir ?? run.run;
|
|
697
|
-
const
|
|
698
|
+
const records = readOutboxRecords(run);
|
|
698
699
|
const previousCount = previousLineCounts.get(key) ?? 0;
|
|
699
|
-
const start = Math.min(previousCount,
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
700
|
+
const start = Math.min(previousCount, records.length);
|
|
701
|
+
const seen = seenEventIds.get(key) ?? new Set<string>();
|
|
702
|
+
for (let index = start; index < records.length; index += 1) {
|
|
703
|
+
const event = parseOutboxRecord(records[index], run, index);
|
|
704
|
+
if (!event || seen.has(event.id)) continue;
|
|
705
|
+
events.push(event);
|
|
706
|
+
seen.add(event.id);
|
|
703
707
|
}
|
|
704
|
-
previousLineCounts.set(key,
|
|
708
|
+
previousLineCounts.set(key, records.length);
|
|
709
|
+
seenEventIds.set(key, seen);
|
|
705
710
|
}
|
|
706
711
|
return events;
|
|
707
712
|
}
|
package/lib/prompts.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
|
29
29
|
- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
|
|
30
30
|
- Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.
|
|
31
31
|
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
|
|
32
|
-
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
|
|
32
|
+
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
|
|
33
33
|
- Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.
|
|
34
34
|
- Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.
|
|
35
35
|
- For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.`;
|
package/lib/recipe-references.ts
CHANGED
|
@@ -311,6 +311,35 @@ function parseMarkdownFrontmatterObject(
|
|
|
311
311
|
return result;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
function normalizeMarkdownFrontmatterField(
|
|
315
|
+
key: string,
|
|
316
|
+
value: unknown,
|
|
317
|
+
): unknown {
|
|
318
|
+
if (key === "args" && typeof value === "string") {
|
|
319
|
+
return value
|
|
320
|
+
.split(",")
|
|
321
|
+
.map((item) => item.trim())
|
|
322
|
+
.filter(Boolean);
|
|
323
|
+
}
|
|
324
|
+
if (key === "defaults" && Array.isArray(value)) {
|
|
325
|
+
return Object.fromEntries(
|
|
326
|
+
value
|
|
327
|
+
.filter((item): item is string => typeof item === "string")
|
|
328
|
+
.map((item) => {
|
|
329
|
+
const separator = item.indexOf(":");
|
|
330
|
+
return separator < 0
|
|
331
|
+
? [item.trim(), ""]
|
|
332
|
+
: [
|
|
333
|
+
item.slice(0, separator).trim(),
|
|
334
|
+
parseMarkdownScalar(item.slice(separator + 1)),
|
|
335
|
+
];
|
|
336
|
+
})
|
|
337
|
+
.filter(([name]) => Boolean(name)),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
|
|
314
343
|
function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
|
|
315
344
|
const result: Record<string, unknown> = {};
|
|
316
345
|
const lines = value.split(/\r?\n/);
|
|
@@ -320,7 +349,10 @@ function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
|
|
|
320
349
|
const match = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*):\s*(.*)$/);
|
|
321
350
|
if (!match) continue;
|
|
322
351
|
if (match[2]) {
|
|
323
|
-
result[match[1]] =
|
|
352
|
+
result[match[1]] = normalizeMarkdownFrontmatterField(
|
|
353
|
+
match[1],
|
|
354
|
+
parseMarkdownScalar(match[2]),
|
|
355
|
+
);
|
|
324
356
|
continue;
|
|
325
357
|
}
|
|
326
358
|
const nested: string[] = [];
|
|
@@ -328,7 +360,10 @@ function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
|
|
|
328
360
|
index += 1;
|
|
329
361
|
nested.push(lines[index]);
|
|
330
362
|
}
|
|
331
|
-
result[match[1]] =
|
|
363
|
+
result[match[1]] = normalizeMarkdownFrontmatterField(
|
|
364
|
+
match[1],
|
|
365
|
+
parseMarkdownFrontmatterObject(nested),
|
|
366
|
+
);
|
|
332
367
|
}
|
|
333
368
|
return result;
|
|
334
369
|
}
|