@llblab/pi-actors 0.20.1 → 0.21.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
@@ -2,46 +2,6 @@
2
2
 
3
3
  ## Open Work
4
4
 
5
- ### Actor Rooms, Roster, and Cross-Branch Messaging
6
-
7
- - Priority: High.
8
- - Goal: Continue evolving actor communication without adding a second public messaging model.
9
- - Direction:
10
- - Evaluate whether room storage/routing should remain built into the tool adapter or move behind a dedicated non-LLM communication actor recipe/script, possibly singleton-scoped. Preserve the same public `room:<run>` address and envelope either way.
11
- - Treat the next backend decision as an evidence-backed experiment, not a rewrite: stress a real room/direct-message workload, compare the current file-backed adapter with a thin communication actor/helper, and record the decision.
12
- - Consider reducing direct file-backed state where it improves coherence: model room/roster state as actor-owned data structures served by helper scripts/actors, with files retained only for durable snapshots, recovery, artifacts, or audit logs.
13
- - Further storage changes should preserve the current burst/read/concurrency safeguards: branch communication snapshot writes are debounced, root snapshots stay current, roster files are not rewritten during bursts when only `last_seen` changes, room status inspection does not parse full timelines, branch-local inbox append/status rewrites are lock-guarded, and legacy no-ID branch inbox records can be claimed exactly once.
14
- - Prevent monolith drift: `actor-rooms.ts` may remain a thin adapter, but growing routing policy, subscription loops, fanout policy, or long-lived state ownership should move behind a focused communication helper/actor rather than accumulating in the tool adapter.
15
- - Exit:
16
- - Any backend/storage change preserves existing `spawn` / `message` / `inspect` semantics and room address compatibility.
17
- - A short decision note or changelog entry explains why the room backend stayed file-backed or moved behind a communication actor/helper.
18
-
19
- ### Graceful Actor Retirement
20
-
21
- - Priority: Medium.
22
- - Goal: Automatically retire coordinator/helper actors that were launched only to supervise a bounded worker tree once their dependent workers have finished.
23
- - Direction:
24
- - Build on the existing `retire_when: "children_terminal"` recipe/run metadata contract and observability retirement-candidate detection for ephemeral supervisors.
25
- - Treat auto-retirement as opt-in only; never infer it for arbitrary long-lived services, user tools, or persistent backlog implementers.
26
- - Extend candidate detection beyond current active command/proc-descendant gating to full observed child async-run state rather than log text: the supervisor may retire only when all launched child async runs are terminal and required artifacts/outbox events have been flushed.
27
- - Prefer graceful stop (`control.stop` / actor message) before process termination; escalate only after a bounded timeout and record the retirement event in run state.
28
- - Preserve manual `cancel` / `kill` semantics and make retirement visible through `inspect` / ambient observability.
29
- - Exit:
30
- - A packaged coordinator recipe can launch worker actors, complete its coordination duties, and shut itself down automatically after the worker tree reaches terminal state.
31
- - Persistent services and implementer actors remain alive unless their recipe explicitly opts into retirement.
32
-
33
- ### Coordinator Strategy Boundary
34
-
35
- - Priority: Medium.
36
- - Goal: Keep the generic coordinator from becoming a second overloaded monolith as room/direct-message workflows mature.
37
- - Direction:
38
- - Split only at real pressure points: branch inbox claim/finalize helpers, participant execution, room transcript synthesis, and mode strategies are likely seams, but avoid cosmetic module churn.
39
- - Preserve the current principle that the locker stays generic/thin and all orchestration policy stays in coordinator strategy code or recipe composition.
40
- - Prefer reusable helper modules or small scripts only when at least two packaged workflows need the same behavior.
41
- - Exit:
42
- - Adding a new coordinator mode or packaged multi-agent workflow does not require editing unrelated mode logic.
43
- - Existing room-swarm, locker, and direct-branch-message tests still cover the extracted seams.
44
-
45
5
  ### Consensus-First Build Recipe
46
6
 
47
7
  - Priority: Medium.
@@ -57,18 +17,6 @@
57
17
  - A packaged recipe can reproduce the interactive-music-instrument workflow shape for another single-artifact task without copying the demo script.
58
18
  - Docs and skills point agents to the packaged recipe and explain when to choose it over a free-form room swarm.
59
19
 
60
- ### Actor OS Scenario Smoke Matrix
61
-
62
- - Priority: Medium.
63
- - Goal: Convert the 0.19.x actor-communication hardening into repeatable end-to-end scenario checks instead of relying on ad hoc demos.
64
- - Direction:
65
- - Cover one scenario each for shared room coordination, direct branch work delivery, branch inbox claim/handle/fail transitions, inspector navigation, recipe context injection, recipe persistence suggestion, and opt-in retirement candidate detection.
66
- - Keep scenarios local-first and bounded: fake `pi`/models where possible, no external services, no long sleeps, no broad golden transcripts.
67
- - Prefer packaged recipes and public `spawn` / `message` / `inspect` calls so the smoke matrix exercises the same surface agents use.
68
- - Exit:
69
- - A single validation command or documented test group verifies the actor OS behaviors that made 0.19.x production-useful.
70
- - The smoke matrix catches regressions in actor communication, recipe memory, and observability without requiring a manual swarm demo.
71
-
72
20
  ### Persistent Backlog Implementer Workflow
73
21
 
74
22
  - Priority: Medium.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.21.0: Native Windows Actor Control and Literate Recipes
6
+
7
+ - `[Async Runs]` Added a platform-adapted run-control path: Unix FIFO behavior remains backward-compatible, native Windows can target named-pipe run-control endpoints recorded in run state, and run message receipts still update events and inbox state through the same actor-message path.
8
+ - `[Async Runs]` Added Windows process-tree termination planning for cancel/kill through `taskkill`, while preserving Unix process-group signaling semantics.
9
+ - `[Scripts]` Migrated `locker.mjs` and coordinator locker calls to platform-adapted control metadata: Unix still uses `control.fifo`, while native Windows can use a deterministic named-pipe endpoint with the same message protocol.
10
+ - `[Branch Messages]` Added bounded branch-inbox terminal retention during status transitions, preserving active queued/claimed work while compacting older handled/failed records for long-lived branch runners.
11
+ - `[Recipe Discovery]` Tightened trust-boundary diagnostics so combined short shell/eval flags such as `bash -lc` and nested recipe command-template objects are surfaced, including the packaged validation wrapper's trusted shell boundary.
12
+ - `[Rooms]` Recorded the backend decision to keep the current file-backed room adapter until real workflows need live subscriptions/fanout or shared mutable state, backed by a mixed room/direct-branch workload regression.
13
+ - `[Recipes]` Added Markdown-authored recipe loading for `.md` files with frontmatter metadata and fenced executable recipe/template blocks, with same-id JSON shadowing Markdown in the same priority layer.
14
+ - `[Retirement]` Extended run summaries to discover nested child async-run state dirs, blocks opt-in retirement while nested children are still running, surfaces child/terminal child counts on candidates, and has the session watcher retire ready candidates with one graceful stop attempt plus owned cancellation fallback. Added an integration smoke where an idle supervisor stops after its nested child is terminal while a non-opt-in service remains running.
15
+ - `[Coordinator]` Consolidated direct branch inbox claim/finalize rewrites behind one locked mutation helper and moved room-swarm mode dispatch behind an explicit mode registry. Unknown coordinator modes now fail closed, and `pipeline-room-swarm` exposes the supported mode enum.
16
+ - `[Docs]` Documented the local Actor OS smoke matrix covered by `npm test`, spanning room coordination, direct branch delivery, inbox claim/handle transitions, inspector navigation, recipe context injection, persistence suggestions, and opt-in retirement smoke.
17
+ - `[Docs/Tests]` Documented native Windows support scope and added regression coverage for Windows endpoint metadata, mocked named-pipe sends, Windows process-control planning, unchanged Unix FIFO behavior, locker control metadata, branch inbox compaction, mixed room/direct workloads, Markdown recipe loading/discovery/validation, nested child-run retirement gating, and packaged recipe trust diagnostics.
18
+ - `[Package]` Bumped package metadata, lockfile metadata, and packaged skill metadata to `0.21.0` for the minor release.
19
+
20
+ ## 0.20.2: Installed Extension Entrypoint Hotfix
21
+
22
+ - `[Packaging]` Added a JavaScript extension entrypoint wrapper and changed package metadata to load `./index.js`, so npm-installed packages import compiled `dist/index.js` instead of asking Node to strip `index.ts` under `node_modules`. Source checkouts still fall back to `index.ts` before a local build exists.
23
+ - `[Build]` Extended the compiled runtime build to emit `dist/index.js` alongside `dist/lib/*.js`, keeping extension entrypoint imports and script runtime imports on the same installed-package path model.
24
+ - `[Rooms]` Fixed immediate room append results to report the true persisted room message count after long timelines instead of the default 40-message preview length; `appendRoomMessage`, existing-member room joins, and `getRoomStatus()` now share the same line-count helper.
25
+ - `[Tests]` Added installed-package coverage that imports the extension entrypoint from package metadata without TypeScript stripping, plus room-count regression coverage beyond the default preview limit.
26
+ - `[Package]` Bumped package metadata and packaged skill metadata to `0.20.2` for the hotfix release.
27
+
3
28
  ## 0.20.1: Installed Packaged Recipe Root Hotfix
4
29
 
5
30
  - `[Recipe Imports]` Fixed installed compiled runtime path resolution so bare user recipe imports can fall back to the packaged standard-library `recipes/` directory instead of looking for a non-existent `dist/recipes` directory.
package/README.md CHANGED
@@ -169,6 +169,7 @@ The persistent tool surface is file-discovered:
169
169
 
170
170
  ```text
171
171
  ~/.pi/agent/recipes/*.json
172
+ ~/.pi/agent/recipes/*.md
172
173
  ```
173
174
 
174
175
  That directory is operator-managed executable memory.
@@ -178,6 +179,7 @@ Rules:
178
179
  - User recipes in `~/.pi/agent/recipes/` are tools by location;
179
180
  - Recipe filenames define tool ids;
180
181
  - User recipes override same-name lower-priority recipes;
182
+ - Same-id JSON recipes shadow Markdown recipes in the same priority layer;
181
183
  - Packaged recipes are standard-library components, not automatically installed operator policy;
182
184
  - `register_tool` creates, updates, lists, or deletes user recipe files through the normal agent interface.
183
185
 
@@ -220,7 +222,7 @@ Templates support:
220
222
  - Retries, recovery, failure policy, delays, and guarded execution;
221
223
  - Async run values such as `{run_id}`, `{state_dir}`, `{actor_address}`, `{default_room}`, and `{communication_file}`.
222
224
 
223
- The template owns execution shape. The recipe owns saved metadata, defaults, imports, mailbox, and artifacts. The run actor owns detached lifecycle, state, messages, cancellation, and inspection. File-backed async recipes also provide child `pi -p` actors with a bounded JSONL recipe context bundle by default, including raw entry/import recipe records and a `"you_are_here": true` marker for the recipe node that launched the child. Set `"actor_context": false` or `"off"` in a recipe to suppress that context for minimal prompts.
225
+ The template owns execution shape. The recipe owns saved metadata, defaults, imports, mailbox, and artifacts. JSON is the canonical precise recipe format; Markdown recipes use frontmatter plus fenced `template`/`json recipe` blocks for literate authoring and compile into the same model. The run actor owns detached lifecycle, state, messages, cancellation, and inspection. File-backed async recipes also provide child `pi -p` actors with a bounded JSONL recipe context bundle by default, including raw entry/import recipe records and a `"you_are_here": true` marker for the recipe node that launched the child. Set `"actor_context": false` or `"off"` in a recipe to suppress that context for minimal prompts.
224
226
 
225
227
  ## Recipe Library
226
228
 
@@ -249,6 +251,10 @@ Use artifacts when outputs should survive context compression.
249
251
 
250
252
  Use mailbox declarations when an actor has a stable conversational surface.
251
253
 
254
+ ## Platform Support
255
+
256
+ Core actor state, inspection, foreground tools, and basic async runs are portable Node.js behavior. Run-local messaging and stop/kill use a platform adapter under the same `message` API: Unix-compatible recipes can use their existing local control endpoint, while native Windows recipes can expose a Windows-native endpoint in run state. Some packaged scripts still depend on Unix tools and are WSL/Linux/macOS-only until migrated; their public recipe surface should stay `spawn` / `message` / `inspect` either way.
257
+
252
258
  ## Safety Boundary
253
259
 
254
260
  `pi-actors` is local-first, not sandbox-first.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * pi-actors — actor runtime and persistent local tool registry for pi.
3
+ * Zones: composition root, pi agent, actor runtime
4
+ *
5
+ * Wraps command templates as callable pi tools, stores durable user tools as recipe files, and exposes actor orchestration across reloads and sessions.
6
+ */
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ export default function toolRegistryExtension(pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ /**
2
+ * pi-actors — actor runtime and persistent local tool registry for pi.
3
+ * Zones: composition root, pi agent, actor runtime
4
+ *
5
+ * Wraps command templates as callable pi tools, stores durable user tools as recipe files, and exposes actor orchestration across reloads and sessions.
6
+ */
7
+ import { existsSync, readdirSync, watch } from "node:fs";
8
+ import * as ActorInspectorTui from "./lib/actor-inspector-tui.js";
9
+ import * as AsyncRuns from "./lib/async-runs.js";
10
+ import * as CommandTemplates from "./lib/command-templates.js";
11
+ import * as Observability from "./lib/observability.js";
12
+ import * as Paths from "./lib/paths.js";
13
+ import * as Prompts from "./lib/prompts.js";
14
+ import * as Runtime from "./lib/runtime.js";
15
+ import * as Temp from "./lib/temp.js";
16
+ import * as Tools from "./lib/tools.js";
17
+ const CONFIG_PATH = Paths.getConfigPath();
18
+ const TEMP_DIR = Paths.getExtensionTmpDir();
19
+ const RUN_STATE_ROOT = Paths.getRunStateRoot();
20
+ const RESERVED_TOOL_NAMES = new Set([
21
+ "read",
22
+ "write",
23
+ "edit",
24
+ "bash",
25
+ "find",
26
+ "grep",
27
+ "ls",
28
+ "register_tool",
29
+ "message",
30
+ "spawn",
31
+ "inspect",
32
+ ]);
33
+ export default function toolRegistryExtension(pi) {
34
+ let runsAnimationInterval;
35
+ let runsNotifyTimeout;
36
+ let recipeReloadTimeout;
37
+ let recipeRootWatcher;
38
+ let stateRootWatcher;
39
+ const runDirWatchers = new Map();
40
+ const observedRuns = new Map();
41
+ const observedRunEventLines = new Map();
42
+ const retirementAttempts = new Set();
43
+ let runStatusFrame = 0;
44
+ let communicationWidgetVisible = false;
45
+ let actorInspectorRows = 12;
46
+ let actorInspectorChannels;
47
+ let actorInspectorMention;
48
+ let actorInspectorBranch;
49
+ let actorInspectorUnreadOnly = false;
50
+ let actorInspectorRoomLimitPerRun = 12;
51
+ let selectedInspectorSequence;
52
+ let recipeWatcherFailureNotified = false;
53
+ const getRunOwnerId = (ctx) => ctx.sessionManager.getSessionId();
54
+ const retireCandidateRuns = (ctx, summary) => {
55
+ void Observability.executeRunRetirements(summary, {
56
+ attempted: retirementAttempts,
57
+ cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
58
+ notify: (message, level) => ctx.ui.notify(message, level),
59
+ sendStop: (candidate) => AsyncRuns.sendRunMessage(candidate.stateDir, "stop"),
60
+ });
61
+ };
62
+ const updateRunUi = (ctx, notify = false) => {
63
+ const ownerId = getRunOwnerId(ctx);
64
+ const summary = Observability.summarizeRuns(undefined, ownerId);
65
+ const status = Observability.renderRunStatus(summary, runStatusFrame++);
66
+ ctx.ui.setStatus("zz-pi-actors-runs", status ? ctx.ui.theme.fg("dim", status) : undefined);
67
+ ctx.ui.setWidget("zz-pi-actors-comms", communicationWidgetVisible
68
+ ? () => {
69
+ const style = {
70
+ actor: (text) => ctx.ui.theme.fg("accent", text),
71
+ muted: (text) => ctx.ui.theme.fg("dim", text),
72
+ preview: (text) => ctx.ui.theme.fg("text", text),
73
+ stripe: (text) => text,
74
+ stripeAlt: (text) => ctx.ui.theme.bg("customMessageBg", text),
75
+ target: (text) => ctx.ui.theme.fg("success", text),
76
+ type: (text) => ctx.ui.theme.fg("warning", text),
77
+ };
78
+ return {
79
+ invalidate() { },
80
+ render(width) {
81
+ const previews = ActorInspectorTui.readActorInspectorPreviews(RUN_STATE_ROOT, actorInspectorRows, {
82
+ channels: actorInspectorChannels,
83
+ currentRunOnly: true,
84
+ branch: actorInspectorBranch,
85
+ mention: actorInspectorMention,
86
+ ownerId,
87
+ roomLimitPerRun: actorInspectorRoomLimitPerRun,
88
+ unreadOnly: actorInspectorUnreadOnly,
89
+ });
90
+ const rows = (selectedInspectorSequence !== undefined
91
+ ? ActorInspectorTui.renderInspectorItemView(previews, width, style, { sequence: selectedInspectorSequence })
92
+ : ActorInspectorTui.renderInspectorWidget(previews, width, style)) ?? [];
93
+ const run = previews[0]?.run;
94
+ const roster = run
95
+ ? ActorInspectorTui.renderInspectorRosterPanel(ActorInspectorTui.readActorInspectorRoster(RUN_STATE_ROOT, run), width, style)
96
+ : undefined;
97
+ return roster ? [...roster, ...rows] : rows;
98
+ },
99
+ };
100
+ }
101
+ : undefined, { placement: "belowEditor" });
102
+ const transitions = Observability.detectRunTransitions(observedRuns, summary);
103
+ const outboxEvents = Observability.detectRunOutboxEvents(observedRunEventLines, summary);
104
+ if (!notify)
105
+ return;
106
+ retireCandidateRuns(ctx, summary);
107
+ for (const transition of transitions) {
108
+ if (!Observability.shouldNotifyRunTransition(transition))
109
+ continue;
110
+ const text = Observability.formatRunTransitionMessage(transition);
111
+ const notificationType = Observability.getRunTransitionNotificationType(transition);
112
+ ctx.ui.notify(text, notificationType);
113
+ if (!Observability.shouldSendRunTransitionFollowUp(transition))
114
+ continue;
115
+ pi.sendMessage({
116
+ customType: "pi-actors-run",
117
+ content: text,
118
+ display: true,
119
+ details: transition,
120
+ }, { deliverAs: "followUp", triggerTurn: true });
121
+ }
122
+ Observability.pruneRunObservationState(observedRuns, observedRunEventLines, summary, transitions.map((transition) => transition.run));
123
+ for (const event of outboxEvents) {
124
+ if (!Observability.shouldNotifyRunOutboxEvent(event))
125
+ continue;
126
+ const text = Observability.formatRunOutboxMessage(event);
127
+ const notificationType = Observability.getRunOutboxNotificationType(event);
128
+ ctx.ui.notify(text, notificationType);
129
+ if (!Observability.shouldSendRunOutboxFollowUp(event))
130
+ continue;
131
+ pi.sendMessage({
132
+ customType: "pi-actors-run-message",
133
+ content: text,
134
+ display: true,
135
+ details: event,
136
+ }, { deliverAs: "followUp", triggerTurn: true });
137
+ }
138
+ };
139
+ const closeRunWatchers = () => {
140
+ stateRootWatcher?.close();
141
+ stateRootWatcher = undefined;
142
+ for (const watcher of runDirWatchers.values())
143
+ watcher.close();
144
+ runDirWatchers.clear();
145
+ if (runsNotifyTimeout)
146
+ clearTimeout(runsNotifyTimeout);
147
+ runsNotifyTimeout = undefined;
148
+ };
149
+ const scheduleRunEventUpdate = (ctx) => {
150
+ if (runsNotifyTimeout)
151
+ clearTimeout(runsNotifyTimeout);
152
+ runsNotifyTimeout = setTimeout(() => {
153
+ refreshRunWatchers(ctx);
154
+ updateRunUi(ctx, true);
155
+ }, 50);
156
+ runsNotifyTimeout.unref?.();
157
+ };
158
+ const watchRunDir = (ctx, stateDir) => {
159
+ if (runDirWatchers.has(stateDir) || !existsSync(stateDir))
160
+ return;
161
+ try {
162
+ const watcher = watch(stateDir, () => scheduleRunEventUpdate(ctx));
163
+ watcher.on("error", () => {
164
+ watcher.close();
165
+ runDirWatchers.delete(stateDir);
166
+ });
167
+ runDirWatchers.set(stateDir, watcher);
168
+ }
169
+ catch {
170
+ // Watching is best-effort; explicit inspect remains available.
171
+ }
172
+ };
173
+ function refreshRunWatchers(ctx) {
174
+ if (!existsSync(RUN_STATE_ROOT))
175
+ return;
176
+ if (!stateRootWatcher) {
177
+ try {
178
+ stateRootWatcher = watch(RUN_STATE_ROOT, () => scheduleRunEventUpdate(ctx));
179
+ stateRootWatcher.on("error", () => {
180
+ stateRootWatcher?.close();
181
+ stateRootWatcher = undefined;
182
+ });
183
+ }
184
+ catch {
185
+ // Watching is best-effort; explicit inspect remains available.
186
+ }
187
+ }
188
+ for (const entry of readdirSync(RUN_STATE_ROOT, { withFileTypes: true })) {
189
+ if (!entry.isDirectory())
190
+ continue;
191
+ watchRunDir(ctx, `${RUN_STATE_ROOT}/${entry.name}`);
192
+ }
193
+ }
194
+ const closeRecipeWatcher = () => {
195
+ recipeRootWatcher?.close();
196
+ recipeRootWatcher = undefined;
197
+ if (recipeReloadTimeout)
198
+ clearTimeout(recipeReloadTimeout);
199
+ recipeReloadTimeout = undefined;
200
+ };
201
+ const notifyRecipeWatcherFailure = (ctx) => {
202
+ if (recipeWatcherFailureNotified)
203
+ return;
204
+ recipeWatcherFailureNotified = true;
205
+ ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
206
+ };
207
+ const scheduleRecipeReload = (ctx) => {
208
+ recipeWatcherFailureNotified = false;
209
+ if (recipeReloadTimeout)
210
+ clearTimeout(recipeReloadTimeout);
211
+ recipeReloadTimeout = setTimeout(() => {
212
+ runtime.loadTools(ctx);
213
+ ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
214
+ }, 150);
215
+ recipeReloadTimeout.unref?.();
216
+ };
217
+ const watchRecipeRoot = (ctx) => {
218
+ const recipeRoot = Paths.getRecipeRoot();
219
+ if (recipeRootWatcher || !existsSync(recipeRoot))
220
+ return;
221
+ try {
222
+ recipeRootWatcher = watch(recipeRoot, () => scheduleRecipeReload(ctx));
223
+ recipeRootWatcher.on("error", () => {
224
+ recipeRootWatcher?.close();
225
+ recipeRootWatcher = undefined;
226
+ notifyRecipeWatcherFailure(ctx);
227
+ });
228
+ }
229
+ catch {
230
+ notifyRecipeWatcherFailure(ctx);
231
+ }
232
+ };
233
+ const actorToolDefinitions = new Map();
234
+ const runtime = Runtime.createAutoToolsRuntime({
235
+ configPath: CONFIG_PATH,
236
+ exec: CommandTemplates.execCommandTemplate,
237
+ getActiveTools: () => pi.getActiveTools(),
238
+ getAllTools: () => pi.getAllTools(),
239
+ registerTool: (definition) => {
240
+ actorToolDefinitions.set(definition.name, definition);
241
+ pi.registerTool(definition);
242
+ },
243
+ reservedToolNames: RESERVED_TOOL_NAMES,
244
+ setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
245
+ });
246
+ pi.on("session_start", async (_event, ctx) => {
247
+ await Temp.prepareExtensionTempDir(TEMP_DIR);
248
+ runtime.loadTools(ctx);
249
+ updateRunUi(ctx);
250
+ closeRunWatchers();
251
+ closeRecipeWatcher();
252
+ refreshRunWatchers(ctx);
253
+ watchRecipeRoot(ctx);
254
+ if (runsAnimationInterval)
255
+ clearInterval(runsAnimationInterval);
256
+ runsAnimationInterval = setInterval(() => updateRunUi(ctx, false), 1000);
257
+ runsAnimationInterval.unref?.();
258
+ });
259
+ pi.on("session_shutdown", async () => {
260
+ if (runsAnimationInterval)
261
+ clearInterval(runsAnimationInterval);
262
+ runsAnimationInterval = undefined;
263
+ closeRunWatchers();
264
+ closeRecipeWatcher();
265
+ });
266
+ pi.registerCommand("actors-inspector-toggle", {
267
+ description: "Toggle actor inspector widget; optional row count",
268
+ handler: async (args, ctx) => {
269
+ const raw = Array.isArray(args) ? args[0] : String(args ?? "");
270
+ if (String(raw).trim()) {
271
+ const rows = Number.parseInt(String(raw), 10);
272
+ if (!Number.isFinite(rows) || rows <= 0) {
273
+ ctx.ui.notify("Usage: /actors-inspector-toggle [rows] where rows > 0", "warning");
274
+ return;
275
+ }
276
+ actorInspectorRows = rows;
277
+ actorInspectorRoomLimitPerRun = rows;
278
+ selectedInspectorSequence = undefined;
279
+ communicationWidgetVisible = true;
280
+ updateRunUi(ctx);
281
+ ctx.ui.notify(`Actor inspector rows ${rows}`, "info");
282
+ return;
283
+ }
284
+ if (selectedInspectorSequence !== undefined) {
285
+ selectedInspectorSequence = undefined;
286
+ communicationWidgetVisible = true;
287
+ updateRunUi(ctx);
288
+ ctx.ui.notify("Actor inspector table", "info");
289
+ return;
290
+ }
291
+ if (communicationWidgetVisible) {
292
+ communicationWidgetVisible = false;
293
+ }
294
+ else {
295
+ actorInspectorRows = 12;
296
+ actorInspectorRoomLimitPerRun = 12;
297
+ communicationWidgetVisible = true;
298
+ }
299
+ updateRunUi(ctx);
300
+ ctx.ui.notify(`Actor inspector ${communicationWidgetVisible ? "shown" : "hidden"}`, "info");
301
+ },
302
+ });
303
+ pi.registerCommand("actors-inspector-filter", {
304
+ description: "Filter actor inspector rows: all, room, direct, broadcast, unread, branch <name>, mention <text>",
305
+ handler: async (args, ctx) => {
306
+ const parts = Array.isArray(args)
307
+ ? args.map(String)
308
+ : String(args ?? "").split(/\s+/);
309
+ const mode = (parts[0] ?? "").trim().toLowerCase();
310
+ if (!mode || mode === "all" || mode === "clear") {
311
+ actorInspectorChannels = undefined;
312
+ actorInspectorMention = undefined;
313
+ actorInspectorBranch = undefined;
314
+ actorInspectorUnreadOnly = false;
315
+ }
316
+ else if (mode === "room" || mode === "direct" || mode === "broadcast") {
317
+ actorInspectorChannels = [mode];
318
+ actorInspectorMention = undefined;
319
+ }
320
+ else if (mode === "unread") {
321
+ actorInspectorUnreadOnly = true;
322
+ }
323
+ else if (mode === "branch" || mode === "current-branch") {
324
+ const branch = parts.slice(1).join(" ").trim();
325
+ if (!branch) {
326
+ ctx.ui.notify(`Usage: /actors-inspector-filter ${mode} <branch-name>`, "warning");
327
+ return;
328
+ }
329
+ actorInspectorBranch = branch;
330
+ }
331
+ else if (mode === "mention") {
332
+ const mention = parts.slice(1).join(" ").trim();
333
+ if (!mention) {
334
+ ctx.ui.notify("Usage: /actors-inspector-filter mention <text>", "warning");
335
+ return;
336
+ }
337
+ actorInspectorChannels = undefined;
338
+ actorInspectorMention = mention;
339
+ }
340
+ else {
341
+ ctx.ui.notify("Usage: /actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|mention <text>", "warning");
342
+ return;
343
+ }
344
+ selectedInspectorSequence = undefined;
345
+ communicationWidgetVisible = true;
346
+ updateRunUi(ctx);
347
+ ctx.ui.notify(`Actor inspector filter ${mode || "all"}`, "info");
348
+ },
349
+ });
350
+ pi.registerCommand("actors-inspect", {
351
+ description: "Inspect actor message by visible number",
352
+ handler: async (args, ctx) => {
353
+ const raw = Array.isArray(args) ? args[0] : String(args ?? "");
354
+ const sequence = Number.parseInt(String(raw), 10);
355
+ if (!Number.isFinite(sequence) || sequence <= 0) {
356
+ ctx.ui.notify("Usage: /actors-inspect <number>", "warning");
357
+ return;
358
+ }
359
+ selectedInspectorSequence = sequence;
360
+ communicationWidgetVisible = true;
361
+ updateRunUi(ctx);
362
+ ctx.ui.notify(`Actor inspect item ${sequence}`, "info");
363
+ },
364
+ });
365
+ pi.on("before_agent_start", async (event) => ({
366
+ systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
367
+ }));
368
+ pi.registerTool(Tools.createRegisterToolDefinition({
369
+ configPath: CONFIG_PATH,
370
+ getActiveTools: () => pi.getActiveTools(),
371
+ getExternalToolConflict: runtime.getExternalToolConflict,
372
+ getTools: runtime.getTools,
373
+ notify: runtime.notify,
374
+ registerRuntimeTool: runtime.registerRuntimeTool,
375
+ reservedToolNames: RESERVED_TOOL_NAMES,
376
+ setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
377
+ }));
378
+ pi.registerTool(Tools.createSpawnToolDefinition());
379
+ pi.registerTool(Tools.createActorMessageToolDefinition({
380
+ getTool: (name) => actorToolDefinitions.get(name),
381
+ }));
382
+ pi.registerTool(Tools.createInspectToolDefinition({
383
+ getTool: (name) => actorToolDefinitions.get(name),
384
+ }));
385
+ }
@@ -67,6 +67,7 @@ export declare function readBranchInboxMessages(stateDir: string, run: string, a
67
67
  queued_at?: string;
68
68
  status?: string;
69
69
  }>;
70
+ export declare function getBranchInboxTerminalRetainLimit(): number;
70
71
  export declare function appendBranchInboxMessage(stateDir: string, run: string, address: string, message: ActorMessage): void;
71
72
  export declare function updateBranchInboxMessageStatus(stateDir: string, run: string, address: string, id: string, status: "claimed" | "handled" | "failed", metadata?: Record<string, unknown>): boolean;
72
73
  export declare function appendRoomMessage(stateDir: string, room: string, message: ActorMessage): RoomAppendResult;
@@ -9,6 +9,7 @@ import * as path from "node:path";
9
9
  const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
10
10
  const STATE_LOCK_TIMEOUT_MS = 5000;
11
11
  const DEFAULT_ROOM_MAX_MESSAGES = 10000;
12
+ const DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED = 2000;
12
13
  const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 250;
13
14
  function roomDir(stateDir, room) {
14
15
  return path.join(stateDir, "rooms", room);
@@ -151,6 +152,16 @@ function readJsonlLineCount(file) {
151
152
  fs.closeSync(fd);
152
153
  }
153
154
  }
155
+ function readRoomMessageCount(stateDir, room) {
156
+ try {
157
+ return readJsonlLineCount(messagesFile(stateDir, room));
158
+ }
159
+ catch (error) {
160
+ if (error.code === "ENOENT")
161
+ return 0;
162
+ throw error;
163
+ }
164
+ }
154
165
  function readJsonlTailLines(file, limit) {
155
166
  const lineLimit = Math.max(1, limit);
156
167
  const stat = fs.statSync(file);
@@ -265,6 +276,18 @@ export function readBranchInboxMessages(stateDir, run, address, limit = 40) {
265
276
  throw error;
266
277
  }
267
278
  }
279
+ export function getBranchInboxTerminalRetainLimit() {
280
+ const value = Number(process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "");
281
+ return Number.isInteger(value) && value >= 0
282
+ ? value
283
+ : DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED;
284
+ }
285
+ function compactBranchInboxMessages(messages) {
286
+ const retainTerminal = getBranchInboxTerminalRetainLimit();
287
+ const active = messages.filter((message) => message.status !== "handled" && message.status !== "failed");
288
+ const terminal = messages.filter((message) => message.status === "handled" || message.status === "failed");
289
+ return [...terminal.slice(-retainTerminal), ...active];
290
+ }
268
291
  export function appendBranchInboxMessage(stateDir, run, address, message) {
269
292
  const branch = branchIdFromAddress(address, run);
270
293
  if (!branch)
@@ -295,7 +318,8 @@ export function updateBranchInboxMessageStatus(stateDir, run, address, id, statu
295
318
  });
296
319
  if (!changed)
297
320
  return false;
298
- fs.writeFileSync(file, `${updated.map((message) => JSON.stringify(message)).join("\n")}\n`);
321
+ const compacted = compactBranchInboxMessages(updated);
322
+ fs.writeFileSync(file, `${compacted.map((message) => JSON.stringify(message)).join("\n")}\n`);
299
323
  return true;
300
324
  }
301
325
  finally {
@@ -318,7 +342,7 @@ export function appendRoomMessage(stateDir, room, message) {
318
342
  }
319
343
  }
320
344
  return {
321
- message_count: readRoomMessages(stateDir, room).length,
345
+ message_count: readRoomMessageCount(stateDir, room),
322
346
  room,
323
347
  roster_count: Object.keys(roster).length,
324
348
  sent: true,
@@ -361,14 +385,7 @@ export function readRoomMessagePreviews(stateDir, room, limit = 40) {
361
385
  }));
362
386
  }
363
387
  export function getRoomStatus(stateDir, room) {
364
- let messageCount = 0;
365
- try {
366
- messageCount = readJsonlLineCount(messagesFile(stateDir, room));
367
- }
368
- catch (error) {
369
- if (error.code !== "ENOENT")
370
- throw error;
371
- }
388
+ const messageCount = readRoomMessageCount(stateDir, room);
372
389
  const [last] = readRoomMessages(stateDir, room, 1);
373
390
  return {
374
391
  ...(last
@@ -388,7 +405,7 @@ export function ensureRoomMember(stateDir, run, room, address, body, summary) {
388
405
  const roster = readRoomRoster(stateDir, room);
389
406
  if (roster[address]) {
390
407
  return {
391
- message_count: readRoomMessages(stateDir, room).length,
408
+ message_count: readRoomMessageCount(stateDir, room),
392
409
  room,
393
410
  roster_count: Object.keys(roster).length,
394
411
  sent: true,
@@ -6,8 +6,13 @@
6
6
  import type { CommandTemplateFailureScope, CommandTemplateValue } from "./command-templates.ts";
7
7
  import * as RecipeReferences from "./recipe-references.ts";
8
8
  export type AsyncRunLaunchSource = "spawn" | "tool";
9
+ export interface AsyncRunControlEndpoint {
10
+ path: string;
11
+ type: "fifo" | "named-pipe";
12
+ }
9
13
  export interface AsyncRunStartParams {
10
14
  async?: boolean;
15
+ control?: AsyncRunControlEndpoint;
11
16
  file?: string;
12
17
  launch_source?: AsyncRunLaunchSource;
13
18
  name?: string;
@@ -72,6 +77,7 @@ export interface AsyncRunMeta {
72
77
  template: CommandTemplateValue;
73
78
  values: Record<string, unknown>;
74
79
  artifacts?: Record<string, string>;
80
+ control?: AsyncRunControlEndpoint;
75
81
  mailbox?: RecipeReferences.TemplateRecipeMailbox;
76
82
  recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
77
83
  retire_when?: "children_terminal";
@@ -96,6 +102,16 @@ export declare function appendRunOutboxEvent(runOrDir: string, event: {
96
102
  to?: string;
97
103
  type?: string;
98
104
  }): Record<string, unknown>;
99
- export declare function sendRunMessage(runOrDir: string, message: string): Record<string, unknown>;
105
+ export interface SendRunMessageOptions {
106
+ namedPipeSend?: (path: string, payload: string) => Promise<number>;
107
+ platform?: NodeJS.Platform;
108
+ }
109
+ export declare function sendRunMessage(runOrDir: string, message: string, options?: SendRunMessageOptions): Promise<Record<string, unknown>>;
110
+ export interface RunProcessSignalPlan {
111
+ args?: string[];
112
+ command?: string;
113
+ signalTarget: "processGroup" | "process" | "processTree";
114
+ }
115
+ export declare function getRunProcessSignalPlan(pid: number, signal: NodeJS.Signals, runtimePlatform?: NodeJS.Platform): RunProcessSignalPlan;
100
116
  export declare function cancelRun(runOrDir: string): Record<string, unknown>;
101
117
  export declare function killRun(runOrDir: string): Record<string, unknown>;