pi-messenger 0.13.1 → 0.14.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/CHANGELOG.md +26 -0
- package/README.md +4 -1
- package/crew/index.ts +14 -1
- package/crew/state-autonomous.ts +30 -0
- package/handlers.ts +113 -0
- package/index.ts +100 -31
- package/lib.ts +2 -1
- package/package.json +1 -1
- package/skills/pi-messenger-crew/SKILL.md +2 -1
- package/store.ts +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.14.0] - 2026-04-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`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.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **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.
|
|
12
|
+
- **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()`.
|
|
13
|
+
|
|
14
|
+
## [0.13.2] - 2026-03-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`work.stop` action** — `pi_messenger({ action: "work.stop" })` now stops autonomous Crew work for the current project and persists the stop state.
|
|
18
|
+
- **Autonomous guard coverage tests** — Added targeted tests for `work.stop` routing and `agent_end` autonomous continuation guard behavior.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Autonomous continuation retry loop guard** — Autonomous Crew mode now stops itself after repeated identical `crew_continue` retries without wave progress (for example when steer turns keep aborting). The extension persists the stopped state and emits a warning instead of looping indefinitely.
|
|
22
|
+
- **Persisted autonomous stop state from index-level stop paths** — `max waves` and `no ready tasks` stop paths now append `crew-state` so restored sessions do not revive stale active autonomous state.
|
|
23
|
+
- **Autonomous continuation no longer runs in unregistered sessions** — If Crew autonomous state is restored but the current session is not joined to pi-messenger, the extension now stops autonomous mode and persists that stop instead of emitting repeated continuation steer messages that cannot execute.
|
|
24
|
+
- **Autonomous continuation now runs only in orchestrator sessions** — Worker and lobby processes (`PI_CREW_WORKER`/`PI_LOBBY_ID`) now skip `agent_end` continuation emission to prevent repeated duplicate `crew_continue` steer loops from non-orchestrator agents.
|
|
25
|
+
- **Autonomous continuation is now cwd-scoped** — `agent_end` continuation now requires the session cwd to match the active autonomous cwd (`isAutonomousForCwd`), preventing stale autonomous state from unrelated projects from triggering unexpected `crew_continue` messages.
|
|
26
|
+
- **Autonomous state restore now validates owner process** — Restored active autonomous state now requires a live owner PID matching the current process; missing/dead/foreign ownership is treated as stale and auto-stopped to avoid unexpected continuation after session restore.
|
|
27
|
+
- **Runtime artifact commit noise** — Added `.pi/` to `.gitignore` to avoid accidentally committing runtime files like feed artifacts.
|
|
28
|
+
|
|
3
29
|
## [0.13.1] - 2026-03-14
|
|
4
30
|
|
|
5
31
|
### 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) |
|
|
@@ -268,6 +270,7 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
|
|
|
268
270
|
|--------|-------------|
|
|
269
271
|
| `plan` | Create plan from PRD or inline prompt (`prd`, `prompt` optional — auto-discovers PRD if omitted, auto-starts workers unless `autoWork: false`) |
|
|
270
272
|
| `work` | Run ready tasks (`autonomous`, `concurrency` optional) |
|
|
273
|
+
| `work.stop` | Stop autonomous work for the current project |
|
|
271
274
|
| `review` | Review implementation (`target` task ID required) |
|
|
272
275
|
| `task.list` | List all tasks |
|
|
273
276
|
| `task.show` | Show task details (`id` required) |
|
package/crew/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { MessengerState, Dirs, AgentMailMessage, NameThemeConfig } from "..
|
|
|
10
10
|
import * as handlers from "../handlers.js";
|
|
11
11
|
import type { CrewParams, AppendEntryFn } from "./types.js";
|
|
12
12
|
import { result } from "./utils/result.js";
|
|
13
|
-
import { isPlanningForCwd, cancelPlanningRun } from "./state.js";
|
|
13
|
+
import { isPlanningForCwd, cancelPlanningRun, autonomousState, isAutonomousForCwd, stopAutonomous } from "./state.js";
|
|
14
14
|
import { logFeedEvent } from "../feed.js";
|
|
15
15
|
|
|
16
16
|
type DeliverFn = (msg: AgentMailMessage) => void;
|
|
@@ -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
|
|
|
@@ -180,6 +183,16 @@ export async function executeCrewAction(
|
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
case 'work': {
|
|
186
|
+
if (op === 'stop') {
|
|
187
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
188
|
+
if (!isAutonomousForCwd(cwd)) {
|
|
189
|
+
return result("No autonomous work running for this project.", { mode: "work.stop" });
|
|
190
|
+
}
|
|
191
|
+
stopAutonomous("manual");
|
|
192
|
+
appendEntry("crew-state", autonomousState);
|
|
193
|
+
return result("Autonomous work stopped.", { mode: "work.stop", autonomous: false });
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
try {
|
|
184
197
|
const workHandler = await import("./handlers/work.js");
|
|
185
198
|
return workHandler.execute(params, dirs, ctx, appendEntry, signal);
|
package/crew/state-autonomous.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface AutonomousState {
|
|
|
25
25
|
stopReason: "completed" | "blocked" | "manual" | null;
|
|
26
26
|
concurrency: number;
|
|
27
27
|
autoOverlayPending: boolean;
|
|
28
|
+
pid: number | null;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export const autonomousState: AutonomousState = {
|
|
@@ -37,11 +38,21 @@ export const autonomousState: AutonomousState = {
|
|
|
37
38
|
stopReason: null,
|
|
38
39
|
concurrency: 2,
|
|
39
40
|
autoOverlayPending: false,
|
|
41
|
+
pid: null,
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
export const MIN_CONCURRENCY = 1;
|
|
43
45
|
export const MAX_CONCURRENCY = 10;
|
|
44
46
|
|
|
47
|
+
function isProcessAlive(pid: number): boolean {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
export function clampConcurrency(value: number, configMax?: number): number {
|
|
46
57
|
if (!Number.isFinite(value)) return MIN_CONCURRENCY;
|
|
47
58
|
const whole = Math.trunc(value);
|
|
@@ -79,6 +90,7 @@ export function startAutonomous(cwd: string, concurrency: number): void {
|
|
|
79
90
|
autonomousState.stopReason = null;
|
|
80
91
|
autonomousState.concurrency = clampConcurrency(concurrency);
|
|
81
92
|
autonomousState.autoOverlayPending = true;
|
|
93
|
+
autonomousState.pid = process.pid;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export function stopAutonomous(reason: "completed" | "blocked" | "manual"): void {
|
|
@@ -86,6 +98,7 @@ export function stopAutonomous(reason: "completed" | "blocked" | "manual"): void
|
|
|
86
98
|
autonomousState.autoOverlayPending = false;
|
|
87
99
|
autonomousState.stoppedAt = new Date().toISOString();
|
|
88
100
|
autonomousState.stopReason = reason;
|
|
101
|
+
autonomousState.pid = null;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
export function addWaveResult(result: WaveResult): void {
|
|
@@ -106,6 +119,23 @@ export function restoreAutonomousState(data: Partial<AutonomousState>): void {
|
|
|
106
119
|
if (data.concurrency !== undefined) {
|
|
107
120
|
autonomousState.concurrency = clampConcurrency(Number(data.concurrency));
|
|
108
121
|
}
|
|
122
|
+
if (data.pid !== undefined) {
|
|
123
|
+
autonomousState.pid = typeof data.pid === "number" ? data.pid : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!autonomousState.active) return;
|
|
127
|
+
|
|
128
|
+
const ownerPid = autonomousState.pid;
|
|
129
|
+
const sameProcess = ownerPid === process.pid;
|
|
130
|
+
const ownerAlive = typeof ownerPid === "number" && isProcessAlive(ownerPid);
|
|
131
|
+
|
|
132
|
+
if (!sameProcess || !ownerAlive) {
|
|
133
|
+
autonomousState.active = false;
|
|
134
|
+
autonomousState.autoOverlayPending = false;
|
|
135
|
+
autonomousState.stopReason = autonomousState.stopReason ?? "manual";
|
|
136
|
+
autonomousState.stoppedAt = autonomousState.stoppedAt ?? new Date().toISOString();
|
|
137
|
+
autonomousState.pid = null;
|
|
138
|
+
}
|
|
109
139
|
}
|
|
110
140
|
|
|
111
141
|
export function isAutonomousForCwd(cwd: string): boolean {
|
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
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
restoreAutonomousState,
|
|
59
59
|
restorePlanningState,
|
|
60
60
|
stopAutonomous,
|
|
61
|
+
isAutonomousForCwd,
|
|
61
62
|
} from "./crew/state.js";
|
|
62
63
|
import { loadCrewConfig } from "./crew/utils/config.js";
|
|
63
64
|
import * as crewStore from "./crew/store.js";
|
|
@@ -79,7 +80,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
79
80
|
// State & Configuration
|
|
80
81
|
// ===========================================================================
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
let config: MessengerConfig = loadConfig(process.cwd());
|
|
83
84
|
|
|
84
85
|
const state: MessengerState = {
|
|
85
86
|
agentName: process.env.PI_AGENT_NAME || "",
|
|
@@ -94,6 +95,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
94
95
|
broadcastHistory: [],
|
|
95
96
|
seenSenders: new Map(),
|
|
96
97
|
model: "",
|
|
98
|
+
cwd: process.cwd(),
|
|
97
99
|
gitBranch: undefined,
|
|
98
100
|
spec: undefined,
|
|
99
101
|
scopeToFolder: config.scopeToFolder,
|
|
@@ -295,6 +297,24 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
295
297
|
const STATUS_HEARTBEAT_MS = 15_000;
|
|
296
298
|
let latestCtx: ExtensionContext | null = null;
|
|
297
299
|
let statusHeartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
300
|
+
const AUTONOMOUS_CONTINUE_REPEAT_LIMIT = 3;
|
|
301
|
+
let autonomousContinueSignature: string | null = null;
|
|
302
|
+
let autonomousContinueRepeats = 0;
|
|
303
|
+
|
|
304
|
+
function resetAutonomousContinueGuard(): void {
|
|
305
|
+
autonomousContinueSignature = null;
|
|
306
|
+
autonomousContinueRepeats = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function trackAutonomousContinue(signature: string): number {
|
|
310
|
+
if (autonomousContinueSignature === signature) {
|
|
311
|
+
autonomousContinueRepeats += 1;
|
|
312
|
+
} else {
|
|
313
|
+
autonomousContinueSignature = signature;
|
|
314
|
+
autonomousContinueRepeats = 1;
|
|
315
|
+
}
|
|
316
|
+
return autonomousContinueRepeats;
|
|
317
|
+
}
|
|
298
318
|
|
|
299
319
|
function startStatusHeartbeat(): void {
|
|
300
320
|
if (statusHeartbeatTimer) return;
|
|
@@ -318,7 +338,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
318
338
|
// ===========================================================================
|
|
319
339
|
|
|
320
340
|
function sendRegistrationContext(ctx: ExtensionContext): void {
|
|
321
|
-
const folder = extractFolder(
|
|
341
|
+
const folder = extractFolder(ctx.cwd ?? state.cwd);
|
|
322
342
|
const locationPart = state.gitBranch
|
|
323
343
|
? `${folder} on ${state.gitBranch}`
|
|
324
344
|
: folder;
|
|
@@ -341,6 +361,7 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
341
361
|
Usage (action-based API - preferred):
|
|
342
362
|
// Coordination
|
|
343
363
|
pi_messenger({ action: "join" }) → Join mesh
|
|
364
|
+
pi_messenger({ action: "leave" }) → Leave mesh for this session
|
|
344
365
|
pi_messenger({ action: "status" }) → Get status
|
|
345
366
|
pi_messenger({ action: "list" }) → List agents with presence
|
|
346
367
|
pi_messenger({ action: "feed", limit: 20 }) → Activity feed
|
|
@@ -358,6 +379,7 @@ Usage (action-based API - preferred):
|
|
|
358
379
|
// Crew: Work through tasks
|
|
359
380
|
pi_messenger({ action: "work" }) → Run ready tasks
|
|
360
381
|
pi_messenger({ action: "work", autonomous: true }) → Run until done/blocked
|
|
382
|
+
pi_messenger({ action: "work.stop" }) → Stop autonomous work for this project
|
|
361
383
|
|
|
362
384
|
// Crew: Tasks
|
|
363
385
|
pi_messenger({ action: "task.show", id: "task-1" }) → Show task
|
|
@@ -448,6 +470,13 @@ Usage (action-based API - preferred):
|
|
|
448
470
|
sendRegistrationContext(ctx);
|
|
449
471
|
}
|
|
450
472
|
|
|
473
|
+
if (action === "leave" && !state.registered) {
|
|
474
|
+
overlayHandle?.hide();
|
|
475
|
+
overlayHandle = null;
|
|
476
|
+
overlayTui = null;
|
|
477
|
+
overlayOpening = false;
|
|
478
|
+
}
|
|
479
|
+
|
|
451
480
|
return result;
|
|
452
481
|
}
|
|
453
482
|
});
|
|
@@ -749,13 +778,19 @@ Usage (action-based API - preferred):
|
|
|
749
778
|
|
|
750
779
|
pi.on("session_start", async (_event, ctx) => {
|
|
751
780
|
latestCtx = ctx;
|
|
781
|
+
state.cwd = ctx.cwd ?? process.cwd();
|
|
782
|
+
config = loadConfig(state.cwd);
|
|
783
|
+
state.scopeToFolder = config.scopeToFolder;
|
|
784
|
+
nameTheme.theme = config.nameTheme;
|
|
785
|
+
nameTheme.customWords = config.nameWords;
|
|
786
|
+
resetAutonomousContinueGuard();
|
|
752
787
|
startStatusHeartbeat();
|
|
753
788
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
754
789
|
if (entry.type === "custom" && entry.customType === "crew-state") {
|
|
755
790
|
restoreAutonomousState(entry.data as Parameters<typeof restoreAutonomousState>[0]);
|
|
756
791
|
}
|
|
757
792
|
}
|
|
758
|
-
const { staleCleared } = restorePlanningState(
|
|
793
|
+
const { staleCleared } = restorePlanningState(state.cwd);
|
|
759
794
|
if (staleCleared && ctx.hasUI) {
|
|
760
795
|
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
761
796
|
}
|
|
@@ -764,7 +799,7 @@ Usage (action-based API - preferred):
|
|
|
764
799
|
try { fs.rmSync(join(homedir(), ".pi/agent/messenger/feed.jsonl"), { force: true }); } catch {}
|
|
765
800
|
|
|
766
801
|
const shouldAutoRegister = config.autoRegister ||
|
|
767
|
-
matchesAutoRegisterPath(
|
|
802
|
+
matchesAutoRegisterPath(state.cwd, config.autoRegisterPaths);
|
|
768
803
|
|
|
769
804
|
if (!shouldAutoRegister) {
|
|
770
805
|
maybeAutoOpenCrewOverlay(ctx);
|
|
@@ -772,7 +807,7 @@ Usage (action-based API - preferred):
|
|
|
772
807
|
}
|
|
773
808
|
|
|
774
809
|
if (store.register(state, dirs, ctx, nameTheme)) {
|
|
775
|
-
const cwd =
|
|
810
|
+
const cwd = state.cwd;
|
|
776
811
|
store.startWatcher(state, dirs, deliverMessage);
|
|
777
812
|
updateStatus(ctx);
|
|
778
813
|
pruneFeed(cwd, config.feedRetention);
|
|
@@ -868,26 +903,6 @@ Usage (action-based API - preferred):
|
|
|
868
903
|
});
|
|
869
904
|
}
|
|
870
905
|
|
|
871
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
872
|
-
latestCtx = ctx;
|
|
873
|
-
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
874
|
-
if (staleCleared && ctx.hasUI) {
|
|
875
|
-
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
876
|
-
}
|
|
877
|
-
recoverWatcherIfNeeded();
|
|
878
|
-
updateStatus(ctx);
|
|
879
|
-
maybeAutoOpenCrewOverlay(ctx);
|
|
880
|
-
});
|
|
881
|
-
pi.on("session_fork", async (_event, ctx) => {
|
|
882
|
-
latestCtx = ctx;
|
|
883
|
-
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
884
|
-
if (staleCleared && ctx.hasUI) {
|
|
885
|
-
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
886
|
-
}
|
|
887
|
-
recoverWatcherIfNeeded();
|
|
888
|
-
updateStatus(ctx);
|
|
889
|
-
maybeAutoOpenCrewOverlay(ctx);
|
|
890
|
-
});
|
|
891
906
|
pi.on("session_tree", async (_event, ctx) => {
|
|
892
907
|
latestCtx = ctx;
|
|
893
908
|
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
@@ -936,6 +951,22 @@ Usage (action-based API - preferred):
|
|
|
936
951
|
// ===========================================================================
|
|
937
952
|
|
|
938
953
|
pi.on("agent_end", async (_event, ctx) => {
|
|
954
|
+
if (process.env.PI_CREW_WORKER === "1" || process.env.PI_LOBBY_ID) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (!state.registered) {
|
|
959
|
+
if (autonomousState.active) {
|
|
960
|
+
stopAutonomous("manual");
|
|
961
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
962
|
+
resetAutonomousContinueGuard();
|
|
963
|
+
if (ctx.hasUI) {
|
|
964
|
+
ctx.ui.notify("Autonomous stopped: this session is not registered in pi-messenger.", "warning");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
939
970
|
// --- Auto-work after plan completion ---
|
|
940
971
|
const autoWork = consumePendingAutoWork();
|
|
941
972
|
if (autoWork && !overlayTui) {
|
|
@@ -955,15 +986,26 @@ Usage (action-based API - preferred):
|
|
|
955
986
|
}
|
|
956
987
|
|
|
957
988
|
// --- Existing autonomous continuation ---
|
|
958
|
-
if (!autonomousState.active)
|
|
989
|
+
if (!autonomousState.active) {
|
|
990
|
+
resetAutonomousContinueGuard();
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const currentCwd = ctx.cwd ?? process.cwd();
|
|
995
|
+
if (!isAutonomousForCwd(currentCwd)) {
|
|
996
|
+
resetAutonomousContinueGuard();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
959
999
|
|
|
960
|
-
const cwd = autonomousState.cwd ??
|
|
1000
|
+
const cwd = autonomousState.cwd ?? currentCwd;
|
|
961
1001
|
const crewDir = join(cwd, ".pi", "messenger", "crew");
|
|
962
1002
|
const crewConfig = loadCrewConfig(crewDir);
|
|
963
1003
|
|
|
964
1004
|
// Check max waves limit
|
|
965
1005
|
if (autonomousState.waveNumber >= crewConfig.work.maxWaves) {
|
|
966
1006
|
stopAutonomous("manual");
|
|
1007
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1008
|
+
resetAutonomousContinueGuard();
|
|
967
1009
|
if (ctx.hasUI) {
|
|
968
1010
|
ctx.ui.notify(`Autonomous stopped: max waves (${crewConfig.work.maxWaves}) reached`, "warning");
|
|
969
1011
|
}
|
|
@@ -972,14 +1014,16 @@ Usage (action-based API - preferred):
|
|
|
972
1014
|
|
|
973
1015
|
// Check for ready tasks
|
|
974
1016
|
const readyTasks = crewStore.getReadyTasks(cwd, { advisory: crewConfig.dependencies === "advisory" });
|
|
975
|
-
|
|
1017
|
+
|
|
976
1018
|
if (readyTasks.length === 0) {
|
|
977
1019
|
// No ready tasks - check if all done or blocked
|
|
978
1020
|
const allTasks = crewStore.getTasks(cwd);
|
|
979
1021
|
const allDone = allTasks.every(t => t.status === "done");
|
|
980
|
-
|
|
1022
|
+
|
|
981
1023
|
stopAutonomous(allDone ? "completed" : "blocked");
|
|
982
|
-
|
|
1024
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1025
|
+
resetAutonomousContinueGuard();
|
|
1026
|
+
|
|
983
1027
|
const plan = crewStore.getPlan(cwd);
|
|
984
1028
|
if (ctx.hasUI) {
|
|
985
1029
|
if (allDone) {
|
|
@@ -992,6 +1036,26 @@ Usage (action-based API - preferred):
|
|
|
992
1036
|
return;
|
|
993
1037
|
}
|
|
994
1038
|
|
|
1039
|
+
const continueSignature = `${cwd}:${autonomousState.waveNumber}:${readyTasks.map(task => task.id).sort().join(",")}`;
|
|
1040
|
+
const continueRepeatCount = trackAutonomousContinue(continueSignature);
|
|
1041
|
+
if (continueRepeatCount >= AUTONOMOUS_CONTINUE_REPEAT_LIMIT) {
|
|
1042
|
+
stopAutonomous("manual");
|
|
1043
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1044
|
+
resetAutonomousContinueGuard();
|
|
1045
|
+
|
|
1046
|
+
const plan = crewStore.getPlan(cwd);
|
|
1047
|
+
const message = `Autonomous work on ${plan?.prd ?? "plan"} stopped after ${continueRepeatCount} repeated continuation retries without wave progress. Resolve the abort condition, then run pi_messenger({ action: "work", autonomous: true }).`;
|
|
1048
|
+
if (ctx.hasUI) {
|
|
1049
|
+
ctx.ui.notify(message, "warning");
|
|
1050
|
+
}
|
|
1051
|
+
pi.sendMessage({
|
|
1052
|
+
customType: "crew_continue_stopped",
|
|
1053
|
+
content: message,
|
|
1054
|
+
display: true,
|
|
1055
|
+
});
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
995
1059
|
// Continue to next wave
|
|
996
1060
|
// Note: waveNumber was already incremented by addWaveResult() in work.ts
|
|
997
1061
|
const plan = crewStore.getPlan(cwd);
|
|
@@ -1029,7 +1093,12 @@ Usage (action-based API - preferred):
|
|
|
1029
1093
|
if (recentTestTimer) { clearTimeout(recentTestTimer); recentTestTimer = null; }
|
|
1030
1094
|
if (recentEditTimer) { clearTimeout(recentEditTimer); recentEditTimer = null; }
|
|
1031
1095
|
store.stopWatcher(state);
|
|
1032
|
-
|
|
1096
|
+
try {
|
|
1097
|
+
store.unregister(state, dirs);
|
|
1098
|
+
} catch {
|
|
1099
|
+
// Safe to ignore during shutdown: the process is exiting, so any leftover
|
|
1100
|
+
// registration will be cleaned up as stale on the next registry read.
|
|
1101
|
+
}
|
|
1033
1102
|
});
|
|
1034
1103
|
|
|
1035
1104
|
// ===========================================================================
|
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
|
@@ -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
|
|
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(
|
|
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(
|
|
469
|
-
} catch {
|
|
470
|
-
|
|
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;
|