@llblab/pi-actors 0.22.5 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +125 -59
- package/BACKLOG.md +88 -403
- package/CHANGELOG.md +39 -0
- package/README.md +14 -1
- package/dist/fixtures/protocol/actor-message-branch.json +13 -0
- package/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/dist/fixtures/protocol/mailbox-contract.json +15 -0
- package/dist/fixtures/protocol/recipe-summary.json +16 -0
- package/dist/fixtures/protocol/room-message.json +11 -0
- package/dist/fixtures/protocol/room-roster.json +11 -0
- package/dist/fixtures/protocol/run-inbox-message.json +9 -0
- package/dist/fixtures/protocol/run-outbox-event.json +9 -0
- package/dist/fixtures/protocol/run-state.json +10 -0
- package/dist/index.js +18 -2
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +58 -44
- package/dist/lib/actor-rooms.d.ts +16 -0
- package/dist/lib/actor-rooms.js +123 -28
- package/dist/lib/actor-worker.d.ts +13 -0
- package/dist/lib/actor-worker.js +86 -0
- package/dist/lib/async-runner.d.ts +5 -0
- package/dist/lib/async-runner.js +134 -0
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +224 -50
- package/dist/lib/build-dist.d.ts +5 -0
- package/dist/lib/build-dist.js +24 -0
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/conformance.d.ts +12 -0
- package/dist/lib/conformance.js +28 -0
- package/dist/lib/coordinator.d.ts +5 -0
- package/dist/lib/coordinator.js +557 -0
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/locker.d.ts +5 -0
- package/dist/lib/locker.js +310 -0
- package/dist/lib/mailbox-loop.d.ts +41 -0
- package/dist/lib/mailbox-loop.js +62 -0
- package/dist/lib/observability.d.ts +2 -2
- package/dist/lib/observability.js +57 -57
- package/dist/lib/output.js +4 -5
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +29 -4
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/recipe-utils.d.ts +5 -0
- package/dist/lib/recipe-utils.js +385 -0
- package/dist/lib/runtime-notifier.js +3 -7
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +198 -39
- package/dist/lib/validate-recipe.d.ts +6 -0
- package/dist/lib/validate-recipe.js +104 -0
- package/dist/recipes/actor-worker.json +35 -0
- package/dist/recipes/coordinator-locker.json +45 -0
- package/dist/recipes/lens-swarm.json +66 -0
- package/dist/recipes/locker.json +45 -0
- package/dist/recipes/music-player.json +38 -0
- package/dist/recipes/pipeline-architect-coordinator.json +95 -0
- package/dist/recipes/pipeline-artifact-bundle.json +100 -0
- package/dist/recipes/pipeline-artifact-report.json +58 -0
- package/dist/recipes/pipeline-artifact-write.json +72 -0
- package/dist/recipes/pipeline-async-run-ops.json +70 -0
- package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
- package/dist/recipes/pipeline-development-tasking.json +81 -0
- package/dist/recipes/pipeline-docs-maintenance.json +80 -0
- package/dist/recipes/pipeline-media-library.json +59 -0
- package/dist/recipes/pipeline-quorum-review.json +79 -0
- package/dist/recipes/pipeline-release-readiness.json +110 -0
- package/dist/recipes/pipeline-release-summary.json +88 -0
- package/dist/recipes/pipeline-repo-health.json +89 -0
- package/dist/recipes/pipeline-research-synthesis.json +94 -0
- package/dist/recipes/pipeline-review-readiness.json +54 -0
- package/dist/recipes/pipeline-room-swarm.json +50 -0
- package/dist/recipes/subagent-artifact.json +32 -0
- package/dist/recipes/subagent-checkpoint.json +33 -0
- package/dist/recipes/subagent-conflict-report.json +32 -0
- package/dist/recipes/subagent-contradiction-map.json +33 -0
- package/dist/recipes/subagent-critic.json +35 -0
- package/dist/recipes/subagent-evidence-map.json +33 -0
- package/dist/recipes/subagent-followup.json +33 -0
- package/dist/recipes/subagent-judge.json +33 -0
- package/dist/recipes/subagent-merge.json +33 -0
- package/dist/recipes/subagent-message.json +34 -0
- package/dist/recipes/subagent-normalize.json +31 -0
- package/dist/recipes/subagent-plan.json +33 -0
- package/dist/recipes/subagent-prompt.json +28 -0
- package/dist/recipes/subagent-quorum.json +43 -0
- package/dist/recipes/subagent-review-coordinator.json +114 -0
- package/dist/recipes/subagent-review.json +37 -0
- package/dist/recipes/subagent-task-card.json +35 -0
- package/dist/recipes/subagent-tools.json +27 -0
- package/dist/recipes/subagent-verify.json +34 -0
- package/dist/recipes/subagents-prompts.json +51 -0
- package/dist/recipes/utility-actor-message.json +23 -0
- package/dist/recipes/utility-artifact-manifest.json +16 -0
- package/dist/recipes/utility-artifact-write.json +16 -0
- package/dist/recipes/utility-changelog-head.json +11 -0
- package/dist/recipes/utility-changelog-section.json +13 -0
- package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
- package/dist/recipes/utility-git-log.json +11 -0
- package/dist/recipes/utility-git-status.json +9 -0
- package/dist/recipes/utility-jsonl-tail.json +10 -0
- package/dist/recipes/utility-markdown-index.json +14 -0
- package/dist/recipes/utility-package-summary.json +11 -0
- package/dist/recipes/utility-playlist-build.json +17 -0
- package/dist/recipes/utility-playlist-scan.json +11 -0
- package/dist/recipes/utility-run-ops-snapshot.json +17 -0
- package/dist/recipes/utility-run-state-files.json +13 -0
- package/dist/recipes/utility-run-summary.json +11 -0
- package/dist/recipes/utility-skill-summary.json +13 -0
- package/dist/recipes/utility-validate-recipe.json +13 -0
- package/dist/recipes/utility-validation-wrapper.json +13 -0
- package/dist/scripts/actor-worker.mjs +31 -0
- package/dist/scripts/async-runner.mjs +31 -0
- package/dist/scripts/build-dist.mjs +25 -0
- package/dist/scripts/conformance.mjs +33 -0
- package/dist/scripts/coordinator.mjs +31 -0
- package/dist/scripts/locker.mjs +33 -0
- package/dist/scripts/music-player.mjs +964 -0
- package/dist/scripts/recipe-utils.mjs +31 -0
- package/dist/scripts/validate-recipe.mjs +34 -0
- package/dist/skills/actors/SKILL.md +375 -0
- package/dist/skills/swarm/SKILL.md +467 -0
- package/dist/skills/swarm/references/development-swarm.md +596 -0
- package/docs/actor-messages.md +2 -2
- package/docs/async-runs.md +13 -1
- package/docs/template-recipes.md +2 -2
- package/docs/tool-registry.md +0 -1
- package/fixtures/protocol/actor-message-branch.json +13 -0
- package/fixtures/protocol/artifact-manifest.json +9 -0
- package/fixtures/protocol/mailbox-contract.json +15 -0
- package/fixtures/protocol/recipe-summary.json +16 -0
- package/fixtures/protocol/room-message.json +11 -0
- package/fixtures/protocol/room-roster.json +11 -0
- package/fixtures/protocol/run-inbox-message.json +9 -0
- package/fixtures/protocol/run-outbox-event.json +9 -0
- package/fixtures/protocol/run-state.json +10 -0
- package/index.ts +21 -0
- package/lib/actor-inspector-tui.ts +138 -59
- package/lib/actor-rooms.ts +241 -60
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +302 -53
- package/lib/build-dist.ts +30 -0
- package/lib/command-templates.ts +5 -5
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +649 -0
- package/lib/limits.ts +12 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +34 -23
- package/lib/output.ts +4 -6
- package/lib/prompts.ts +1 -1
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +57 -6
- package/lib/recipe-usage.ts +31 -4
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/runtime.ts +31 -7
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +297 -58
- package/lib/validate-recipe.ts +110 -0
- package/package.json +11 -2
- package/recipes/actor-worker.json +35 -0
- package/recipes/pipeline-quorum-review.json +12 -7
- package/scripts/actor-worker.mjs +31 -0
- package/scripts/async-runner.mjs +11 -195
- package/scripts/build-dist.mjs +25 -0
- package/scripts/conformance.mjs +33 -0
- package/scripts/coordinator.mjs +19 -616
- package/scripts/locker.mjs +23 -322
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +20 -467
- package/scripts/validate-recipe.mjs +23 -113
- package/skills/actors/SKILL.md +7 -4
- package/skills/swarm/SKILL.md +3 -1
package/lib/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"
|
|
@@ -144,7 +145,7 @@ function getUpdatedAt(status: Record<string, unknown>): string | undefined {
|
|
|
144
145
|
: undefined;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
function
|
|
148
|
+
function scanRunStateDirs(
|
|
148
149
|
stateRoot: string,
|
|
149
150
|
depth = 0,
|
|
150
151
|
seen = new Set<string>(),
|
|
@@ -163,7 +164,7 @@ function listRunStateDirs(
|
|
|
163
164
|
const child = join(stateRoot, entry.name);
|
|
164
165
|
if (existsSync(join(child, "run.json"))) result.push(child);
|
|
165
166
|
if (depth + 1 < RUN_STATE_DISCOVERY_MAX_DEPTH)
|
|
166
|
-
result.push(...
|
|
167
|
+
result.push(...scanRunStateDirs(child, depth + 1, seen));
|
|
167
168
|
}
|
|
168
169
|
return result;
|
|
169
170
|
}
|
|
@@ -226,7 +227,10 @@ export function summarizeRuns(
|
|
|
226
227
|
total: 0,
|
|
227
228
|
};
|
|
228
229
|
}
|
|
229
|
-
const runs =
|
|
230
|
+
const runs = (
|
|
231
|
+
AsyncRuns.readRunStateIndex(stateRoot)?.map((entry) => entry.state_dir) ??
|
|
232
|
+
scanRunStateDirs(stateRoot)
|
|
233
|
+
)
|
|
230
234
|
.map((stateDir) => observeRun(stateDir))
|
|
231
235
|
.filter((run): run is RunObservation => Boolean(run))
|
|
232
236
|
.filter((run) => ownerId === undefined || run.ownerId === ownerId)
|
|
@@ -331,7 +335,10 @@ function summarizeRunsWithoutSubagents(
|
|
|
331
335
|
runs: [],
|
|
332
336
|
total: 0,
|
|
333
337
|
};
|
|
334
|
-
const runs =
|
|
338
|
+
const runs = (
|
|
339
|
+
AsyncRuns.readRunStateIndex(stateRoot)?.map((entry) => entry.state_dir) ??
|
|
340
|
+
scanRunStateDirs(stateRoot)
|
|
341
|
+
)
|
|
335
342
|
.map((stateDir) => observeRun(stateDir))
|
|
336
343
|
.filter((run): run is RunObservation => Boolean(run))
|
|
337
344
|
.filter((run) => ownerId === undefined || run.ownerId === ownerId)
|
|
@@ -599,15 +606,12 @@ function normalizeOutboxLevel(value: unknown): RunOutboxLevel {
|
|
|
599
606
|
return value === "warning" || value === "error" ? value : "info";
|
|
600
607
|
}
|
|
601
608
|
|
|
602
|
-
function
|
|
603
|
-
|
|
609
|
+
function parseOutboxRecord(
|
|
610
|
+
raw: Record<string, unknown>,
|
|
604
611
|
run: RunObservation,
|
|
605
612
|
index: number,
|
|
606
613
|
): RunOutboxEvent | undefined {
|
|
607
614
|
if (!run.stateDir) return undefined;
|
|
608
|
-
try {
|
|
609
|
-
const raw = JSON.parse(line) as Record<string, unknown>;
|
|
610
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
611
615
|
const event =
|
|
612
616
|
typeof raw.event === "string" && raw.event.trim()
|
|
613
617
|
? raw.event.trim()
|
|
@@ -641,17 +645,13 @@ function parseOutboxLine(
|
|
|
641
645
|
summary,
|
|
642
646
|
ts,
|
|
643
647
|
};
|
|
644
|
-
} catch {
|
|
645
|
-
return undefined;
|
|
646
|
-
}
|
|
647
648
|
}
|
|
648
649
|
|
|
649
|
-
function
|
|
650
|
+
function readOutboxRecords(run: RunObservation): Record<string, unknown>[] {
|
|
650
651
|
if (!run.stateDir) return [];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
return content ? content.split("\n") : [];
|
|
652
|
+
return readJsonlFileResilient<Record<string, unknown>>(
|
|
653
|
+
join(run.stateDir, "outbox.jsonl"),
|
|
654
|
+
).records;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
export function pruneRunObservationState(
|
|
@@ -659,6 +659,7 @@ export function pruneRunObservationState(
|
|
|
659
659
|
previousLineCounts: Map<string, number>,
|
|
660
660
|
summary: RunSummary,
|
|
661
661
|
terminalRuns: Iterable<string> = [],
|
|
662
|
+
seenEventIds: Map<string, Set<string>> = new Map(),
|
|
662
663
|
): void {
|
|
663
664
|
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
664
665
|
const terminalRunSet = new Set(terminalRuns);
|
|
@@ -679,23 +680,33 @@ export function pruneRunObservationState(
|
|
|
679
680
|
previousLineCounts.delete(key);
|
|
680
681
|
}
|
|
681
682
|
}
|
|
683
|
+
for (const key of seenEventIds.keys()) {
|
|
684
|
+
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
|
|
685
|
+
seenEventIds.delete(key);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
682
688
|
}
|
|
683
689
|
|
|
684
690
|
export function detectRunOutboxEvents(
|
|
685
691
|
previousLineCounts: Map<string, number>,
|
|
686
692
|
summary: RunSummary,
|
|
693
|
+
seenEventIds: Map<string, Set<string>> = new Map(),
|
|
687
694
|
): RunOutboxEvent[] {
|
|
688
695
|
const events: RunOutboxEvent[] = [];
|
|
689
696
|
for (const run of summary.runs) {
|
|
690
697
|
const key = run.stateDir ?? run.run;
|
|
691
|
-
const
|
|
698
|
+
const records = readOutboxRecords(run);
|
|
692
699
|
const previousCount = previousLineCounts.get(key) ?? 0;
|
|
693
|
-
const start = Math.min(previousCount,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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);
|
|
697
707
|
}
|
|
698
|
-
previousLineCounts.set(key,
|
|
708
|
+
previousLineCounts.set(key, records.length);
|
|
709
|
+
seenEventIds.set(key, seen);
|
|
699
710
|
}
|
|
700
711
|
return events;
|
|
701
712
|
}
|
package/lib/output.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { sanitizeFilePart } from "./identity.ts";
|
|
11
|
+
import * as Limits from "./limits.ts";
|
|
11
12
|
import * as Paths from "./paths.ts";
|
|
12
13
|
|
|
13
14
|
export interface FormattedOutput {
|
|
@@ -16,9 +17,6 @@ export interface FormattedOutput {
|
|
|
16
17
|
fullOutputPath?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
20
|
-
const MAX_OUTPUT_LINES = 2_000;
|
|
21
|
-
|
|
22
20
|
export function writeFullOutput(
|
|
23
21
|
toolName: string,
|
|
24
22
|
stream: string,
|
|
@@ -70,10 +68,10 @@ export function truncateTailContent(content: string): {
|
|
|
70
68
|
const lines = content.split("\n");
|
|
71
69
|
const totalLines = lines.length;
|
|
72
70
|
let output =
|
|
73
|
-
totalLines >
|
|
74
|
-
? lines.slice(-
|
|
71
|
+
totalLines > Limits.TOOL_OUTPUT_MAX_LINES
|
|
72
|
+
? lines.slice(-Limits.TOOL_OUTPUT_MAX_LINES).join("\n")
|
|
75
73
|
: content;
|
|
76
|
-
output = trimToTailBytes(output,
|
|
74
|
+
output = trimToTailBytes(output, Limits.TOOL_OUTPUT_MAX_BYTES);
|
|
77
75
|
const outputBytes = byteLength(output);
|
|
78
76
|
const outputLines = output ? output.split("\n").length : 0;
|
|
79
77
|
return {
|
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-discovery.ts
CHANGED
|
@@ -493,6 +493,7 @@ export function summarizeDiscovery(
|
|
|
493
493
|
disabled: entry.disabled,
|
|
494
494
|
invalid: entry.invalid,
|
|
495
495
|
shadows: entry.shadows,
|
|
496
|
+
...(entry.config?.imports ? { imports: entry.config.imports } : {}),
|
|
496
497
|
...(recipeUsage(entry.config)
|
|
497
498
|
? { usage: recipeUsage(entry.config) }
|
|
498
499
|
: {}),
|