pi-squad 0.19.3 → 0.19.5
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 +15 -0
- package/package.json +1 -1
- package/src/scheduler-runtime.ts +49 -8
- package/src/scheduler.ts +17 -1
- package/src/types.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.5] - 2026-07-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Terminal task failures are now reported to the main session immediately when the rest of the squad keeps running (new `squad-task-failed` notification with the exact task, agent, error, and repair guidance). Previously an individual agent death — e.g. `Agent devops exited before RPC response` — was invisible in chat until the whole squad stalled or finished. When no runnable work remains, the existing `squad-failed` summary is sent instead, never both.
|
|
15
|
+
- A task that completes after an interim failure (spawn retry, RPC race during reopen) no longer displays the stale error annotation forever; successful completion clears `task.error`.
|
|
16
|
+
|
|
17
|
+
## [0.19.4] - 2026-07-24
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- The mandatory review-required report can no longer be silently lost or invisibly stalled. Successful delivery is durably recorded as `review.notifiedAt`; the 60s reconcile loop re-raises an unrecorded pending gate (covers delivery exceptions and disabled-mode drops), and an immediate TUI notification surfaces the pending review even while a long or stalled main-session run delays the queued follow-up report. Delivered gates are never re-notified while a human review is in progress.
|
|
22
|
+
- `squad_failed` stall notifications now fire only on the actual transition to `failed`. Repeated reconciles over an already-failed squad no longer queue duplicate notifications, each of which previously triggered its own main-session turn.
|
|
23
|
+
- Review-required delivery failures are now logged to `~/.pi/squad/debug.log` instead of being swallowed.
|
|
24
|
+
|
|
10
25
|
## [0.19.3] - 2026-07-19
|
|
11
26
|
|
|
12
27
|
### Fixed
|
package/package.json
CHANGED
package/src/scheduler-runtime.ts
CHANGED
|
@@ -22,14 +22,32 @@ export function wireSchedulerEvents(pi: ExtensionAPI, scheduler: Scheduler, squa
|
|
|
22
22
|
const totalCost = tasks.reduce((sum, task) => sum + task.usage.cost, 0);
|
|
23
23
|
scheduler.updateContext();
|
|
24
24
|
if (!squad) break;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
try {
|
|
26
|
+
pi.sendMessage({
|
|
27
|
+
customType: "squad-review-required",
|
|
28
|
+
content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\n` +
|
|
29
|
+
`Squad claims (review inputs only):\n${summary}\n\n` +
|
|
30
|
+
`Total cost: $${totalCost.toFixed(4)}\n\n` +
|
|
31
|
+
buildOrchestratorReviewGate(squad, tasks),
|
|
32
|
+
display: true,
|
|
33
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
34
|
+
// Record durable delivery so reconcile stops re-raising the gate.
|
|
35
|
+
// An unrecorded emission (throw here, or the disabled-mode drop
|
|
36
|
+
// above) is re-emitted by the next reconcile pass.
|
|
37
|
+
const fresh = store.loadSquad(squadId);
|
|
38
|
+
if (fresh?.review?.status === "pending" && !fresh.review.notifiedAt) {
|
|
39
|
+
fresh.review.notifiedAt = store.now();
|
|
40
|
+
store.saveSquad(fresh);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logError("squad-scheduler", `review-required delivery failed for ${squadId}: ${(error as Error).message}`);
|
|
44
|
+
}
|
|
45
|
+
// The followUp above waits for the current main run to settle. Surface
|
|
46
|
+
// the pending gate immediately in the TUI so a long/stalled run cannot
|
|
47
|
+
// hide it for hours.
|
|
48
|
+
try {
|
|
49
|
+
if (runtime.uiCtx?.hasUI) runtime.uiCtx.ui.notify(`[squad] "${squadId}" finished — awaiting your independent review (report queued for end of current turn)`, "info");
|
|
50
|
+
} catch { /* toast is best-effort */ }
|
|
33
51
|
// Keep the settled scheduler addressable. A later exact-task message
|
|
34
52
|
// can reopen the task immediately on its bound durable Pi session.
|
|
35
53
|
break;
|
|
@@ -48,6 +66,29 @@ export function wireSchedulerEvents(pi: ExtensionAPI, scheduler: Scheduler, squa
|
|
|
48
66
|
}
|
|
49
67
|
break;
|
|
50
68
|
}
|
|
69
|
+
case "task_failed": {
|
|
70
|
+
// A task died terminally while the squad continues. Without this the
|
|
71
|
+
// main session learns about individual failures only when the whole
|
|
72
|
+
// squad later stalls or finishes. When no runnable work remains the
|
|
73
|
+
// imminent squad_failed event carries the failure summary instead, so
|
|
74
|
+
// skip the task-level message to avoid duplicate notifications.
|
|
75
|
+
try {
|
|
76
|
+
const tasks = store.loadAllTasks(squadId);
|
|
77
|
+
const squadContinues = tasks.some((task) => task.status === "in_progress" || task.status === "pending");
|
|
78
|
+
if (!squadContinues) break;
|
|
79
|
+
const failed = tasks.find((task) => task.id === event.taskId);
|
|
80
|
+
pi.sendMessage({
|
|
81
|
+
customType: "squad-task-failed",
|
|
82
|
+
content: `[squad] Task '${event.taskId}'${failed ? ` (${failed.agent})` : ""} FAILED in '${squadId}' while other tasks continue.\n` +
|
|
83
|
+
`Error: ${event.message ?? failed?.error ?? "unknown"}\n` +
|
|
84
|
+
`Dependents of this task stay blocked. Repair now with squad_modify — resume_task to retry it, set_dependencies to reroute, or cancel_task if obsolete — or the squad will stall after the remaining tasks finish.`,
|
|
85
|
+
display: true,
|
|
86
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logError("squad-scheduler", `task-failed delivery failed for ${squadId}/${event.taskId}: ${(error as Error).message}`);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
51
92
|
case "squad_failed": {
|
|
52
93
|
const tasks = store.loadAllTasks(squadId);
|
|
53
94
|
const failed = tasks.filter((task) => task.status === "failed");
|
package/src/scheduler.ts
CHANGED
|
@@ -403,6 +403,15 @@ export class Scheduler {
|
|
|
403
403
|
if (freshSquad && (freshSquad.status === "running" || freshSquad.status === "failed")) {
|
|
404
404
|
this.checkSquadCompletion(store.loadAllTasks(this.squadId), freshSquad);
|
|
405
405
|
}
|
|
406
|
+
|
|
407
|
+
// A pending review whose notification never reached the main session
|
|
408
|
+
// (delivery threw, or disabled mode dropped the event) is re-raised until
|
|
409
|
+
// the delivery handler durably records review.notifiedAt. Successful
|
|
410
|
+
// delivery stops the re-raise, so a slow human review never re-notifies.
|
|
411
|
+
const reviewSquad = freshSquad?.status === "review" ? freshSquad : store.loadSquad(this.squadId);
|
|
412
|
+
if (reviewSquad?.status === "review" && reviewSquad.review?.status === "pending" && !reviewSquad.review.notifiedAt) {
|
|
413
|
+
this.emit({ type: "squad_review_required", squadId: this.squadId });
|
|
414
|
+
}
|
|
406
415
|
}
|
|
407
416
|
|
|
408
417
|
// =========================================================================
|
|
@@ -985,8 +994,11 @@ export class Scheduler {
|
|
|
985
994
|
.filter((m) => m.from === task.agent && (m.type === "text" || m.type === "done"));
|
|
986
995
|
const output = agentMessages.map((m) => m.text).join("\n");
|
|
987
996
|
|
|
997
|
+
// Clear any interim failure annotation (spawn retry, RPC race): a task
|
|
998
|
+
// that ultimately completed must not display a stale error forever.
|
|
988
999
|
store.updateTaskStatus(this.squadId, taskId, "done", {
|
|
989
1000
|
output: output || "Task completed",
|
|
1001
|
+
error: null,
|
|
990
1002
|
completed: store.now(),
|
|
991
1003
|
});
|
|
992
1004
|
|
|
@@ -1328,7 +1340,11 @@ export class Scheduler {
|
|
|
1328
1340
|
store.saveSquad(squad);
|
|
1329
1341
|
this.emit({ type: "squad_review_required", squadId: this.squadId });
|
|
1330
1342
|
} else if (anyFailed && !anyInProgress) {
|
|
1331
|
-
// All remaining tasks are blocked/failed with no way forward
|
|
1343
|
+
// All remaining tasks are blocked/failed with no way forward.
|
|
1344
|
+
// Emit only on the transition: repeated reconciles over an already-failed
|
|
1345
|
+
// squad must not queue duplicate stall notifications (each would trigger
|
|
1346
|
+
// its own main-session turn).
|
|
1347
|
+
if (squad.status === "failed") return;
|
|
1332
1348
|
const blockedCount = relevant.filter((task) => task.status === "blocked").length;
|
|
1333
1349
|
const failedCount = relevant.filter((task) => task.status === "failed").length;
|
|
1334
1350
|
if (blockedCount + failedCount === relevant.filter((task) => task.status !== "done").length) {
|
package/src/types.ts
CHANGED
|
@@ -78,6 +78,9 @@ export type SquadStatus = "planning" | "running" | "paused" | "review" | "done"
|
|
|
78
78
|
export interface SquadReview {
|
|
79
79
|
status: "pending" | "passed" | "failed";
|
|
80
80
|
requestedAt: string;
|
|
81
|
+
/** When the review-required notification was handed to the main session.
|
|
82
|
+
* Absent/null = not yet delivered; reconcile re-raises the gate. */
|
|
83
|
+
notifiedAt?: string | null;
|
|
81
84
|
completedAt: string | null;
|
|
82
85
|
verdict: "pass" | "pass_with_issues" | "fail" | null;
|
|
83
86
|
contractChecks: string[];
|