@llblab/pi-actors 0.26.2 → 0.27.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/BACKLOG.md CHANGED
@@ -225,12 +225,35 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
225
225
  - Packaged recipes declare `stop`/`cancel` only when the actor-specific behavior is meaningful.
226
226
  - Tests assert that generic mailbox-loop termination is not triggered by `control.stop` or `control.cancel`.
227
227
 
228
+ ### M-12 Runtime and Session Observability UX
229
+
230
+ - Priority: High.
231
+ - Status: Planned.
232
+ - Goal: Make reload/session/runtime mismatches visible without changing ownership or lifecycle policy.
233
+ - Why now: 0.26 dogfood showed that actors, mailbox workers, and hotfixes work after a full Pi restart, but ordinary reloads can leave operators unsure which extension code is live. Session ownership mismatches also surface as terse strings instead of structured diagnostics or navigation hints.
234
+ - Direction:
235
+ - Add an intentional runtime/version inspection surface that reports loaded package version, entrypoint path, source/dist mode, package root, recipe roots, and git commit when available.
236
+ - Make session ownership denials structured in tool details with compact `reason=session_mismatch owner_session=... current_session=...` text and hints to inspect the owning session.
237
+ - Make coordinator/session status show when other sessions or other-session runs exist so `runs=0` is not misleading after reload or resume.
238
+ - Document reload vs full restart verification in the actors/swarm guidance.
239
+ - Opportunistically improve invalid shadowing recipe launch hints only if it stays diagnostic-only.
240
+ - Non-goals:
241
+ - No cross-session force kill.
242
+ - No session attach/adopt/reparent policy.
243
+ - No relaxation of current ownership gates.
244
+ - Acceptance:
245
+ - Operators can verify the loaded pi-actors version/path from an inspect/tool surface after reload.
246
+ - Session mismatch responses carry structured details and a compact human hint.
247
+ - Coordinator/session status exposes other-session counts without leaking unrelated run details by default.
248
+ - Tests cover version inspection, session mismatch shape, and other-session count summaries.
249
+
228
250
  ## Explicitly Deferred
229
251
 
230
252
  These are valid ideas but not current focus. Reintroduce only with concrete evidence from real actor workflows.
231
253
 
232
254
  - Spawn preflight mode: useful later, but lower value than resilient inspect and mailbox-loop consolidation.
233
255
  - Run restart/reattach policy: risky for isolation; defer until corruption recovery and protocol fixtures are stronger.
256
+ - Cross-session force kill or attach/adopt/reparent: useful later, but ownership policy should not change until observability makes current boundaries clear.
234
257
  - Actor address helper CLI: keep diagnostics improving opportunistically inside existing parser/tests.
235
258
  - Documentation refactor: defer until the canonical mailbox loop and worker recipe exist; avoid rewriting docs twice.
236
259
  - Host-level tool unregistration: blocked on host API support.
@@ -240,5 +263,5 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
240
263
  ## Suggested Milestone Order
241
264
 
242
265
  ```text
243
- Next milestone: choose from deferred items only after concrete actor workflow evidence appears.
266
+ Next milestone: M-12 Runtime and Session Observability UX.
244
267
  ```
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - `[Inspect]` Added `inspect target=tool:pi-actors view=status` as a runtime verification surface with loaded version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available.
6
+ - `[Sessions]` Started structured session mismatch diagnostics with `reason=session_mismatch`, owner/current session fields, and compact inspect-session hints while preserving current ownership gates.
7
+ - `[Sessions]` Added other-session run counts to coordinator/session status summaries so reload/resume states do not misleadingly report only `runs=0` without nearby context.
8
+
9
+ ## 0.26.3: Branch Message Delivery UX Hotfix
10
+
11
+ - `[Actor Messages]` Treat branch mailbox persistence as a successful queued outcome even when the parent run control endpoint is unavailable, returning compact `queued=true` delivery diagnostics instead of throwing an unframed tool error.
12
+ - `[UX]` Preserve the compact action-output contract for branch `message` calls by keeping delivery diagnostics in the normal result formatter, including the leading blank-line separation used by other tool actions.
13
+
5
14
  ## 0.26.2: Terminal Progress Hotfix
6
15
 
7
16
  - `[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.
package/dist/lib/tools.js CHANGED
@@ -3,6 +3,9 @@
3
3
  * Zones: pi tools, registry tools, async run launchers
4
4
  * Owns generated runtime tool schemas and the register_tool management tool schema
5
5
  */
6
+ import { execFileSync } from "node:child_process";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
6
9
  import * as ActorMessages from "./actor-messages.js";
7
10
  import * as ActorRooms from "./actor-rooms.js";
8
11
  import * as AsyncRuns from "./async-runs.js";
@@ -345,10 +348,21 @@ function compactActorFiles(status) {
345
348
  : "";
346
349
  return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
347
350
  }
348
- function compactSessionRuns(session, runs) {
351
+ function summarizeOtherSessions(currentSession, allRuns) {
352
+ const otherRuns = allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession);
353
+ return {
354
+ other_runs: otherRuns.length,
355
+ other_sessions: new Set(otherRuns.map((run) => run.ownerId)).size,
356
+ };
357
+ }
358
+ function compactSessionRuns(session, runs, summary = {}) {
359
+ const suffix = [
360
+ summary.other_sessions !== undefined ? `other_sessions=${String(summary.other_sessions)}` : "",
361
+ summary.other_runs !== undefined ? `other_runs=${String(summary.other_runs)}` : "",
362
+ ].filter(Boolean).join(" ");
349
363
  if (runs.length === 0)
350
- return `\nsession=${session} runs=0`;
351
- return `\nsession=${session} runs=${runs.length}\n${runs
364
+ return `\nsession=${session} runs=0${suffix ? ` ${suffix}` : ""}`;
365
+ return `\nsession=${session} runs=${runs.length}${suffix ? ` ${suffix}` : ""}\n${runs
352
366
  .map((run) => {
353
367
  const tokens = [
354
368
  `run=${String(run.run ?? "")}`,
@@ -362,6 +376,38 @@ function compactSessionRuns(session, runs) {
362
376
  })
363
377
  .join("\n")}`;
364
378
  }
379
+ function getPiActorsRuntimeStatus() {
380
+ const packagedRecipeRoot = Paths.getPackagedRecipeRoot();
381
+ const packageRoot = dirname(packagedRecipeRoot);
382
+ const packageJsonPath = join(packageRoot, "package.json");
383
+ const packageJson = existsSync(packageJsonPath)
384
+ ? JSON.parse(readFileSync(packageJsonPath, "utf8"))
385
+ : {};
386
+ let git_commit;
387
+ try {
388
+ git_commit = execFileSync("git", ["-C", packageRoot, "rev-parse", "--short", "HEAD"], {
389
+ encoding: "utf8",
390
+ stdio: ["ignore", "pipe", "ignore"],
391
+ }).trim();
392
+ }
393
+ catch {
394
+ git_commit = undefined;
395
+ }
396
+ const entrypoint = new URL(import.meta.url).pathname;
397
+ return {
398
+ entrypoint,
399
+ git_commit,
400
+ mode: entrypoint.includes("/dist/") ? "dist" : "source",
401
+ package_name: packageJson.name ?? "@llblab/pi-actors",
402
+ package_root: packageRoot,
403
+ recipe_root: Paths.getRecipeRoot(),
404
+ packaged_recipe_root: packagedRecipeRoot,
405
+ version: packageJson.version ?? "unknown",
406
+ };
407
+ }
408
+ function compactPiActorsRuntimeStatus(status) {
409
+ return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
410
+ }
365
411
  function compactToolActor(name, tool) {
366
412
  const parameters = asRecord(tool.parameters);
367
413
  const required = Array.isArray(parameters.required)
@@ -441,6 +487,8 @@ function compactActorMessageResult(message, result) {
441
487
  ];
442
488
  if (result.bytes !== undefined)
443
489
  tokens.push(`bytes=${String(result.bytes)}`);
490
+ if (result.queued === true)
491
+ tokens.push("queued=true");
444
492
  if (result.control)
445
493
  tokens.push(`control=${String(result.control)}`);
446
494
  if (result.outbox)
@@ -459,6 +507,9 @@ function compactActorMessageResult(message, result) {
459
507
  tokens.push(`signal=${String(result.signal)}`);
460
508
  if (result.invoked === true)
461
509
  tokens.push("invoked=true");
510
+ if (result.delivery_error) {
511
+ tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
512
+ }
462
513
  return `\n${tokens.join(" ")}`;
463
514
  }
464
515
  function maybeJsonText(value, verbose, compact) {
@@ -543,7 +594,25 @@ function assertMessageSenderBelongsToRun(message, run, routeLabel) {
543
594
  async function routeBranchEnvelope(stateDir, runId, recipient, message, _options) {
544
595
  const branchMessage = { ...message, to: recipient };
545
596
  ActorRooms.appendBranchInboxMessage(stateDir, runId, recipient, branchMessage);
546
- return AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
597
+ try {
598
+ return await AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
599
+ }
600
+ catch (error) {
601
+ const record = error && typeof error === "object" ? error : {};
602
+ if (record.queued === true) {
603
+ return {
604
+ control_path: record.control_path,
605
+ control_type: record.control_type,
606
+ delivery_error: record.delivery_error ?? (error instanceof Error ? error.message : String(error)),
607
+ inbox_id: record.inbox_id,
608
+ queued: true,
609
+ run: runId,
610
+ sent: false,
611
+ state_dir: stateDir,
612
+ };
613
+ }
614
+ throw error;
615
+ }
547
616
  }
548
617
  function getRoomMulticastRecipients(message, run) {
549
618
  const raw = message.metadata?.recipients;
@@ -637,7 +706,13 @@ function assertRunAccessibleToContext(runId, ctx) {
637
706
  const status = AsyncRuns.getRunStatus(runId);
638
707
  const sessionId = getContextSessionId(ctx);
639
708
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
640
- throw new Error(`run:${runId} is owned by session:${status.ownerId}; current session is ${sessionId}.`);
709
+ throw Object.assign(new Error(`run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`), {
710
+ current_session: sessionId,
711
+ hint: `inspect target=session:${status.ownerId} view=status`,
712
+ owner_session: status.ownerId,
713
+ reason: "session_mismatch",
714
+ run: runId,
715
+ });
641
716
  }
642
717
  return status;
643
718
  }
@@ -693,37 +768,54 @@ export function createInspectToolDefinition(deps = {}) {
693
768
  throw new Error("inspect coordinator supports view=status or view=runs.");
694
769
  }
695
770
  const session = requireContextSessionId(ctx, "inspect coordinator");
696
- const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
697
- .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
698
- .filter((run) => run.ownerId === session);
771
+ const allRuns = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
772
+ const runs = allRuns.filter((run) => run.ownerId === session);
773
+ const sessionSummary = summarizeOtherSessions(session, allRuns);
699
774
  return {
700
775
  content: [
701
776
  {
702
777
  type: "text",
703
- text: maybeJsonText({ session, runs }, input.verbose === true, compactSessionRuns(session, runs)),
778
+ text: maybeJsonText({ session, runs, ...sessionSummary }, input.verbose === true, compactSessionRuns(session, runs, sessionSummary)),
704
779
  },
705
780
  ],
706
- details: { session, runs },
781
+ details: { session, runs, ...sessionSummary },
707
782
  };
708
783
  }
709
784
  if (address.kind === "session") {
710
785
  if (view !== "status" && view !== "runs") {
711
786
  throw new Error("inspect session:<id> supports view=status or view=runs.");
712
787
  }
713
- const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
714
- .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
715
- .filter((run) => address.value === "all" || run.ownerId === address.value);
788
+ const allRuns = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
789
+ const runs = allRuns.filter((run) => address.value === "all" || run.ownerId === address.value);
790
+ const sessionSummary = address.value === "all"
791
+ ? {}
792
+ : summarizeOtherSessions(address.value || "", allRuns);
716
793
  return {
717
794
  content: [
718
795
  {
719
796
  type: "text",
720
- text: maybeJsonText({ session: address.value, runs }, input.verbose === true, compactSessionRuns(address.value || "", runs)),
797
+ text: maybeJsonText({ session: address.value, runs, ...sessionSummary }, input.verbose === true, compactSessionRuns(address.value || "", runs, sessionSummary)),
721
798
  },
722
799
  ],
723
- details: { session: address.value, runs },
800
+ details: { session: address.value, runs, ...sessionSummary },
724
801
  };
725
802
  }
726
803
  if (address.kind === "tool" && address.value) {
804
+ if (address.value === "pi-actors") {
805
+ if (view !== "status") {
806
+ throw new Error("inspect tool:pi-actors supports view=status.");
807
+ }
808
+ const details = getPiActorsRuntimeStatus();
809
+ return {
810
+ content: [
811
+ {
812
+ type: "text",
813
+ text: maybeJsonText(details, input.verbose === true, compactPiActorsRuntimeStatus(details)),
814
+ },
815
+ ],
816
+ details,
817
+ };
818
+ }
727
819
  if (view !== "status" && view !== "schema") {
728
820
  throw new Error("inspect tool:<name> supports view=status or view=schema.");
729
821
  }
@@ -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.2
5
+ version: 0.27.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -41,7 +41,7 @@ Trusted local capability
41
41
  - **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
42
42
  - **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
43
43
  - **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
44
- - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`.
44
+ - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
45
45
  - **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
46
46
  - **Mailbox**: public interaction contract: message types the actor accepts/emits.
47
47
  - **Artifact**: named durable output path declared by a recipe/run.
@@ -102,6 +102,7 @@ Check `inspect view=mailbox` before domain-specific messages.
102
102
  { "target": "room:repo-health", "view": "roster" }
103
103
  { "target": "room:repo-health", "view": "contacts" }
104
104
  { "target": "room:repo-health", "view": "previews" }
105
+ { "target": "tool:pi-actors", "view": "status" }
105
106
  { "target": "tool:music_player", "view": "status" }
106
107
  { "target": "recipes", "view": "status" }
107
108
  { "target": "coordinator", "view": "status" }
@@ -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.2
5
+ version: 0.27.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -19,15 +19,18 @@ Because the user recipe directory is sticky agent muscle memory, runtime launche
19
19
 
20
20
  `register_tool` is the preferred agent-facing mutation API. It creates, updates, and deletes recipe files in `~/.pi/agent/recipes`; agents do not need to edit the files directly for normal registration. Direct file edits are still valid for operators and advanced agents. Runtime behavior is reactive: file creation, deletion, or edits in the user recipe root trigger validation and tool-set refresh, with invalid recipes surfaced as diagnostics rather than silently ignored.
21
21
 
22
- Inspect the discovered registry with:
22
+ Inspect the loaded pi-actors runtime and discovered registry with:
23
23
 
24
24
  ```text
25
+ inspect target=tool:pi-actors view=status
25
26
  inspect target=recipes view=status
26
27
  inspect target=recipes view=doctor
27
28
  inspect target=recipes view=summary verbose=true
28
29
  ```
29
30
 
30
- The summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority candidate paths when a broken or disabled higher-priority recipe masks a fallback.
31
+ `tool:pi-actors` is a reserved runtime-status actor: it reports the loaded package version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available. Use it after reloads to confirm which extension code is actually live.
32
+
33
+ The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority candidate paths when a broken or disabled higher-priority recipe masks a fallback.
31
34
 
32
35
  ## Registering Tools
33
36
 
package/lib/tools.ts CHANGED
@@ -4,6 +4,10 @@
4
4
  * Owns generated runtime tool schemas and the register_tool management tool schema
5
5
  */
6
6
 
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+
7
11
  import * as ActorMessages from "./actor-messages.ts";
8
12
  import * as ActorRooms from "./actor-rooms.ts";
9
13
  import * as AsyncRuns from "./async-runs.ts";
@@ -424,12 +428,30 @@ function compactActorFiles(status: Record<string, unknown>): string {
424
428
  return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
425
429
  }
426
430
 
431
+ function summarizeOtherSessions(
432
+ currentSession: string,
433
+ allRuns: Array<Record<string, unknown>>,
434
+ ): Record<string, unknown> {
435
+ const otherRuns = allRuns.filter(
436
+ (run) => run.ownerId && run.ownerId !== currentSession,
437
+ );
438
+ return {
439
+ other_runs: otherRuns.length,
440
+ other_sessions: new Set(otherRuns.map((run) => run.ownerId)).size,
441
+ };
442
+ }
443
+
427
444
  function compactSessionRuns(
428
445
  session: string,
429
446
  runs: Array<Record<string, unknown>>,
447
+ summary: Record<string, unknown> = {},
430
448
  ): string {
431
- if (runs.length === 0) return `\nsession=${session} runs=0`;
432
- return `\nsession=${session} runs=${runs.length}\n${runs
449
+ const suffix = [
450
+ summary.other_sessions !== undefined ? `other_sessions=${String(summary.other_sessions)}` : "",
451
+ summary.other_runs !== undefined ? `other_runs=${String(summary.other_runs)}` : "",
452
+ ].filter(Boolean).join(" ");
453
+ if (runs.length === 0) return `\nsession=${session} runs=0${suffix ? ` ${suffix}` : ""}`;
454
+ return `\nsession=${session} runs=${runs.length}${suffix ? ` ${suffix}` : ""}\n${runs
433
455
  .map((run) => {
434
456
  const tokens = [
435
457
  `run=${String(run.run ?? "")}`,
@@ -443,6 +465,39 @@ function compactSessionRuns(
443
465
  .join("\n")}`;
444
466
  }
445
467
 
468
+ function getPiActorsRuntimeStatus(): Record<string, unknown> {
469
+ const packagedRecipeRoot = Paths.getPackagedRecipeRoot();
470
+ const packageRoot = dirname(packagedRecipeRoot);
471
+ const packageJsonPath = join(packageRoot, "package.json");
472
+ const packageJson = existsSync(packageJsonPath)
473
+ ? JSON.parse(readFileSync(packageJsonPath, "utf8")) as Record<string, unknown>
474
+ : {};
475
+ let git_commit: string | undefined;
476
+ try {
477
+ git_commit = execFileSync("git", ["-C", packageRoot, "rev-parse", "--short", "HEAD"], {
478
+ encoding: "utf8",
479
+ stdio: ["ignore", "pipe", "ignore"],
480
+ }).trim();
481
+ } catch {
482
+ git_commit = undefined;
483
+ }
484
+ const entrypoint = new URL(import.meta.url).pathname;
485
+ return {
486
+ entrypoint,
487
+ git_commit,
488
+ mode: entrypoint.includes("/dist/") ? "dist" : "source",
489
+ package_name: packageJson.name ?? "@llblab/pi-actors",
490
+ package_root: packageRoot,
491
+ recipe_root: Paths.getRecipeRoot(),
492
+ packaged_recipe_root: packagedRecipeRoot,
493
+ version: packageJson.version ?? "unknown",
494
+ };
495
+ }
496
+
497
+ function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
498
+ return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
499
+ }
500
+
446
501
  function compactToolActor(name: string, tool: Record<string, unknown>): string {
447
502
  const parameters = asRecord(tool.parameters);
448
503
  const required = Array.isArray(parameters.required)
@@ -535,6 +590,7 @@ function compactActorMessageResult(
535
590
  `message=${result.sent === true || result.stopped === true ? "sent" : "not_sent"}`,
536
591
  ];
537
592
  if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
593
+ if (result.queued === true) tokens.push("queued=true");
538
594
  if (result.control) tokens.push(`control=${String(result.control)}`);
539
595
  if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
540
596
  if (result.message_count !== undefined)
@@ -546,6 +602,9 @@ function compactActorMessageResult(
546
602
  if (result.stopped === true) tokens.push("stopped=true");
547
603
  if (result.signal) tokens.push(`signal=${String(result.signal)}`);
548
604
  if (result.invoked === true) tokens.push("invoked=true");
605
+ if (result.delivery_error) {
606
+ tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
607
+ }
549
608
  return `\n${tokens.join(" ")}`;
550
609
  }
551
610
 
@@ -695,7 +754,24 @@ async function routeBranchEnvelope(
695
754
  recipient,
696
755
  branchMessage,
697
756
  );
698
- return AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
757
+ try {
758
+ return await AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
759
+ } catch (error) {
760
+ const record = error && typeof error === "object" ? (error as Record<string, unknown>) : {};
761
+ if (record.queued === true) {
762
+ return {
763
+ control_path: record.control_path,
764
+ control_type: record.control_type,
765
+ delivery_error: record.delivery_error ?? (error instanceof Error ? error.message : String(error)),
766
+ inbox_id: record.inbox_id,
767
+ queued: true,
768
+ run: runId,
769
+ sent: false,
770
+ state_dir: stateDir,
771
+ };
772
+ }
773
+ throw error;
774
+ }
699
775
  }
700
776
 
701
777
  function getRoomMulticastRecipients(
@@ -850,8 +926,17 @@ function assertRunAccessibleToContext(
850
926
  const status = AsyncRuns.getRunStatus(runId);
851
927
  const sessionId = getContextSessionId(ctx);
852
928
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
853
- throw new Error(
854
- `run:${runId} is owned by session:${status.ownerId}; current session is ${sessionId}.`,
929
+ throw Object.assign(
930
+ new Error(
931
+ `run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`,
932
+ ),
933
+ {
934
+ current_session: sessionId,
935
+ hint: `inspect target=session:${status.ownerId} view=status`,
936
+ owner_session: status.ownerId,
937
+ reason: "session_mismatch",
938
+ run: runId,
939
+ },
855
940
  );
856
941
  }
857
942
  return status;
@@ -940,24 +1025,24 @@ export function createInspectToolDefinition<TContext = unknown>(
940
1025
  );
941
1026
  }
942
1027
  const session = requireContextSessionId(ctx, "inspect coordinator");
943
- const runs = AsyncRuns.listRuns(
1028
+ const allRuns = AsyncRuns.listRuns(
944
1029
  undefined,
945
1030
  typeof input.status === "string" ? input.status : undefined,
946
- )
947
- .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
948
- .filter((run) => run.ownerId === session);
1031
+ ).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
1032
+ const runs = allRuns.filter((run) => run.ownerId === session);
1033
+ const sessionSummary = summarizeOtherSessions(session, allRuns);
949
1034
  return {
950
1035
  content: [
951
1036
  {
952
1037
  type: "text" as const,
953
1038
  text: maybeJsonText(
954
- { session, runs },
1039
+ { session, runs, ...sessionSummary },
955
1040
  input.verbose === true,
956
- compactSessionRuns(session, runs),
1041
+ compactSessionRuns(session, runs, sessionSummary),
957
1042
  ),
958
1043
  },
959
1044
  ],
960
- details: { session, runs },
1045
+ details: { session, runs, ...sessionSummary },
961
1046
  };
962
1047
  }
963
1048
  if (address.kind === "session") {
@@ -966,29 +1051,50 @@ export function createInspectToolDefinition<TContext = unknown>(
966
1051
  "inspect session:<id> supports view=status or view=runs.",
967
1052
  );
968
1053
  }
969
- const runs = AsyncRuns.listRuns(
1054
+ const allRuns = AsyncRuns.listRuns(
970
1055
  undefined,
971
1056
  typeof input.status === "string" ? input.status : undefined,
972
- )
973
- .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
974
- .filter(
975
- (run) => address.value === "all" || run.ownerId === address.value,
976
- );
1057
+ ).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
1058
+ const runs = allRuns.filter(
1059
+ (run) => address.value === "all" || run.ownerId === address.value,
1060
+ );
1061
+ const sessionSummary = address.value === "all"
1062
+ ? {}
1063
+ : summarizeOtherSessions(address.value || "", allRuns);
977
1064
  return {
978
1065
  content: [
979
1066
  {
980
1067
  type: "text" as const,
981
1068
  text: maybeJsonText(
982
- { session: address.value, runs },
1069
+ { session: address.value, runs, ...sessionSummary },
983
1070
  input.verbose === true,
984
- compactSessionRuns(address.value || "", runs),
1071
+ compactSessionRuns(address.value || "", runs, sessionSummary),
985
1072
  ),
986
1073
  },
987
1074
  ],
988
- details: { session: address.value, runs },
1075
+ details: { session: address.value, runs, ...sessionSummary },
989
1076
  };
990
1077
  }
991
1078
  if (address.kind === "tool" && address.value) {
1079
+ if (address.value === "pi-actors") {
1080
+ if (view !== "status") {
1081
+ throw new Error("inspect tool:pi-actors supports view=status.");
1082
+ }
1083
+ const details = getPiActorsRuntimeStatus();
1084
+ return {
1085
+ content: [
1086
+ {
1087
+ type: "text" as const,
1088
+ text: maybeJsonText(
1089
+ details,
1090
+ input.verbose === true,
1091
+ compactPiActorsRuntimeStatus(details),
1092
+ ),
1093
+ },
1094
+ ],
1095
+ details,
1096
+ };
1097
+ }
992
1098
  if (view !== "status" && view !== "schema") {
993
1099
  throw new Error(
994
1100
  "inspect tool:<name> supports view=status or view=schema.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
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.2
5
+ version: 0.27.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -41,7 +41,7 @@ Trusted local capability
41
41
  - **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
42
42
  - **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
43
43
  - **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
44
- - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`.
44
+ - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
45
45
  - **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
46
46
  - **Mailbox**: public interaction contract: message types the actor accepts/emits.
47
47
  - **Artifact**: named durable output path declared by a recipe/run.
@@ -102,6 +102,7 @@ Check `inspect view=mailbox` before domain-specific messages.
102
102
  { "target": "room:repo-health", "view": "roster" }
103
103
  { "target": "room:repo-health", "view": "contacts" }
104
104
  { "target": "room:repo-health", "view": "previews" }
105
+ { "target": "tool:pi-actors", "view": "status" }
105
106
  { "target": "tool:music_player", "view": "status" }
106
107
  { "target": "recipes", "view": "status" }
107
108
  { "target": "coordinator", "view": "status" }
@@ -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.2
5
+ version: 0.27.0
6
6
  ---
7
7
 
8
8
  # Swarm