import { randomUUID } from "node:crypto"; import { existsSync, unlinkSync } from "node:fs"; import type { ExtensionAPI, ExtensionCommandContext, } from "@earendil-works/pi-coding-agent"; import { SessionManager } from "@earendil-works/pi-coding-agent"; import { loadConfig, type SubagentsConfig } from "./config.js"; import { getPiInvocation } from "./lib/launch.js"; import { buildResearchCommandLine, shq, writeResearchScript, } from "./lib/launch-script.js"; import { researchScriptLogPath, researchScriptPath } from "./lib/paths.js"; import type { ResearchReporter } from "./lib/research-ipc.js"; import { createResearchSession, getResearchSession, listResearchSessions, type ResearchSessionState, removeResearchSession, } from "./lib/research-state.js"; import { extractLastAssistantOutput, generateChildSessionFile, resolveSessionId, seedForkSession, } from "./lib/session.js"; import { tmuxActive, tmuxGetSessionName, tmuxKillPane, tmuxPaneAlive, tmuxSelectPane, tmuxSendKeys, tmuxSplitWindow, } from "./lib/tmux.js"; import type { AgentConfig } from "./lib/types.js"; // ── Command names ─────────────────────────────────────────────────────────── /** Pi command names — single source of truth for the /rsh* command surface. */ export const RSH_COMMANDS = { /** Fork an interactive research session into a tmux pane. */ research: "rsh", /** Close a research session pane and clean up its state. */ close: "rsh-close", /** Send research findings back to the parent session. */ report: "rsh-report", } as const; // ── Research fork identity ────────────────────────────────────────────────── function makeResearchAgent(): AgentConfig { return { name: "research", }; } // ── Stale session sweep ───────────────────────────────────────────────────── /** Probe whether a PID is alive by sending signal 0 (same as pi-web). */ function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } /** * Sweep research sessions left behind by unclean child exits (crash, * SIGKILL, power loss). A session is stale when its pane is gone — or, for * the no-tmux fallback, when its child process is dead. Sessions with an * unknown liveness (no paneId, no childPid — pre-childPid sessions) are * left alone. Run at session start, mirroring pi-web's searxng unclean- * shutdown audit. Returns the number of sessions cleaned. */ export function sweepStaleResearchSessions(): number { let cleaned = 0; for (const state of listResearchSessions()) { if (state.status !== "running") { continue; } let alive: boolean; if (state.paneId) { alive = tmuxPaneAlive(state.paneId); } else if (state.childPid !== undefined) { alive = isProcessAlive(state.childPid); } else { continue; // unknown liveness — leave it } if (!alive) { closeResearchSessionById(state.id); cleaned++; } } return cleaned; } // ── Command line construction ─────────────────────────────────────────────── /** Collect PI_* env vars + research session identity to pass to the child pane. */ function collectPiEnv(sessionId: string, task: string): Record { const env: Record = {}; for (const [k, v] of Object.entries(process.env)) { if (k.startsWith("PI_") && v) { env[k] = v; } } env.PI_RSH_SESSION_ID = sessionId; env.PI_RSH_TASK = task; return env; } // ── /rsh ─────────────────────────────────────────────────────────────── /** Minimal session identity shared by the tmux and no-tmux launch paths. */ interface NewResearchSession { id: string; task: string; sessionFile: string; } /** * Build the `bash