@llblab/pi-actors 0.20.0 → 0.20.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/BACKLOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## Open Work
4
4
 
5
+ ### Branch Inbox Retention and Transition Scaling
6
+
7
+ - Priority: Medium.
8
+ - Goal: Keep direct branch message queues reliable for long-lived interactive branch runners without unbounded rewrite amplification.
9
+ - Direction:
10
+ - Evaluate current whole-file branch inbox status rewrites under realistic long-lived direct-message workloads.
11
+ - Consider bounded retention, compaction, or append-only transition logs for `queued` / `claimed` / `handled` / `failed` state changes while preserving stable message IDs and exact-once claim semantics.
12
+ - Keep branch-local inbox append/status mutations lock-guarded and preserve inspector visibility for unread/current-branch filters.
13
+ - Exit:
14
+ - A documented decision or implementation explains how branch inboxes scale for persistent runners and proves existing direct-message semantics remain compatible.
15
+
16
+ ### Installed Recipe Trust Boundary Hardening
17
+
18
+ - Priority: Medium.
19
+ - Goal: Keep recipe-library growth local-first without letting operator muscle memory become an accidental sandbox bypass.
20
+ - Direction:
21
+ - Review packaged recipes, examples, and docs for destructive or external side effects and ensure they require explicit paths, typed args, narrow helper scripts, and clear operator gates.
22
+ - Keep warnings framed as diagnostics, not a security boundary.
23
+ - Prefer small audited helper scripts over broad shell templates when recipes touch files, processes, networks, or external services.
24
+ - Exit:
25
+ - A trust-boundary review confirms packaged recipes and docs preserve the current local-first/not-sandbox-first contract, with any needed hardening captured in tests or docs.
26
+
27
+ ### Direct Branch Message Consumption Semantics
28
+
29
+ - Priority: Medium.
30
+ - Goal: Make it impossible to misunderstand branch inbox delivery as universal active delivery without a consuming coordinator or runner protocol.
31
+ - Direction:
32
+ - Audit README, actor-message docs, async-run docs, actors skill, and recipe guidance for branch inbox wording.
33
+ - Clarify that direct branch messages are queued and become active work only when the relevant coordinator/runner claims, injects, handles, or fails them.
34
+ - Add or update a bounded smoke scenario that demonstrates queued direct messages, claim/handle transitions, and inspector visibility.
35
+ - Exit:
36
+ - Public docs and tests show both halves of direct branch delivery: durable branch-local queueing and explicit worker consumption semantics.
37
+
5
38
  ### Actor Rooms, Roster, and Cross-Branch Messaging
6
39
 
7
40
  - Priority: High.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.2: Installed Extension Entrypoint Hotfix
4
+
5
+ - `[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.
6
+ - `[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.
7
+ - `[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.
8
+ - `[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.
9
+ - `[Package]` Bumped package metadata and packaged skill metadata to `0.20.2` for the hotfix release.
10
+
11
+ ## 0.20.1: Installed Packaged Recipe Root Hotfix
12
+
13
+ - `[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.
14
+ - `[Tests]` Added installed-package validation coverage for a user recipe that imports a packaged recipe by bare name, preserving the documented priority order for user, adjacent, and packaged recipes.
15
+ - `[Package]` Bumped package metadata and packaged skill metadata to `0.20.1` for the hotfix release.
16
+
3
17
  ## 0.20.0: Compiled Runtime Entrypoints
4
18
 
5
19
  - `[Packaging]` Added a build step that emits compiled `dist/lib/*.js` and declaration files from the TypeScript runtime modules, with relative `.ts` imports rewritten to `.js` for installed package execution.
@@ -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,374 @@
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 CommandTemplates from "./lib/command-templates.js";
10
+ import * as Observability from "./lib/observability.js";
11
+ import * as Paths from "./lib/paths.js";
12
+ import * as Prompts from "./lib/prompts.js";
13
+ import * as Runtime from "./lib/runtime.js";
14
+ import * as Temp from "./lib/temp.js";
15
+ import * as Tools from "./lib/tools.js";
16
+ const CONFIG_PATH = Paths.getConfigPath();
17
+ const TEMP_DIR = Paths.getExtensionTmpDir();
18
+ const RUN_STATE_ROOT = Paths.getRunStateRoot();
19
+ const RESERVED_TOOL_NAMES = new Set([
20
+ "read",
21
+ "write",
22
+ "edit",
23
+ "bash",
24
+ "find",
25
+ "grep",
26
+ "ls",
27
+ "register_tool",
28
+ "message",
29
+ "spawn",
30
+ "inspect",
31
+ ]);
32
+ export default function toolRegistryExtension(pi) {
33
+ let runsAnimationInterval;
34
+ let runsNotifyTimeout;
35
+ let recipeReloadTimeout;
36
+ let recipeRootWatcher;
37
+ let stateRootWatcher;
38
+ const runDirWatchers = new Map();
39
+ const observedRuns = new Map();
40
+ const observedRunEventLines = new Map();
41
+ let runStatusFrame = 0;
42
+ let communicationWidgetVisible = false;
43
+ let actorInspectorRows = 12;
44
+ let actorInspectorChannels;
45
+ let actorInspectorMention;
46
+ let actorInspectorBranch;
47
+ let actorInspectorUnreadOnly = false;
48
+ let actorInspectorRoomLimitPerRun = 12;
49
+ let selectedInspectorSequence;
50
+ let recipeWatcherFailureNotified = false;
51
+ const getRunOwnerId = (ctx) => ctx.sessionManager.getSessionId();
52
+ const updateRunUi = (ctx, notify = false) => {
53
+ const ownerId = getRunOwnerId(ctx);
54
+ const summary = Observability.summarizeRuns(undefined, ownerId);
55
+ const status = Observability.renderRunStatus(summary, runStatusFrame++);
56
+ ctx.ui.setStatus("zz-pi-actors-runs", status ? ctx.ui.theme.fg("dim", status) : undefined);
57
+ ctx.ui.setWidget("zz-pi-actors-comms", communicationWidgetVisible
58
+ ? () => {
59
+ const style = {
60
+ actor: (text) => ctx.ui.theme.fg("accent", text),
61
+ muted: (text) => ctx.ui.theme.fg("dim", text),
62
+ preview: (text) => ctx.ui.theme.fg("text", text),
63
+ stripe: (text) => text,
64
+ stripeAlt: (text) => ctx.ui.theme.bg("customMessageBg", text),
65
+ target: (text) => ctx.ui.theme.fg("success", text),
66
+ type: (text) => ctx.ui.theme.fg("warning", text),
67
+ };
68
+ return {
69
+ invalidate() { },
70
+ render(width) {
71
+ const previews = ActorInspectorTui.readActorInspectorPreviews(RUN_STATE_ROOT, actorInspectorRows, {
72
+ channels: actorInspectorChannels,
73
+ currentRunOnly: true,
74
+ branch: actorInspectorBranch,
75
+ mention: actorInspectorMention,
76
+ ownerId,
77
+ roomLimitPerRun: actorInspectorRoomLimitPerRun,
78
+ unreadOnly: actorInspectorUnreadOnly,
79
+ });
80
+ const rows = (selectedInspectorSequence !== undefined
81
+ ? ActorInspectorTui.renderInspectorItemView(previews, width, style, { sequence: selectedInspectorSequence })
82
+ : ActorInspectorTui.renderInspectorWidget(previews, width, style)) ?? [];
83
+ const run = previews[0]?.run;
84
+ const roster = run
85
+ ? ActorInspectorTui.renderInspectorRosterPanel(ActorInspectorTui.readActorInspectorRoster(RUN_STATE_ROOT, run), width, style)
86
+ : undefined;
87
+ return roster ? [...roster, ...rows] : rows;
88
+ },
89
+ };
90
+ }
91
+ : undefined, { placement: "belowEditor" });
92
+ const transitions = Observability.detectRunTransitions(observedRuns, summary);
93
+ const outboxEvents = Observability.detectRunOutboxEvents(observedRunEventLines, summary);
94
+ if (!notify)
95
+ return;
96
+ for (const transition of transitions) {
97
+ if (!Observability.shouldNotifyRunTransition(transition))
98
+ continue;
99
+ const text = Observability.formatRunTransitionMessage(transition);
100
+ const notificationType = Observability.getRunTransitionNotificationType(transition);
101
+ ctx.ui.notify(text, notificationType);
102
+ if (!Observability.shouldSendRunTransitionFollowUp(transition))
103
+ continue;
104
+ pi.sendMessage({
105
+ customType: "pi-actors-run",
106
+ content: text,
107
+ display: true,
108
+ details: transition,
109
+ }, { deliverAs: "followUp", triggerTurn: true });
110
+ }
111
+ Observability.pruneRunObservationState(observedRuns, observedRunEventLines, summary, transitions.map((transition) => transition.run));
112
+ for (const event of outboxEvents) {
113
+ if (!Observability.shouldNotifyRunOutboxEvent(event))
114
+ continue;
115
+ const text = Observability.formatRunOutboxMessage(event);
116
+ const notificationType = Observability.getRunOutboxNotificationType(event);
117
+ ctx.ui.notify(text, notificationType);
118
+ if (!Observability.shouldSendRunOutboxFollowUp(event))
119
+ continue;
120
+ pi.sendMessage({
121
+ customType: "pi-actors-run-message",
122
+ content: text,
123
+ display: true,
124
+ details: event,
125
+ }, { deliverAs: "followUp", triggerTurn: true });
126
+ }
127
+ };
128
+ const closeRunWatchers = () => {
129
+ stateRootWatcher?.close();
130
+ stateRootWatcher = undefined;
131
+ for (const watcher of runDirWatchers.values())
132
+ watcher.close();
133
+ runDirWatchers.clear();
134
+ if (runsNotifyTimeout)
135
+ clearTimeout(runsNotifyTimeout);
136
+ runsNotifyTimeout = undefined;
137
+ };
138
+ const scheduleRunEventUpdate = (ctx) => {
139
+ if (runsNotifyTimeout)
140
+ clearTimeout(runsNotifyTimeout);
141
+ runsNotifyTimeout = setTimeout(() => {
142
+ refreshRunWatchers(ctx);
143
+ updateRunUi(ctx, true);
144
+ }, 50);
145
+ runsNotifyTimeout.unref?.();
146
+ };
147
+ const watchRunDir = (ctx, stateDir) => {
148
+ if (runDirWatchers.has(stateDir) || !existsSync(stateDir))
149
+ return;
150
+ try {
151
+ const watcher = watch(stateDir, () => scheduleRunEventUpdate(ctx));
152
+ watcher.on("error", () => {
153
+ watcher.close();
154
+ runDirWatchers.delete(stateDir);
155
+ });
156
+ runDirWatchers.set(stateDir, watcher);
157
+ }
158
+ catch {
159
+ // Watching is best-effort; explicit inspect remains available.
160
+ }
161
+ };
162
+ function refreshRunWatchers(ctx) {
163
+ if (!existsSync(RUN_STATE_ROOT))
164
+ return;
165
+ if (!stateRootWatcher) {
166
+ try {
167
+ stateRootWatcher = watch(RUN_STATE_ROOT, () => scheduleRunEventUpdate(ctx));
168
+ stateRootWatcher.on("error", () => {
169
+ stateRootWatcher?.close();
170
+ stateRootWatcher = undefined;
171
+ });
172
+ }
173
+ catch {
174
+ // Watching is best-effort; explicit inspect remains available.
175
+ }
176
+ }
177
+ for (const entry of readdirSync(RUN_STATE_ROOT, { withFileTypes: true })) {
178
+ if (!entry.isDirectory())
179
+ continue;
180
+ watchRunDir(ctx, `${RUN_STATE_ROOT}/${entry.name}`);
181
+ }
182
+ }
183
+ const closeRecipeWatcher = () => {
184
+ recipeRootWatcher?.close();
185
+ recipeRootWatcher = undefined;
186
+ if (recipeReloadTimeout)
187
+ clearTimeout(recipeReloadTimeout);
188
+ recipeReloadTimeout = undefined;
189
+ };
190
+ const notifyRecipeWatcherFailure = (ctx) => {
191
+ if (recipeWatcherFailureNotified)
192
+ return;
193
+ recipeWatcherFailureNotified = true;
194
+ ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
195
+ };
196
+ const scheduleRecipeReload = (ctx) => {
197
+ recipeWatcherFailureNotified = false;
198
+ if (recipeReloadTimeout)
199
+ clearTimeout(recipeReloadTimeout);
200
+ recipeReloadTimeout = setTimeout(() => {
201
+ runtime.loadTools(ctx);
202
+ ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
203
+ }, 150);
204
+ recipeReloadTimeout.unref?.();
205
+ };
206
+ const watchRecipeRoot = (ctx) => {
207
+ const recipeRoot = Paths.getRecipeRoot();
208
+ if (recipeRootWatcher || !existsSync(recipeRoot))
209
+ return;
210
+ try {
211
+ recipeRootWatcher = watch(recipeRoot, () => scheduleRecipeReload(ctx));
212
+ recipeRootWatcher.on("error", () => {
213
+ recipeRootWatcher?.close();
214
+ recipeRootWatcher = undefined;
215
+ notifyRecipeWatcherFailure(ctx);
216
+ });
217
+ }
218
+ catch {
219
+ notifyRecipeWatcherFailure(ctx);
220
+ }
221
+ };
222
+ const actorToolDefinitions = new Map();
223
+ const runtime = Runtime.createAutoToolsRuntime({
224
+ configPath: CONFIG_PATH,
225
+ exec: CommandTemplates.execCommandTemplate,
226
+ getActiveTools: () => pi.getActiveTools(),
227
+ getAllTools: () => pi.getAllTools(),
228
+ registerTool: (definition) => {
229
+ actorToolDefinitions.set(definition.name, definition);
230
+ pi.registerTool(definition);
231
+ },
232
+ reservedToolNames: RESERVED_TOOL_NAMES,
233
+ setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
234
+ });
235
+ pi.on("session_start", async (_event, ctx) => {
236
+ await Temp.prepareExtensionTempDir(TEMP_DIR);
237
+ runtime.loadTools(ctx);
238
+ updateRunUi(ctx);
239
+ closeRunWatchers();
240
+ closeRecipeWatcher();
241
+ refreshRunWatchers(ctx);
242
+ watchRecipeRoot(ctx);
243
+ if (runsAnimationInterval)
244
+ clearInterval(runsAnimationInterval);
245
+ runsAnimationInterval = setInterval(() => updateRunUi(ctx, false), 1000);
246
+ runsAnimationInterval.unref?.();
247
+ });
248
+ pi.on("session_shutdown", async () => {
249
+ if (runsAnimationInterval)
250
+ clearInterval(runsAnimationInterval);
251
+ runsAnimationInterval = undefined;
252
+ closeRunWatchers();
253
+ closeRecipeWatcher();
254
+ });
255
+ pi.registerCommand("actors-inspector-toggle", {
256
+ description: "Toggle actor inspector widget; optional row count",
257
+ handler: async (args, ctx) => {
258
+ const raw = Array.isArray(args) ? args[0] : String(args ?? "");
259
+ if (String(raw).trim()) {
260
+ const rows = Number.parseInt(String(raw), 10);
261
+ if (!Number.isFinite(rows) || rows <= 0) {
262
+ ctx.ui.notify("Usage: /actors-inspector-toggle [rows] where rows > 0", "warning");
263
+ return;
264
+ }
265
+ actorInspectorRows = rows;
266
+ actorInspectorRoomLimitPerRun = rows;
267
+ selectedInspectorSequence = undefined;
268
+ communicationWidgetVisible = true;
269
+ updateRunUi(ctx);
270
+ ctx.ui.notify(`Actor inspector rows ${rows}`, "info");
271
+ return;
272
+ }
273
+ if (selectedInspectorSequence !== undefined) {
274
+ selectedInspectorSequence = undefined;
275
+ communicationWidgetVisible = true;
276
+ updateRunUi(ctx);
277
+ ctx.ui.notify("Actor inspector table", "info");
278
+ return;
279
+ }
280
+ if (communicationWidgetVisible) {
281
+ communicationWidgetVisible = false;
282
+ }
283
+ else {
284
+ actorInspectorRows = 12;
285
+ actorInspectorRoomLimitPerRun = 12;
286
+ communicationWidgetVisible = true;
287
+ }
288
+ updateRunUi(ctx);
289
+ ctx.ui.notify(`Actor inspector ${communicationWidgetVisible ? "shown" : "hidden"}`, "info");
290
+ },
291
+ });
292
+ pi.registerCommand("actors-inspector-filter", {
293
+ description: "Filter actor inspector rows: all, room, direct, broadcast, unread, branch <name>, mention <text>",
294
+ handler: async (args, ctx) => {
295
+ const parts = Array.isArray(args)
296
+ ? args.map(String)
297
+ : String(args ?? "").split(/\s+/);
298
+ const mode = (parts[0] ?? "").trim().toLowerCase();
299
+ if (!mode || mode === "all" || mode === "clear") {
300
+ actorInspectorChannels = undefined;
301
+ actorInspectorMention = undefined;
302
+ actorInspectorBranch = undefined;
303
+ actorInspectorUnreadOnly = false;
304
+ }
305
+ else if (mode === "room" || mode === "direct" || mode === "broadcast") {
306
+ actorInspectorChannels = [mode];
307
+ actorInspectorMention = undefined;
308
+ }
309
+ else if (mode === "unread") {
310
+ actorInspectorUnreadOnly = true;
311
+ }
312
+ else if (mode === "branch" || mode === "current-branch") {
313
+ const branch = parts.slice(1).join(" ").trim();
314
+ if (!branch) {
315
+ ctx.ui.notify(`Usage: /actors-inspector-filter ${mode} <branch-name>`, "warning");
316
+ return;
317
+ }
318
+ actorInspectorBranch = branch;
319
+ }
320
+ else if (mode === "mention") {
321
+ const mention = parts.slice(1).join(" ").trim();
322
+ if (!mention) {
323
+ ctx.ui.notify("Usage: /actors-inspector-filter mention <text>", "warning");
324
+ return;
325
+ }
326
+ actorInspectorChannels = undefined;
327
+ actorInspectorMention = mention;
328
+ }
329
+ else {
330
+ ctx.ui.notify("Usage: /actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|mention <text>", "warning");
331
+ return;
332
+ }
333
+ selectedInspectorSequence = undefined;
334
+ communicationWidgetVisible = true;
335
+ updateRunUi(ctx);
336
+ ctx.ui.notify(`Actor inspector filter ${mode || "all"}`, "info");
337
+ },
338
+ });
339
+ pi.registerCommand("actors-inspect", {
340
+ description: "Inspect actor message by visible number",
341
+ handler: async (args, ctx) => {
342
+ const raw = Array.isArray(args) ? args[0] : String(args ?? "");
343
+ const sequence = Number.parseInt(String(raw), 10);
344
+ if (!Number.isFinite(sequence) || sequence <= 0) {
345
+ ctx.ui.notify("Usage: /actors-inspect <number>", "warning");
346
+ return;
347
+ }
348
+ selectedInspectorSequence = sequence;
349
+ communicationWidgetVisible = true;
350
+ updateRunUi(ctx);
351
+ ctx.ui.notify(`Actor inspect item ${sequence}`, "info");
352
+ },
353
+ });
354
+ pi.on("before_agent_start", async (event) => ({
355
+ systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
356
+ }));
357
+ pi.registerTool(Tools.createRegisterToolDefinition({
358
+ configPath: CONFIG_PATH,
359
+ getActiveTools: () => pi.getActiveTools(),
360
+ getExternalToolConflict: runtime.getExternalToolConflict,
361
+ getTools: runtime.getTools,
362
+ notify: runtime.notify,
363
+ registerRuntimeTool: runtime.registerRuntimeTool,
364
+ reservedToolNames: RESERVED_TOOL_NAMES,
365
+ setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
366
+ }));
367
+ pi.registerTool(Tools.createSpawnToolDefinition());
368
+ pi.registerTool(Tools.createActorMessageToolDefinition({
369
+ getTool: (name) => actorToolDefinitions.get(name),
370
+ }));
371
+ pi.registerTool(Tools.createInspectToolDefinition({
372
+ getTool: (name) => actorToolDefinitions.get(name),
373
+ }));
374
+ }
@@ -151,6 +151,16 @@ function readJsonlLineCount(file) {
151
151
  fs.closeSync(fd);
152
152
  }
153
153
  }
154
+ function readRoomMessageCount(stateDir, room) {
155
+ try {
156
+ return readJsonlLineCount(messagesFile(stateDir, room));
157
+ }
158
+ catch (error) {
159
+ if (error.code === "ENOENT")
160
+ return 0;
161
+ throw error;
162
+ }
163
+ }
154
164
  function readJsonlTailLines(file, limit) {
155
165
  const lineLimit = Math.max(1, limit);
156
166
  const stat = fs.statSync(file);
@@ -318,7 +328,7 @@ export function appendRoomMessage(stateDir, room, message) {
318
328
  }
319
329
  }
320
330
  return {
321
- message_count: readRoomMessages(stateDir, room).length,
331
+ message_count: readRoomMessageCount(stateDir, room),
322
332
  room,
323
333
  roster_count: Object.keys(roster).length,
324
334
  sent: true,
@@ -361,14 +371,7 @@ export function readRoomMessagePreviews(stateDir, room, limit = 40) {
361
371
  }));
362
372
  }
363
373
  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
- }
374
+ const messageCount = readRoomMessageCount(stateDir, room);
372
375
  const [last] = readRoomMessages(stateDir, room, 1);
373
376
  return {
374
377
  ...(last
@@ -388,7 +391,7 @@ export function ensureRoomMember(stateDir, run, room, address, body, summary) {
388
391
  const roster = readRoomRoster(stateDir, room);
389
392
  if (roster[address]) {
390
393
  return {
391
- message_count: readRoomMessages(stateDir, room).length,
394
+ message_count: readRoomMessageCount(stateDir, room),
392
395
  room,
393
396
  roster_count: Object.keys(roster).length,
394
397
  sent: true,
package/dist/lib/paths.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Zones: paths, registry config, temp directory
4
4
  * Owns agent directory, tools config, recipe root, and actor run state root resolution
5
5
  */
6
+ import { existsSync } from "node:fs";
6
7
  import { homedir } from "node:os";
7
8
  import { dirname, join, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
@@ -24,5 +25,9 @@ export function getRecipeRoot(agentDir = getAgentDir()) {
24
25
  return join(agentDir, "recipes");
25
26
  }
26
27
  export function getPackagedRecipeRoot() {
27
- return resolve(dirname(fileURLToPath(import.meta.url)), "..", "recipes");
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ const compiledRoot = resolve(here, "..", "..", "recipes");
30
+ if (existsSync(compiledRoot))
31
+ return compiledRoot;
32
+ return resolve(here, "..", "recipes");
28
33
  }
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Runtime extension entrypoint wrapper.
3
+ *
4
+ * Installed npm packages load compiled JS from dist so Node does not try to strip
5
+ * TypeScript under node_modules. Source checkouts fall back to index.ts for local
6
+ * development before dist has been built.
7
+ */
8
+
9
+ import { existsSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const compiledEntry = resolve(here, "dist", "index.js");
15
+ const sourceEntry = resolve(here, "index.ts");
16
+ const entry = existsSync(compiledEntry) ? compiledEntry : sourceEntry;
17
+ const entryModule = await import(pathToFileURL(entry).href);
18
+
19
+ export default entryModule.default;
@@ -241,6 +241,15 @@ function readJsonlLineCount(file: string): number {
241
241
  }
242
242
  }
243
243
 
244
+ function readRoomMessageCount(stateDir: string, room: string): number {
245
+ try {
246
+ return readJsonlLineCount(messagesFile(stateDir, room));
247
+ } catch (error) {
248
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
249
+ throw error;
250
+ }
251
+ }
252
+
244
253
  function readJsonlTailLines(file: string, limit: number): string[] {
245
254
  const lineLimit = Math.max(1, limit);
246
255
  const stat = fs.statSync(file);
@@ -446,7 +455,7 @@ export function appendRoomMessage(
446
455
  }
447
456
  }
448
457
  return {
449
- message_count: readRoomMessages(stateDir, room).length,
458
+ message_count: readRoomMessageCount(stateDir, room),
450
459
  room,
451
460
  roster_count: Object.keys(roster).length,
452
461
  sent: true,
@@ -496,12 +505,7 @@ export function readRoomMessagePreviews(
496
505
  }
497
506
 
498
507
  export function getRoomStatus(stateDir: string, room: string): RoomStatus {
499
- let messageCount = 0;
500
- try {
501
- messageCount = readJsonlLineCount(messagesFile(stateDir, room));
502
- } catch (error) {
503
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
504
- }
508
+ const messageCount = readRoomMessageCount(stateDir, room);
505
509
  const [last] = readRoomMessages(stateDir, room, 1);
506
510
  return {
507
511
  ...(last
@@ -529,7 +533,7 @@ export function ensureRoomMember(
529
533
  const roster = readRoomRoster(stateDir, room);
530
534
  if (roster[address]) {
531
535
  return {
532
- message_count: readRoomMessages(stateDir, room).length,
536
+ message_count: readRoomMessageCount(stateDir, room),
533
537
  room,
534
538
  roster_count: Object.keys(roster).length,
535
539
  sent: true,
package/lib/paths.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Owns agent directory, tools config, recipe root, and actor run state root resolution
5
5
  */
6
6
 
7
+ import { existsSync } from "node:fs";
7
8
  import { homedir } from "node:os";
8
9
  import { dirname, join, resolve } from "node:path";
9
10
  import { fileURLToPath } from "node:url";
@@ -36,5 +37,8 @@ export function getRecipeRoot(agentDir = getAgentDir()): string {
36
37
  }
37
38
 
38
39
  export function getPackagedRecipeRoot(): string {
39
- return resolve(dirname(fileURLToPath(import.meta.url)), "..", "recipes");
40
+ const here = dirname(fileURLToPath(import.meta.url));
41
+ const compiledRoot = resolve(here, "..", "..", "recipes");
42
+ if (existsSync(compiledRoot)) return compiledRoot;
43
+ return resolve(here, "..", "recipes");
40
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -33,6 +33,7 @@
33
33
  "prepack": "npm run build"
34
34
  },
35
35
  "files": [
36
+ "index.js",
36
37
  "index.ts",
37
38
  "lib",
38
39
  "scripts",
@@ -48,7 +49,7 @@
48
49
  ],
49
50
  "pi": {
50
51
  "extensions": [
51
- "./index.ts"
52
+ "./index.js"
52
53
  ],
53
54
  "skills": [
54
55
  "./skills/actors/SKILL.md",
@@ -57,7 +58,8 @@
57
58
  "image": "https://github.com/llblab/pi-actors/raw/main/banner.jpg"
58
59
  },
59
60
  "peerDependencies": {
60
- "@earendil-works/pi-coding-agent": "*"
61
+ "@earendil-works/pi-coding-agent": "*",
62
+ "@earendil-works/pi-tui": "*"
61
63
  },
62
64
  "devDependencies": {
63
65
  "@types/node": "latest",
@@ -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.20.0
5
+ version: 0.20.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.20.0
5
+ version: 0.20.2
6
6
  ---
7
7
 
8
8
  # Swarm