Pi/oh-my-pi extensions using spawnSync cause subagent stalls because all subagents run in-process on the same Node.js event loop
When a Pi extension uses spawnSync or execSync in lifecycle handlers (session_start, turn_start, session_shutdown), it blocks the entire Node.js event loop. This is catastrophic when subagents are involved because:
createAgentSession() WITHOUT disableExtensionDiscovery, so extensions are fully loaded and initialized for every subagentspawnSync, ALL other subagents' promises stallSymptoms: subagents appear to stall/hang, MCP transport lost/reconnect cycles spike (~120/day observed), stopping and resuming the session works.
Replace all spawnSync/execSync calls with async spawn wrapped in a promise. The handlers are already async — they just need to await async subprocess calls instead of blocking synchronously.
Additionally, use ctx.hasUI === false as a proxy to detect subagent/non-interactive context and skip hooks entirely — subagents don't need auth, skill updates, contribution nudges, or session-end analysis.
// asyncSpawn helper — wraps child_process.spawn in a promise
function asyncSpawn(cmd: string, args: string[], opts: { input?: string; timeout?: number; cwd?: string }): Promise<SpawnResult> {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { cwd: opts.cwd, stdio: ["pipe", "pipe", "pipe"], timeout: opts.timeout });
let stdout = "", stderr = "";
child.stdout?.on("data", (d: Buffer) => { stdout += d.toString(); });
child.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); });
if (opts.input && child.stdin) { child.stdin.write(opts.input); child.stdin.end(); }
child.on("error", reject);
child.on("close", (code) => resolve({ stdout, stderr, exitCode: code }));
});
}
// Skip hooks in subagent context
pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return; // subagent / non-interactive — skip
// ... use await asyncSpawn() instead of spawnSync()
});