pi-squad 0.16.3 → 0.16.4
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 +18 -12
- package/package.json +1 -1
- package/src/agent-pool.ts +123 -44
- package/src/index.ts +160 -177
- package/src/panel/message-view.ts +58 -50
- package/src/panel/squad-widget.ts +35 -10
- package/src/panel/task-list.ts +13 -3
- package/src/router.ts +37 -35
- package/src/scheduler.ts +254 -64
- package/src/store.ts +195 -1
- package/src/types.ts +24 -0
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ Bundled agent definitions are copied to `~/.pi/squad/agents/` on first run. Edit
|
|
|
139
139
|
|
|
140
140
|
**@mention routing**: Agents write `@frontend what token format?` in their output. The router delivers it in real-time via RPC `steer()`.
|
|
141
141
|
|
|
142
|
-
**Main-session request/reply**: `squad_message`
|
|
142
|
+
**Main-session request/reply**: `squad_message` addresses one exact task ID. An agent name is accepted only as shorthand for exactly one currently live task, so two tasks assigned to the same role never share or steal messages. Requests are written first to the task's durable mailbox, then delivered with correlated Pi RPC. A completed task is reopened and resumed with `pi --session <its-original-session-file>`; only a task without a session binding receives a new session. This also works after scheduler/main-process reconstruction. By default (`expectReply: true`), the next substantive response is durably marked as the reply, pushed into main Pi, and wakes it; use `expectReply: false` for fire-and-forget steering. pi-squad waits for Pi's final `agent_settled` event before marking the task done or closing the child; low-level `agent_end` never kills queued continuations.
|
|
143
143
|
|
|
144
144
|
### Smart Planner
|
|
145
145
|
|
|
@@ -171,7 +171,7 @@ Shows live squad progress. Truncated to terminal width — no wrapping, determin
|
|
|
171
171
|
|
|
172
172
|
### Panel (Ctrl+Q)
|
|
173
173
|
|
|
174
|
-
Full overlay with task list, live activity preview, and scrollable message view.
|
|
174
|
+
Full overlay with task list, live activity preview, and scrollable message view. The view opens at the live tail but can scroll back through the complete durable history, including acknowledged and older orchestrator/human messages; multiline bodies are not shortened. Main-Pi requests are labeled `ORCHESTRATOR` and panel/user input is labeled `YOU`.
|
|
175
175
|
|
|
176
176
|
| Key | Action |
|
|
177
177
|
|---|---|
|
|
@@ -191,7 +191,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
191
191
|
| `/squad list` | List project squads |
|
|
192
192
|
| `/squad all` | List all squads |
|
|
193
193
|
| `/squad agents` | Manage agent definitions |
|
|
194
|
-
| `/squad msg [agent] text` |
|
|
194
|
+
| `/squad msg [task-id\|running-agent] text` | Message an exact task, or use an agent name only when it has one live task |
|
|
195
195
|
| `/squad widget` | Toggle widget |
|
|
196
196
|
| `/squad panel` | Toggle panel |
|
|
197
197
|
| `/squad cancel` | Cancel running squad |
|
|
@@ -205,7 +205,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
205
205
|
|---|---|
|
|
206
206
|
| `squad` | Start a squad with goal + optional tasks/config |
|
|
207
207
|
| `squad_status` | Check progress, costs, task states |
|
|
208
|
-
| `squad_message` |
|
|
208
|
+
| `squad_message` | Durably message an exact task; completed tasks reopen on their original session |
|
|
209
209
|
| `squad_modify` | Add/cancel/complete/pause/resume tasks or squads |
|
|
210
210
|
|
|
211
211
|
The main agent sees available agents in its system prompt and squad state when a squad is active.
|
|
@@ -245,7 +245,8 @@ squad({
|
|
|
245
245
|
- **Cost**: the agent pays the entire conversation history as input tokens on every turn — use sparingly
|
|
246
246
|
- **Context-window guard**: the fork is skipped automatically when the estimated session size exceeds 50% of the agent model's context window (agents on smaller-context models silently degrade to standard squad context; the skip is recorded in the task's message log and `debug.log`)
|
|
247
247
|
- Requires the main session to have a session file (skipped under `--no-session`)
|
|
248
|
-
-
|
|
248
|
+
- Each child session is stored under its task directory at `~/.pi/squad/<squad-id>/<task-id>/session/`, not in your project's session list
|
|
249
|
+
- Once created, that task-to-session binding is immutable; later resumes pass the original file through `--session`
|
|
249
250
|
- Prefer restating the 3-5 key decisions in the task description — reach for `inheritContext` only when that's impractical
|
|
250
251
|
|
|
251
252
|
### Custom Agents
|
|
@@ -298,14 +299,17 @@ Configure with `/squad advisor` (on/off, model, max calls per task, reasoning ef
|
|
|
298
299
|
|
|
299
300
|
### Meaningful Work Check
|
|
300
301
|
|
|
301
|
-
Agents must complete at least
|
|
302
|
+
Agents must complete at least one LLM turn and produce either a tool call or a substantive assistant artifact before they can be marked `done`. This permits legitimate report-only planning/review work while rejecting empty exits. A child that exits before final `agent_settled`, or settles without meaningful work, is resumed once on the same task session and then failed if the retry is exhausted.
|
|
302
303
|
|
|
303
304
|
### Session Resilience
|
|
304
305
|
|
|
305
|
-
-
|
|
306
|
-
-
|
|
307
|
-
-
|
|
308
|
-
-
|
|
306
|
+
- Every task owns one durable Pi session. New tasks create it under their task directory; stale, suspended, failed, or explicitly reopened tasks resume that same session rather than starting over. A pre-prompt retry may refresh Pi's provisional session ID only while the bound file is the same and its JSONL has not materialized; afterward both file and ID are immutable.
|
|
307
|
+
- Legacy tasks without a session binding migrate on first reopen by creating a task-owned session and seeding its first prompt with the complete persisted multiline message history and prior task output—without truncation.
|
|
308
|
+
- In-progress tasks are **suspended** on orderly shutdown; ordinary orphaned work is paused for explicit resume. If a scheduler itself is reconstructed, stale `in_progress` state is resumed by reconciliation. Independently, extension startup always reconstructs any project squad with pending mailbox entries—including `review` or already accepted `done` squads—and resumes delivery without another user action.
|
|
309
|
+
- A durable message to a completed task clears its prior completion/review state, reopens the squad, keeps the task `in_progress` while its agent is live, and requires a fresh orchestrator review after the final `agent_settled`. Every transitive descendant is re-blocked and reruns in dependency order, so results derived from the reopened dependency cannot remain falsely complete.
|
|
310
|
+
- 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.
|
|
311
|
+
- Mail is acknowledged only after Pi accepts the correlated RPC command. Pending and acknowledged entries remain task-addressed on disk, survive process restart, and remain visible in history. Queue and acknowledgement read/modify/write operations are serialized across processes so concurrent mutations cannot overwrite messages or delivery state.
|
|
312
|
+
- Squads are fully reconstructable from JSON files on disk. Unexpected child exits are retried once after 2 seconds on the same bound task session.
|
|
309
313
|
- All errors logged to `~/.pi/squad/debug.log` (always for errors, `PI_SQUAD_DEBUG=1` for verbose)
|
|
310
314
|
|
|
311
315
|
### Health Monitoring
|
|
@@ -331,8 +335,10 @@ All state in `~/.pi/squad/`. No database, no daemon. Writes are atomic. JSONL re
|
|
|
331
335
|
├── squad.json — goal, status, config, cwd
|
|
332
336
|
├── context.json — live state snapshot
|
|
333
337
|
└── {task-id}/
|
|
334
|
-
├── task.json
|
|
335
|
-
|
|
338
|
+
├── task.json — status, output, usage, immutable session binding, retry metadata
|
|
339
|
+
├── messages.jsonl — append-only conversation history
|
|
340
|
+
├── mailbox.json — task-addressed inbound mail, including delivery acknowledgements
|
|
341
|
+
└── session/ — this task's durable Pi session JSONL
|
|
336
342
|
```
|
|
337
343
|
|
|
338
344
|
## Architecture
|
package/package.json
CHANGED
package/src/agent-pool.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
9
10
|
import * as fs from "node:fs";
|
|
10
11
|
import * as os from "node:os";
|
|
11
12
|
import * as path from "node:path";
|
|
12
|
-
import type { AgentDef, AgentActivity,
|
|
13
|
+
import type { AgentDef, AgentActivity, TaskSession } from "./types.js";
|
|
13
14
|
import { buildAgentSystemPrompt, type ProtocolBuildOptions } from "./protocol.js";
|
|
14
15
|
import { debug, logError } from "./logger.js";
|
|
15
16
|
|
|
@@ -22,8 +23,8 @@ export interface AgentProcess {
|
|
|
22
23
|
agentName: string;
|
|
23
24
|
process: ChildProcess;
|
|
24
25
|
activity: AgentActivity;
|
|
25
|
-
/**
|
|
26
|
-
|
|
26
|
+
/** Durable Pi session owned by this task. */
|
|
27
|
+
session: TaskSession;
|
|
27
28
|
/** Abort controller for cleanup */
|
|
28
29
|
aborted: boolean;
|
|
29
30
|
}
|
|
@@ -34,6 +35,7 @@ export type AgentEventType =
|
|
|
34
35
|
| "tool_execution_end"
|
|
35
36
|
| "turn_end"
|
|
36
37
|
| "agent_end"
|
|
38
|
+
| "agent_settled"
|
|
37
39
|
| "error";
|
|
38
40
|
|
|
39
41
|
export interface AgentEvent {
|
|
@@ -77,7 +79,12 @@ function attachLineReader(
|
|
|
77
79
|
export class AgentPool {
|
|
78
80
|
private agents = new Map<string, AgentProcess>();
|
|
79
81
|
private listeners: AgentEventListener[] = [];
|
|
80
|
-
private
|
|
82
|
+
private responseWaiters = new Map<string, {
|
|
83
|
+
process: ChildProcess;
|
|
84
|
+
resolve: (event: any) => void;
|
|
85
|
+
reject: (error: Error) => void;
|
|
86
|
+
timer: ReturnType<typeof setTimeout>;
|
|
87
|
+
}>();
|
|
81
88
|
|
|
82
89
|
/** Subscribe to agent events */
|
|
83
90
|
onEvent(listener: AgentEventListener): () => void {
|
|
@@ -124,20 +131,6 @@ export class AgentPool {
|
|
|
124
131
|
.map((a) => a.agentName);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
/** Queue a message for an agent (delivered on next spawn or via steer if running) */
|
|
128
|
-
queueMessage(agentName: string, message: TaskMessage): void {
|
|
129
|
-
const queue = this.messageQueues.get(agentName) || [];
|
|
130
|
-
queue.push(message);
|
|
131
|
-
this.messageQueues.set(agentName, queue);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Consume queued messages for an agent */
|
|
135
|
-
consumeQueue(agentName: string): TaskMessage[] {
|
|
136
|
-
const queue = this.messageQueues.get(agentName) || [];
|
|
137
|
-
this.messageQueues.delete(agentName);
|
|
138
|
-
return queue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
134
|
/**
|
|
142
135
|
* Spawn a pi process in RPC mode for a task.
|
|
143
136
|
*/
|
|
@@ -147,10 +140,14 @@ export class AgentPool {
|
|
|
147
140
|
protocolOptions: ProtocolBuildOptions;
|
|
148
141
|
cwd: string;
|
|
149
142
|
skillPaths: string[];
|
|
150
|
-
/**
|
|
143
|
+
/** Resume this task's already-bound durable Pi session. */
|
|
144
|
+
resumeSession?: TaskSession;
|
|
145
|
+
/** Create a new durable session here (new tasks only). */
|
|
146
|
+
sessionDir?: string;
|
|
147
|
+
/** Fork the given session file so a new task inherits main-session context. */
|
|
151
148
|
forkSession?: { file: string; sessionDir: string };
|
|
152
149
|
}): Promise<AgentProcess> {
|
|
153
|
-
const { taskId, agentDef, protocolOptions, cwd, skillPaths, forkSession } = options;
|
|
150
|
+
const { taskId, agentDef, protocolOptions, cwd, skillPaths, resumeSession, sessionDir, forkSession } = options;
|
|
154
151
|
|
|
155
152
|
// Kill existing process for this task if any
|
|
156
153
|
if (this.agents.has(taskId)) {
|
|
@@ -164,7 +161,7 @@ export class AgentPool {
|
|
|
164
161
|
fs.writeFileSync(promptFile, systemPrompt, "utf-8");
|
|
165
162
|
|
|
166
163
|
// Build pi CLI args
|
|
167
|
-
const args = buildPiArgs(agentDef, promptFile, skillPaths, forkSession);
|
|
164
|
+
const args = buildPiArgs(agentDef, promptFile, skillPaths, { resumeSession, sessionDir, forkSession });
|
|
168
165
|
|
|
169
166
|
// Spawn pi process — set env var to prevent recursive squad extension loading
|
|
170
167
|
const invocation = getPiInvocation(["--mode", "rpc", ...args]);
|
|
@@ -190,7 +187,8 @@ export class AgentPool {
|
|
|
190
187
|
agentName: agentDef.name,
|
|
191
188
|
process: proc,
|
|
192
189
|
activity,
|
|
193
|
-
|
|
190
|
+
// Replaced with the authoritative get_state values before spawn returns.
|
|
191
|
+
session: resumeSession ?? { file: "" },
|
|
194
192
|
aborted: false,
|
|
195
193
|
};
|
|
196
194
|
|
|
@@ -212,26 +210,31 @@ export class AgentPool {
|
|
|
212
210
|
}
|
|
213
211
|
});
|
|
214
212
|
|
|
215
|
-
let
|
|
213
|
+
let terminalEventEmitted = false;
|
|
216
214
|
let stdoutLines = 0;
|
|
217
215
|
proc.on("exit", (code, signal) => {
|
|
216
|
+
this.rejectResponseWaiters(proc, new Error(`Agent ${agentDef.name} exited before RPC response`));
|
|
218
217
|
// Log diagnostic info for debugging spawn failures
|
|
219
218
|
if (code !== 0 && code !== null) {
|
|
220
219
|
logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr || "(empty)"}`);
|
|
221
220
|
}
|
|
222
|
-
// Capture activity stats
|
|
221
|
+
// Capture activity stats before cleanup. A delayed exit callback from an
|
|
222
|
+
// old child must never delete or mutate a replacement registered for the
|
|
223
|
+
// same task ID.
|
|
223
224
|
const finalActivity = agentProc.activity;
|
|
224
|
-
|
|
225
|
-
this.agents.delete(taskId);
|
|
226
|
-
// Only
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
const isCurrentChild = this.agents.get(taskId) === agentProc;
|
|
226
|
+
if (isCurrentChild) this.agents.delete(taskId);
|
|
227
|
+
// Only the currently registered child may report an unexpected exit.
|
|
228
|
+
// Intentional shutdown must not reopen a suspended or cancelled task.
|
|
229
|
+
if (isCurrentChild && !terminalEventEmitted && !agentProc.aborted) {
|
|
230
|
+
terminalEventEmitted = true;
|
|
229
231
|
this.emit({
|
|
230
232
|
type: "agent_end",
|
|
231
233
|
taskId,
|
|
232
234
|
agentName: agentDef.name,
|
|
233
235
|
data: {
|
|
234
236
|
exitCode: code,
|
|
237
|
+
unexpectedExit: true,
|
|
235
238
|
// Preserve complete diagnostics for task failure reports and recovery.
|
|
236
239
|
stderr,
|
|
237
240
|
turnCount: finalActivity.turnCount,
|
|
@@ -247,8 +250,8 @@ export class AgentPool {
|
|
|
247
250
|
}, 500);
|
|
248
251
|
});
|
|
249
252
|
|
|
250
|
-
// Expose the guard so handleRpcEvent can
|
|
251
|
-
(agentProc as any).
|
|
253
|
+
// Expose the guard so handleRpcEvent can mark final settlement.
|
|
254
|
+
(agentProc as any)._terminalEventEmitted = () => { terminalEventEmitted = true; };
|
|
252
255
|
|
|
253
256
|
// Wait for process to initialize — pi needs time to load extensions, models, etc.
|
|
254
257
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -259,25 +262,43 @@ export class AgentPool {
|
|
|
259
262
|
);
|
|
260
263
|
}
|
|
261
264
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
this.
|
|
265
|
+
// Persisted session identity is authoritative. Capture it before any prompt
|
|
266
|
+
// is sent so a crash/retry can only resume this task's original context.
|
|
267
|
+
const state = await this.requestRpc(proc, { type: "get_state" });
|
|
268
|
+
const rawSessionFile = state?.data?.sessionFile;
|
|
269
|
+
if (!state?.success || typeof rawSessionFile !== "string" || rawSessionFile.length === 0) {
|
|
270
|
+
await this.kill(taskId);
|
|
271
|
+
throw new Error(`Agent ${agentDef.name} did not expose a durable session file`);
|
|
272
|
+
}
|
|
273
|
+
agentProc.session = {
|
|
274
|
+
file: path.isAbsolute(rawSessionFile) ? path.normalize(rawSessionFile) : path.resolve(cwd, rawSessionFile),
|
|
275
|
+
...(typeof state.data?.sessionId === "string" && state.data.sessionId
|
|
276
|
+
? { sessionId: state.data.sessionId }
|
|
277
|
+
: {}),
|
|
278
|
+
};
|
|
265
279
|
|
|
266
280
|
return agentProc;
|
|
267
281
|
}
|
|
268
282
|
|
|
283
|
+
/** Start a turn in an initialized task session. */
|
|
284
|
+
async prompt(taskId: string, message: string): Promise<boolean> {
|
|
285
|
+
const agent = this.agents.get(taskId);
|
|
286
|
+
if (!agent || agent.aborted || agent.process.exitCode !== null) return false;
|
|
287
|
+
return this.requestAccepted(agent.process, { type: "prompt", message });
|
|
288
|
+
}
|
|
289
|
+
|
|
269
290
|
/** Inject a steering message into a running agent */
|
|
270
291
|
async steer(taskId: string, message: string): Promise<boolean> {
|
|
271
292
|
const agent = this.agents.get(taskId);
|
|
272
293
|
if (!agent || agent.aborted || agent.process.exitCode !== null) return false;
|
|
273
|
-
return this.
|
|
294
|
+
return this.requestAccepted(agent.process, { type: "steer", message });
|
|
274
295
|
}
|
|
275
296
|
|
|
276
297
|
/** Queue a follow-up message for after the current turn */
|
|
277
298
|
async followUp(taskId: string, message: string): Promise<boolean> {
|
|
278
299
|
const agent = this.agents.get(taskId);
|
|
279
300
|
if (!agent || agent.aborted || agent.process.exitCode !== null) return false;
|
|
280
|
-
return this.
|
|
301
|
+
return this.requestAccepted(agent.process, { type: "follow_up", message });
|
|
281
302
|
}
|
|
282
303
|
|
|
283
304
|
/** Abort the current operation */
|
|
@@ -348,7 +369,55 @@ export class AgentPool {
|
|
|
348
369
|
}
|
|
349
370
|
}
|
|
350
371
|
|
|
372
|
+
private async requestAccepted(proc: ChildProcess, command: Record<string, unknown>): Promise<boolean> {
|
|
373
|
+
try {
|
|
374
|
+
const response = await this.requestRpc(proc, command);
|
|
375
|
+
return response?.success === true;
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private requestRpc(proc: ChildProcess, command: Record<string, unknown>): Promise<any> {
|
|
382
|
+
const id = randomUUID();
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
const timer = setTimeout(() => {
|
|
385
|
+
this.responseWaiters.delete(id);
|
|
386
|
+
reject(new Error(`Timed out waiting for RPC ${String(command.type)}`));
|
|
387
|
+
}, 10_000);
|
|
388
|
+
timer.unref();
|
|
389
|
+
this.responseWaiters.set(id, { process: proc, resolve, reject, timer });
|
|
390
|
+
if (!this.sendRpcCommand(proc, { ...command, id })) {
|
|
391
|
+
clearTimeout(timer);
|
|
392
|
+
this.responseWaiters.delete(id);
|
|
393
|
+
reject(new Error(`Failed to send RPC ${String(command.type)}`));
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private rejectResponseWaiters(proc: ChildProcess, error: Error): void {
|
|
399
|
+
for (const [id, waiter] of this.responseWaiters) {
|
|
400
|
+
if (waiter.process !== proc) continue;
|
|
401
|
+
clearTimeout(waiter.timer);
|
|
402
|
+
this.responseWaiters.delete(id);
|
|
403
|
+
waiter.reject(error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
351
407
|
private handleRpcEvent(agent: AgentProcess, event: any): void {
|
|
408
|
+
if (event.type === "response" && typeof event.id === "string") {
|
|
409
|
+
const waiter = this.responseWaiters.get(event.id);
|
|
410
|
+
if (waiter) {
|
|
411
|
+
clearTimeout(waiter.timer);
|
|
412
|
+
this.responseWaiters.delete(event.id);
|
|
413
|
+
waiter.resolve(event);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Process stdout can drain after a replacement has already been installed.
|
|
418
|
+
// Ignore every stale lifecycle/activity event so the old child cannot
|
|
419
|
+
// evict, settle, or complete the new task process.
|
|
420
|
+
if (this.agents.get(agent.taskId) !== agent) return;
|
|
352
421
|
// Only genuine agent activity advances the idle clock. Command acks
|
|
353
422
|
// (type=response) and echoes of injected user messages (steer messages
|
|
354
423
|
// recorded as user-role message events) must NOT reset it — otherwise the
|
|
@@ -407,14 +476,14 @@ export class AgentPool {
|
|
|
407
476
|
} else if (event.type === "agent_settled") {
|
|
408
477
|
debug("squad-pool", `agent_settled from RPC: ${agent.agentName} (task: ${agent.taskId})`);
|
|
409
478
|
// Mark the guard to prevent double-emit from proc.on("exit")
|
|
410
|
-
const guardFn = (agent as any).
|
|
479
|
+
const guardFn = (agent as any)._terminalEventEmitted;
|
|
411
480
|
if (guardFn) guardFn();
|
|
412
481
|
// Capture activity stats BEFORE deleting
|
|
413
482
|
const endActivity = agent.activity;
|
|
414
483
|
// Remove from agents map BEFORE emitting so getRunningAgents() doesn't count it
|
|
415
484
|
this.agents.delete(agent.taskId);
|
|
416
485
|
this.emit({
|
|
417
|
-
type: "
|
|
486
|
+
type: "agent_settled",
|
|
418
487
|
taskId: agent.taskId,
|
|
419
488
|
agentName: agent.agentName,
|
|
420
489
|
data: {
|
|
@@ -450,13 +519,23 @@ function buildPiArgs(
|
|
|
450
519
|
agentDef: AgentDef,
|
|
451
520
|
promptFile: string,
|
|
452
521
|
skillPaths: string[],
|
|
453
|
-
|
|
522
|
+
sessionOptions: {
|
|
523
|
+
resumeSession?: TaskSession;
|
|
524
|
+
sessionDir?: string;
|
|
525
|
+
forkSession?: { file: string; sessionDir: string };
|
|
526
|
+
},
|
|
454
527
|
): string[] {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
528
|
+
const { resumeSession, sessionDir, forkSession } = sessionOptions;
|
|
529
|
+
if (resumeSession && forkSession) throw new Error("Cannot resume and fork a task session simultaneously");
|
|
530
|
+
// Existing tasks always reopen the exact bound file. Only tasks without a
|
|
531
|
+
// binding create a new session (optionally as a main-session fork).
|
|
532
|
+
const sessionArgs = resumeSession
|
|
533
|
+
? ["--session", resumeSession.file]
|
|
534
|
+
: forkSession
|
|
535
|
+
? ["--fork", forkSession.file, "--session-dir", forkSession.sessionDir]
|
|
536
|
+
: sessionDir
|
|
537
|
+
? ["--session-dir", sessionDir]
|
|
538
|
+
: (() => { throw new Error("A durable task session is required"); })();
|
|
460
539
|
const args: string[] = [...sessionArgs, "--append-system-prompt", promptFile];
|
|
461
540
|
|
|
462
541
|
if (agentDef.model) {
|