pi-messenger 0.13.1 → 0.13.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/CHANGELOG.md +17 -0
- package/README.md +1 -0
- package/crew/index.ts +11 -1
- package/crew/state-autonomous.ts +30 -0
- package/index.ts +77 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.13.2] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`work.stop` action** — `pi_messenger({ action: "work.stop" })` now stops autonomous Crew work for the current project and persists the stop state.
|
|
9
|
+
- **Autonomous guard coverage tests** — Added targeted tests for `work.stop` routing and `agent_end` autonomous continuation guard behavior.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **Runtime artifact commit noise** — Added `.pi/` to `.gitignore` to avoid accidentally committing runtime files like feed artifacts.
|
|
19
|
+
|
|
3
20
|
## [0.13.1] - 2026-03-14
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -268,6 +268,7 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
|
|
|
268
268
|
|--------|-------------|
|
|
269
269
|
| `plan` | Create plan from PRD or inline prompt (`prd`, `prompt` optional — auto-discovers PRD if omitted, auto-starts workers unless `autoWork: false`) |
|
|
270
270
|
| `work` | Run ready tasks (`autonomous`, `concurrency` optional) |
|
|
271
|
+
| `work.stop` | Stop autonomous work for the current project |
|
|
271
272
|
| `review` | Review implementation (`target` task ID required) |
|
|
272
273
|
| `task.list` | List all tasks |
|
|
273
274
|
| `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;
|
|
@@ -180,6 +180,16 @@ export async function executeCrewAction(
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
case 'work': {
|
|
183
|
+
if (op === 'stop') {
|
|
184
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
185
|
+
if (!isAutonomousForCwd(cwd)) {
|
|
186
|
+
return result("No autonomous work running for this project.", { mode: "work.stop" });
|
|
187
|
+
}
|
|
188
|
+
stopAutonomous("manual");
|
|
189
|
+
appendEntry("crew-state", autonomousState);
|
|
190
|
+
return result("Autonomous work stopped.", { mode: "work.stop", autonomous: false });
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
try {
|
|
184
194
|
const workHandler = await import("./handlers/work.js");
|
|
185
195
|
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/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";
|
|
@@ -295,6 +296,24 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
295
296
|
const STATUS_HEARTBEAT_MS = 15_000;
|
|
296
297
|
let latestCtx: ExtensionContext | null = null;
|
|
297
298
|
let statusHeartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
299
|
+
const AUTONOMOUS_CONTINUE_REPEAT_LIMIT = 3;
|
|
300
|
+
let autonomousContinueSignature: string | null = null;
|
|
301
|
+
let autonomousContinueRepeats = 0;
|
|
302
|
+
|
|
303
|
+
function resetAutonomousContinueGuard(): void {
|
|
304
|
+
autonomousContinueSignature = null;
|
|
305
|
+
autonomousContinueRepeats = 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function trackAutonomousContinue(signature: string): number {
|
|
309
|
+
if (autonomousContinueSignature === signature) {
|
|
310
|
+
autonomousContinueRepeats += 1;
|
|
311
|
+
} else {
|
|
312
|
+
autonomousContinueSignature = signature;
|
|
313
|
+
autonomousContinueRepeats = 1;
|
|
314
|
+
}
|
|
315
|
+
return autonomousContinueRepeats;
|
|
316
|
+
}
|
|
298
317
|
|
|
299
318
|
function startStatusHeartbeat(): void {
|
|
300
319
|
if (statusHeartbeatTimer) return;
|
|
@@ -358,6 +377,7 @@ Usage (action-based API - preferred):
|
|
|
358
377
|
// Crew: Work through tasks
|
|
359
378
|
pi_messenger({ action: "work" }) → Run ready tasks
|
|
360
379
|
pi_messenger({ action: "work", autonomous: true }) → Run until done/blocked
|
|
380
|
+
pi_messenger({ action: "work.stop" }) → Stop autonomous work for this project
|
|
361
381
|
|
|
362
382
|
// Crew: Tasks
|
|
363
383
|
pi_messenger({ action: "task.show", id: "task-1" }) → Show task
|
|
@@ -749,6 +769,7 @@ Usage (action-based API - preferred):
|
|
|
749
769
|
|
|
750
770
|
pi.on("session_start", async (_event, ctx) => {
|
|
751
771
|
latestCtx = ctx;
|
|
772
|
+
resetAutonomousContinueGuard();
|
|
752
773
|
startStatusHeartbeat();
|
|
753
774
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
754
775
|
if (entry.type === "custom" && entry.customType === "crew-state") {
|
|
@@ -870,6 +891,7 @@ Usage (action-based API - preferred):
|
|
|
870
891
|
|
|
871
892
|
pi.on("session_switch", async (_event, ctx) => {
|
|
872
893
|
latestCtx = ctx;
|
|
894
|
+
resetAutonomousContinueGuard();
|
|
873
895
|
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
874
896
|
if (staleCleared && ctx.hasUI) {
|
|
875
897
|
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
@@ -880,6 +902,7 @@ Usage (action-based API - preferred):
|
|
|
880
902
|
});
|
|
881
903
|
pi.on("session_fork", async (_event, ctx) => {
|
|
882
904
|
latestCtx = ctx;
|
|
905
|
+
resetAutonomousContinueGuard();
|
|
883
906
|
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
884
907
|
if (staleCleared && ctx.hasUI) {
|
|
885
908
|
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
@@ -936,6 +959,22 @@ Usage (action-based API - preferred):
|
|
|
936
959
|
// ===========================================================================
|
|
937
960
|
|
|
938
961
|
pi.on("agent_end", async (_event, ctx) => {
|
|
962
|
+
if (process.env.PI_CREW_WORKER === "1" || process.env.PI_LOBBY_ID) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!state.registered) {
|
|
967
|
+
if (autonomousState.active) {
|
|
968
|
+
stopAutonomous("manual");
|
|
969
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
970
|
+
resetAutonomousContinueGuard();
|
|
971
|
+
if (ctx.hasUI) {
|
|
972
|
+
ctx.ui.notify("Autonomous stopped: this session is not registered in pi-messenger.", "warning");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
939
978
|
// --- Auto-work after plan completion ---
|
|
940
979
|
const autoWork = consumePendingAutoWork();
|
|
941
980
|
if (autoWork && !overlayTui) {
|
|
@@ -955,15 +994,26 @@ Usage (action-based API - preferred):
|
|
|
955
994
|
}
|
|
956
995
|
|
|
957
996
|
// --- Existing autonomous continuation ---
|
|
958
|
-
if (!autonomousState.active)
|
|
997
|
+
if (!autonomousState.active) {
|
|
998
|
+
resetAutonomousContinueGuard();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
959
1001
|
|
|
960
|
-
const
|
|
1002
|
+
const currentCwd = ctx.cwd ?? process.cwd();
|
|
1003
|
+
if (!isAutonomousForCwd(currentCwd)) {
|
|
1004
|
+
resetAutonomousContinueGuard();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const cwd = autonomousState.cwd ?? currentCwd;
|
|
961
1009
|
const crewDir = join(cwd, ".pi", "messenger", "crew");
|
|
962
1010
|
const crewConfig = loadCrewConfig(crewDir);
|
|
963
1011
|
|
|
964
1012
|
// Check max waves limit
|
|
965
1013
|
if (autonomousState.waveNumber >= crewConfig.work.maxWaves) {
|
|
966
1014
|
stopAutonomous("manual");
|
|
1015
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1016
|
+
resetAutonomousContinueGuard();
|
|
967
1017
|
if (ctx.hasUI) {
|
|
968
1018
|
ctx.ui.notify(`Autonomous stopped: max waves (${crewConfig.work.maxWaves}) reached`, "warning");
|
|
969
1019
|
}
|
|
@@ -972,14 +1022,16 @@ Usage (action-based API - preferred):
|
|
|
972
1022
|
|
|
973
1023
|
// Check for ready tasks
|
|
974
1024
|
const readyTasks = crewStore.getReadyTasks(cwd, { advisory: crewConfig.dependencies === "advisory" });
|
|
975
|
-
|
|
1025
|
+
|
|
976
1026
|
if (readyTasks.length === 0) {
|
|
977
1027
|
// No ready tasks - check if all done or blocked
|
|
978
1028
|
const allTasks = crewStore.getTasks(cwd);
|
|
979
1029
|
const allDone = allTasks.every(t => t.status === "done");
|
|
980
|
-
|
|
1030
|
+
|
|
981
1031
|
stopAutonomous(allDone ? "completed" : "blocked");
|
|
982
|
-
|
|
1032
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1033
|
+
resetAutonomousContinueGuard();
|
|
1034
|
+
|
|
983
1035
|
const plan = crewStore.getPlan(cwd);
|
|
984
1036
|
if (ctx.hasUI) {
|
|
985
1037
|
if (allDone) {
|
|
@@ -992,6 +1044,26 @@ Usage (action-based API - preferred):
|
|
|
992
1044
|
return;
|
|
993
1045
|
}
|
|
994
1046
|
|
|
1047
|
+
const continueSignature = `${cwd}:${autonomousState.waveNumber}:${readyTasks.map(task => task.id).sort().join(",")}`;
|
|
1048
|
+
const continueRepeatCount = trackAutonomousContinue(continueSignature);
|
|
1049
|
+
if (continueRepeatCount >= AUTONOMOUS_CONTINUE_REPEAT_LIMIT) {
|
|
1050
|
+
stopAutonomous("manual");
|
|
1051
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1052
|
+
resetAutonomousContinueGuard();
|
|
1053
|
+
|
|
1054
|
+
const plan = crewStore.getPlan(cwd);
|
|
1055
|
+
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 }).`;
|
|
1056
|
+
if (ctx.hasUI) {
|
|
1057
|
+
ctx.ui.notify(message, "warning");
|
|
1058
|
+
}
|
|
1059
|
+
pi.sendMessage({
|
|
1060
|
+
customType: "crew_continue_stopped",
|
|
1061
|
+
content: message,
|
|
1062
|
+
display: true,
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
995
1067
|
// Continue to next wave
|
|
996
1068
|
// Note: waveNumber was already incremented by addWaveResult() in work.ts
|
|
997
1069
|
const plan = crewStore.getPlan(cwd);
|