pi-squad 0.14.0 → 0.15.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/README.md +7 -4
- package/package.json +4 -1
- package/src/index.ts +15 -3
- package/src/monitor.ts +35 -14
- package/src/scheduler.ts +125 -8
- package/src/store.ts +3 -1
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
183
183
|
| `squad` | Start a squad with goal + optional tasks/config |
|
|
184
184
|
| `squad_status` | Check progress, costs, task states |
|
|
185
185
|
| `squad_message` | Send message to a running agent |
|
|
186
|
-
| `squad_modify` | Add/cancel/pause/resume tasks or squads |
|
|
186
|
+
| `squad_modify` | Add/cancel/complete/pause/resume tasks or squads |
|
|
187
187
|
|
|
188
188
|
The main agent sees available agents in its system prompt and squad state when a squad is active.
|
|
189
189
|
|
|
@@ -280,18 +280,21 @@ Agents must complete at least 1 LLM turn AND make at least 1 tool call to be mar
|
|
|
280
280
|
### Session Resilience
|
|
281
281
|
|
|
282
282
|
- In-progress tasks are **suspended** on session crash, **resumed** on next startup
|
|
283
|
+
- Failure is never terminal: `resume` recovers failed squads (failed tasks reset to pending), `complete_task` marks recovered work done and schedules dependents, and a 60s reconcile loop re-derives scheduling from persisted state so out-of-band store edits can't strand ready tasks
|
|
283
284
|
- Squads are fully reconstructable from JSON files on disk
|
|
284
285
|
- Spawn failures are retried once with a 2-second delay
|
|
285
286
|
- All errors logged to `~/.pi/squad/debug.log` (always for errors, `PI_SQUAD_DEBUG=1` for verbose)
|
|
286
287
|
|
|
287
288
|
### Health Monitoring
|
|
288
289
|
|
|
290
|
+
The monitor never kills or blocks work on its own — its strongest action is notifying the main Pi session so you (or the main agent) can decide.
|
|
291
|
+
|
|
289
292
|
| Check | Threshold | Action |
|
|
290
293
|
|---|---|---|
|
|
291
294
|
| Idle warning | 3 minutes no output | Steer agent with nudge |
|
|
292
|
-
| Stuck detection | 5 minutes no output |
|
|
295
|
+
| Stuck detection | 5 minutes no output | Steer, then escalate to main session |
|
|
293
296
|
| Loop detection | Same tool call 5x | Steer with warning |
|
|
294
|
-
|
|
|
297
|
+
| Long-running check-in | Every 30 minutes total (`PI_SQUAD_CEILING_MS`) | Notify main session — work continues |
|
|
295
298
|
|
|
296
299
|
## Data Layout
|
|
297
300
|
|
|
@@ -320,7 +323,7 @@ src/
|
|
|
320
323
|
├── agent-pool.ts — pi RPC process management, activity tracking
|
|
321
324
|
├── protocol.ts — system prompt builder (chain context, sibling awareness, knowledge)
|
|
322
325
|
├── router.ts — @mention parsing, cross-agent messaging
|
|
323
|
-
├── monitor.ts — health checks (idle, stuck, loop,
|
|
326
|
+
├── monitor.ts — health checks (idle, stuck, loop, long-run notify)
|
|
324
327
|
├── planner.ts — one-shot goal decomposition via LLM
|
|
325
328
|
├── logger.ts — file-based logging (never writes to stderr)
|
|
326
329
|
├── panel/ — TUI overlay panel and widget
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"registry": "https://registry.npmjs.org/",
|
|
50
50
|
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.1.1"
|
|
51
54
|
}
|
|
52
55
|
}
|
package/src/index.ts
CHANGED
|
@@ -461,7 +461,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
461
461
|
pi.registerTool({
|
|
462
462
|
name: "squad_modify",
|
|
463
463
|
label: "Squad Modify",
|
|
464
|
-
description: "Modify the running squad: add_task, cancel_task, pause, resume, cancel (entire squad).",
|
|
464
|
+
description: "Modify the running squad: add_task, cancel_task, complete_task (mark done + schedule dependents), pause, resume (also recovers failed squads), cancel (entire squad).",
|
|
465
465
|
parameters: Type.Object({
|
|
466
466
|
action: Type.Union(
|
|
467
467
|
[
|
|
@@ -469,6 +469,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
469
469
|
Type.Literal("cancel_task"),
|
|
470
470
|
Type.Literal("pause_task"),
|
|
471
471
|
Type.Literal("resume_task"),
|
|
472
|
+
Type.Literal("complete_task"),
|
|
472
473
|
Type.Literal("pause"),
|
|
473
474
|
Type.Literal("resume"),
|
|
474
475
|
Type.Literal("cancel"),
|
|
@@ -476,6 +477,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
476
477
|
{ description: "Action to perform" },
|
|
477
478
|
),
|
|
478
479
|
taskId: Type.Optional(Type.String({ description: "Task ID for task-specific actions" })),
|
|
480
|
+
output: Type.Optional(Type.String({ description: "Result summary for complete_task (what was accomplished)" })),
|
|
479
481
|
task: Type.Optional(
|
|
480
482
|
Type.Object({
|
|
481
483
|
id: Type.String(),
|
|
@@ -493,11 +495,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
493
495
|
if (params.action === "resume") {
|
|
494
496
|
// Find a squad to resume: use activeSquadId or find the latest paused one
|
|
495
497
|
const squadId = activeSquadId || store.findActiveSquads()
|
|
496
|
-
.filter((s) => s.cwd === ctx.cwd && s.status === "paused")
|
|
498
|
+
.filter((s) => s.cwd === ctx.cwd && (s.status === "paused" || s.status === "failed"))
|
|
497
499
|
.sort((a, b) => b.created.localeCompare(a.created))[0]?.id;
|
|
498
500
|
|
|
499
501
|
if (!squadId) {
|
|
500
|
-
return { content: [{ type: "text" as const, text: "No paused squad found to resume." }], details: undefined };
|
|
502
|
+
return { content: [{ type: "text" as const, text: "No paused or failed squad found to resume." }], details: undefined };
|
|
501
503
|
}
|
|
502
504
|
|
|
503
505
|
// Create a fresh scheduler if needed
|
|
@@ -632,6 +634,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
632
634
|
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }], details: undefined };
|
|
633
635
|
}
|
|
634
636
|
|
|
637
|
+
case "complete_task": {
|
|
638
|
+
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
|
|
639
|
+
try {
|
|
640
|
+
await activeScheduler.completeTask(params.taskId, params.output);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
return { content: [{ type: "text" as const, text: `complete_task failed: ${(err as Error).message}` }], details: undefined };
|
|
643
|
+
}
|
|
644
|
+
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' marked done — dependents unblocked and scheduled.` }], details: undefined };
|
|
645
|
+
}
|
|
646
|
+
|
|
635
647
|
case "pause": {
|
|
636
648
|
const squad = store.loadSquad(activeSquadId);
|
|
637
649
|
if (squad) {
|
package/src/monitor.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* - Idle timeout (no output for N minutes)
|
|
6
6
|
* - Stuck detection (no output for longer)
|
|
7
7
|
* - Loop detection (same tool call repeated)
|
|
8
|
-
* -
|
|
8
|
+
* - Long-running check-in (notify the main session; never aborts)
|
|
9
9
|
* - File conflict warnings
|
|
10
|
+
*
|
|
11
|
+
* The monitor NEVER kills or blocks work on its own. Its strongest action is
|
|
12
|
+
* `notify`: tell the main Pi session so a human/main agent can check in.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import type { AgentPool } from "./agent-pool.js";
|
|
@@ -25,7 +28,7 @@ function envMs(name: string, fallback: number): number {
|
|
|
25
28
|
|
|
26
29
|
const IDLE_WARNING_MS = envMs("PI_SQUAD_IDLE_MS", 3 * 60 * 1000); // no output → warn
|
|
27
30
|
const STUCK_TIMEOUT_MS = envMs("PI_SQUAD_STUCK_MS", 5 * 60 * 1000); // no output → intervene
|
|
28
|
-
const
|
|
31
|
+
const LONG_RUN_NOTIFY_MS = envMs("PI_SQUAD_CEILING_MS", 30 * 60 * 1000); // total → notify main session (no abort)
|
|
29
32
|
const LOOP_THRESHOLD = 5; // same tool call 5x → looping
|
|
30
33
|
const POLL_INTERVAL_MS = envMs("PI_SQUAD_POLL_MS", 30 * 1000); // check interval
|
|
31
34
|
|
|
@@ -33,7 +36,7 @@ const POLL_INTERVAL_MS = envMs("PI_SQUAD_POLL_MS", 30 * 1000); // check interval
|
|
|
33
36
|
// Types
|
|
34
37
|
// ============================================================================
|
|
35
38
|
|
|
36
|
-
export type MonitorActionType = "steer" | "
|
|
39
|
+
export type MonitorActionType = "steer" | "escalate" | "notify";
|
|
37
40
|
|
|
38
41
|
export interface MonitorAction {
|
|
39
42
|
type: MonitorActionType;
|
|
@@ -60,6 +63,8 @@ export class Monitor {
|
|
|
60
63
|
private stuckSteered = new Set<string>();
|
|
61
64
|
/** Track which tasks have been escalated (one escalation per stuck episode) */
|
|
62
65
|
private escalated = new Set<string>();
|
|
66
|
+
/** Last long-run threshold multiple notified per task (notify once per multiple) */
|
|
67
|
+
private longRunNotified = new Map<string, number>();
|
|
63
68
|
|
|
64
69
|
constructor(pool: AgentPool, squadId: string) {
|
|
65
70
|
this.pool = pool;
|
|
@@ -100,6 +105,7 @@ export class Monitor {
|
|
|
100
105
|
this.warned.clear();
|
|
101
106
|
this.stuckSteered.clear();
|
|
102
107
|
this.escalated.clear();
|
|
108
|
+
this.longRunNotified.clear();
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/** Check all running agents */
|
|
@@ -112,7 +118,7 @@ export class Monitor {
|
|
|
112
118
|
if (!activity) continue;
|
|
113
119
|
|
|
114
120
|
const health = this.checkHealth(activity);
|
|
115
|
-
this.handleHealth(taskId, agentName, health);
|
|
121
|
+
this.handleHealth(taskId, agentName, health, activity);
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
|
|
@@ -133,7 +139,7 @@ export class Monitor {
|
|
|
133
139
|
if (unique.size === 1) return "looping";
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
if (totalMs >=
|
|
142
|
+
if (totalMs >= LONG_RUN_NOTIFY_MS) return "long_running";
|
|
137
143
|
if (idleMs >= STUCK_TIMEOUT_MS) return "stuck";
|
|
138
144
|
if (idleMs >= IDLE_WARNING_MS) return "idle_warning";
|
|
139
145
|
|
|
@@ -141,7 +147,12 @@ export class Monitor {
|
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
/** Take action based on health status */
|
|
144
|
-
private handleHealth(
|
|
150
|
+
private handleHealth(
|
|
151
|
+
taskId: string,
|
|
152
|
+
agentName: string,
|
|
153
|
+
status: HealthStatus,
|
|
154
|
+
activity?: { startedAt: number },
|
|
155
|
+
): void {
|
|
145
156
|
if (status !== "healthy") debug("squad-monitor", `${taskId} (${agentName}): ${status}`);
|
|
146
157
|
switch (status) {
|
|
147
158
|
case "healthy":
|
|
@@ -203,15 +214,25 @@ export class Monitor {
|
|
|
203
214
|
});
|
|
204
215
|
break;
|
|
205
216
|
|
|
206
|
-
case "
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
case "long_running": {
|
|
218
|
+
// Never abort. Notify the main Pi session once per threshold multiple
|
|
219
|
+
// (30m, 60m, 90m, …) so it can check in if needed — work continues.
|
|
220
|
+
const totalMs = Date.now() - (activity?.startedAt ?? Date.now());
|
|
221
|
+
const multiple = Math.floor(totalMs / LONG_RUN_NOTIFY_MS);
|
|
222
|
+
const lastNotified = this.longRunNotified.get(taskId) ?? 0;
|
|
223
|
+
if (multiple > lastNotified) {
|
|
224
|
+
this.longRunNotified.set(taskId, multiple);
|
|
225
|
+
const minutes = Math.round(totalMs / 60_000);
|
|
226
|
+
this.emit({
|
|
227
|
+
type: "notify",
|
|
228
|
+
taskId,
|
|
229
|
+
agentName,
|
|
230
|
+
reason: `Agent ${agentName} has been working on ${taskId} for ${minutes}m. Still running — check on it if needed (steer, pause, or let it continue).`,
|
|
231
|
+
message: "",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
214
234
|
break;
|
|
235
|
+
}
|
|
215
236
|
}
|
|
216
237
|
}
|
|
217
238
|
}
|
package/src/scheduler.ts
CHANGED
|
@@ -72,6 +72,8 @@ export class Scheduler {
|
|
|
72
72
|
private running = false;
|
|
73
73
|
/** Track spawn retries to allow one retry per task */
|
|
74
74
|
private spawnRetries = new Set<string>();
|
|
75
|
+
/** Periodic level-triggered reconcile (heals missed events / out-of-band store edits) */
|
|
76
|
+
private reconcileTimer: ReturnType<typeof setInterval> | null = null;
|
|
75
77
|
|
|
76
78
|
/** Get the project cwd for this squad (from squad.json) */
|
|
77
79
|
getProjectCwd(): string | undefined {
|
|
@@ -93,8 +95,16 @@ export class Scheduler {
|
|
|
93
95
|
this.monitor.onAction((action) => {
|
|
94
96
|
if (action.type === "steer") {
|
|
95
97
|
this.pool.steer(action.taskId, action.message);
|
|
96
|
-
} else if (action.type === "
|
|
97
|
-
|
|
98
|
+
} else if (action.type === "notify") {
|
|
99
|
+
// Informational only — tell the main Pi session directly (no advisor
|
|
100
|
+
// detour, no kill). The agent keeps working.
|
|
101
|
+
this.emit({
|
|
102
|
+
type: "escalation",
|
|
103
|
+
squadId: this.squadId,
|
|
104
|
+
taskId: action.taskId,
|
|
105
|
+
agentName: action.agentName,
|
|
106
|
+
message: action.reason,
|
|
107
|
+
});
|
|
98
108
|
} else if (action.type === "escalate") {
|
|
99
109
|
// Advisor-first: try a strong-model rescue before interrupting the human
|
|
100
110
|
void this.tryAdvisorRescue(action.taskId, action.agentName, action.reason).then((rescued) => {
|
|
@@ -160,13 +170,22 @@ export class Scheduler {
|
|
|
160
170
|
async start(): Promise<void> {
|
|
161
171
|
this.running = true;
|
|
162
172
|
this.monitor.start();
|
|
163
|
-
await this.
|
|
173
|
+
await this.reconcile();
|
|
174
|
+
// Level-triggered safety net: periodically re-derive scheduling decisions
|
|
175
|
+
// from persisted state so missed in-memory events (crashes, out-of-band
|
|
176
|
+
// store edits, external recovery) cannot strand ready tasks.
|
|
177
|
+
this.reconcileTimer = setInterval(() => void this.reconcile(), 60_000);
|
|
178
|
+
(this.reconcileTimer as { unref?: () => void }).unref?.();
|
|
164
179
|
}
|
|
165
180
|
|
|
166
181
|
/** Stop the scheduler — kills all agents, saves state */
|
|
167
182
|
async stop(): Promise<void> {
|
|
168
183
|
this.running = false;
|
|
169
184
|
this.monitor.stop();
|
|
185
|
+
if (this.reconcileTimer) {
|
|
186
|
+
clearInterval(this.reconcileTimer);
|
|
187
|
+
this.reconcileTimer = null;
|
|
188
|
+
}
|
|
170
189
|
|
|
171
190
|
// Suspend in-progress tasks
|
|
172
191
|
const tasks = store.loadAllTasks(this.squadId);
|
|
@@ -179,17 +198,26 @@ export class Scheduler {
|
|
|
179
198
|
await this.pool.killAll();
|
|
180
199
|
}
|
|
181
200
|
|
|
182
|
-
/** Resume from suspended state
|
|
201
|
+
/** Resume from suspended OR failed state. Failure is never terminal:
|
|
202
|
+
* failed tasks are reset to pending and a failed squad becomes running. */
|
|
183
203
|
async resume(): Promise<void> {
|
|
184
204
|
const tasks = store.loadAllTasks(this.squadId);
|
|
185
205
|
for (const task of tasks) {
|
|
186
206
|
if (task.status === "suspended") {
|
|
187
207
|
store.updateTaskStatus(this.squadId, task.id, "pending");
|
|
208
|
+
} else if (task.status === "failed") {
|
|
209
|
+
store.updateTaskStatus(this.squadId, task.id, "pending", { error: null });
|
|
210
|
+
store.appendMessage(this.squadId, task.id, {
|
|
211
|
+
ts: store.now(),
|
|
212
|
+
from: "system",
|
|
213
|
+
type: "status",
|
|
214
|
+
text: "Reset failed → pending on squad resume",
|
|
215
|
+
});
|
|
188
216
|
}
|
|
189
217
|
}
|
|
190
218
|
|
|
191
219
|
const squad = store.loadSquad(this.squadId);
|
|
192
|
-
if (squad && squad.status === "paused") {
|
|
220
|
+
if (squad && (squad.status === "paused" || squad.status === "failed")) {
|
|
193
221
|
squad.status = "running";
|
|
194
222
|
store.saveSquad(squad);
|
|
195
223
|
}
|
|
@@ -197,6 +225,52 @@ export class Scheduler {
|
|
|
197
225
|
await this.start();
|
|
198
226
|
}
|
|
199
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Level-triggered reconciliation — derive scheduling from persisted state:
|
|
230
|
+
* 1. Unblock blocked tasks whose deps are all done (missed autoUnblock events)
|
|
231
|
+
* 2. Self-heal a "failed" squad when runnable work exists (out-of-band recovery)
|
|
232
|
+
* 3. Schedule ready tasks
|
|
233
|
+
* Safe to call any time; no-ops when nothing changed.
|
|
234
|
+
*/
|
|
235
|
+
async reconcile(): Promise<void> {
|
|
236
|
+
if (!this.running) return;
|
|
237
|
+
const squad = store.loadSquad(this.squadId);
|
|
238
|
+
if (!squad) return;
|
|
239
|
+
|
|
240
|
+
const tasks = store.loadAllTasks(this.squadId);
|
|
241
|
+
|
|
242
|
+
// 1. Blocked → pending when all deps are done (respects autoUnblock config)
|
|
243
|
+
if (squad.config.autoUnblock) {
|
|
244
|
+
for (const task of tasks) {
|
|
245
|
+
if (task.status !== "blocked") continue;
|
|
246
|
+
const allDepsDone = task.depends.every((depId) => {
|
|
247
|
+
const dep = tasks.find((t) => t.id === depId);
|
|
248
|
+
return dep?.status === "done";
|
|
249
|
+
});
|
|
250
|
+
if (allDepsDone) {
|
|
251
|
+
task.status = "pending";
|
|
252
|
+
store.updateTaskStatus(this.squadId, task.id, "pending");
|
|
253
|
+
this.emit({ type: "task_unblocked", squadId: this.squadId, taskId: task.id });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2. A "failed" squad with runnable work self-heals to running. This makes
|
|
259
|
+
// the terminal status derivable from task state instead of a one-way latch.
|
|
260
|
+
const hasRunnable = tasks.some((t) => {
|
|
261
|
+
if (t.status === "in_progress") return true;
|
|
262
|
+
if (t.status !== "pending") return false;
|
|
263
|
+
return t.depends.every((depId) => tasks.find((x) => x.id === depId)?.status === "done");
|
|
264
|
+
});
|
|
265
|
+
if (squad.status === "failed" && hasRunnable) {
|
|
266
|
+
squad.status = "running";
|
|
267
|
+
store.saveSquad(squad);
|
|
268
|
+
debug("squad-scheduler", "reconcile: failed squad has runnable work — healing to running");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await this.scheduleReadyTasks();
|
|
272
|
+
}
|
|
273
|
+
|
|
200
274
|
// =========================================================================
|
|
201
275
|
// Task Scheduling
|
|
202
276
|
// =========================================================================
|
|
@@ -1073,10 +1147,53 @@ export class Scheduler {
|
|
|
1073
1147
|
this.updateContext();
|
|
1074
1148
|
}
|
|
1075
1149
|
|
|
1076
|
-
/** Resume a suspended task */
|
|
1150
|
+
/** Resume a suspended/failed task. Reconciles so a failed squad heals too. */
|
|
1077
1151
|
async resumeTask(taskId: string): Promise<void> {
|
|
1078
|
-
store.updateTaskStatus(this.squadId, taskId, "pending");
|
|
1079
|
-
await this.
|
|
1152
|
+
store.updateTaskStatus(this.squadId, taskId, "pending", { error: null });
|
|
1153
|
+
await this.reconcile();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Mark a task done through the normal completion flow (admin/recovery path).
|
|
1158
|
+
* Unlike editing the store directly, this fires auto-unblock and scheduling,
|
|
1159
|
+
* so dependents transition pending → running. Skips the QA rework check —
|
|
1160
|
+
* a human/main agent marking a task done is an explicit override.
|
|
1161
|
+
*/
|
|
1162
|
+
async completeTask(taskId: string, output?: string): Promise<void> {
|
|
1163
|
+
const task = store.loadTask(this.squadId, taskId);
|
|
1164
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
1165
|
+
if (task.status === "done") return;
|
|
1166
|
+
|
|
1167
|
+
if (this.pool.isRunning(taskId)) {
|
|
1168
|
+
await this.pool.kill(taskId);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
store.updateTaskStatus(this.squadId, taskId, "done", {
|
|
1172
|
+
output: output ?? task.output ?? "Marked done (recovered/admin)",
|
|
1173
|
+
error: null,
|
|
1174
|
+
completed: store.now(),
|
|
1175
|
+
});
|
|
1176
|
+
store.appendMessage(this.squadId, taskId, {
|
|
1177
|
+
ts: store.now(),
|
|
1178
|
+
from: "system",
|
|
1179
|
+
type: "done",
|
|
1180
|
+
text: "Task marked done (recovered/admin)",
|
|
1181
|
+
});
|
|
1182
|
+
this.emit({
|
|
1183
|
+
type: "task_completed",
|
|
1184
|
+
squadId: this.squadId,
|
|
1185
|
+
taskId,
|
|
1186
|
+
agentName: task.agent,
|
|
1187
|
+
message: output ?? "",
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
this.autoUnblock(taskId);
|
|
1191
|
+
await this.reconcile();
|
|
1192
|
+
|
|
1193
|
+
const freshTasks = store.loadAllTasks(this.squadId);
|
|
1194
|
+
const freshSquad = store.loadSquad(this.squadId);
|
|
1195
|
+
if (freshSquad) this.checkSquadCompletion(freshTasks, freshSquad);
|
|
1196
|
+
this.updateContext();
|
|
1080
1197
|
}
|
|
1081
1198
|
|
|
1082
1199
|
/** Cancel a task */
|
package/src/store.ts
CHANGED
|
@@ -269,9 +269,11 @@ export function listSquads(): string[] {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
export function findActiveSquads(): Squad[] {
|
|
272
|
+
// Includes "failed": failure is recoverable (resume resets failed tasks), so
|
|
273
|
+
// failed squads must stay discoverable. All callers filter by status.
|
|
272
274
|
return listSquads()
|
|
273
275
|
.map((id) => loadSquad(id))
|
|
274
|
-
.filter((s): s is Squad => s !== null && (s.status === "running" || s.status === "paused"));
|
|
276
|
+
.filter((s): s is Squad => s !== null && (s.status === "running" || s.status === "paused" || s.status === "failed"));
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
/** List squads filtered by project cwd. If no cwd, returns all. */
|
package/src/types.ts
CHANGED
|
@@ -223,7 +223,7 @@ export interface AgentActivity {
|
|
|
223
223
|
modifiedFiles: Set<string>;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
export type HealthStatus = "healthy" | "idle_warning" | "stuck" | "looping" | "
|
|
226
|
+
export type HealthStatus = "healthy" | "idle_warning" | "stuck" | "looping" | "long_running";
|
|
227
227
|
|
|
228
228
|
// ============================================================================
|
|
229
229
|
// Supervisor
|