@llblab/pi-actors 0.21.0 → 0.22.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/BACKLOG.md +39 -0
- package/CHANGELOG.md +18 -1
- package/dist/lib/actor-rooms.js +18 -0
- package/dist/lib/async-runs.d.ts +19 -1
- package/dist/lib/async-runs.js +201 -10
- package/dist/lib/runtime-notifier.d.ts +48 -0
- package/dist/lib/runtime-notifier.js +137 -0
- package/dist/lib/tools.js +13 -4
- package/docs/actor-messages.md +5 -3
- package/docs/async-runs.md +4 -2
- package/docs/recipe-library.md +11 -4
- package/lib/actor-rooms.ts +23 -0
- package/lib/async-runs.ts +261 -15
- package/lib/runtime-notifier.ts +207 -0
- package/lib/tools.ts +26 -5
- package/package.json +2 -3
- package/recipes/music-player.json +1 -1
- package/scripts/coordinator.mjs +227 -114
- package/scripts/locker.mjs +30 -9
- package/scripts/music-player.mjs +401 -94
- package/skills/actors/SKILL.md +9 -1
- package/skills/swarm/SKILL.md +1 -1
- package/index.js +0 -19
package/scripts/coordinator.mjs
CHANGED
|
@@ -41,7 +41,8 @@ function parseRoles(raw) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
async function loadRoles(raw, path) {
|
|
44
|
-
if (path.trim())
|
|
44
|
+
if (path.trim())
|
|
45
|
+
return normalizeRoles(JSON.parse(await readFile(path, "utf8")));
|
|
45
46
|
return parseRoles(raw);
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -67,7 +68,8 @@ const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
|
67
68
|
async function waitForPath(path, timeoutMs = 5000) {
|
|
68
69
|
const started = Date.now();
|
|
69
70
|
while (!existsSync(path)) {
|
|
70
|
-
if (Date.now() - started > timeoutMs)
|
|
71
|
+
if (Date.now() - started > timeoutMs)
|
|
72
|
+
throw new Error(`timed out waiting for ${path}`);
|
|
71
73
|
await sleep(50);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -79,7 +81,11 @@ async function acquireStateLock(parentDir, name, label) {
|
|
|
79
81
|
while (true) {
|
|
80
82
|
try {
|
|
81
83
|
await mkdir(lockDir);
|
|
82
|
-
await writeFile(
|
|
84
|
+
await writeFile(
|
|
85
|
+
`${lockDir}/owner.json`,
|
|
86
|
+
`${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`,
|
|
87
|
+
"utf8",
|
|
88
|
+
);
|
|
83
89
|
return async () => rm(lockDir, { recursive: true, force: true });
|
|
84
90
|
} catch (error) {
|
|
85
91
|
try {
|
|
@@ -100,7 +106,11 @@ async function acquireStateLock(parentDir, name, label) {
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
async function acquireBranchInboxLock(runId, branchName) {
|
|
103
|
-
return acquireStateLock(
|
|
109
|
+
return acquireStateLock(
|
|
110
|
+
`${runStateDir(runId)}/branches/${branchName}`,
|
|
111
|
+
".inbox.lock",
|
|
112
|
+
`Branch inbox ${branchName}`,
|
|
113
|
+
);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
async function readLockerControl(locker) {
|
|
@@ -126,10 +136,11 @@ function writeNamedPipeLine(path, line) {
|
|
|
126
136
|
|
|
127
137
|
async function writeLockerMessage(locker, message) {
|
|
128
138
|
if (!locker) return;
|
|
129
|
-
const control = locker.control ?? await readLockerControl(locker);
|
|
139
|
+
const control = locker.control ?? (await readLockerControl(locker));
|
|
130
140
|
locker.control = control;
|
|
131
141
|
const line = `${JSON.stringify(message)}\n`;
|
|
132
|
-
if (control.type === "named-pipe")
|
|
142
|
+
if (control.type === "named-pipe")
|
|
143
|
+
await writeNamedPipeLine(control.path, line);
|
|
133
144
|
else await writeFile(control.path, line, { flag: "a" });
|
|
134
145
|
await sleep(50);
|
|
135
146
|
}
|
|
@@ -138,23 +149,31 @@ async function startLocker(config) {
|
|
|
138
149
|
if (!config.locker) return undefined;
|
|
139
150
|
const lockerStateDir = `${runStateDir(config.runId)}/locker`;
|
|
140
151
|
await mkdir(lockerStateDir, { recursive: true });
|
|
141
|
-
const child = spawn(
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
const child = spawn(
|
|
153
|
+
new URL("./locker.mjs", import.meta.url).pathname,
|
|
154
|
+
[
|
|
155
|
+
"serve",
|
|
156
|
+
"--state-dir",
|
|
157
|
+
lockerStateDir,
|
|
158
|
+
"--lease-ms",
|
|
159
|
+
String(config.lockerLeaseMs),
|
|
160
|
+
],
|
|
161
|
+
{
|
|
162
|
+
env: { ...process.env, run_id: config.runId },
|
|
163
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
164
|
+
},
|
|
165
|
+
);
|
|
151
166
|
child.stdout.on("data", (chunk) => process.stdout.write(`[locker] ${chunk}`));
|
|
152
167
|
child.stderr.on("data", (chunk) => process.stderr.write(`[locker] ${chunk}`));
|
|
153
168
|
const locker = { child, stateDir: lockerStateDir };
|
|
154
169
|
locker.control = await readLockerControl(locker);
|
|
155
170
|
await writeLockerMessage(locker, {
|
|
156
171
|
type: "lock.enqueue",
|
|
157
|
-
body: {
|
|
172
|
+
body: {
|
|
173
|
+
id: "coordinator-artifact",
|
|
174
|
+
task: "Own final artifact synthesis",
|
|
175
|
+
resources: [config.artifactPath || "artifact"],
|
|
176
|
+
},
|
|
158
177
|
});
|
|
159
178
|
return locker;
|
|
160
179
|
}
|
|
@@ -172,7 +191,8 @@ async function stopLocker(locker) {
|
|
|
172
191
|
function runPi(prompt, model, thinking) {
|
|
173
192
|
return new Promise((resolve) => {
|
|
174
193
|
const args = [
|
|
175
|
-
"--tools",
|
|
194
|
+
"--tools",
|
|
195
|
+
"inspect,message",
|
|
176
196
|
"--no-context-files",
|
|
177
197
|
"--no-skills",
|
|
178
198
|
"--no-session",
|
|
@@ -188,10 +208,16 @@ function runPi(prompt, model, thinking) {
|
|
|
188
208
|
const child = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
189
209
|
let stdout = "";
|
|
190
210
|
let stderr = "";
|
|
191
|
-
child.stdout.on("data", (chunk) => {
|
|
192
|
-
|
|
211
|
+
child.stdout.on("data", (chunk) => {
|
|
212
|
+
stdout += chunk;
|
|
213
|
+
});
|
|
214
|
+
child.stderr.on("data", (chunk) => {
|
|
215
|
+
stderr += chunk;
|
|
216
|
+
});
|
|
193
217
|
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
194
|
-
child.on("error", (error) =>
|
|
218
|
+
child.on("error", (error) =>
|
|
219
|
+
resolve({ code: 1, stdout, stderr: String(error) }),
|
|
220
|
+
);
|
|
195
221
|
});
|
|
196
222
|
}
|
|
197
223
|
|
|
@@ -206,9 +232,15 @@ async function readRoomTranscript(config) {
|
|
|
206
232
|
.map((line) => {
|
|
207
233
|
try {
|
|
208
234
|
const message = JSON.parse(line);
|
|
209
|
-
const from = String(message.from || "unknown").replace(
|
|
235
|
+
const from = String(message.from || "unknown").replace(
|
|
236
|
+
/^branch:[^/]+\//,
|
|
237
|
+
"",
|
|
238
|
+
);
|
|
210
239
|
const type = String(message.type || "message");
|
|
211
|
-
const body =
|
|
240
|
+
const body =
|
|
241
|
+
typeof message.body === "string"
|
|
242
|
+
? message.body
|
|
243
|
+
: JSON.stringify(message.body ?? "");
|
|
212
244
|
return `${from} [${type}]: ${body}`;
|
|
213
245
|
} catch {
|
|
214
246
|
return line;
|
|
@@ -231,7 +263,13 @@ async function synthesize(config, locker) {
|
|
|
231
263
|
const transcript = await readRoomTranscript(config);
|
|
232
264
|
const prompt = `Synthesize this transcript into a concise Markdown artifact. Mission: ${config.mission}. Include: Title, Consensus, Roles, Protocol, Final Artifact Shape, Next Actions, Open Questions. Use only the transcript evidence below.\n\nTRANSCRIPT:\n${transcript.slice(-24000)}`;
|
|
233
265
|
const result = await runPi(prompt, config.model, config.thinking);
|
|
234
|
-
await writeFile(
|
|
266
|
+
await writeFile(
|
|
267
|
+
config.artifactPath,
|
|
268
|
+
result.stdout.trim()
|
|
269
|
+
? result.stdout
|
|
270
|
+
: "# Synthesis Artifact\n\nNo synthesis output.\n",
|
|
271
|
+
"utf8",
|
|
272
|
+
);
|
|
235
273
|
if (locker) {
|
|
236
274
|
await writeLockerMessage(locker, {
|
|
237
275
|
type: "lock.complete",
|
|
@@ -248,11 +286,18 @@ async function synthesize(config, locker) {
|
|
|
248
286
|
async function readInboxLines(inboxPath) {
|
|
249
287
|
if (!existsSync(inboxPath)) return [];
|
|
250
288
|
const content = await readFile(inboxPath, "utf8");
|
|
251
|
-
return content
|
|
289
|
+
return content
|
|
290
|
+
.split("\n")
|
|
291
|
+
.filter(Boolean)
|
|
292
|
+
.map((line) => JSON.parse(line));
|
|
252
293
|
}
|
|
253
294
|
|
|
254
295
|
async function writeInboxMessages(inboxPath, messages) {
|
|
255
|
-
await writeFile(
|
|
296
|
+
await writeFile(
|
|
297
|
+
inboxPath,
|
|
298
|
+
messages.map((message) => JSON.stringify(message)).join("\n") + "\n",
|
|
299
|
+
"utf8",
|
|
300
|
+
);
|
|
256
301
|
}
|
|
257
302
|
|
|
258
303
|
async function mutateBranchInbox(runId, branchName, mutate, fallback) {
|
|
@@ -271,25 +316,30 @@ async function mutateBranchInbox(runId, branchName, mutate, fallback) {
|
|
|
271
316
|
}
|
|
272
317
|
|
|
273
318
|
async function claimQueuedInboxMessages(runId, branchName) {
|
|
274
|
-
return mutateBranchInbox(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
319
|
+
return mutateBranchInbox(
|
|
320
|
+
runId,
|
|
321
|
+
branchName,
|
|
322
|
+
(messages) => {
|
|
323
|
+
const claimedAt = new Date().toISOString();
|
|
324
|
+
const queuedMessages = [];
|
|
325
|
+
const updated = messages.map((msg, index) => {
|
|
326
|
+
if (msg.status !== "queued" && msg.status) return msg;
|
|
327
|
+
const claimed = {
|
|
328
|
+
...msg,
|
|
329
|
+
claimed_at: claimedAt,
|
|
330
|
+
id: msg.id || `legacy-${Date.now()}-${index}`,
|
|
331
|
+
status: "claimed",
|
|
332
|
+
};
|
|
333
|
+
queuedMessages.push(claimed);
|
|
334
|
+
return claimed;
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
messages: queuedMessages.length > 0 ? updated : undefined,
|
|
338
|
+
value: queuedMessages,
|
|
284
339
|
};
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
messages: queuedMessages.length > 0 ? updated : undefined,
|
|
290
|
-
value: queuedMessages,
|
|
291
|
-
};
|
|
292
|
-
}, []);
|
|
340
|
+
},
|
|
341
|
+
[],
|
|
342
|
+
);
|
|
293
343
|
}
|
|
294
344
|
|
|
295
345
|
async function updateInboxMessagesStatus(runId, branchName, ids, status) {
|
|
@@ -305,19 +355,27 @@ async function updateInboxMessagesStatus(runId, branchName, ids, status) {
|
|
|
305
355
|
|
|
306
356
|
async function executeParticipantPrompt(role, basePrompt, config) {
|
|
307
357
|
const branchName = role.name;
|
|
308
|
-
const queuedMessages = await claimQueuedInboxMessages(
|
|
358
|
+
const queuedMessages = await claimQueuedInboxMessages(
|
|
359
|
+
config.runId,
|
|
360
|
+
branchName,
|
|
361
|
+
);
|
|
309
362
|
|
|
310
363
|
let finalPrompt = basePrompt;
|
|
311
364
|
const claimedIds = [];
|
|
312
365
|
|
|
313
366
|
if (queuedMessages.length > 0) {
|
|
314
|
-
let inboxSection =
|
|
367
|
+
let inboxSection =
|
|
368
|
+
"\n\nDIRECT INBOX MESSAGES FOR YOU (queued mailbox work):\n";
|
|
315
369
|
for (const msg of queuedMessages) {
|
|
316
|
-
const bodyText =
|
|
370
|
+
const bodyText =
|
|
371
|
+
typeof msg.body === "string"
|
|
372
|
+
? msg.body
|
|
373
|
+
: JSON.stringify(msg.body ?? "");
|
|
317
374
|
inboxSection += `- From: ${msg.from || "unknown"} (Type: ${msg.type || "message"})\n Body: ${bodyText}\n`;
|
|
318
375
|
if (msg.id) claimedIds.push(msg.id);
|
|
319
376
|
}
|
|
320
|
-
inboxSection +=
|
|
377
|
+
inboxSection +=
|
|
378
|
+
"\nPlease acknowledge and address these direct messages in your response.\n";
|
|
321
379
|
finalPrompt += inboxSection;
|
|
322
380
|
}
|
|
323
381
|
|
|
@@ -325,7 +383,12 @@ async function executeParticipantPrompt(role, basePrompt, config) {
|
|
|
325
383
|
|
|
326
384
|
if (claimedIds.length > 0) {
|
|
327
385
|
const finalStatus = result.code === 0 ? "handled" : "failed";
|
|
328
|
-
await updateInboxMessagesStatus(
|
|
386
|
+
await updateInboxMessagesStatus(
|
|
387
|
+
config.runId,
|
|
388
|
+
branchName,
|
|
389
|
+
claimedIds,
|
|
390
|
+
finalStatus,
|
|
391
|
+
);
|
|
329
392
|
}
|
|
330
393
|
|
|
331
394
|
return result;
|
|
@@ -336,8 +399,11 @@ async function participantRound(role, round, config) {
|
|
|
336
399
|
const address = `branch:${config.runId}/${role.name}`;
|
|
337
400
|
const prompt = `You are ${displayName} (${address}), ${role.persona}. Mission: ${config.mission}. Round ${round}/${config.rounds}. First call inspect target=${config.room} view=previews lines=30 and inspect target=${config.room} view=contacts. Then call message once to ${config.room} from ${address} type=chat.message. Body: 2-4 sentences that react to a named participant, propose the next coordination step, and refine the shared artifact. Use contacts for peer names and addresses. End stdout with summary <=160 chars.`;
|
|
338
401
|
const result = await executeParticipantPrompt(role, prompt, config);
|
|
339
|
-
process.stdout.write(
|
|
340
|
-
|
|
402
|
+
process.stdout.write(
|
|
403
|
+
`[${role.name} round ${round}] code=${result.code}\n${result.stdout}\n`,
|
|
404
|
+
);
|
|
405
|
+
if (result.stderr.trim())
|
|
406
|
+
process.stderr.write(`[${role.name} round ${round}] ${result.stderr}\n`);
|
|
341
407
|
}
|
|
342
408
|
|
|
343
409
|
async function participantJoin(role, config) {
|
|
@@ -358,8 +424,11 @@ async function participantLeave(role, config) {
|
|
|
358
424
|
async function runConsensus(config, locker) {
|
|
359
425
|
await Promise.all(config.roles.map((role) => participantJoin(role, config)));
|
|
360
426
|
for (let round = 1; round <= config.rounds; round += 1) {
|
|
361
|
-
await Promise.all(
|
|
362
|
-
|
|
427
|
+
await Promise.all(
|
|
428
|
+
config.roles.map((role) => participantRound(role, round, config)),
|
|
429
|
+
);
|
|
430
|
+
if (round < config.rounds && config.delay > 0)
|
|
431
|
+
await sleep(config.delay * 1000);
|
|
363
432
|
}
|
|
364
433
|
await Promise.all(config.roles.map((role) => participantLeave(role, config)));
|
|
365
434
|
}
|
|
@@ -370,7 +439,8 @@ async function runPipeline(config, locker) {
|
|
|
370
439
|
await participantJoin(role, config);
|
|
371
440
|
for (let round = 1; round <= config.rounds; round += 1) {
|
|
372
441
|
await participantRound(role, round, config);
|
|
373
|
-
if (round < config.rounds && config.delay > 0)
|
|
442
|
+
if (round < config.rounds && config.delay > 0)
|
|
443
|
+
await sleep(config.delay * 1000);
|
|
374
444
|
}
|
|
375
445
|
await participantLeave(role, config);
|
|
376
446
|
}
|
|
@@ -378,14 +448,17 @@ async function runPipeline(config, locker) {
|
|
|
378
448
|
|
|
379
449
|
// 3. fanout mode: run completely parallel independent processes
|
|
380
450
|
async function runFanout(config, locker) {
|
|
381
|
-
await Promise.all(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
451
|
+
await Promise.all(
|
|
452
|
+
config.roles.map(async (role) => {
|
|
453
|
+
await participantJoin(role, config);
|
|
454
|
+
for (let round = 1; round <= config.rounds; round += 1) {
|
|
455
|
+
await participantRound(role, round, config);
|
|
456
|
+
if (round < config.rounds && config.delay > 0)
|
|
457
|
+
await sleep(config.delay * 1000);
|
|
458
|
+
}
|
|
459
|
+
await participantLeave(role, config);
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
389
462
|
}
|
|
390
463
|
|
|
391
464
|
// 4. pool mode: dynamic task pulling from the locker queue
|
|
@@ -397,56 +470,66 @@ async function runPool(config, locker) {
|
|
|
397
470
|
for (let index = 1; index <= 3; index += 1) {
|
|
398
471
|
await writeLockerMessage(locker, {
|
|
399
472
|
type: "lock.enqueue",
|
|
400
|
-
body: {
|
|
473
|
+
body: {
|
|
474
|
+
id: `subtask-${index}`,
|
|
475
|
+
task: `Execute subtask ${index} under mission: ${config.mission}`,
|
|
476
|
+
resources: [`resource-${index}`],
|
|
477
|
+
},
|
|
401
478
|
});
|
|
402
479
|
}
|
|
403
480
|
|
|
404
481
|
// Workers concurrently poll task queue
|
|
405
|
-
await Promise.all(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const result = await executeParticipantPrompt(role, prompt, config);
|
|
429
|
-
|
|
430
|
-
process.stdout.write(`[${role.name} completed ${taskId}] code=${result.code}\n${result.stdout}\n`);
|
|
482
|
+
await Promise.all(
|
|
483
|
+
config.roles.map(async (role) => {
|
|
484
|
+
const address = `branch:${config.runId}/${role.name}`;
|
|
485
|
+
await participantJoin(role, config);
|
|
486
|
+
|
|
487
|
+
while (true) {
|
|
488
|
+
// Dequeue/Claim a task from the locker
|
|
489
|
+
await writeLockerMessage(locker, {
|
|
490
|
+
type: "lock.claim",
|
|
491
|
+
body: { owner: role.name },
|
|
492
|
+
});
|
|
493
|
+
await sleep(100);
|
|
494
|
+
|
|
495
|
+
// Check locks and queue state from locker snapshot
|
|
496
|
+
const lockerData = await readJsonFile(`${locker.stateDir}/locks.json`);
|
|
497
|
+
const assignedTask = Object.values(lockerData).find(
|
|
498
|
+
(lock) => lock.owner === role.name,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
if (!assignedTask) {
|
|
502
|
+
// No task assigned or queue is empty
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
431
505
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
506
|
+
const taskId = assignedTask.task;
|
|
507
|
+
const prompt = `You are ${role.name}, ${role.persona}. You have been assigned task ${taskId}: "${assignedTask.task}". Solve it as part of the overall mission: ${config.mission}. End your response with a clear summary.`;
|
|
508
|
+
const result = await executeParticipantPrompt(role, prompt, config);
|
|
509
|
+
|
|
510
|
+
process.stdout.write(
|
|
511
|
+
`[${role.name} completed ${taskId}] code=${result.code}\n${result.stdout}\n`,
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// Report task completion back to locker and release resource locks
|
|
515
|
+
await writeLockerMessage(locker, {
|
|
516
|
+
type: "lock.complete",
|
|
517
|
+
body: { id: taskId, result: result.stdout },
|
|
518
|
+
});
|
|
519
|
+
// Release any locks associated with this resource
|
|
520
|
+
for (const [res, lock] of Object.entries(lockerData)) {
|
|
521
|
+
if (lock.owner === role.name) {
|
|
522
|
+
await writeLockerMessage(locker, {
|
|
523
|
+
type: "lock.release",
|
|
524
|
+
body: { resource: res },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
444
527
|
}
|
|
445
528
|
}
|
|
446
|
-
}
|
|
447
529
|
|
|
448
|
-
|
|
449
|
-
|
|
530
|
+
await participantLeave(role, config);
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
450
533
|
}
|
|
451
534
|
|
|
452
535
|
const coordinatorModes = {
|
|
@@ -465,20 +548,50 @@ async function readJsonFile(path) {
|
|
|
465
548
|
}
|
|
466
549
|
|
|
467
550
|
const defaultRoles = [
|
|
468
|
-
{
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
{
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
551
|
+
{
|
|
552
|
+
name: "mapper",
|
|
553
|
+
persona: "systems mapper; tracks shared structure and dependencies",
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "memory",
|
|
557
|
+
persona: "memory keeper; preserves decisions and unresolved questions",
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: "risk",
|
|
561
|
+
persona: "risk scout; challenges weak coordination and missing owners",
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "flow",
|
|
565
|
+
persona: "flow designer; turns scattered ideas into process rhythm",
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "operator",
|
|
569
|
+
persona: "operator; converts ideas into concrete next actions",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
name: "narrative",
|
|
573
|
+
persona: "narrative synthesizer; keeps the artifact coherent",
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "interface",
|
|
577
|
+
persona: "interface designer; makes outputs visible and usable",
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "facilitator",
|
|
581
|
+
persona: "facilitator; asks for consensus and convergence",
|
|
582
|
+
},
|
|
476
583
|
];
|
|
477
584
|
|
|
478
585
|
const config = {
|
|
479
|
-
runId: arg(
|
|
586
|
+
runId: arg(
|
|
587
|
+
"run-id",
|
|
588
|
+
process.env.run_id || process.env.RUN_ID || "coordinator-run",
|
|
589
|
+
),
|
|
480
590
|
mode: arg("mode", "consensus"), // "consensus" (default/swarm), "pipeline", "fanout", "pool"
|
|
481
|
-
mission: arg(
|
|
591
|
+
mission: arg(
|
|
592
|
+
"mission",
|
|
593
|
+
"Coordinate a shared artifact and converge on next actions",
|
|
594
|
+
),
|
|
482
595
|
model: arg("model", ""),
|
|
483
596
|
thinking: arg("thinking", "off"),
|
|
484
597
|
roles: await loadRoles(arg("roles", ""), arg("roles-path", "")),
|
package/scripts/locker.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
2
4
|
import {
|
|
3
5
|
appendFileSync,
|
|
4
6
|
existsSync,
|
|
@@ -7,10 +9,8 @@ import {
|
|
|
7
9
|
rmSync,
|
|
8
10
|
writeFileSync,
|
|
9
11
|
} from "node:fs";
|
|
10
|
-
import { dirname, join, resolve } from "node:path";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
13
12
|
import { createServer } from "node:net";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
14
|
import readline from "node:readline";
|
|
15
15
|
|
|
16
16
|
function parseArgs(argv) {
|
|
@@ -53,14 +53,18 @@ function writeJson(path, value) {
|
|
|
53
53
|
}
|
|
54
54
|
function getControlEndpoint() {
|
|
55
55
|
if (process.platform !== "win32") return { path: controlPath, type: "fifo" };
|
|
56
|
-
const hash = createHash("sha256")
|
|
56
|
+
const hash = createHash("sha256")
|
|
57
|
+
.update(resolve(stateDir))
|
|
58
|
+
.digest("hex")
|
|
59
|
+
.slice(0, 20);
|
|
57
60
|
return { path: `\\\\.\\pipe\\pi-actors-locker-${hash}`, type: "named-pipe" };
|
|
58
61
|
}
|
|
59
62
|
function writeControlEndpoint(endpoint) {
|
|
60
63
|
writeJson(join(stateDir, "control.json"), endpoint);
|
|
61
64
|
const runPath = join(stateDir, "run.json");
|
|
62
65
|
const run = readJson(runPath, undefined);
|
|
63
|
-
if (run && typeof run === "object")
|
|
66
|
+
if (run && typeof run === "object")
|
|
67
|
+
writeJson(runPath, { ...run, control: endpoint });
|
|
64
68
|
}
|
|
65
69
|
function journal(event, data = {}) {
|
|
66
70
|
appendFileSync(
|
|
@@ -218,9 +222,19 @@ function handle(message) {
|
|
|
218
222
|
if (!resource) throw new Error("lock.renew body.resource is required");
|
|
219
223
|
const current = locks[resource];
|
|
220
224
|
if (!current) {
|
|
221
|
-
outbox(
|
|
225
|
+
outbox(
|
|
226
|
+
"lock.denied",
|
|
227
|
+
`Lock renew denied ${resource}`,
|
|
228
|
+
{ resource, owner, reason: "missing" },
|
|
229
|
+
"warning",
|
|
230
|
+
);
|
|
222
231
|
} else if (current.owner !== owner) {
|
|
223
|
-
outbox(
|
|
232
|
+
outbox(
|
|
233
|
+
"lock.denied",
|
|
234
|
+
`Lock renew denied ${resource}`,
|
|
235
|
+
{ resource, owner, current },
|
|
236
|
+
"warning",
|
|
237
|
+
);
|
|
224
238
|
} else {
|
|
225
239
|
locks[resource] = { ...current, expiresAt: now() + leaseMs };
|
|
226
240
|
outbox("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
|
|
@@ -235,8 +249,15 @@ function handle(message) {
|
|
|
235
249
|
outbox("lock.released", `Lock released ${resource}`, { resource });
|
|
236
250
|
return;
|
|
237
251
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
252
|
+
if (
|
|
253
|
+
type === "lock.complete" ||
|
|
254
|
+
type === "lock.fail" ||
|
|
255
|
+
type === "coord.complete" ||
|
|
256
|
+
type === "coord.fail"
|
|
257
|
+
) {
|
|
258
|
+
const eventType = type.startsWith("coord.")
|
|
259
|
+
? type.replace("coord.", "lock.")
|
|
260
|
+
: type;
|
|
240
261
|
journal(eventType, body);
|
|
241
262
|
outbox(
|
|
242
263
|
eventType,
|