pi-squad 0.16.1 → 0.16.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/README.md CHANGED
@@ -139,6 +139,8 @@ 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 steering**: `squad_message` resolves an agent name to its live task, durably records the full message, and writes the documented JSONL RPC command `{"type":"steer","message":"..."}` to the child. pi-squad waits for Pi's final `agent_settled` event before closing the child; low-level `agent_end` no longer kills queued steering/follow-up continuations.
143
+
142
144
  ### Smart Planner
143
145
 
144
146
  The planner creates task breakdowns with proper dependency ordering:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/agent-pool.ts CHANGED
@@ -270,16 +270,14 @@ export class AgentPool {
270
270
  async steer(taskId: string, message: string): Promise<boolean> {
271
271
  const agent = this.agents.get(taskId);
272
272
  if (!agent || agent.aborted || agent.process.exitCode !== null) return false;
273
- this.sendRpcCommand(agent.process, { type: "steer", message });
274
- return true;
273
+ return this.sendRpcCommand(agent.process, { type: "steer", message });
275
274
  }
276
275
 
277
276
  /** Queue a follow-up message for after the current turn */
278
277
  async followUp(taskId: string, message: string): Promise<boolean> {
279
278
  const agent = this.agents.get(taskId);
280
279
  if (!agent || agent.aborted || agent.process.exitCode !== null) return false;
281
- this.sendRpcCommand(agent.process, { type: "follow_up", message });
282
- return true;
280
+ return this.sendRpcCommand(agent.process, { type: "follow_up", message });
283
281
  }
284
282
 
285
283
  /** Abort the current operation */
@@ -301,7 +299,7 @@ export class AgentPool {
301
299
  agent.process.kill("SIGTERM");
302
300
  // Force kill after 5s
303
301
  const timer = setTimeout(() => {
304
- if (!agent.process.killed) agent.process.kill("SIGKILL");
302
+ if (agent.process.exitCode === null) agent.process.kill("SIGKILL");
305
303
  }, 5000);
306
304
  await new Promise<void>((resolve) => {
307
305
  agent.process.on("exit", () => {
@@ -337,9 +335,17 @@ export class AgentPool {
337
335
  // Internal
338
336
  // =========================================================================
339
337
 
340
- private sendRpcCommand(proc: ChildProcess, command: Record<string, unknown>): void {
341
- if (!proc.stdin || proc.stdin.destroyed) return;
342
- proc.stdin.write(serializeJsonLine(command));
338
+ private sendRpcCommand(proc: ChildProcess, command: Record<string, unknown>): boolean {
339
+ if (!proc.stdin || proc.stdin.destroyed) return false;
340
+ try {
341
+ // Writable.write(false) means backpressure, not rejection; the bytes are
342
+ // still buffered. Reaching write() without throwing means the JSONL
343
+ // command was handed to the child stream.
344
+ proc.stdin.write(serializeJsonLine(command));
345
+ return true;
346
+ } catch {
347
+ return false;
348
+ }
343
349
  }
344
350
 
345
351
  private handleRpcEvent(agent: AgentProcess, event: any): void {
@@ -394,10 +400,12 @@ export class AgentPool {
394
400
  data: event,
395
401
  });
396
402
  } else if (event.type === "agent_end") {
397
- // Pi RPC mode emits agent_end when the agent loop finishes.
398
- // The RPC process stays alive waiting for more commands,
399
- // so we need to explicitly kill it and emit our own agent_end.
400
- debug("squad-pool", `agent_end from RPC: ${agent.agentName} (task: ${agent.taskId})`);
403
+ // agent_end is one low-level run. Pi may still process queued steer,
404
+ // follow-up, retry, or compaction continuations. Killing here races and
405
+ // drops main-session steering. agent_settled is the final lifecycle edge.
406
+ debug("squad-pool", `agent_end from RPC (awaiting settled): ${agent.agentName} (task: ${agent.taskId})`);
407
+ } else if (event.type === "agent_settled") {
408
+ debug("squad-pool", `agent_settled from RPC: ${agent.agentName} (task: ${agent.taskId})`);
401
409
  // Mark the guard to prevent double-emit from proc.on("exit")
402
410
  const guardFn = (agent as any)._agentEndEmitted;
403
411
  if (guardFn) guardFn();
@@ -417,11 +425,12 @@ export class AgentPool {
417
425
  filesModified: endActivity.modifiedFiles.size,
418
426
  },
419
427
  });
420
- // Kill the RPC process since the agent's work is done
428
+ // The session is fully settled, so the RPC process can now close.
421
429
  agent.process.kill("SIGTERM");
422
- setTimeout(() => {
423
- if (!agent.process.killed) agent.process.kill("SIGKILL");
430
+ const forceKill = setTimeout(() => {
431
+ if (agent.process.exitCode === null) agent.process.kill("SIGKILL");
424
432
  }, 3000);
433
+ forceKill.unref();
425
434
  } else if (event.type === "error") {
426
435
  this.emit({
427
436
  type: "error",