@llblab/pi-actors 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +22 -11
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +22 -1
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  21. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  22. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
  23. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
  24. package/dist/lib/registry.d.ts +1 -1
  25. package/dist/lib/registry.js +22 -22
  26. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  27. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  28. package/dist/lib/runs-artifacts.d.ts +21 -0
  29. package/dist/lib/runs-artifacts.js +68 -0
  30. package/dist/lib/runs-control.d.ts +13 -0
  31. package/dist/lib/runs-control.js +59 -0
  32. package/dist/lib/runs-identity.d.ts +5 -0
  33. package/dist/lib/runs-identity.js +11 -0
  34. package/dist/lib/runs-index.d.ts +18 -0
  35. package/dist/lib/runs-index.js +119 -0
  36. package/dist/lib/runs-mailbox.d.ts +25 -0
  37. package/dist/lib/runs-mailbox.js +146 -0
  38. package/dist/lib/runs-messages.d.ts +15 -0
  39. package/dist/lib/runs-messages.js +179 -0
  40. package/dist/lib/runs-outbox.d.ts +41 -0
  41. package/dist/lib/runs-outbox.js +87 -0
  42. package/dist/lib/runs-process.d.ts +7 -0
  43. package/dist/lib/runs-process.js +36 -0
  44. package/dist/lib/runs-retention.d.ts +8 -0
  45. package/dist/lib/runs-retention.js +56 -0
  46. package/dist/lib/runs-start.d.ts +9 -0
  47. package/dist/lib/runs-start.js +67 -0
  48. package/dist/lib/runs-status.d.ts +8 -0
  49. package/dist/lib/runs-status.js +60 -0
  50. package/dist/lib/runtime.d.ts +2 -2
  51. package/dist/lib/runtime.js +5 -5
  52. package/dist/lib/schema.d.ts +9 -0
  53. package/dist/lib/schema.js +40 -0
  54. package/dist/lib/tools-access.d.ts +19 -0
  55. package/dist/lib/tools-access.js +44 -0
  56. package/dist/lib/tools-inspect.d.ts +11 -0
  57. package/dist/lib/tools-inspect.js +534 -0
  58. package/dist/lib/tools-local.d.ts +14 -0
  59. package/dist/lib/tools-local.js +156 -0
  60. package/dist/lib/tools-mailbox.d.ts +8 -0
  61. package/dist/lib/tools-mailbox.js +48 -0
  62. package/dist/lib/tools-message.d.ts +9 -0
  63. package/dist/lib/tools-message.js +346 -0
  64. package/dist/lib/tools-register.d.ts +18 -0
  65. package/dist/lib/tools-register.js +40 -0
  66. package/dist/lib/tools-response.d.ts +21 -0
  67. package/dist/lib/tools-response.js +219 -0
  68. package/dist/lib/tools-spawn.d.ts +12 -0
  69. package/dist/lib/tools-spawn.js +159 -0
  70. package/dist/lib/tools.d.ts +8 -39
  71. package/dist/lib/tools.js +11 -1490
  72. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  73. package/dist/scripts/actor-worker.mjs +186 -7
  74. package/dist/scripts/async-runner.mjs +171 -8
  75. package/dist/scripts/build-dist.mjs +1 -1
  76. package/dist/scripts/conformance.mjs +3 -3
  77. package/dist/scripts/coordinator.mjs +733 -11
  78. package/dist/scripts/locker.mjs +345 -16
  79. package/dist/scripts/recipe-utils.mjs +478 -14
  80. package/dist/scripts/validate-recipe.mjs +110 -10
  81. package/dist/skills/actors/SKILL.md +1 -1
  82. package/dist/skills/swarm/SKILL.md +1 -1
  83. package/docs/recipe-library.md +1 -1
  84. package/docs/template-recipes.md +1 -1
  85. package/index.ts +11 -18
  86. package/lib/async-runs.ts +124 -1016
  87. package/lib/command-templates.ts +3 -3
  88. package/lib/config.ts +7 -7
  89. package/lib/{output.ts → execution-output.ts} +3 -3
  90. package/lib/execution.ts +5 -1
  91. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  92. package/lib/mailbox-loop.ts +3 -3
  93. package/lib/{actor-messages.ts → messages.ts} +3 -3
  94. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  95. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
  96. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  97. package/lib/registry.ts +37 -24
  98. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  99. package/lib/runs-artifacts.ts +96 -0
  100. package/lib/runs-control.ts +85 -0
  101. package/lib/runs-identity.ts +14 -0
  102. package/lib/runs-index.ts +156 -0
  103. package/lib/runs-mailbox.ts +208 -0
  104. package/lib/runs-messages.ts +252 -0
  105. package/lib/runs-outbox.ts +144 -0
  106. package/lib/runs-process.ts +51 -0
  107. package/lib/runs-retention.ts +77 -0
  108. package/lib/runs-start.ts +98 -0
  109. package/lib/runs-status.ts +78 -0
  110. package/lib/runtime.ts +6 -6
  111. package/lib/schema.ts +55 -0
  112. package/lib/tools-access.ts +68 -0
  113. package/lib/tools-inspect.ts +735 -0
  114. package/lib/tools-local.ts +234 -0
  115. package/lib/tools-mailbox.ts +56 -0
  116. package/lib/tools-message.ts +483 -0
  117. package/lib/tools-register.ts +64 -0
  118. package/lib/tools-response.ts +267 -0
  119. package/lib/tools-spawn.ts +240 -0
  120. package/lib/tools.ts +20 -2040
  121. package/package.json +1 -1
  122. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  123. package/scripts/actor-worker.mjs +186 -7
  124. package/scripts/async-runner.mjs +171 -8
  125. package/scripts/build-dist.mjs +1 -1
  126. package/scripts/conformance.mjs +3 -3
  127. package/scripts/coordinator.mjs +733 -11
  128. package/scripts/locker.mjs +345 -16
  129. package/scripts/recipe-utils.mjs +478 -14
  130. package/scripts/validate-recipe.mjs +110 -10
  131. package/skills/actors/SKILL.md +1 -1
  132. package/skills/swarm/SKILL.md +1 -1
  133. package/dist/lib/actor-worker.d.ts +0 -16
  134. package/dist/lib/actor-worker.js +0 -134
  135. package/dist/lib/async-runner.d.ts +0 -5
  136. package/dist/lib/async-runner.js +0 -134
  137. package/dist/lib/coordinator.d.ts +0 -5
  138. package/dist/lib/coordinator.js +0 -660
  139. package/dist/lib/locker.d.ts +0 -5
  140. package/dist/lib/locker.js +0 -310
  141. package/dist/lib/recipe-utils.d.ts +0 -5
  142. package/dist/lib/recipe-utils.js +0 -385
  143. package/dist/lib/validate-recipe.d.ts +0 -6
  144. package/dist/lib/validate-recipe.js +0 -104
  145. package/lib/actor-worker.ts +0 -174
  146. package/lib/async-runner.ts +0 -173
  147. package/lib/coordinator.ts +0 -742
  148. package/lib/locker.ts +0 -340
  149. package/lib/recipe-utils.ts +0 -486
  150. package/lib/validate-recipe.ts +0 -110
  151. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  152. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  153. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  154. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  155. /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
@@ -1,29 +1,358 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Local coordination locker service shim.
4
+ * Local coordination locker service.
5
5
  *
6
- * Runtime logic lives in lib/locker.ts and is compiled to dist/lib/locker.js
7
- * for installed JS-only packages.
6
+ * Owns queue, lease-lock, journal, snapshot, and platform control endpoint
7
+ * behavior for the locker/coordinator-locker recipe surfaces. Kept standalone
8
+ * because no non-script TypeScript domain consumes this implementation.
8
9
  */
9
10
 
10
- import { existsSync } from "node:fs";
11
- import { basename, dirname, join } from "node:path";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
11
+ import { spawnSync } from "node:child_process";
12
+ import { createHash } from "node:crypto";
13
+ import {
14
+ appendFileSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ readFileSync,
18
+ rmSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { createServer } from "node:net";
22
+ import { dirname, join, resolve } from "node:path";
23
+ import readline from "node:readline";
13
24
 
14
- function packageRoot() {
15
- const scriptDir = dirname(fileURLToPath(import.meta.url));
16
- const parent = dirname(scriptDir);
17
- return basename(parent) === "dist" ? dirname(parent) : parent;
25
+ function parseArgs(argv) {
26
+ const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const arg = argv[index];
29
+ if (arg === "serve" || arg === "snapshot") args.mode = arg;
30
+ else if (arg === "--state-dir") args.stateDir = argv[++index] ?? "";
31
+ else if (arg === "--lease-ms")
32
+ args.leaseMs = Number(argv[++index] ?? args.leaseMs);
33
+ else if (arg === "--lines")
34
+ args.lines = Number(argv[++index] ?? args.lines);
35
+ }
36
+ if (!args.stateDir) throw new Error("--state-dir is required");
37
+ if (!Number.isFinite(args.leaseMs) || args.leaseMs <= 0)
38
+ args.leaseMs = 600000;
39
+ if (!Number.isFinite(args.lines) || args.lines <= 0) args.lines = 20;
40
+ return args;
18
41
  }
19
42
 
20
- function mainModulePath() {
21
- const root = packageRoot();
22
- const compiled = join(root, "dist", "lib", "locker.js");
23
- return existsSync(compiled) ? compiled : join(root, "lib", "locker.ts");
24
- }
43
+ async function runLocker(argv = process.argv.slice(2)) {
44
+ const { mode, stateDir, leaseMs, lines } = parseArgs(argv);
45
+ const queuePath = join(stateDir, "queue.json");
46
+ const locksPath = join(stateDir, "locks.json");
47
+ const journalPath = join(stateDir, "journal.jsonl");
48
+ const outboxPath = join(stateDir, "outbox.jsonl");
49
+ const controlPath = join(stateDir, "control.fifo");
50
+ mkdirSync(stateDir, { recursive: true });
51
+
52
+ function readJson(path, fallback) {
53
+ if (!existsSync(path)) return fallback;
54
+ try {
55
+ return JSON.parse(readFileSync(path, "utf8"));
56
+ } catch {
57
+ return fallback;
58
+ }
59
+ }
60
+
61
+ function writeJson(path, value) {
62
+ mkdirSync(dirname(path), { recursive: true });
63
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
64
+ }
65
+
66
+ function getControlEndpoint() {
67
+ if (process.platform !== "win32") return { path: controlPath, type: "fifo" };
68
+ const hash = createHash("sha256")
69
+ .update(resolve(stateDir))
70
+ .digest("hex")
71
+ .slice(0, 20);
72
+ return { path: `\\\\.\\pipe\\pi-actors-locker-${hash}`, type: "named-pipe" };
73
+ }
74
+
75
+ function writeControlEndpoint(endpoint) {
76
+ writeJson(join(stateDir, "control.json"), endpoint);
77
+ const runPath = join(stateDir, "run.json");
78
+ const run = readJson(runPath, undefined);
79
+ if (run && typeof run === "object")
80
+ writeJson(runPath, { ...run, control: endpoint });
81
+ }
82
+
83
+ function journal(event, data = {}) {
84
+ appendFileSync(
85
+ journalPath,
86
+ `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
87
+ );
88
+ }
89
+
90
+ function outbox(type, summary, body = {}, level = "info") {
91
+ appendFileSync(
92
+ outboxPath,
93
+ `${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`,
94
+ );
95
+ }
96
+
97
+ function now() {
98
+ return Date.now();
99
+ }
100
+
101
+ function cleanExpiredLocks(locks) {
102
+ const current = now();
103
+ const kept = {};
104
+ for (const [key, lock] of Object.entries(locks)) {
105
+ if (Number(lock.expiresAt) > current) kept[key] = lock;
106
+ else journal("lock.expired", { resource: key, owner: lock.owner });
107
+ }
108
+ return kept;
109
+ }
110
+
111
+ function normalizeMessage(line) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed) return undefined;
114
+ if (["stop", "quit", "exit", "cancel"].includes(trimmed.toLowerCase())) {
115
+ return { type: "control.stop", body: {} };
116
+ }
117
+ try {
118
+ return JSON.parse(trimmed);
119
+ } catch {
120
+ return { type: "lock.enqueue", body: { task: trimmed } };
121
+ }
122
+ }
25
123
 
26
- const { runLocker } = await import(pathToFileURL(mainModulePath()).href);
124
+ function tailJournal(count) {
125
+ if (!existsSync(journalPath)) return [];
126
+ return readFileSync(journalPath, "utf8")
127
+ .trimEnd()
128
+ .split("\n")
129
+ .filter(Boolean)
130
+ .slice(-count)
131
+ .map((line) => {
132
+ try {
133
+ return JSON.parse(line);
134
+ } catch {
135
+ return { raw: line };
136
+ }
137
+ });
138
+ }
139
+
140
+ function printSnapshot() {
141
+ const locks = cleanExpiredLocks(readJson(locksPath, {}));
142
+ writeJson(locksPath, locks);
143
+ const queue = readJson(queuePath, { items: [] });
144
+ console.log(
145
+ JSON.stringify(
146
+ {
147
+ queueDepth: Array.isArray(queue.items) ? queue.items.length : 0,
148
+ queue,
149
+ locks,
150
+ journal: tailJournal(lines),
151
+ },
152
+ null,
153
+ 2,
154
+ ),
155
+ );
156
+ }
157
+
158
+ function nextTask(queue, locks) {
159
+ const items = Array.isArray(queue.items) ? queue.items : [];
160
+ const index = items.findIndex((item) => {
161
+ const resources = Array.isArray(item.resources) ? item.resources : [];
162
+ return resources.every((resource) => !locks[resource]);
163
+ });
164
+ if (index < 0) return undefined;
165
+ return items.splice(index, 1)[0];
166
+ }
167
+
168
+ function handle(message) {
169
+ const type = message.type || message.event || "lock.enqueue";
170
+ const body =
171
+ message.body && typeof message.body === "object" ? message.body : message;
172
+ let queue = readJson(queuePath, { items: [] });
173
+ let locks = cleanExpiredLocks(readJson(locksPath, {}));
174
+ if (type === "control.stop" || type === "control.cancel") {
175
+ writeJson(locksPath, locks);
176
+ journal("control.stop", {});
177
+ outbox("lock.stopped", "Locker stopped", {
178
+ queueDepth: queue.items?.length ?? 0,
179
+ });
180
+ process.exit(0);
181
+ }
182
+ if (type === "lock.enqueue" || type === "coord.enqueue") {
183
+ const item = {
184
+ id: body.id || `task-${Date.now()}`,
185
+ task: body.task ?? body,
186
+ resources: body.resources ?? [],
187
+ enqueuedAt: new Date().toISOString(),
188
+ };
189
+ queue.items = [...(queue.items ?? []), item];
190
+ writeJson(queuePath, queue);
191
+ writeJson(locksPath, locks);
192
+ journal("lock.enqueued", { id: item.id, resources: item.resources });
193
+ outbox("lock.enqueued", `Queued task ${item.id}`, {
194
+ id: item.id,
195
+ queueDepth: queue.items.length,
196
+ });
197
+ return;
198
+ }
199
+ if (type === "lock.claim" || type === "coord.claim") {
200
+ const owner = body.owner || message.from || "worker";
201
+ const item = nextTask(queue, locks);
202
+ if (!item) {
203
+ writeJson(queuePath, queue);
204
+ writeJson(locksPath, locks);
205
+ outbox("lock.empty", "No claimable task", {
206
+ owner,
207
+ queueDepth: queue.items?.length ?? 0,
208
+ });
209
+ return;
210
+ }
211
+ for (const resource of item.resources ?? [])
212
+ locks[resource] = { owner, task: item.id, expiresAt: now() + leaseMs };
213
+ writeJson(queuePath, queue);
214
+ writeJson(locksPath, locks);
215
+ journal("lock.assigned", {
216
+ id: item.id,
217
+ owner,
218
+ resources: item.resources,
219
+ });
220
+ outbox("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
221
+ return;
222
+ }
223
+ if (type === "lock.acquire") {
224
+ const resource = body.resource;
225
+ const owner = body.owner || message.from || "worker";
226
+ if (!resource) throw new Error("lock.acquire body.resource is required");
227
+ if (locks[resource])
228
+ outbox(
229
+ "lock.denied",
230
+ `Lock denied ${resource}`,
231
+ { resource, owner, current: locks[resource] },
232
+ "warning",
233
+ );
234
+ else {
235
+ locks[resource] = { owner, expiresAt: now() + leaseMs };
236
+ outbox("lock.granted", `Lock granted ${resource}`, { resource, owner });
237
+ }
238
+ writeJson(locksPath, locks);
239
+ return;
240
+ }
241
+ if (type === "lock.renew") {
242
+ const resource = body.resource;
243
+ const owner = body.owner || message.from || "worker";
244
+ if (!resource) throw new Error("lock.renew body.resource is required");
245
+ const current = locks[resource];
246
+ if (!current) {
247
+ outbox(
248
+ "lock.denied",
249
+ `Lock renew denied ${resource}`,
250
+ { resource, owner, reason: "missing" },
251
+ "warning",
252
+ );
253
+ } else if (current.owner !== owner) {
254
+ outbox(
255
+ "lock.denied",
256
+ `Lock renew denied ${resource}`,
257
+ { resource, owner, current },
258
+ "warning",
259
+ );
260
+ } else {
261
+ locks[resource] = { ...current, expiresAt: now() + leaseMs };
262
+ outbox("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
263
+ }
264
+ writeJson(locksPath, locks);
265
+ return;
266
+ }
267
+ if (type === "lock.release") {
268
+ const resource = body.resource;
269
+ if (resource) delete locks[resource];
270
+ writeJson(locksPath, locks);
271
+ outbox("lock.released", `Lock released ${resource}`, { resource });
272
+ return;
273
+ }
274
+ if (
275
+ type === "lock.complete" ||
276
+ type === "lock.fail" ||
277
+ type === "coord.complete" ||
278
+ type === "coord.fail"
279
+ ) {
280
+ const eventType = type.startsWith("coord.")
281
+ ? type.replace("coord.", "lock.")
282
+ : type;
283
+ journal(eventType, body);
284
+ outbox(
285
+ eventType,
286
+ `${eventType} ${body.id ?? ""}`.trim(),
287
+ body,
288
+ eventType === "lock.fail" ? "error" : "info",
289
+ );
290
+ writeJson(locksPath, locks);
291
+ writeJson(queuePath, queue);
292
+ return;
293
+ }
294
+ journal("lock.unknown", { type, body });
295
+ outbox("lock.unknown", `Unknown message ${type}`, { type, body }, "warning");
296
+ }
297
+
298
+ if (mode === "snapshot") {
299
+ printSnapshot();
300
+ process.exit(0);
301
+ }
302
+
303
+ function handleLine(line) {
304
+ const message = normalizeMessage(line);
305
+ if (!message) return;
306
+ try {
307
+ handle(message);
308
+ } catch (error) {
309
+ const text = error instanceof Error ? error.message : String(error);
310
+ journal("lock.error", { error: text });
311
+ outbox("lock.error", text, { error: text }, "error");
312
+ }
313
+ }
314
+
315
+ async function serveFifo(endpoint) {
316
+ if (!existsSync(endpoint.path)) {
317
+ const result = spawnSync("mkfifo", [endpoint.path]);
318
+ if (result.status !== 0)
319
+ throw new Error(`mkfifo failed: ${result.stderr?.toString?.() ?? ""}`);
320
+ }
321
+ writeControlEndpoint(endpoint);
322
+ while (true) {
323
+ const stream = await import("node:fs").then((fs) =>
324
+ fs.createReadStream(endpoint.path, { encoding: "utf8" }),
325
+ );
326
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
327
+ for await (const line of rl) handleLine(line);
328
+ }
329
+ }
330
+
331
+ async function serveNamedPipe(endpoint) {
332
+ rmSync(endpoint.path, { force: true });
333
+ const server = createServer((socket) => {
334
+ const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
335
+ rl.on("line", handleLine);
336
+ });
337
+ await new Promise((resolveReady, rejectReady) => {
338
+ server.once("error", rejectReady);
339
+ server.listen(endpoint.path, () => {
340
+ server.off("error", rejectReady);
341
+ writeControlEndpoint(endpoint);
342
+ resolveReady();
343
+ });
344
+ });
345
+ await new Promise(() => {});
346
+ }
347
+
348
+ const endpoint = getControlEndpoint();
349
+ writeJson(queuePath, readJson(queuePath, { items: [] }));
350
+ writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
351
+ journal("lock.started", { leaseMs, control: endpoint.type });
352
+ outbox("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
353
+ if (endpoint.type === "named-pipe") await serveNamedPipe(endpoint);
354
+ else await serveFifo(endpoint);
355
+ }
27
356
 
28
357
  try {
29
358
  await runLocker(process.argv.slice(2));