@llblab/pi-actors 0.26.0 → 0.26.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.26.2: Terminal Progress Hotfix
6
+
7
+ - `[Async Runs]` Keep `progress.json` aligned with terminal kill/cancel handling by writing `phase=killed` or `phase=cancelled` and clearing active subagent counts when a run is stopped by runtime control.
8
+ - `[Inspector]` Prevent killed runs from showing contradictory `status=killed` with `progress.phase=running`, preserving compact operator status trust after `control.kill`.
9
+
10
+ ## 0.26.1: Coordinator Empty-Synthesis Hotfix
11
+
12
+ - `[Coordinator]` Treat all-failed participant rounds and empty synthesis as a failed coordinator run instead of a clean success, preserving operator trust when swarm branches all exit non-zero.
13
+ - `[Artifacts]` Write compact diagnostics into empty synthesis artifacts, including participant attempt/success/failure counts and transcript size, so failed swarms leave actionable evidence instead of only `No synthesis output`.
14
+
5
15
  ## 0.26.0: Actor Worker v2 and Package Contract Hardening
6
16
 
7
17
  - `[Actor Worker]` Promoted the packaged `actor-worker` into a v2 reference pattern with compact `worker-status.json`, optional per-task result artifacts under `worker-artifacts/`, stale-claim surfacing through status, and artifact metadata on `task.result` room messages.
@@ -565,6 +565,10 @@ export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
565
565
  });
566
566
  if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
567
567
  return undefined;
568
+ const indexedDirs = new Set(valid.map((entry) => resolve(entry.state_dir)));
569
+ const stateDirs = listRunStateDirs(stateRoot).map((stateDir) => resolve(stateDir));
570
+ if (stateDirs.some((stateDir) => !indexedDirs.has(stateDir)))
571
+ return undefined;
568
572
  return valid;
569
573
  }
570
574
  export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
@@ -1010,6 +1014,20 @@ function markTerminalHandled(stateDir, details) {
1010
1014
  ts: new Date().toISOString(),
1011
1015
  });
1012
1016
  }
1017
+ function markTerminalProgress(stateDir, phase) {
1018
+ const existing = readJson(join(stateDir, "progress.json"));
1019
+ const progress = existing && typeof existing === "object" && !Array.isArray(existing)
1020
+ ? existing
1021
+ : {};
1022
+ const { activeSubagents: _activeSubagents, ...rest } = progress;
1023
+ writeJsonAtomic(join(stateDir, "progress.json"), {
1024
+ ...rest,
1025
+ completed: typeof progress.completed === "number" ? progress.completed : 0,
1026
+ failures: Array.isArray(progress.failures) ? progress.failures : [],
1027
+ phase,
1028
+ updatedAt: new Date().toISOString(),
1029
+ });
1030
+ }
1013
1031
  function stopRun(runOrDir, signal, event) {
1014
1032
  const status = getRunStatus(runOrDir);
1015
1033
  const pid = Number(status.pid || 0);
@@ -1024,6 +1042,10 @@ function stopRun(runOrDir, signal, event) {
1024
1042
  const signalResult = signalOwnedRunProcess(pid, signal);
1025
1043
  writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
1026
1044
  markTerminalHandled(stateDir, { event, signal });
1045
+ if (event === "run.kill")
1046
+ markTerminalProgress(stateDir, "killed");
1047
+ if (event === "run.cancel")
1048
+ markTerminalProgress(stateDir, "cancelled");
1027
1049
  return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
1028
1050
  }
1029
1051
  export function cancelRun(runOrDir) {
@@ -259,7 +259,7 @@ async function readRoomTranscript(config) {
259
259
  }
260
260
  async function synthesize(config, locker) {
261
261
  if (!config.artifactPath)
262
- return;
262
+ return { code: 0, empty: false };
263
263
  if (locker) {
264
264
  await writeLockerMessage(locker, {
265
265
  type: "lock.claim",
@@ -269,9 +269,13 @@ async function synthesize(config, locker) {
269
269
  const transcript = await readRoomTranscript(config);
270
270
  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)}`;
271
271
  const result = await runPi(prompt, config.model, config.thinking);
272
- await writeFile(config.artifactPath, result.stdout.trim()
272
+ const output = result.stdout.trim();
273
+ const diagnostics = config.stats
274
+ ? `\n\n## Diagnostics\n\nparticipant_attempts=${config.stats.participantAttempts}\nparticipant_success=${config.stats.participantSuccess}\nparticipant_failures=${config.stats.participantFailures}\nsynthesis_code=${result.code}\ntranscript_messages=${transcript.trim() ? transcript.split("\n").length : 0}\n`
275
+ : "";
276
+ await writeFile(config.artifactPath, output
273
277
  ? result.stdout
274
- : "# Synthesis Artifact\n\nNo synthesis output.\n", "utf8");
278
+ : `# Synthesis Artifact\n\nNo synthesis output.${diagnostics}\n`, "utf8");
275
279
  if (locker) {
276
280
  await writeLockerMessage(locker, {
277
281
  type: "lock.complete",
@@ -284,6 +288,7 @@ async function synthesize(config, locker) {
284
288
  });
285
289
  }
286
290
  process.stdout.write(`artifact=${config.artifactPath}\n`);
291
+ return { code: result.code, empty: !output };
287
292
  }
288
293
  async function readInboxLines(inboxPath) {
289
294
  if (!existsSync(inboxPath))
@@ -378,9 +383,17 @@ async function participantRound(role, round, config) {
378
383
  const address = `branch:${config.runId}/${role.name}`;
379
384
  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.`;
380
385
  const result = await executeParticipantPrompt(role, prompt, config);
386
+ if (config.stats) {
387
+ config.stats.participantAttempts += 1;
388
+ if (result.code === 0)
389
+ config.stats.participantSuccess += 1;
390
+ else
391
+ config.stats.participantFailures += 1;
392
+ }
381
393
  process.stdout.write(`[${role.name} round ${round}] code=${result.code}\n${result.stdout}\n`);
382
394
  if (result.stderr.trim())
383
395
  process.stderr.write(`[${role.name} round ${round}] ${result.stderr}\n`);
396
+ return result;
384
397
  }
385
398
  async function participantJoin(role, config) {
386
399
  const displayName = role.name;
@@ -546,6 +559,11 @@ export async function runCoordinator(argv = process.argv.slice(2)) {
546
559
  artifactPath: arg("artifact-path", ""),
547
560
  locker: boolArg("locker", false),
548
561
  lockerLeaseMs: numberArg("locker-lease-ms", 600000),
562
+ stats: {
563
+ participantAttempts: 0,
564
+ participantSuccess: 0,
565
+ participantFailures: 0,
566
+ },
549
567
  };
550
568
  if (!config.model) {
551
569
  console.error("--model is required");
@@ -559,7 +577,16 @@ export async function runCoordinator(argv = process.argv.slice(2)) {
559
577
  if (!runMode)
560
578
  throw new Error(`Unknown coordinator mode: ${config.mode}`);
561
579
  await runMode(config, locker);
562
- await synthesize(config, locker);
580
+ const synthesis = await synthesize(config, locker);
581
+ if (config.stats.participantAttempts > 0 && config.stats.participantSuccess === 0) {
582
+ failures.push(`All participant rounds failed: attempts=${config.stats.participantAttempts}`);
583
+ }
584
+ if (synthesis?.empty && config.stats.participantFailures > 0) {
585
+ failures.push(`Synthesis output was empty after participant failures: failures=${config.stats.participantFailures}`);
586
+ }
587
+ if (synthesis && synthesis.code !== 0) {
588
+ failures.push(`Synthesis command failed: code=${synthesis.code}`);
589
+ }
563
590
  }
564
591
  catch (globalError) {
565
592
  failures.push(`Global coordinator error: ${globalError.message}`);
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.26.0
5
+ version: 0.26.2
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.26.0
5
+ version: 0.26.2
6
6
  ---
7
7
 
8
8
  # Swarm
package/lib/async-runs.ts CHANGED
@@ -805,6 +805,9 @@ export function readRunStateIndex(
805
805
  });
806
806
  if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
807
807
  return undefined;
808
+ const indexedDirs = new Set(valid.map((entry) => resolve(entry.state_dir)));
809
+ const stateDirs = listRunStateDirs(stateRoot).map((stateDir) => resolve(stateDir));
810
+ if (stateDirs.some((stateDir) => !indexedDirs.has(stateDir))) return undefined;
808
811
  return valid;
809
812
  }
810
813
 
@@ -1403,6 +1406,25 @@ function markTerminalHandled(
1403
1406
  });
1404
1407
  }
1405
1408
 
1409
+ function markTerminalProgress(
1410
+ stateDir: string,
1411
+ phase: "cancelled" | "killed",
1412
+ ): void {
1413
+ const existing = readJson(join(stateDir, "progress.json"));
1414
+ const progress =
1415
+ existing && typeof existing === "object" && !Array.isArray(existing)
1416
+ ? (existing as Record<string, unknown>)
1417
+ : {};
1418
+ const { activeSubagents: _activeSubagents, ...rest } = progress;
1419
+ writeJsonAtomic(join(stateDir, "progress.json"), {
1420
+ ...rest,
1421
+ completed: typeof progress.completed === "number" ? progress.completed : 0,
1422
+ failures: Array.isArray(progress.failures) ? progress.failures : [],
1423
+ phase,
1424
+ updatedAt: new Date().toISOString(),
1425
+ });
1426
+ }
1427
+
1406
1428
  function stopRun(
1407
1429
  runOrDir: string,
1408
1430
  signal: NodeJS.Signals,
@@ -1425,6 +1447,8 @@ function stopRun(
1425
1447
  { flag: "a" },
1426
1448
  );
1427
1449
  markTerminalHandled(stateDir, { event, signal });
1450
+ if (event === "run.kill") markTerminalProgress(stateDir, "killed");
1451
+ if (event === "run.cancel") markTerminalProgress(stateDir, "cancelled");
1428
1452
  return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
1429
1453
  }
1430
1454
 
@@ -285,7 +285,7 @@ async function readRoomTranscript(config) {
285
285
  }
286
286
 
287
287
  async function synthesize(config, locker) {
288
- if (!config.artifactPath) return;
288
+ if (!config.artifactPath) return { code: 0, empty: false };
289
289
  if (locker) {
290
290
  await writeLockerMessage(locker, {
291
291
  type: "lock.claim",
@@ -295,11 +295,15 @@ async function synthesize(config, locker) {
295
295
  const transcript = await readRoomTranscript(config);
296
296
  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)}`;
297
297
  const result = await runPi(prompt, config.model, config.thinking);
298
+ const output = result.stdout.trim();
299
+ const diagnostics = config.stats
300
+ ? `\n\n## Diagnostics\n\nparticipant_attempts=${config.stats.participantAttempts}\nparticipant_success=${config.stats.participantSuccess}\nparticipant_failures=${config.stats.participantFailures}\nsynthesis_code=${result.code}\ntranscript_messages=${transcript.trim() ? transcript.split("\n").length : 0}\n`
301
+ : "";
298
302
  await writeFile(
299
303
  config.artifactPath,
300
- result.stdout.trim()
304
+ output
301
305
  ? result.stdout
302
- : "# Synthesis Artifact\n\nNo synthesis output.\n",
306
+ : `# Synthesis Artifact\n\nNo synthesis output.${diagnostics}\n`,
303
307
  "utf8",
304
308
  );
305
309
  if (locker) {
@@ -314,6 +318,7 @@ async function synthesize(config, locker) {
314
318
  });
315
319
  }
316
320
  process.stdout.write(`artifact=${config.artifactPath}\n`);
321
+ return { code: result.code, empty: !output };
317
322
  }
318
323
 
319
324
  async function readInboxLines(inboxPath) {
@@ -432,11 +437,17 @@ async function participantRound(role, round, config) {
432
437
  const address = `branch:${config.runId}/${role.name}`;
433
438
  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.`;
434
439
  const result = await executeParticipantPrompt(role, prompt, config);
440
+ if (config.stats) {
441
+ config.stats.participantAttempts += 1;
442
+ if (result.code === 0) config.stats.participantSuccess += 1;
443
+ else config.stats.participantFailures += 1;
444
+ }
435
445
  process.stdout.write(
436
446
  `[${role.name} round ${round}] code=${result.code}\n${result.stdout}\n`,
437
447
  );
438
448
  if (result.stderr.trim())
439
449
  process.stderr.write(`[${role.name} round ${round}] ${result.stderr}\n`);
450
+ return result;
440
451
  }
441
452
 
442
453
  async function participantJoin(role, config) {
@@ -635,6 +646,11 @@ const config = {
635
646
  artifactPath: arg("artifact-path", ""),
636
647
  locker: boolArg("locker", false),
637
648
  lockerLeaseMs: numberArg("locker-lease-ms", 600000),
649
+ stats: {
650
+ participantAttempts: 0,
651
+ participantSuccess: 0,
652
+ participantFailures: 0,
653
+ },
638
654
  };
639
655
 
640
656
  if (!config.model) {
@@ -650,7 +666,20 @@ try {
650
666
  const runMode = coordinatorModes[config.mode];
651
667
  if (!runMode) throw new Error(`Unknown coordinator mode: ${config.mode}`);
652
668
  await runMode(config, locker);
653
- await synthesize(config, locker);
669
+ const synthesis = await synthesize(config, locker);
670
+ if (config.stats.participantAttempts > 0 && config.stats.participantSuccess === 0) {
671
+ failures.push(
672
+ `All participant rounds failed: attempts=${config.stats.participantAttempts}`,
673
+ );
674
+ }
675
+ if (synthesis?.empty && config.stats.participantFailures > 0) {
676
+ failures.push(
677
+ `Synthesis output was empty after participant failures: failures=${config.stats.participantFailures}`,
678
+ );
679
+ }
680
+ if (synthesis && synthesis.code !== 0) {
681
+ failures.push(`Synthesis command failed: code=${synthesis.code}`);
682
+ }
654
683
  } catch (globalError) {
655
684
  failures.push(`Global coordinator error: ${globalError.message}`);
656
685
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.26.0
5
+ version: 0.26.2
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.26.0
5
+ version: 0.26.2
6
6
  ---
7
7
 
8
8
  # Swarm