fixed freezing on session resume

This commit is contained in:
Amos
2026-06-27 02:49:01 +03:00
parent eb281b63d4
commit af4e032ed4
5 changed files with 38 additions and 50 deletions
-29
View File
@@ -2,8 +2,6 @@
Async subagents for [pi](https://github.com/badlogic/pi-mono) — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. **Fully non-blocking** — the main agent keeps working while subagents run in the background. Async subagents for [pi](https://github.com/badlogic/pi-mono) — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. **Fully non-blocking** — the main agent keeps working while subagents run in the background.
https://github.com/user-attachments/assets/30adb156-cfb4-4c47-84ca-dd4aa80cba9f
## How It Works ## How It Works
Call `subagent()` and it **returns immediately**. The sub-agent runs in its own terminal pane. A live widget above the input shows all running agents with their current state — `starting`, `active`, `waiting`, `stalled`, or `running`. When a sub-agent finishes, its result is **steered back** into the main session as an async notification — triggering a new turn so the agent can process it. Call `subagent()` and it **returns immediately**. The sub-agent runs in its own terminal pane. A live widget above the input shows all running agents with their current state — `starting`, `active`, `waiting`, `stalled`, or `running`. When a sub-agent finishes, its result is **steered back** into the main session as an async notification — triggering a new turn so the agent can process it.
@@ -23,33 +21,6 @@ subagent({ name: "Scout: DB", agent: "scout", task: "Map database schema" });
// Both return immediately, results steer back independently // Both return immediately, results steer back independently
``` ```
## Install
```bash
pi install git:github.com/HazAT/pi-interactive-subagents
```
Supported multiplexers:
- [cmux](https://github.com/manaflow-ai/cmux)
- [tmux](https://github.com/tmux/tmux)
- [zellij](https://zellij.dev)
- [WezTerm](https://wezfurlong.org/wezterm/) (terminal emulator with built-in multiplexing)
Start pi inside one of them:
```bash
cmux pi
# or
tmux new -A -s pi 'pi'
# or
zellij --session pi # then run: pi
# or
# just run pi inside WezTerm — no wrapper needed
```
Optional: set `PI_SUBAGENT_MUX=cmux|tmux|zellij|wezterm` to force a specific backend.
If your shell startup is slow and subagent commands sometimes get dropped before the prompt is ready, set `PI_SUBAGENT_SHELL_READY_DELAY_MS` to a higher value (defaults to `500`): If your shell startup is slow and subagent commands sometimes get dropped before the prompt is ready, set `PI_SUBAGENT_SHELL_READY_DELAY_MS` to a higher value (defaults to `500`):
```bash ```bash
+1 -1
View File
@@ -2,7 +2,7 @@
name: researcher name: researcher
description: Web researcher — searches the web and synthesizes findings description: Web researcher — searches the web and synthesizes findings
tools: web_search, web_fetch tools: web_search, web_fetch
model: anthropic/claude-sonnet-4-6 model: openrouter/z-ai/glm-5.2
thinking: medium thinking: medium
system-prompt: append system-prompt: append
auto-exit: true auto-exit: true
+2 -2
View File
@@ -2,8 +2,8 @@
name: scout name: scout
description: Fast codebase recon — explores files, finds patterns, maps architecture description: Fast codebase recon — explores files, finds patterns, maps architecture
tools: read, grep, find, ls tools: read, grep, find, ls
model: anthropic/claude-haiku-4-5 model: openrouter/z-ai/glm-5.2
thinking: medium thinking: low
system-prompt: append system-prompt: append
auto-exit: true auto-exit: true
--- ---
+1 -1
View File
@@ -3,7 +3,7 @@ name: worker
description: General-purpose worker — reads, writes, and edits code description: General-purpose worker — reads, writes, and edits code
tools: read, write, edit, safe_bash, web_search, web_fetch tools: read, write, edit, safe_bash, web_search, web_fetch
subagent_agents: scout, researcher subagent_agents: scout, researcher
model: anthropic/claude-sonnet-4-6 model: openrouter/z-ai/glm-5.2
thinking: medium thinking: medium
system-prompt: append system-prompt: append
--- ---
+34 -17
View File
@@ -1,9 +1,12 @@
import { import {
appendFileSync, appendFileSync,
closeSync,
copyFileSync, copyFileSync,
existsSync, existsSync,
mkdirSync, mkdirSync,
openSync,
readFileSync, readFileSync,
readSync,
readdirSync, readdirSync,
statSync, statSync,
writeFileSync, writeFileSync,
@@ -99,30 +102,44 @@ export function getLeafId(sessionFile: string): string | null {
* then prefix), NOT the filename — so this is the value to hand back to the * then prefix), NOT the filename — so this is the value to hand back to the
* orchestrator for follow-ups. * orchestrator for follow-ups.
*/ */
export function getSessionId(sessionFile: string): string | null { /**
* Read only the first line of a file without loading the whole thing into
* memory. Session files grow to many MB, but the header we need is always the
* first JSON line, so reading a small prefix keeps header lookups cheap — this
* is what makes scanning a large session tree fast enough to avoid blocking the
* event loop. Returns the first line (sans trailing newline), or null.
*/
function readFirstLine(path: string, maxBytes = 65536): string | null {
let fd: number | undefined;
try { try {
const raw = readFileSync(sessionFile, "utf8"); fd = openSync(path, "r");
for (const line of raw.split("\n")) { const buf = Buffer.allocUnsafe(maxBytes);
const trimmed = line.trim(); const bytes = readSync(fd, buf, 0, maxBytes, 0);
if (!trimmed) continue; if (bytes <= 0) return null;
const entry = JSON.parse(trimmed) as { type?: string; id?: string }; const nl = buf.indexOf(0x0a); // '\n'
if (entry.type === "session" && typeof entry.id === "string") { const end = nl === -1 || nl >= bytes ? bytes : nl;
return entry.id; return buf.toString("utf8", 0, end);
}
// Header is always the first entry; bail after the first parse.
break;
}
} catch { } catch {
// fall through return null;
} finally {
if (fd !== undefined) {
try {
closeSync(fd);
} catch {
/* ignore */
}
}
} }
return null; }
export function getSessionId(sessionFile: string): string | null {
return readHeaderId(sessionFile);
} }
function readHeaderId(sessionFile: string): string | null { function readHeaderId(sessionFile: string): string | null {
const firstLine = readFirstLine(sessionFile)?.trim();
if (!firstLine) return null;
try { try {
const raw = readFileSync(sessionFile, "utf8");
const firstLine = raw.split("\n", 1)[0]?.trim();
if (!firstLine) return null;
const entry = JSON.parse(firstLine) as { type?: string; id?: string }; const entry = JSON.parse(firstLine) as { type?: string; id?: string };
return entry.type === "session" && typeof entry.id === "string" ? entry.id : null; return entry.type === "session" && typeof entry.id === "string" ? entry.id : null;
} catch { } catch {