pi-messenger 0.13.2 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.14.1] - 2026-04-04
6
+
7
+ ### Changed
8
+ - Added a `promptSnippet` for `pi_messenger` so Pi 0.59+ includes it in the default tool prompt section and reliably surfaces Crew/coordination workflows.
9
+
10
+ ## [0.14.0] - 2026-04-03
11
+
12
+ ### Added
13
+ - **`leave` action** — `pi_messenger({ action: "leave" })` now lets the current session leave the mesh without restarting pi. It releases reservations, auto-unclaims the session's active swarm claim, closes the messenger overlay, and allows later rejoin from the same session.
14
+
15
+ ### Fixed
16
+ - **Leave guardrails for Crew state** — `leave` now refuses while project planning, autonomous work, or session-owned in-progress Crew tasks are still active, preventing stranded coordination state.
17
+ - **Immutable-session cwd handling** — Registration, folder scoping, auto-register path matching, and messenger context now follow the live session cwd after pi runtime/session replacement instead of relying on `process.cwd()`.
18
+
5
19
  ## [0.13.2] - 2026-03-19
6
20
 
7
21
  ### Added
package/README.md CHANGED
@@ -47,6 +47,7 @@ pi_messenger({ action: "join" })
47
47
  pi_messenger({ action: "reserve", paths: ["src/auth/"], reason: "Refactoring" })
48
48
  pi_messenger({ action: "send", to: "GoldFalcon", message: "auth is done" })
49
49
  pi_messenger({ action: "release" })
50
+ pi_messenger({ action: "leave" })
50
51
  ```
51
52
 
52
53
  For multi-agent task orchestration from a PRD:
@@ -67,7 +68,7 @@ pi_messenger({ action: "review", target: "task-1" }) // Reviewer checks imple
67
68
 
68
69
  **Messaging** - Send messages between agents. Recipients wake up immediately and see the message as a steering prompt.
69
70
 
70
- **File Reservations** - Claim files or directories. Other agents get blocked with a clear message telling them who to coordinate with. Auto-releases on exit.
71
+ **File Reservations** - Claim files or directories. Other agents get blocked with a clear message telling them who to coordinate with. Auto-releases on `leave` or exit.
71
72
 
72
73
  **Stuck Detection** - Agents idle too long with an open task or reservation are flagged as stuck. Peers get a notification.
73
74
 
@@ -251,6 +252,7 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
251
252
  | Action | Description |
252
253
  |--------|-------------|
253
254
  | `join` | Join the agent mesh |
255
+ | `leave` | Leave the mesh for the current session |
254
256
  | `list` | List agents with presence info |
255
257
  | `status` | Show your status or crew progress |
256
258
  | `whois` | Detailed info about an agent (`name` required) |
package/crew/index.ts CHANGED
@@ -77,6 +77,9 @@ export async function executeCrewAction(
77
77
  case 'status':
78
78
  return handlers.executeStatus(state, dirs, ctx.cwd ?? process.cwd());
79
79
 
80
+ case 'leave':
81
+ return handlers.executeLeave(state, dirs, ctx);
82
+
80
83
  case 'list':
81
84
  return handlers.executeList(state, dirs, ctx.cwd ?? process.cwd(), { stuckThreshold: config?.stuckThreshold });
82
85
 
package/handlers.ts CHANGED
@@ -26,6 +26,7 @@ import * as store from "./store.js";
26
26
  import * as crewStore from "./crew/store.js";
27
27
  import { getAutoRegisterPaths, saveAutoRegisterPaths, matchesAutoRegisterPath } from "./config.js";
28
28
  import { readFeedEvents, logFeedEvent, pruneFeed, formatFeedLine, isCrewEvent, type FeedEvent } from "./feed.js";
29
+ import { isAutonomousForCwd, isPlanningForCwd } from "./crew/state.js";
29
30
  import { loadCrewConfig } from "./crew/utils/config.js";
30
31
 
31
32
  let messagesSentThisSession = 0;
@@ -127,6 +128,118 @@ export function executeJoin(
127
128
  });
128
129
  }
129
130
 
131
+ export async function executeLeave(
132
+ state: MessengerState,
133
+ dirs: Dirs,
134
+ ctx: ExtensionContext,
135
+ ) {
136
+ if (!state.registered) {
137
+ return notRegisteredError();
138
+ }
139
+
140
+ const cwd = ctx.cwd ?? process.cwd();
141
+
142
+ if (isPlanningForCwd(cwd)) {
143
+ return result(
144
+ "Cannot leave while Crew planning is active for this project. Cancel it first with pi_messenger({ action: \"plan.cancel\" }).",
145
+ { mode: "leave", error: "planning_active" }
146
+ );
147
+ }
148
+
149
+ if (isAutonomousForCwd(cwd)) {
150
+ return result(
151
+ "Cannot leave while autonomous Crew work is active for this project. Stop it first with pi_messenger({ action: \"work.stop\" }).",
152
+ { mode: "leave", error: "autonomous_active" }
153
+ );
154
+ }
155
+
156
+ const inProgressTasks = crewStore
157
+ .getTasks(cwd)
158
+ .filter(task => task.status === "in_progress" && task.assigned_to === state.agentName)
159
+ .map(task => task.id);
160
+
161
+ if (inProgressTasks.length > 0) {
162
+ return result(
163
+ `Cannot leave while Crew task${inProgressTasks.length === 1 ? "" : "s"} assigned to you ${inProgressTasks.length === 1 ? "is" : "are"} still in progress: ${inProgressTasks.join(", ")}. Finish, block, or reset ${inProgressTasks.length === 1 ? "it" : "them"} first.`,
164
+ { mode: "leave", error: "crew_tasks_in_progress", taskIds: inProgressTasks }
165
+ );
166
+ }
167
+
168
+ const activeClaim = store.getAgentCurrentClaim(dirs, state.agentName);
169
+ let releasedClaim: { spec: string; taskId: string; reason?: string } | undefined;
170
+ if (activeClaim) {
171
+ const claimDisplay = displaySpecPath(activeClaim.spec, cwd);
172
+ try {
173
+ const unclaimResult = await store.unclaimTask(dirs, activeClaim.spec, activeClaim.taskId, state.agentName);
174
+ if (!store.isUnclaimSuccess(unclaimResult)) {
175
+ return result(
176
+ `Cannot leave because the active swarm claim ${activeClaim.taskId} in ${claimDisplay} could not be released. Resolve it first and retry.`,
177
+ {
178
+ mode: "leave",
179
+ error: unclaimResult.error,
180
+ activeClaim: { ...activeClaim, spec: claimDisplay },
181
+ ...(store.isUnclaimNotYours(unclaimResult) ? { claimedBy: unclaimResult.claimedBy } : {}),
182
+ }
183
+ );
184
+ }
185
+ releasedClaim = activeClaim;
186
+ } catch (error) {
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ return result(
189
+ `Cannot leave because the active swarm claim ${activeClaim.taskId} in ${claimDisplay} could not be released: ${message}`,
190
+ {
191
+ mode: "leave",
192
+ error: "unclaim_failed",
193
+ message,
194
+ activeClaim: { ...activeClaim, spec: claimDisplay },
195
+ }
196
+ );
197
+ }
198
+ }
199
+
200
+ const releasedReservations = state.reservations.map(r => r.pattern);
201
+ state.reservations = [];
202
+ store.updateRegistration(state, dirs, ctx);
203
+ for (const pattern of releasedReservations) {
204
+ logFeedEvent(cwd, state.agentName, "release", pattern);
205
+ }
206
+
207
+ try {
208
+ store.unregister(state, dirs);
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ return result(
212
+ `Could not leave pi-messenger: ${message}`,
213
+ { mode: "leave", error: "unregister_failed", message }
214
+ );
215
+ }
216
+
217
+ logFeedEvent(cwd, state.agentName, "leave");
218
+ store.stopWatcher(state);
219
+
220
+ if (ctx.hasUI) {
221
+ ctx.ui.setStatus("messenger", undefined);
222
+ }
223
+
224
+ const claimText = releasedClaim
225
+ ? `\nReleased claim: ${releasedClaim.taskId} in ${displaySpecPath(releasedClaim.spec, cwd)}`
226
+ : "";
227
+
228
+ return result(
229
+ `Left pi-messenger.${releasedReservations.length > 0 ? `\nReleased reservations: ${releasedReservations.join(", ")}` : ""}${claimText}`,
230
+ {
231
+ mode: "leave",
232
+ releasedReservations,
233
+ releasedClaim: releasedClaim
234
+ ? {
235
+ ...releasedClaim,
236
+ spec: displaySpecPath(releasedClaim.spec, cwd),
237
+ }
238
+ : undefined,
239
+ }
240
+ );
241
+ }
242
+
130
243
  export function executeStatus(state: MessengerState, dirs: Dirs, cwd: string = process.cwd()) {
131
244
  if (!state.registered) {
132
245
  return notRegisteredError();
package/index.ts CHANGED
@@ -80,7 +80,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
80
80
  // State & Configuration
81
81
  // ===========================================================================
82
82
 
83
- const config: MessengerConfig = loadConfig(process.cwd());
83
+ let config: MessengerConfig = loadConfig(process.cwd());
84
84
 
85
85
  const state: MessengerState = {
86
86
  agentName: process.env.PI_AGENT_NAME || "",
@@ -95,6 +95,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
95
95
  broadcastHistory: [],
96
96
  seenSenders: new Map(),
97
97
  model: "",
98
+ cwd: process.cwd(),
98
99
  gitBranch: undefined,
99
100
  spec: undefined,
100
101
  scopeToFolder: config.scopeToFolder,
@@ -337,7 +338,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
337
338
  // ===========================================================================
338
339
 
339
340
  function sendRegistrationContext(ctx: ExtensionContext): void {
340
- const folder = extractFolder(process.cwd());
341
+ const folder = extractFolder(ctx.cwd ?? state.cwd);
341
342
  const locationPart = state.gitBranch
342
343
  ? `${folder} on ${state.gitBranch}`
343
344
  : folder;
@@ -360,6 +361,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
360
361
  Usage (action-based API - preferred):
361
362
  // Coordination
362
363
  pi_messenger({ action: "join" }) → Join mesh
364
+ pi_messenger({ action: "leave" }) → Leave mesh for this session
363
365
  pi_messenger({ action: "status" }) → Get status
364
366
  pi_messenger({ action: "list" }) → List agents with presence
365
367
  pi_messenger({ action: "feed", limit: 20 }) → Activity feed
@@ -390,6 +392,8 @@ Usage (action-based API - preferred):
390
392
 
391
393
  // Crew: Review
392
394
  pi_messenger({ action: "review", target: "task-1" }) → Review impl`,
395
+ promptSnippet:
396
+ "Use for multi-agent coordination and Crew workflows: join/status/feed, create plans, run work waves, manage tasks, reserve files, and message agents.",
393
397
  parameters: Type.Object({
394
398
  action: Type.Optional(Type.String({
395
399
  description: "Action to perform (e.g., 'join', 'plan', 'work', 'task.start')"
@@ -468,6 +472,13 @@ Usage (action-based API - preferred):
468
472
  sendRegistrationContext(ctx);
469
473
  }
470
474
 
475
+ if (action === "leave" && !state.registered) {
476
+ overlayHandle?.hide();
477
+ overlayHandle = null;
478
+ overlayTui = null;
479
+ overlayOpening = false;
480
+ }
481
+
471
482
  return result;
472
483
  }
473
484
  });
@@ -769,6 +780,11 @@ Usage (action-based API - preferred):
769
780
 
770
781
  pi.on("session_start", async (_event, ctx) => {
771
782
  latestCtx = ctx;
783
+ state.cwd = ctx.cwd ?? process.cwd();
784
+ config = loadConfig(state.cwd);
785
+ state.scopeToFolder = config.scopeToFolder;
786
+ nameTheme.theme = config.nameTheme;
787
+ nameTheme.customWords = config.nameWords;
772
788
  resetAutonomousContinueGuard();
773
789
  startStatusHeartbeat();
774
790
  for (const entry of ctx.sessionManager.getEntries()) {
@@ -776,7 +792,7 @@ Usage (action-based API - preferred):
776
792
  restoreAutonomousState(entry.data as Parameters<typeof restoreAutonomousState>[0]);
777
793
  }
778
794
  }
779
- const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
795
+ const { staleCleared } = restorePlanningState(state.cwd);
780
796
  if (staleCleared && ctx.hasUI) {
781
797
  ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
782
798
  }
@@ -785,7 +801,7 @@ Usage (action-based API - preferred):
785
801
  try { fs.rmSync(join(homedir(), ".pi/agent/messenger/feed.jsonl"), { force: true }); } catch {}
786
802
 
787
803
  const shouldAutoRegister = config.autoRegister ||
788
- matchesAutoRegisterPath(process.cwd(), config.autoRegisterPaths);
804
+ matchesAutoRegisterPath(state.cwd, config.autoRegisterPaths);
789
805
 
790
806
  if (!shouldAutoRegister) {
791
807
  maybeAutoOpenCrewOverlay(ctx);
@@ -793,7 +809,7 @@ Usage (action-based API - preferred):
793
809
  }
794
810
 
795
811
  if (store.register(state, dirs, ctx, nameTheme)) {
796
- const cwd = ctx.cwd ?? process.cwd();
812
+ const cwd = state.cwd;
797
813
  store.startWatcher(state, dirs, deliverMessage);
798
814
  updateStatus(ctx);
799
815
  pruneFeed(cwd, config.feedRetention);
@@ -889,28 +905,6 @@ Usage (action-based API - preferred):
889
905
  });
890
906
  }
891
907
 
892
- pi.on("session_switch", async (_event, ctx) => {
893
- latestCtx = ctx;
894
- resetAutonomousContinueGuard();
895
- const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
896
- if (staleCleared && ctx.hasUI) {
897
- ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
898
- }
899
- recoverWatcherIfNeeded();
900
- updateStatus(ctx);
901
- maybeAutoOpenCrewOverlay(ctx);
902
- });
903
- pi.on("session_fork", async (_event, ctx) => {
904
- latestCtx = ctx;
905
- resetAutonomousContinueGuard();
906
- const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
907
- if (staleCleared && ctx.hasUI) {
908
- ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
909
- }
910
- recoverWatcherIfNeeded();
911
- updateStatus(ctx);
912
- maybeAutoOpenCrewOverlay(ctx);
913
- });
914
908
  pi.on("session_tree", async (_event, ctx) => {
915
909
  latestCtx = ctx;
916
910
  const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
@@ -1101,7 +1095,12 @@ Usage (action-based API - preferred):
1101
1095
  if (recentTestTimer) { clearTimeout(recentTestTimer); recentTestTimer = null; }
1102
1096
  if (recentEditTimer) { clearTimeout(recentEditTimer); recentEditTimer = null; }
1103
1097
  store.stopWatcher(state);
1104
- store.unregister(state, dirs);
1098
+ try {
1099
+ store.unregister(state, dirs);
1100
+ } catch {
1101
+ // Safe to ignore during shutdown: the process is exiting, so any leftover
1102
+ // registration will be cleaned up as stale on the next registry read.
1103
+ }
1105
1104
  });
1106
1105
 
1107
1106
  // ===========================================================================
package/lib.ts CHANGED
@@ -73,6 +73,7 @@ export interface MessengerState {
73
73
  broadcastHistory: AgentMailMessage[];
74
74
  seenSenders: Map<string, string>;
75
75
  model: string;
76
+ cwd: string;
76
77
  gitBranch?: string;
77
78
  spec?: string;
78
79
  scopeToFolder: boolean;
@@ -388,8 +389,8 @@ export function buildSelfRegistration(state: MessengerState): AgentRegistration
388
389
  name: state.agentName,
389
390
  pid: process.pid,
390
391
  sessionId: "",
391
- cwd: process.cwd(),
392
392
  model: state.model,
393
+ cwd: state.cwd,
393
394
  startedAt: state.sessionStartedAt,
394
395
  gitBranch: state.gitBranch,
395
396
  spec: state.spec,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-messenger",
3
- "version": "0.13.2",
3
+ "version": "0.14.1",
4
4
  "description": "Inter-agent messaging and file reservation system for pi coding agent",
5
5
  "type": "module",
6
6
  "author": "Nico Bailon",
@@ -9,9 +9,10 @@ Use pi-messenger for multi-agent coordination and Crew task orchestration.
9
9
 
10
10
  ## Quick Reference
11
11
 
12
- ### Join the Mesh (Required First)
12
+ ### Join or Leave the Mesh
13
13
  ```typescript
14
14
  pi_messenger({ action: "join" })
15
+ pi_messenger({ action: "leave" })
15
16
  ```
16
17
 
17
18
  ### Check Status
package/store.ts CHANGED
@@ -167,7 +167,7 @@ export function getRegistrationPath(state: MessengerState, dirs: Dirs): string {
167
167
  export function getActiveAgents(state: MessengerState, dirs: Dirs): AgentRegistration[] {
168
168
  const now = Date.now();
169
169
  const excludeName = state.agentName;
170
- const myCwd = normalizeCwd(process.cwd());
170
+ const myCwd = normalizeCwd(state.cwd);
171
171
  const scopeToFolder = state.scopeToFolder;
172
172
 
173
173
  // Cache key includes scopeToFolder and cwd for proper cache invalidation
@@ -339,7 +339,7 @@ export function register(state: MessengerState, dirs: Dirs, ctx: ExtensionContex
339
339
 
340
340
  ensureDirSync(getMyInbox(state, dirs));
341
341
 
342
- const cwd = normalizeCwd(process.cwd());
342
+ const cwd = normalizeCwd(ctx.cwd ?? process.cwd());
343
343
  const gitBranch = getGitBranch(cwd);
344
344
  const now = new Date().toISOString();
345
345
  const registration: AgentRegistration = {
@@ -378,6 +378,7 @@ export function register(state: MessengerState, dirs: Dirs, ctx: ExtensionContex
378
378
  if (verified) {
379
379
  state.registered = true;
380
380
  state.model = ctx.model?.id ?? "unknown";
381
+ state.cwd = cwd;
381
382
  state.gitBranch = gitBranch;
382
383
  state.activity.lastActivityAt = now;
383
384
  invalidateAgentsCache();
@@ -426,6 +427,7 @@ export function updateRegistration(state: MessengerState, dirs: Dirs, ctx: Exten
426
427
  const currentModel = ctx.model?.id ?? reg.model;
427
428
  reg.model = currentModel;
428
429
  state.model = currentModel;
430
+ reg.cwd = state.cwd;
429
431
  reg.reservations = state.reservations.length > 0 ? state.reservations : undefined;
430
432
  if (state.spec) {
431
433
  reg.spec = state.spec;
@@ -452,6 +454,7 @@ export function flushActivityToRegistry(state: MessengerState, dirs: Dirs, ctx:
452
454
  const currentModel = ctx.model?.id ?? reg.model;
453
455
  reg.model = currentModel;
454
456
  state.model = currentModel;
457
+ reg.cwd = state.cwd;
455
458
  reg.session = { ...state.session };
456
459
  reg.activity = { ...state.activity };
457
460
  reg.statusMessage = state.statusMessage;
@@ -464,11 +467,15 @@ export function flushActivityToRegistry(state: MessengerState, dirs: Dirs, ctx:
464
467
  export function unregister(state: MessengerState, dirs: Dirs): void {
465
468
  if (!state.registered) return;
466
469
 
470
+ const regPath = getRegistrationPath(state, dirs);
467
471
  try {
468
- fs.unlinkSync(getRegistrationPath(state, dirs));
469
- } catch {
470
- // Ignore errors
472
+ fs.unlinkSync(regPath);
473
+ } catch (error) {
474
+ if (fs.existsSync(regPath)) {
475
+ throw error;
476
+ }
471
477
  }
478
+
472
479
  state.registered = false;
473
480
  invalidateAgentsCache();
474
481
  }
@@ -515,7 +522,7 @@ export function renameAgent(
515
522
 
516
523
  processAllPendingMessages(state, dirs, deliverFn);
517
524
 
518
- const cwd = normalizeCwd(process.cwd());
525
+ const cwd = normalizeCwd(ctx.cwd ?? process.cwd());
519
526
  const gitBranch = getGitBranch(cwd);
520
527
  const now = new Date().toISOString();
521
528
  const registration: AgentRegistration = {
@@ -598,6 +605,7 @@ export function renameAgent(
598
605
  }
599
606
 
600
607
  state.model = ctx.model?.id ?? "unknown";
608
+ state.cwd = cwd;
601
609
  state.gitBranch = gitBranch;
602
610
  state.sessionStartedAt = now;
603
611
  state.activity.lastActivityAt = now;