@llblab/pi-actors 0.27.0 → 0.28.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
@@ -228,7 +228,7 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
228
228
  ### M-12 Runtime and Session Observability UX
229
229
 
230
230
  - Priority: High.
231
- - Status: Planned.
231
+ - Status: Done.
232
232
  - Goal: Make reload/session/runtime mismatches visible without changing ownership or lifecycle policy.
233
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
234
  - Direction:
@@ -247,6 +247,52 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
247
247
  - Coordinator/session status exposes other-session counts without leaking unrelated run details by default.
248
248
  - Tests cover version inspection, session mismatch shape, and other-session count summaries.
249
249
 
250
+ ### M-13 Shadowed Recipe Launch Diagnostics
251
+
252
+ - Priority: High.
253
+ - Status: Done.
254
+ - Goal: Make broken user recipes that shadow packaged/ad hoc candidates obvious only at the moment a launch already fails.
255
+ - Why now: 0.26 dogfood found a broken `~/.pi/agent/recipes/actor-worker.json` shadowing the packaged `actor-worker`; Recipe Doctor exposed the evidence, but the launch path did not provide a direct hint.
256
+ - Direction:
257
+ - Treat shadowing as a normal, intentional override mechanism; do not warn on healthy shadowing.
258
+ - When recipe resolution or launch fails because the active user recipe is invalid/disabled and a lower-priority candidate exists, surface `reason=shadowed_invalid` or `reason=shadowed_disabled` where practical.
259
+ - Treat disabled template recipes as non-launchable so disabled shadowing fails consistently instead of silently executing.
260
+ - Include minimal compact tokens: active path, blocked candidate path, and `hint=inspect_recipes_doctor`.
261
+ - Keep remediation advisory only; do not auto-disable, delete, rewrite, or nag about user recipes.
262
+ - Acceptance:
263
+ - Healthy user overrides remain silent.
264
+ - Launch failures caused by invalid/disabled shadowing include a compact actionable hint.
265
+ - Verbose details expose the active broken recipe and blocked fallback candidate.
266
+ - Tests cover invalid and disabled user recipes shadowing a packaged candidate.
267
+
268
+ ### M-14 Session Mismatch Follow-through
269
+
270
+ - Priority: Medium.
271
+ - Status: Planned.
272
+ - Goal: Extend 0.27 structured session diagnostics consistently across room, branch, run, coordinator, and session workflows.
273
+ - Why now: M-12 established the shape; dogfood should now make every ownership denial equally actionable without relaxing ownership gates.
274
+ - Direction:
275
+ - Audit all session mismatch errors for consistent `reason`, owner/current session fields, and inspect-session hints.
276
+ - Keep read/write ownership policy unchanged.
277
+ - Update docs with session mismatch examples and recovery inspection paths.
278
+ - Acceptance:
279
+ - Room, branch, run, coordinator, and session denials share the same compact/verbose shape.
280
+ - Tests cover representative inspect and message paths.
281
+
282
+ ### M-15 Worker Stale-Claim Dogfood
283
+
284
+ - Priority: Medium.
285
+ - Status: Planned.
286
+ - Goal: Validate and harden actor-worker v2 stale-claim visibility under intentionally stale claimed branch messages.
287
+ - Why now: M-09 exposed `stale_claims`; real dogfood should verify the operator can diagnose stuck claimed work before adding recovery policy.
288
+ - Direction:
289
+ - Create deterministic stale claimed branch inbox fixtures or smoke tests.
290
+ - Verify `worker-status.json`, room events, and inspect surfaces make stale claims visible.
291
+ - Defer auto-recovery unless workflow evidence proves it is safe.
292
+ - Acceptance:
293
+ - Stale claims are reproducible and visible in worker status.
294
+ - Tests cover stale-claim counting without adding scheduler/broker policy.
295
+
250
296
  ## Explicitly Deferred
251
297
 
252
298
  These are valid ideas but not current focus. Reintroduce only with concrete evidence from real actor workflows.
@@ -263,5 +309,5 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
263
309
  ## Suggested Milestone Order
264
310
 
265
311
  ```text
266
- Next milestone: M-12 Runtime and Session Observability UX.
312
+ Next milestone: M-14 Session Mismatch Follow-through.
267
313
  ```
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.28.0: Shadowed Recipe Launch Diagnostics
6
+
7
+ - `[Spawn]` Added minimal shadowed-recipe diagnostics when a bare recipe launch already fails because an invalid or disabled higher-priority recipe blocks a lower-priority fallback; healthy recipe overrides remain silent.
8
+ - `[Async Runs]` Treat disabled template recipes as non-launchable so disabled shadowing fails consistently instead of silently executing.
9
+ - `[Docs]` Clarified the quiet shadowing contract, disabled recipe launch behavior, and recipe-doctor hint path.
10
+ - `[Backlog]` Reframed shadowed recipe launch diagnostics as diagnostic-on-failure only, preserving shadowing as an intentional user override mechanism without startup warnings or automatic remediation.
11
+
12
+ ## 0.27.1: Changelog and Backlog Hotfix
13
+
14
+ - `[Changelog]` Moved the 0.27.0 runtime/session observability notes out of `Unreleased` into a proper release section so published package history matches the npm/tag release.
15
+ - `[Backlog]` Marked runtime/session observability complete and added the next evidence-backed candidates for shadowed recipe launch UX, session mismatch follow-through, and worker stale-claim dogfood.
16
+
17
+ ## 0.27.0: Runtime and Session Observability UX
18
+
5
19
  - `[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
20
  - `[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
21
  - `[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.
@@ -164,6 +164,9 @@ function readRecipeFile(file) {
164
164
  if (!config) {
165
165
  throw new Error(`Template recipe must define template: ${path}`);
166
166
  }
167
+ if (config.disabled === true) {
168
+ throw new Error(`Template recipe is disabled: ${path}`);
169
+ }
167
170
  return {
168
171
  ...config,
169
172
  file: path,
@@ -46,5 +46,6 @@ export interface RecipeDiscoverySource {
46
46
  export declare function discoverRecipeSources(sources: RecipeDiscoverySource[]): RecipeDiscoveryResult;
47
47
  export declare function discoverRecipes(roots: string[]): RecipeDiscoveryResult;
48
48
  export declare function createRecipeIntegrityManifest(result: RecipeDiscoveryResult): RecipeIntegrityManifestEntry[];
49
+ export declare function getShadowedLaunchDiagnostic(result: RecipeDiscoveryResult, id: string): Record<string, unknown> | undefined;
49
50
  export declare function summarizeDiscovery(result: RecipeDiscoveryResult): Record<string, unknown>;
50
51
  export declare function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | undefined;
@@ -413,6 +413,19 @@ function recommendationForEntry(entry, activePath) {
413
413
  }
414
414
  return recommendation;
415
415
  }
416
+ export function getShadowedLaunchDiagnostic(result, id) {
417
+ const active = result.active.get(id.trim());
418
+ if (!active || active.shadows.length === 0)
419
+ return undefined;
420
+ if (!active.invalid && !active.disabled)
421
+ return undefined;
422
+ return {
423
+ active_path: active.path,
424
+ blocked_candidate: active.shadows[0],
425
+ hint: "inspect_recipes_doctor",
426
+ reason: active.invalid ? "shadowed_invalid" : "shadowed_disabled",
427
+ };
428
+ }
416
429
  export function summarizeDiscovery(result) {
417
430
  const recommendations = result.entries
418
431
  .map((entry) => recommendationForEntry(entry, result.active.get(entry.id)?.path))
package/dist/lib/tools.js CHANGED
@@ -572,6 +572,25 @@ function formatToolActorFailure(tool, message, params, error) {
572
572
  tool,
573
573
  });
574
574
  }
575
+ function shadowedRecipeLaunchDiagnostic(recipe) {
576
+ if (typeof recipe !== "string" || recipe.includes("/") || recipe.includes("~"))
577
+ return undefined;
578
+ const discovery = RecipeDiscovery.discoverRecipeSources([
579
+ { root: Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
580
+ { root: Paths.getPackagedRecipeRoot(), defaultTool: false },
581
+ ]);
582
+ return RecipeDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
583
+ }
584
+ function enhanceSpawnRecipeError(error, recipe) {
585
+ const diagnostic = shadowedRecipeLaunchDiagnostic(recipe);
586
+ if (!diagnostic)
587
+ return error instanceof Error ? error : new Error(String(error));
588
+ const original = error instanceof Error ? error.message : String(error);
589
+ return Object.assign(new Error(`${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_candidate=${diagnostic.blocked_candidate} hint=${diagnostic.hint}`), {
590
+ ...diagnostic,
591
+ original_error: original,
592
+ });
593
+ }
575
594
  function runIdFromActorAddress(address) {
576
595
  if (!address)
577
596
  return undefined;
@@ -654,30 +673,37 @@ export function createSpawnToolDefinition() {
654
673
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
655
674
  const input = asRecord(params);
656
675
  const runId = runIdFromActorAddress(typeof input.as === "string" ? input.as : undefined);
657
- const meta = AsyncRuns.startRun({
658
- file: typeof input.file === "string"
659
- ? input.file
660
- : typeof input.recipe === "string"
661
- ? input.recipe
662
- : undefined,
663
- launch_source: "spawn",
664
- ownerId: getRunOwnerId(ctx),
665
- run_id: runId,
666
- state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
667
- ...(input.template !== undefined
668
- ? {
669
- template: input.template,
670
- }
671
- : {}),
672
- values: asRecord(input.values),
673
- ...(input.artifacts &&
674
- typeof input.artifacts === "object" &&
675
- !Array.isArray(input.artifacts)
676
- ? {
677
- artifacts: input.artifacts,
678
- }
679
- : {}),
680
- }, ctx.cwd);
676
+ const recipe = typeof input.file === "string"
677
+ ? input.file
678
+ : typeof input.recipe === "string"
679
+ ? input.recipe
680
+ : undefined;
681
+ let meta;
682
+ try {
683
+ meta = AsyncRuns.startRun({
684
+ file: recipe,
685
+ launch_source: "spawn",
686
+ ownerId: getRunOwnerId(ctx),
687
+ run_id: runId,
688
+ state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
689
+ ...(input.template !== undefined
690
+ ? {
691
+ template: input.template,
692
+ }
693
+ : {}),
694
+ values: asRecord(input.values),
695
+ ...(input.artifacts &&
696
+ typeof input.artifacts === "object" &&
697
+ !Array.isArray(input.artifacts)
698
+ ? {
699
+ artifacts: input.artifacts,
700
+ }
701
+ : {}),
702
+ }, ctx.cwd);
703
+ }
704
+ catch (error) {
705
+ throw enhanceSpawnRecipeError(error, recipe);
706
+ }
681
707
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
682
708
  ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
683
709
  return {
@@ -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.27.0
5
+ version: 0.28.0
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.27.0
5
+ version: 0.28.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -105,7 +105,7 @@ Recipe priority only matters when two discovered recipes have the same filename
105
105
 
106
106
  The high-priority user recipe directory is also the default tool set: recipes placed there are agent tools by location. This preserves the old advantage of a tool-only registry because listing `~/.pi/agent/recipes` shows the operator-managed tool surface. Packaged and ad hoc recipes are recipe components by default; they become tools only when copied or registered into the agent recipe root.
107
107
 
108
- Higher-priority files shadow lower-priority files with the same basename. Within one priority layer, same-id JSON shadows Markdown because JSON is the canonical precise format. A highest-priority invalid recipe is still visible and blocks fallback so operators do not accidentally run packaged behavior when a user override is broken. A highest-priority recipe with `disabled: true` also blocks fallback and intentionally disables that id.
108
+ Higher-priority files shadow lower-priority files with the same basename. Within one priority layer, same-id JSON shadows Markdown because JSON is the canonical precise format. A highest-priority invalid recipe is still visible and blocks fallback so operators do not accidentally run packaged behavior when a user override is broken. A highest-priority recipe with `disabled: true` also blocks fallback, is not launchable, and intentionally disables that id. Healthy overrides are silent; failed bare-name launches caused by invalid or disabled shadowing include compact `reason=shadowed_invalid` or `reason=shadowed_disabled` diagnostics with the active path, blocked candidate, and recipe-doctor hint.
109
109
 
110
110
  ## Usage Metadata
111
111
 
@@ -32,6 +32,8 @@ inspect target=recipes view=summary verbose=true
32
32
 
33
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.
34
34
 
35
+ Routine shadowing is quiet. If a bare `spawn` recipe launch already fails because an invalid or `disabled: true` user recipe blocks a lower-priority candidate, the launch error adds compact tokens such as `reason=shadowed_invalid` or `reason=shadowed_disabled`, `active_path`, `blocked_candidate`, and `hint=inspect_recipes_doctor`.
36
+
35
37
  ## Registering Tools
36
38
 
37
39
  `register_tool` is the interactive API for listing, creating, updating, or deleting persistent tools. Call it without arguments to list registered tools.
package/lib/async-runs.ts CHANGED
@@ -318,6 +318,9 @@ function readRecipeFile(file: string): AsyncRunStartParams {
318
318
  if (!config) {
319
319
  throw new Error(`Template recipe must define template: ${path}`);
320
320
  }
321
+ if (config.disabled === true) {
322
+ throw new Error(`Template recipe is disabled: ${path}`);
323
+ }
321
324
  return {
322
325
  ...(config as AsyncRunStartParams),
323
326
  file: path,
@@ -473,7 +473,8 @@ function remediationForEntry(
473
473
  reason: blockedCandidate
474
474
  ? "invalid higher-priority recipe blocks a lower-priority candidate"
475
475
  : "recipe is invalid and cannot be exposed as a tool",
476
- action: "fix recipe syntax/config, or disable/delete/archive it to restore fallback",
476
+ action:
477
+ "fix recipe syntax/config, or disable/delete/archive it to restore fallback",
477
478
  };
478
479
  }
479
480
  if (entry.disabled && entry.active) {
@@ -486,7 +487,8 @@ function remediationForEntry(
486
487
  reason: blockedCandidate
487
488
  ? "disabled higher-priority recipe intentionally blocks a lower-priority candidate"
488
489
  : "recipe is disabled and not exposed as a tool",
489
- action: "keep disabled intentionally, re-enable, or delete/archive the file",
490
+ action:
491
+ "keep disabled intentionally, re-enable, or delete/archive the file",
490
492
  };
491
493
  }
492
494
  if (riskyDiagnostics.length > 0) {
@@ -496,7 +498,8 @@ function remediationForEntry(
496
498
  severity: "warning",
497
499
  path: entry.path,
498
500
  reason: riskyDiagnostics[0],
499
- action: "audit trusted command boundary; keep only if the recipe is local and intentional",
501
+ action:
502
+ "audit trusted command boundary; keep only if the recipe is local and intentional",
500
503
  };
501
504
  }
502
505
  if (entry.shadowed) {
@@ -557,6 +560,21 @@ function recommendationForEntry(
557
560
  return recommendation;
558
561
  }
559
562
 
563
+ export function getShadowedLaunchDiagnostic(
564
+ result: RecipeDiscoveryResult,
565
+ id: string,
566
+ ): Record<string, unknown> | undefined {
567
+ const active = result.active.get(id.trim());
568
+ if (!active || active.shadows.length === 0) return undefined;
569
+ if (!active.invalid && !active.disabled) return undefined;
570
+ return {
571
+ active_path: active.path,
572
+ blocked_candidate: active.shadows[0],
573
+ hint: "inspect_recipes_doctor",
574
+ reason: active.invalid ? "shadowed_invalid" : "shadowed_disabled",
575
+ };
576
+ }
577
+
560
578
  export function summarizeDiscovery(
561
579
  result: RecipeDiscoveryResult,
562
580
  ): Record<string, unknown> {
package/lib/tools.ts CHANGED
@@ -710,6 +710,33 @@ function formatToolActorFailure(
710
710
  );
711
711
  }
712
712
 
713
+ function shadowedRecipeLaunchDiagnostic(
714
+ recipe: unknown,
715
+ ): Record<string, unknown> | undefined {
716
+ if (typeof recipe !== "string" || recipe.includes("/") || recipe.includes("~"))
717
+ return undefined;
718
+ const discovery = RecipeDiscovery.discoverRecipeSources([
719
+ { root: Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
720
+ { root: Paths.getPackagedRecipeRoot(), defaultTool: false },
721
+ ]);
722
+ return RecipeDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
723
+ }
724
+
725
+ function enhanceSpawnRecipeError(error: unknown, recipe: unknown): Error {
726
+ const diagnostic = shadowedRecipeLaunchDiagnostic(recipe);
727
+ if (!diagnostic) return error instanceof Error ? error : new Error(String(error));
728
+ const original = error instanceof Error ? error.message : String(error);
729
+ return Object.assign(
730
+ new Error(
731
+ `${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_candidate=${diagnostic.blocked_candidate} hint=${diagnostic.hint}`,
732
+ ),
733
+ {
734
+ ...diagnostic,
735
+ original_error: original,
736
+ },
737
+ );
738
+ }
739
+
713
740
  function runIdFromActorAddress(
714
741
  address: string | undefined,
715
742
  ): string | undefined {
@@ -845,39 +872,45 @@ export function createSpawnToolDefinition<
845
872
  const runId = runIdFromActorAddress(
846
873
  typeof input.as === "string" ? input.as : undefined,
847
874
  );
848
- const meta = AsyncRuns.startRun(
849
- {
850
- file:
851
- typeof input.file === "string"
852
- ? input.file
853
- : typeof input.recipe === "string"
854
- ? input.recipe
855
- : undefined,
856
- launch_source: "spawn",
857
- ownerId: getRunOwnerId(ctx),
858
- run_id: runId,
859
- state_dir:
860
- typeof input.state_dir === "string" ? input.state_dir : undefined,
861
- ...(input.template !== undefined
862
- ? {
863
- template:
864
- input.template as AsyncRuns.AsyncRunStartParams["template"],
865
- }
866
- : {}),
867
- values: asRecord(input.values),
868
- ...(input.artifacts &&
869
- typeof input.artifacts === "object" &&
870
- !Array.isArray(input.artifacts)
871
- ? {
872
- artifacts: input.artifacts as Record<
873
- string,
874
- AsyncRuns.RunArtifactDeclaration
875
- >,
876
- }
877
- : {}),
878
- },
879
- ctx.cwd,
880
- );
875
+ const recipe =
876
+ typeof input.file === "string"
877
+ ? input.file
878
+ : typeof input.recipe === "string"
879
+ ? input.recipe
880
+ : undefined;
881
+ let meta: AsyncRuns.AsyncRunMeta;
882
+ try {
883
+ meta = AsyncRuns.startRun(
884
+ {
885
+ file: recipe,
886
+ launch_source: "spawn",
887
+ ownerId: getRunOwnerId(ctx),
888
+ run_id: runId,
889
+ state_dir:
890
+ typeof input.state_dir === "string" ? input.state_dir : undefined,
891
+ ...(input.template !== undefined
892
+ ? {
893
+ template:
894
+ input.template as AsyncRuns.AsyncRunStartParams["template"],
895
+ }
896
+ : {}),
897
+ values: asRecord(input.values),
898
+ ...(input.artifacts &&
899
+ typeof input.artifacts === "object" &&
900
+ !Array.isArray(input.artifacts)
901
+ ? {
902
+ artifacts: input.artifacts as Record<
903
+ string,
904
+ AsyncRuns.RunArtifactDeclaration
905
+ >,
906
+ }
907
+ : {}),
908
+ },
909
+ ctx.cwd,
910
+ );
911
+ } catch (error) {
912
+ throw enhanceSpawnRecipeError(error, recipe);
913
+ }
881
914
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
882
915
  ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
883
916
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.27.0",
3
+ "version": "0.28.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.27.0
5
+ version: 0.28.0
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.27.0
5
+ version: 0.28.0
6
6
  ---
7
7
 
8
8
  # Swarm