@llblab/pi-actors 0.21.0 → 0.22.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.
@@ -41,7 +41,8 @@ function parseRoles(raw) {
41
41
  }
42
42
 
43
43
  async function loadRoles(raw, path) {
44
- if (path.trim()) return normalizeRoles(JSON.parse(await readFile(path, "utf8")));
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) throw new Error(`timed out waiting for ${path}`);
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(`${lockDir}/owner.json`, `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`, "utf8");
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(`${runStateDir(runId)}/branches/${branchName}`, ".inbox.lock", `Branch inbox ${branchName}`);
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") await writeNamedPipeLine(control.path, line);
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(new URL("./locker.mjs", import.meta.url).pathname, [
142
- "serve",
143
- "--state-dir",
144
- lockerStateDir,
145
- "--lease-ms",
146
- String(config.lockerLeaseMs),
147
- ], {
148
- env: { ...process.env, run_id: config.runId },
149
- stdio: ["ignore", "pipe", "pipe"],
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: { id: "coordinator-artifact", task: "Own final artifact synthesis", resources: [config.artifactPath || "artifact"] },
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", "inspect,message",
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) => { stdout += chunk; });
192
- child.stderr.on("data", (chunk) => { stderr += chunk; });
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) => resolve({ code: 1, stdout, stderr: String(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(/^branch:[^/]+\//, "");
235
+ const from = String(message.from || "unknown").replace(
236
+ /^branch:[^/]+\//,
237
+ "",
238
+ );
210
239
  const type = String(message.type || "message");
211
- const body = typeof message.body === "string" ? message.body : JSON.stringify(message.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(config.artifactPath, result.stdout.trim() ? result.stdout : "# Synthesis Artifact\n\nNo synthesis output.\n", "utf8");
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.split("\n").filter(Boolean).map((line) => JSON.parse(line));
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(inboxPath, messages.map((message) => JSON.stringify(message)).join("\n") + "\n", "utf8");
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(runId, branchName, (messages) => {
275
- const claimedAt = new Date().toISOString();
276
- const queuedMessages = [];
277
- const updated = messages.map((msg, index) => {
278
- if (msg.status !== "queued" && msg.status) return msg;
279
- const claimed = {
280
- ...msg,
281
- claimed_at: claimedAt,
282
- id: msg.id || `legacy-${Date.now()}-${index}`,
283
- status: "claimed",
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
- queuedMessages.push(claimed);
286
- return claimed;
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(config.runId, branchName);
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 = "\n\nDIRECT INBOX MESSAGES FOR YOU (FIFO queue):\n";
367
+ let inboxSection =
368
+ "\n\nDIRECT INBOX MESSAGES FOR YOU (queued mailbox work):\n";
315
369
  for (const msg of queuedMessages) {
316
- const bodyText = typeof msg.body === "string" ? msg.body : JSON.stringify(msg.body ?? "");
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 += "\nPlease acknowledge and address these direct messages in your response.\n";
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(config.runId, branchName, claimedIds, finalStatus);
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(`[${role.name} round ${round}] code=${result.code}\n${result.stdout}\n`);
340
- if (result.stderr.trim()) process.stderr.write(`[${role.name} round ${round}] ${result.stderr}\n`);
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(config.roles.map((role) => participantRound(role, round, config)));
362
- if (round < config.rounds && config.delay > 0) await sleep(config.delay * 1000);
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) await sleep(config.delay * 1000);
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(config.roles.map(async (role) => {
382
- await participantJoin(role, config);
383
- for (let round = 1; round <= config.rounds; round += 1) {
384
- await participantRound(role, round, config);
385
- if (round < config.rounds && config.delay > 0) await sleep(config.delay * 1000);
386
- }
387
- await participantLeave(role, config);
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: { id: `subtask-${index}`, task: `Execute subtask ${index} under mission: ${config.mission}`, resources: [`resource-${index}`] },
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(config.roles.map(async (role) => {
406
- const address = `branch:${config.runId}/${role.name}`;
407
- await participantJoin(role, config);
408
-
409
- while (true) {
410
- // Dequeue/Claim a task from the locker
411
- await writeLockerMessage(locker, {
412
- type: "lock.claim",
413
- body: { owner: role.name },
414
- });
415
- await sleep(100);
416
-
417
- // Check locks and queue state from locker snapshot
418
- const lockerData = await readJsonFile(`${locker.stateDir}/locks.json`);
419
- const assignedTask = Object.values(lockerData).find(lock => lock.owner === role.name);
420
-
421
- if (!assignedTask) {
422
- // No task assigned or queue is empty
423
- break;
424
- }
425
-
426
- const taskId = assignedTask.task;
427
- 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.`;
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
- // Report task completion back to locker and release resource locks
433
- await writeLockerMessage(locker, {
434
- type: "lock.complete",
435
- body: { id: taskId, result: result.stdout },
436
- });
437
- // Release any locks associated with this resource
438
- for (const [res, lock] of Object.entries(lockerData)) {
439
- if (lock.owner === role.name) {
440
- await writeLockerMessage(locker, {
441
- type: "lock.release",
442
- body: { resource: res },
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
- await participantLeave(role, config);
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
- { name: "mapper", persona: "systems mapper; tracks shared structure and dependencies" },
469
- { name: "memory", persona: "memory keeper; preserves decisions and unresolved questions" },
470
- { name: "risk", persona: "risk scout; challenges weak coordination and missing owners" },
471
- { name: "flow", persona: "flow designer; turns scattered ideas into process rhythm" },
472
- { name: "operator", persona: "operator; converts ideas into concrete next actions" },
473
- { name: "narrative", persona: "narrative synthesizer; keeps the artifact coherent" },
474
- { name: "interface", persona: "interface designer; makes outputs visible and usable" },
475
- { name: "facilitator", persona: "facilitator; asks for consensus and convergence" },
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("run-id", process.env.run_id || process.env.RUN_ID || "coordinator-run"),
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("mission", "Coordinate a shared artifact and converge on next actions"),
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", "")),
@@ -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").update(resolve(stateDir)).digest("hex").slice(0, 20);
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") writeJson(runPath, { ...run, control: endpoint });
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("lock.denied", `Lock renew denied ${resource}`, { resource, owner, reason: "missing" }, "warning");
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("lock.denied", `Lock renew denied ${resource}`, { resource, owner, current }, "warning");
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 (type === "lock.complete" || type === "lock.fail" || type === "coord.complete" || type === "coord.fail") {
239
- const eventType = type.startsWith("coord.") ? type.replace("coord.", "lock.") : type;
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,