import { createHash, randomUUID } from "node:crypto"; import { chmod, mkdir, readdir, readFile, realpath, rename, rm, stat, symlink, unlink, writeFile, } from "node:fs/promises"; import path from "node:path"; import { startPiRpcAdapter } from "./pi-rpc-adapter.js"; import { createRecoverySupervisor } from "./recovery-supervisor.js"; import { createWorkspaceStore } from "./workspace-store.js"; const SESSION_DIRECTORY_MODE = 0o700; const SESSION_REFERENCE_FILE = "bridge-agent.json"; const DEFAULT_EVENT_LIMIT = 1_000; const DEFAULT_CLOSE_TIMEOUT_MS = 2_000; const RESTORE_CONCURRENCY = 2; const RESOURCE_WARNING_COUNT = 6; const INITIAL_LAUNCH_GET_STATE_TIMEOUT_MS = 90_000; export class UnknownAgentError extends Error { constructor(agentId) { super(`unknown agent: ${agentId}`); this.name = "UnknownAgentError"; } } export class UnknownRuntimeError extends Error { constructor(runtimeId) { super(`unknown session runtime: ${runtimeId}`); this.name = "UnknownRuntimeError"; } } function sessionDirectoryFor(sessionRoot, worktreePath) { const identity = createHash("sha256") .update(worktreePath) .digest("hex") .slice(0, 24); return path.join(sessionRoot, `agent-${identity}`); } async function readSessionReference(sessionDir) { try { const value = JSON.parse( await readFile(path.join(sessionDir, SESSION_REFERENCE_FILE), "utf8"), ); return { sessionPath: typeof value?.sessionPath === "string" && path.isAbsolute(value.sessionPath) ? value.sessionPath : undefined, worktreePath: typeof value?.worktreePath === "string" && path.isAbsolute(value.worktreePath) ? value.worktreePath : undefined, }; } catch (error) { if (error?.code === "ENOENT" || error instanceof SyntaxError) return {}; throw error; } } async function persistSessionReference(sessionDir, sessionPath, worktreePath) { await writeFile( path.join(sessionDir, SESSION_REFERENCE_FILE), `${JSON.stringify({ sessionPath, worktreePath })}\n`, { encoding: "utf8", mode: 0o600 }, ); } export async function promoteQuickSessionDirectory( quickDir, sessionDir, { renameFile = rename, makeSymlink = symlink } = {}, ) { const stagingDir = `${quickDir}.promoting-${randomUUID()}`; const moved = []; let quickStaged = false; let quickLinked = false; async function rollback() { if (quickLinked) await unlink(quickDir).catch(() => {}); for (const entry of moved.reverse()) await renameFile(path.join(sessionDir, entry), path.join(stagingDir, entry)); if (quickStaged) await renameFile(stagingDir, quickDir); } try { await mkdir(sessionDir, { recursive: true, mode: SESSION_DIRECTORY_MODE }); await chmod(sessionDir, SESSION_DIRECTORY_MODE); await renameFile(quickDir, stagingDir); quickStaged = true; await makeSymlink(sessionDir, quickDir, "dir"); quickLinked = true; for (const entry of await readdir(stagingDir)) { const destination = path.join(sessionDir, entry); try { await stat(destination); throw new Error("quick session conflicts with an existing session file"); } catch (error) { if (error?.code !== "ENOENT") throw error; } await renameFile(path.join(stagingDir, entry), destination); moved.push(entry); } } catch (error) { try { await rollback(); } catch (rollbackError) { throw new AggregateError( [error, rollbackError], "could not promote quick session directory; staging retained", ); } throw error; } return { async rollback() { try { await rollback(); } catch (error) { throw new Error( `could not roll back quick session promotion; staging retained: ${error.message}`, ); } }, async finalize() { await rm(stagingDir, { recursive: true, force: true }).catch(() => {}); }, }; } function sessionPreview(content) { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .filter((part) => part?.type === "text" && typeof part.text === "string") .map((part) => part.text) .join(" "); } async function readSessionSummary(sessionPath, currentPath) { const [content, metadata] = await Promise.all([ readFile(sessionPath, "utf8"), stat(sessionPath), ]); let header; let name; let firstMessage; let messageCount = 0; for (const line of content.split("\n")) { if (!line) continue; let entry; try { entry = JSON.parse(line); } catch { continue; } if (entry.type === "session") header = entry; if (entry.type === "session_info" && typeof entry.name === "string") name = entry.name; if (entry.type === "message") { messageCount += 1; if (!firstMessage && entry.message?.role === "user") firstMessage = sessionPreview(entry.message.content); } } if (!header || typeof header.id !== "string" || typeof header.cwd !== "string") return undefined; return { path: sessionPath, id: header.id, cwd: header.cwd, name, parentSessionPath: header.parentSession, created: header.timestamp, modified: metadata.mtime.toISOString(), messageCount, firstMessage, isCurrent: sessionPath === currentPath, }; } async function listSessionSummaries(sessionDir, currentPath) { let entries; try { entries = await readdir(sessionDir, { withFileTypes: true }); } catch (error) { if (error?.code === "ENOENT") return []; throw error; } const summaries = await Promise.all( entries .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")) .map(async (entry) => { try { return await readSessionSummary( path.join(sessionDir, entry.name), currentPath, ); } catch { return undefined; } }), ); return summaries .filter(Boolean) .sort((left, right) => right.modified.localeCompare(left.modified)); } function runtimeLabel(runtime) { return runtime.sessionName || runtime.firstMessage || "New session"; } function publicAgent(runtime) { return { id: runtime.agentId, worktreePath: runtime.worktreePath, sessionDir: runtime.sessionDir, state: runtime.state, runtimeId: runtime.runtimeId, ...(runtime.sessionId ? { sessionId: runtime.sessionId } : {}), label: runtimeLabel(runtime), attention: runtime.attention, queueCount: runtime.queueCount, lastActivity: runtime.lastActivity, }; } function publicRuntime(runtime) { return { runtimeId: runtime.runtimeId, worktreePath: runtime.worktreePath, state: runtime.state, label: runtimeLabel(runtime), attention: runtime.attention, queueCount: runtime.queueCount, activeTool: runtime.activeTool, lastActivity: runtime.lastActivity, openedAt: runtime.openedAt, ...(runtime.agentId ? { agentId: runtime.agentId } : {}), ...(runtime.sessionId ? { sessionId: runtime.sessionId } : {}), ...(runtime.sessionPath ? { sessionPath: runtime.sessionPath } : {}), ...(runtime.error ? { error: runtime.error } : {}), }; } function routeCommand(adapter, operation, payload = {}, runtimeState) { switch (operation) { case "prompt": case "steer": case "follow_up": return adapter.send({ type: operation, message: payload.message }); case "submit_prompt": return adapter.send({ type: runtimeState === "streaming" ? "follow_up" : "prompt", message: payload.message, }); case "abort": case "get_state": case "get_session_stats": case "get_available_models": case "get_commands": return adapter.send({ type: operation }); case "get_transcript": return adapter.send({ type: "get_messages" }); case "compact": return adapter.send({ type: "compact", ...(payload.customInstructions ? { customInstructions: payload.customInstructions } : {}), }); case "set_model": return adapter.send({ type: "set_model", provider: payload.provider, modelId: payload.modelId, }); case "set_thinking_level": return adapter.send({ type: "set_thinking_level", level: payload.level }); case "set_session_name": return adapter.send({ type: "set_session_name", name: payload.name }); case "pi_rpc_command": return adapter.send({ type: payload.command, ...payload.input }); case "extension_response": adapter.respondToExtension(payload.requestId, payload.response); return Promise.resolve({ type: "response", command: "extension_ui_response", success: true, }); default: return Promise.reject( new Error(`unsupported agent operation: ${operation}`), ); } } export function createAgentRegistry({ homeWorktree, sessionRoot, startAdapter = startPiRpcAdapter, workspaceStoreFactory = createWorkspaceStore, eventLimit = DEFAULT_EVENT_LIMIT, maxRecoveryAttempts, recoveryDelayForAttempt, sleep, closeTimeoutMs = DEFAULT_CLOSE_TIMEOUT_MS, }) { if (typeof homeWorktree !== "string" || !path.isAbsolute(homeWorktree)) throw new TypeError("homeWorktree must be an absolute path"); if (typeof sessionRoot !== "string" || !path.isAbsolute(sessionRoot)) throw new TypeError("sessionRoot must be an absolute path"); if (typeof startAdapter !== "function") throw new TypeError("startAdapter must be a function"); if (typeof workspaceStoreFactory !== "function") throw new TypeError("workspaceStoreFactory must be a function"); const bridgeInstanceId = randomUUID(); const workspaceStore = workspaceStoreFactory(sessionRoot); const runtimesByRuntimeId = new Map(); const runtimesByAgentId = new Map(); const runtimesByWorktreePath = new Map(); const runtimesBySessionPath = new Map(); const managedWorktrees = new Set(); const pendingSessionLeases = new Map(); const creatingByKey = new Map(); const pendingOpenOperations = new Set(); const workspaceListeners = new Set(); const workspaceEvents = []; let workspaceSequence = 0; let manifestIssue; let manifestWriteQueue = Promise.resolve(); let canonicalHomeWorktree; let lastSuccessfulModelCatalog; let stopping = false; function publishWorkspace(type, runtime, data = {}) { if (runtime?.ephemeral) return undefined; const event = { version: "v1", bridgeInstanceId, seq: ++workspaceSequence, type, ...(runtime?.agentId ? { agentId: runtime.agentId } : {}), data: { ...(runtime ? { runtimeId: runtime.runtimeId, worktreePath: runtime.worktreePath, ...(runtime.sessionId ? { sessionId: runtime.sessionId } : {}), } : {}), ...data, }, }; workspaceEvents.push(event); if (workspaceEvents.length > eventLimit) workspaceEvents.shift(); for (const listener of workspaceListeners) listener(event); return event; } function manifest() { return { version: 2, migrated: true, directories: [...managedWorktrees].sort(), runtimes: [...runtimesByRuntimeId.values()] .filter((runtime) => !runtime.ephemeral) .map((runtime) => ({ runtimeId: runtime.runtimeId, worktreePath: runtime.worktreePath, ...(runtime.sessionPath ? { sessionPath: runtime.sessionPath } : {}), ...(runtime.sessionId ? { sessionId: runtime.sessionId } : {}), ...(runtime.legacyDefault ? { legacyDefault: true } : {}), openedAt: runtime.openedAt, })), }; } function persistWorkspace() { const operation = manifestWriteQueue .catch(() => {}) .then(async () => { await workspaceStore.save(manifest()); manifestIssue = undefined; }); manifestWriteQueue = operation.catch(() => {}); return operation; } function addWorktreeRuntime(runtime) { if (!runtime.ephemeral) managedWorktrees.add(runtime.worktreePath); let runtimes = runtimesByWorktreePath.get(runtime.worktreePath); if (!runtimes) { runtimes = new Set(); runtimesByWorktreePath.set(runtime.worktreePath, runtimes); } runtimes.add(runtime); } function indexRuntime(runtime, { leaseSession = true } = {}) { if (runtimesByRuntimeId.has(runtime.runtimeId)) throw new Error(`runtime already exists: ${runtime.runtimeId}`); if ( leaseSession && runtime.sessionPath && runtimesBySessionPath.has(runtime.sessionPath) ) throw new Error("session is already open in another runtime"); runtimesByRuntimeId.set(runtime.runtimeId, runtime); if (runtime.agentId) runtimesByAgentId.set(runtime.agentId, runtime); if (leaseSession && runtime.sessionPath) runtimesBySessionPath.set(runtime.sessionPath, runtime); addWorktreeRuntime(runtime); return runtime; } function unindexRuntime(runtime, { releaseSessionLease = true } = {}) { runtimesByRuntimeId.delete(runtime.runtimeId); if (runtime.agentId) runtimesByAgentId.delete(runtime.agentId); if ( releaseSessionLease && runtime.sessionPath && runtimesBySessionPath.get(runtime.sessionPath) === runtime ) runtimesBySessionPath.delete(runtime.sessionPath); const worktreeRuntimes = runtimesByWorktreePath.get(runtime.worktreePath); worktreeRuntimes?.delete(runtime); if (worktreeRuntimes?.size === 0) runtimesByWorktreePath.delete(runtime.worktreePath); } function getRuntime(runtimeId) { const runtime = runtimesByRuntimeId.get(runtimeId); if (!runtime) throw new UnknownRuntimeError(runtimeId); return runtime; } function getAgent(agentId) { const runtime = runtimesByAgentId.get(agentId); if (!runtime) throw new UnknownAgentError(agentId); return runtime; } function enqueue(runtime, operation) { if (stopping) return Promise.reject(new Error("agent registry is stopping")); if ( runtime.state === "closing" || runtime.state === "stopped" || runtime.stopped ) return Promise.reject(new Error("session runtime is closing")); // Work admitted before a close request must drain; the close operation // marks the runtime synchronously so later work is rejected here. const result = runtime.operationQueue.then(operation); runtime.operationQueue = result.catch(() => {}); return result; } function reserveSessionPath(sessionPath, runtime) { const leased = runtimesBySessionPath.get(sessionPath); const pending = pendingSessionLeases.get(sessionPath); if ((leased && leased !== runtime) || (pending && pending !== runtime)) throw new Error("session is already open in another runtime"); pendingSessionLeases.set(sessionPath, runtime); } function releaseSessionReservation(sessionPath, runtime) { if (pendingSessionLeases.get(sessionPath) === runtime) pendingSessionLeases.delete(sessionPath); } async function ownedSessionPath( sessionDir, sessionPath, { allowMissing = false } = {}, ) { const resolvedSessionDir = await realpath(sessionDir); let resolvedSessionPath; try { resolvedSessionPath = await realpath(sessionPath); } catch (error) { if (error?.code !== "ENOENT" || !allowMissing) throw error; const resolvedParent = await realpath(path.dirname(sessionPath)); resolvedSessionPath = path.join(resolvedParent, path.basename(sessionPath)); } const relative = path.relative(resolvedSessionDir, resolvedSessionPath); if ( relative === "" || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !resolvedSessionPath.endsWith(".jsonl") ) throw new TypeError("sessionPath must belong to the selected directory"); return resolvedSessionPath; } async function projectQuickSessionPath(quickDir, sessionDir, sessionPath) { const activePath = await ownedSessionPath(quickDir, sessionPath); const relativePath = path.relative(await realpath(quickDir), activePath); return ownedSessionPath(sessionDir, path.join(sessionDir, relativePath), { allowMissing: true, }); } async function updateRuntimeIdentity( runtime, stateResponse, { expectedSessionPath, adapter = runtime.adapter, persist = true, indexSession = true, } = {}, ) { if ( runtime.stopped || runtime.state === "closing" || runtime.state === "stopped" || runtime.adapter !== adapter || runtimesByRuntimeId.get(runtime.runtimeId) !== runtime ) throw new Error("session runtime is closing"); const state = stateResponse?.data ?? {}; const nextPath = state.sessionFile; let resolved; if (typeof nextPath === "string" && path.isAbsolute(nextPath)) { try { const candidate = await ownedSessionPath(runtime.sessionDir, nextPath, { allowMissing: true, }); await stat(candidate); resolved = candidate; } catch (error) { if (error?.code !== "ENOENT") throw new Error("Pi reported a session outside its managed directory"); } } if (expectedSessionPath && resolved !== expectedSessionPath) throw new Error("Pi did not resume the requested session"); if (resolved) { const leased = runtimesBySessionPath.get(resolved); const pending = pendingSessionLeases.get(resolved); if ((leased && leased !== runtime) || (pending && pending !== runtime)) throw new Error("session is already open in another runtime"); } if ( runtime.stopped || runtime.state === "closing" || runtime.adapter !== adapter || runtimesByRuntimeId.get(runtime.runtimeId) !== runtime ) throw new Error("session runtime is closing"); if (typeof state.sessionName === "string") runtime.sessionName = state.sessionName; if (typeof state.sessionId === "string") runtime.sessionId = state.sessionId; if (resolved) { if (indexSession) { if ( runtime.sessionPath && runtime.sessionPath !== resolved && runtimesBySessionPath.get(runtime.sessionPath) === runtime ) runtimesBySessionPath.delete(runtime.sessionPath); runtimesBySessionPath.set(resolved, runtime); releaseSessionReservation(resolved, runtime); } runtime.sessionPath = resolved; if (runtime.legacyDefault) await persistSessionReference( runtime.sessionDir, resolved, runtime.worktreePath, ); } if (persist) await persistWorkspace(); } async function restoreSessionReference(sessionDir, previous) { const referencePath = path.join(sessionDir, SESSION_REFERENCE_FILE); if (previous === undefined) { await rm(referencePath, { force: true }); return; } await writeFile(referencePath, previous, { encoding: "utf8", mode: 0o600 }); } function publishAgent(runtime, type, data) { const event = { version: "v1", seq: runtime.nextSequence++, type, agentId: runtime.agentId, data, }; runtime.events.push(event); if (runtime.events.length > eventLimit) runtime.events.shift(); for (const listener of runtime.listeners) listener(event); return event; } function cacheEvent(runtime, type, data) { runtime.lastActivity = new Date().toISOString(); const event = data?.event ?? {}; if (type === "agent_state" && typeof data?.state === "string") { if (runtime.state === "closing" || runtime.state === "stopped") return; runtime.state = data.state; runtime.stateVersion += 1; if (data.state === "idle") { runtime.activeTool = undefined; runtime.supervisor?.markHealthy(); } } if (type === "queue") { const followUp = Array.isArray(event.followUp) ? event.followUp.length : 0; const steering = Array.isArray(event.steering) ? event.steering.length : 0; runtime.queueCount = typeof event.pendingMessageCount === "number" ? event.pendingMessageCount : followUp + steering; } if (type === "tool") { if (String(event.type ?? "").endsWith("_end")) runtime.activeTool = undefined; else if (typeof event.toolName === "string") runtime.activeTool = event.toolName; } if (type === "extension_ui_request") { const method = event.method; if (["select", "confirm", "input", "editor"].includes(method)) { runtime.attention = true; if (typeof event.id === "string") runtime.extensions.set(event.id, event); } } const message = event.message; if ( !runtime.firstMessage && message?.role === "user" && message.content !== undefined ) runtime.firstMessage = sessionPreview(message.content); } function createRuntimeRecord({ runtimeId = randomUUID(), worktreePath, sessionDir, sessionPath, sessionId, legacyDefault = false, ephemeral = false, openedAt = new Date().toISOString(), }) { return { runtimeId, agentId: `agent-${randomUUID()}`, worktreePath, sessionDir, sessionPath, sessionId, legacyDefault, ephemeral, openedAt, state: "starting", stateVersion: 0, errorAttention: false, attention: false, queueCount: 0, activeTool: undefined, lastActivity: openedAt, events: [], nextSequence: 1, listeners: new Set(), extensions: new Map(), operationQueue: Promise.resolve(), adapter: undefined, supervisor: undefined, stopped: false, promoting: false, promotionPromise: undefined, recoveryPromise: undefined, }; } async function launch(runtime) { let adapter; const expectedSessionPath = runtime.sessionPath; adapter = startAdapter({ cwd: runtime.worktreePath, sessionDir: runtime.sessionDir, ...(runtime.sessionPath ? { sessionPath: runtime.sessionPath } : {}), onEvent: (event) => { if (runtime.adapter !== adapter || runtime.stopped) return; cacheEvent(runtime, event.type, event.data); publishAgent(runtime, event.type, event.data); publishWorkspace("runtime_event", runtime, { eventType: event.type, eventData: event.data, runtime: publicRuntime(runtime), }); if (event.type === "agent_state" && event.data?.state === "idle") { void enqueue(runtime, async () => { const state = await adapter.send({ type: "get_state" }); await updateRuntimeIdentity(runtime, state, { adapter }); }).catch((error) => { if (runtime.stopped || runtime.state === "closing" || stopping) return; runtime.error = { code: "identity_refresh", message: error.message, }; runtime.errorAttention = true; runtime.attention = true; }); } }, onError: (error, { terminal = true } = {}) => { if (runtime.adapter !== adapter || runtime.stopped) return; if (!terminal) { const diagnostic = { code: error.code ?? "unknown", message: error.message, }; publishAgent(runtime, "diagnostic", { error: diagnostic }); publishWorkspace("runtime_diagnostic", runtime, { error: diagnostic, }); return; } runtime.state = "error"; runtime.stateVersion += 1; runtime.error = { code: error.code ?? "unknown", message: error.message, }; runtime.errorAttention = true; runtime.attention = true; publishAgent(runtime, "agent_state", { state: "error", error: runtime.error, }); publishWorkspace("runtime_event", runtime, { eventType: "agent_state", eventData: { state: "error", error: runtime.error }, runtime: publicRuntime(runtime), }); if (["child_exited", "child_error"].includes(error.code)) { // Dialog request IDs belong to the dead child and cannot be answered // after recovery creates a replacement process. runtime.extensions.clear(); runtime.recoveryPromise = runtime.supervisor .handleUnexpectedExit() .then((nextAdapter) => { if (!runtime.stopped) { runtime.state = nextAdapter ? "idle" : "failed"; runtime.stateVersion += 1; if (nextAdapter) { runtime.error = undefined; runtime.errorAttention = false; runtime.attention = runtime.extensions.size > 0; } publishAgent(runtime, "agent_state", { state: runtime.state }); publishWorkspace("runtime_recovered", runtime, { runtime: publicRuntime(runtime), }); } return nextAdapter; }); } }, }); runtime.adapter = adapter; try { const state = await adapter.send( { type: "get_state" }, { timeoutMs: INITIAL_LAUNCH_GET_STATE_TIMEOUT_MS }, ); runtime.state = state?.data?.isStreaming ? "streaming" : "idle"; runtime.stateVersion += 1; await updateRuntimeIdentity(runtime, state, { ...(expectedSessionPath ? { expectedSessionPath } : {}), adapter, }); return adapter; } catch (error) { runtime.stopped = true; await adapter.stop().catch(() => {}); runtime.stopped = false; if (runtime.adapter === adapter) runtime.adapter = undefined; throw error; } } async function openRuntimeInternal( { runtimeId = randomUUID(), worktreePath, sessionPath, sessionId, legacyDefault = false, ephemeral = false, openedAt, }, { restore = false } = {}, ) { if (stopping) throw new Error("agent registry is stopping"); const canonicalPath = await realpath(worktreePath); const sessionDir = ephemeral ? path.join(sessionRoot, "quick", runtimeId) : sessionDirectoryFor(sessionRoot, canonicalPath); await mkdir(sessionDir, { recursive: true, mode: SESSION_DIRECTORY_MODE }); await chmod(sessionDir, SESSION_DIRECTORY_MODE); let ownedPath; if (sessionPath) { ownedPath = await ownedSessionPath(sessionDir, sessionPath); const existing = runtimesBySessionPath.get(ownedPath); if (existing) { if (existing.state === "closing" || existing.stopped) throw new Error("session runtime is closing"); if (restore && existing.runtimeId !== runtimeId) throw new Error("session is already open in another runtime"); return publicRuntime(existing); } } const key = ownedPath ?? `${canonicalPath}:new:${runtimeId}`; if (creatingByKey.has(key)) { if (restore) throw new Error("session is already opening in another runtime"); return creatingByKey.get(key); } if (stopping) throw new Error("agent registry is stopping"); if (ownedPath) reserveSessionPath(ownedPath, { runtimeId }); const creation = (async () => { if (stopping) throw new Error("agent registry is stopping"); const runtime = createRuntimeRecord({ runtimeId, worktreePath: canonicalPath, sessionDir, sessionPath: ownedPath, sessionId, legacyDefault, ephemeral, openedAt, }); if (stopping) throw new Error("agent registry is stopping"); indexRuntime(runtime); if (ownedPath) pendingSessionLeases.set(ownedPath, runtime); runtime.supervisor = createRecoverySupervisor({ start: () => launch(runtime), ...(maxRecoveryAttempts ? { maxAttempts: maxRecoveryAttempts } : {}), ...(recoveryDelayForAttempt ? { delayForAttempt: recoveryDelayForAttempt } : {}), ...(sleep ? { sleep } : {}), onState: (recovery) => { runtime.state = recovery.state === "recovering" ? "recovering" : runtime.state; publishAgent(runtime, "recovery", recovery); publishWorkspace("runtime_recovery", runtime, { recovery }); }, }); try { await launch(runtime); if (!runtime.ephemeral) await persistWorkspace(); publishWorkspace("runtime_opened", runtime, { runtime: publicRuntime(runtime), }); return publicRuntime(runtime); } catch (error) { if (ownedPath) releaseSessionReservation(ownedPath, runtime); await runtime.adapter?.stop().catch(() => {}); if (!restore) { unindexRuntime(runtime); throw error; } if ( runtime.sessionPath && runtimesBySessionPath.get(runtime.sessionPath) === runtime ) runtimesBySessionPath.delete(runtime.sessionPath); // Preserve the manifest's requested path for dormant intent. The // canonical lease is only meaningful while a child is live. if (sessionPath) runtime.sessionPath = sessionPath; if (runtime.agentId) runtimesByAgentId.delete(runtime.agentId); runtime.agentId = undefined; runtime.adapter = undefined; runtime.state = "failed"; runtime.errorAttention = true; runtime.attention = true; runtime.error = { code: "restore_failed", message: error instanceof Error ? error.message : String(error), }; await persistWorkspace(); publishWorkspace("runtime_restore_failed", runtime, { runtime: publicRuntime(runtime), }); return publicRuntime(runtime); } })(); creatingByKey.set(key, creation); try { return await creation; } finally { creatingByKey.delete(key); if (ownedPath) { const pending = pendingSessionLeases.get(ownedPath); if (pending?.runtimeId === runtimeId) pendingSessionLeases.delete(ownedPath); } } } function openRuntime(record, options) { if (stopping) return Promise.reject(new Error("agent registry is stopping")); const operation = openRuntimeInternal(record, options); pendingOpenOperations.add(operation); void operation .finally(() => pendingOpenOperations.delete(operation)) .catch(() => {}); return operation; } function retainFailedRestore(record, error) { const sessionDir = sessionDirectoryFor(sessionRoot, record.worktreePath); const dormant = createRuntimeRecord({ ...record, sessionDir }); dormant.agentId = undefined; dormant.state = "failed"; dormant.errorAttention = true; dormant.attention = true; dormant.error = { code: "restore_failed", message: error instanceof Error ? error.message : String(error), }; if (!runtimesByRuntimeId.has(dormant.runtimeId)) indexRuntime(dormant, { leaseSession: false }); publishWorkspace("runtime_restore_failed", dormant, { runtime: publicRuntime(dormant), }); } async function restoreRecords(records) { // Canonicalize saved-session identities before launching any children. This // makes alias collisions deterministic and prevents a losing dormant record // from racing the winner's lease acquisition or manifest persistence. const prepared = []; for (const record of records) { try { let canonicalSession; if (record.sessionPath) { const canonicalWorktree = await realpath(record.worktreePath); const sessionDir = sessionDirectoryFor(sessionRoot, canonicalWorktree); canonicalSession = await ownedSessionPath(sessionDir, record.sessionPath); } prepared.push({ record, canonicalSession }); } catch (error) { retainFailedRestore(record, error); } } const preferredBySession = new Map(); for (const entry of prepared) { if (!entry.canonicalSession) continue; const current = preferredBySession.get(entry.canonicalSession); if ( !current || (entry.record.sessionPath === entry.canonicalSession && current.record.sessionPath !== current.canonicalSession) ) preferredBySession.set(entry.canonicalSession, entry); } const restorable = []; for (const entry of prepared) { if ( entry.canonicalSession && preferredBySession.get(entry.canonicalSession) !== entry ) { retainFailedRestore( entry.record, new Error("session is already open in another runtime"), ); } else restorable.push(entry.record); } let next = 0; const workers = []; const workerCount = Math.min(RESTORE_CONCURRENCY, restorable.length); for (let workerIndex = 0; workerIndex < workerCount; workerIndex += 1) { workers.push( (async () => { while (next < restorable.length) { const record = restorable[next++]; try { await openRuntime(record, { restore: true }); } catch (error) { retainFailedRestore(record, error); } } })(), ); } await Promise.all(workers); await persistWorkspace(); } async function setLegacyDefault(runtime) { for (const candidate of runtimesByWorktreePath.get(runtime.worktreePath) ?? []) candidate.legacyDefault = candidate === runtime; runtime.legacyDefault = true; await persistSessionReference( runtime.sessionDir, runtime.sessionPath, runtime.worktreePath, ); } function releaseClosedSessionLease(runtime) { if ( runtime.sessionPath && runtimesBySessionPath.get(runtime.sessionPath) === runtime ) runtimesBySessionPath.delete(runtime.sessionPath); } async function performClose(runtime, wasWorkingAtRequest) { const wasWorking = wasWorkingAtRequest || runtime.state === "streaming" || runtime.queueCount > 0; if (runtime.legacyDefault) { const replacementDefault = [ ...(runtimesByWorktreePath.get(runtime.worktreePath) ?? []), ].find( (candidate) => candidate !== runtime && candidate.agentId && !candidate.stopped && candidate.state !== "closing" && candidate.state !== "stopped", ); runtime.legacyDefault = false; if (replacementDefault) await setLegacyDefault(replacementDefault); else await persistSessionReference( runtime.sessionDir, undefined, runtime.worktreePath, ); } // Remove desired-open/UI intent now, but retain the session lease until // the old child can no longer write to its JSONL file. unindexRuntime(runtime, { releaseSessionLease: false }); if (!runtime.ephemeral) await persistWorkspace(); runtime.stopped = true; runtime.supervisor?.stop(); let stopConfirmed = true; if (runtime.adapter) { if (wasWorking) await runtime.adapter.send({ type: "abort" }).catch(() => {}); const stopPromise = Promise.resolve().then(() => runtime.adapter.stop()); let timer; const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), closeTimeoutMs); }); const stopped = await Promise.race([ stopPromise.then(() => "stopped"), timeout, ]); clearTimeout(timer); stopConfirmed = stopped === "stopped"; if (!stopConfirmed) { runtime.adapter.child?.kill?.("SIGKILL"); // A kill request is not exit confirmation. Keep the lease until the // adapter observes child exit; an unresolved adapter keeps it reserved. void stopPromise .then(() => releaseClosedSessionLease(runtime)) .catch(() => {}); } } if (stopConfirmed) releaseClosedSessionLease(runtime); if (runtime.ephemeral && stopConfirmed) await rm(runtime.sessionDir, { recursive: true, force: true }).catch( () => {}, ); runtime.state = "stopped"; publishWorkspace("runtime_closed", runtime, { runtimeId: runtime.runtimeId, worktreePath: runtime.worktreePath, }); return { runtimeId: runtime.runtimeId, sessionPath: runtime.sessionPath }; } function closeRuntime(runtimeId) { const runtime = getRuntime(runtimeId); if (runtime.promoting) return runtime.promotionPromise.then(() => closeRuntime(runtimeId)); if (runtime.state === "closing" || runtime.state === "stopped") return Promise.reject(new Error("session runtime is closing")); const wasWorkingAtRequest = runtime.state === "streaming" || runtime.queueCount > 0; // Mark synchronously so concurrent default promotion cannot choose a // runtime whose close has already been requested. Previously accepted work // still drains before performClose. runtime.state = "closing"; publishWorkspace("runtime_closing", runtime, { runtime: publicRuntime(runtime), }); const result = runtime.operationQueue.then(() => performClose(runtime, wasWorkingAtRequest), ); runtime.operationQueue = result.catch(() => {}); return result; } function promoteQuickRuntime(runtimeId) { const runtime = getRuntime(runtimeId); if (!runtime.ephemeral) return Promise.resolve(publicRuntime(runtime)); if (runtime.state === "closing" || runtime.state === "stopped") return Promise.reject(new Error("session runtime is closing")); if (runtime.promotionPromise) return runtime.promotionPromise; runtime.promoting = true; const promotion = runtime.operationQueue.then(async () => { if (!runtime.ephemeral) return publicRuntime(runtime); if (runtime.state === "closing" || runtime.state === "stopped") throw new Error("session runtime is closing"); const adapter = runtime.adapter; if (!adapter) throw new Error("Pi is not currently running"); const quickDir = runtime.sessionDir; const sessionDir = sessionDirectoryFor(sessionRoot, runtime.worktreePath); const previous = { sessionDir: runtime.sessionDir, sessionPath: runtime.sessionPath, sessionId: runtime.sessionId, sessionName: runtime.sessionName, sessionLease: runtime.sessionPath ? runtimesBySessionPath.get(runtime.sessionPath) === runtime : false, managed: managedWorktrees.has(runtime.worktreePath), }; let directoryPromotion; let previousReference; let reservedSessionPath; let manifestStateChanged = false; try { await mkdir(sessionDir, { recursive: true, mode: SESSION_DIRECTORY_MODE, }); await chmod(sessionDir, SESSION_DIRECTORY_MODE); if (runtime.sessionPath) { reservedSessionPath = await projectQuickSessionPath( quickDir, sessionDir, runtime.sessionPath, ); reserveSessionPath(reservedSessionPath, runtime); } directoryPromotion = await promoteQuickSessionDirectory( quickDir, sessionDir, ); runtime.sessionDir = sessionDir; if (reservedSessionPath) runtime.sessionPath = reservedSessionPath; const state = await adapter.send({ type: "get_state" }); await updateRuntimeIdentity(runtime, state, { adapter, persist: false, indexSession: false, }); // A changed identity still needs a lease through manifest commit. if (runtime.sessionPath && runtime.sessionPath !== reservedSessionPath) reserveSessionPath(runtime.sessionPath, runtime); if ( previous.sessionPath && previous.sessionPath !== runtime.sessionPath && runtimesBySessionPath.get(previous.sessionPath) === runtime ) runtimesBySessionPath.delete(previous.sessionPath); runtime.ephemeral = false; managedWorktrees.add(runtime.worktreePath); manifestStateChanged = true; try { previousReference = await readFile( path.join(sessionDir, SESSION_REFERENCE_FILE), "utf8", ); } catch (error) { if (error?.code !== "ENOENT") throw error; } await persistSessionReference( sessionDir, runtime.sessionPath, runtime.worktreePath, ); await persistWorkspace(); if (runtime.sessionPath) runtimesBySessionPath.set(runtime.sessionPath, runtime); if (reservedSessionPath) releaseSessionReservation(reservedSessionPath, runtime); if (runtime.sessionPath !== reservedSessionPath) releaseSessionReservation(runtime.sessionPath, runtime); await directoryPromotion.finalize(); publishWorkspace("runtime_promoted", runtime, { runtime: publicRuntime(runtime), }); return publicRuntime(runtime); } catch (error) { if (directoryPromotion) await restoreSessionReference(sessionDir, previousReference).catch( () => {}, ); if (reservedSessionPath) releaseSessionReservation(reservedSessionPath, runtime); if (runtime.sessionPath !== reservedSessionPath) releaseSessionReservation(runtime.sessionPath, runtime); if ( runtime.sessionPath && runtimesBySessionPath.get(runtime.sessionPath) === runtime ) runtimesBySessionPath.delete(runtime.sessionPath); runtime.sessionDir = previous.sessionDir; runtime.sessionPath = previous.sessionPath; runtime.sessionId = previous.sessionId; runtime.sessionName = previous.sessionName; runtime.ephemeral = true; if (previous.sessionLease && previous.sessionPath) runtimesBySessionPath.set(previous.sessionPath, runtime); if (previous.managed) managedWorktrees.add(runtime.worktreePath); else managedWorktrees.delete(runtime.worktreePath); if (manifestStateChanged) await persistWorkspace().catch(() => {}); if (directoryPromotion) await directoryPromotion.rollback(); throw error; } }); const tracked = promotion.finally(() => { runtime.promoting = false; runtime.promotionPromise = undefined; }); runtime.operationQueue = tracked.catch(() => {}); runtime.promotionPromise = tracked; return tracked; } async function refreshAfterLegacySessionChange( runtime, response, { expectedSessionPath } = {}, ) { if (!response?.data?.cancelled) { const state = await runtime.adapter.send({ type: "get_state" }); await updateRuntimeIdentity(runtime, state, { ...(expectedSessionPath ? { expectedSessionPath } : {}), adapter: runtime.adapter, }); } return response; } async function beginLegacyNewSession(runtime) { const response = await runtime.adapter.send({ type: "new_session" }); if (response?.data?.cancelled) return response; if ( runtime.sessionPath && runtimesBySessionPath.get(runtime.sessionPath) === runtime ) runtimesBySessionPath.delete(runtime.sessionPath); runtime.sessionPath = undefined; runtime.sessionId = undefined; runtime.sessionName = undefined; runtime.firstMessage = undefined; runtime.extensions.clear(); runtime.queueCount = 0; runtime.activeTool = undefined; runtime.error = undefined; runtime.errorAttention = false; runtime.attention = false; runtime.lastActivity = new Date().toISOString(); if (runtime.legacyDefault) await persistSessionReference( runtime.sessionDir, undefined, runtime.worktreePath, ); await persistWorkspace(); const state = await runtime.adapter.send({ type: "get_state" }); await updateRuntimeIdentity(runtime, state, { adapter: runtime.adapter }); runtime.state = state?.data?.isStreaming ? "streaming" : "idle"; runtime.stateVersion += 1; publishAgent(runtime, "agent_state", { state: runtime.state }); publishWorkspace("runtime_renewed", runtime, { runtime: publicRuntime(runtime), }); return response; } function workspaceReplay(cursor = 0) { if (!Number.isSafeInteger(cursor) || cursor < 0) throw new TypeError("cursor must be a non-negative integer"); const firstAvailableSeq = workspaceEvents[0]?.seq ?? workspaceSequence + 1; return { bridgeInstanceId, firstAvailableSeq, latestSeq: workspaceSequence, truncated: cursor < firstAvailableSeq - 1, events: workspaceEvents.filter((event) => event.seq > cursor), }; } function workspace() { const grouped = new Map( [...managedWorktrees].map((worktreePath) => [worktreePath, []]), ); for (const runtime of runtimesByRuntimeId.values()) { if (runtime.ephemeral) continue; let group = grouped.get(runtime.worktreePath); if (!group) { group = []; grouped.set(runtime.worktreePath, group); } group.push(publicRuntime(runtime)); } return { bridgeInstanceId, latestSeq: workspaceSequence, directories: [...grouped.entries()] .map(([worktreePath, runtimes]) => ({ worktreePath, isHome: worktreePath === canonicalHomeWorktree, openCount: runtimes.length, workingCount: runtimes.filter((entry) => entry.state === "streaming") .length, attentionCount: runtimes.filter((entry) => entry.attention).length, recoveringCount: runtimes.filter((entry) => entry.state === "recovering") .length, errorCount: runtimes.filter((entry) => ["error", "failed"].includes(entry.state), ).length, runtimes, })) .sort((left, right) => left.worktreePath.localeCompare(right.worktreePath)), ...(manifestIssue ? { issue: manifestIssue } : {}), }; } return { get bridgeInstanceId() { return bridgeInstanceId; }, async start() { canonicalHomeWorktree = await realpath(homeWorktree); managedWorktrees.add(canonicalHomeWorktree); const loaded = await workspaceStore.load(); manifestIssue = loaded.issue; if (loaded.migrating) { const sessionDir = sessionDirectoryFor(sessionRoot, canonicalHomeWorktree); await mkdir(sessionDir, { recursive: true, mode: SESSION_DIRECTORY_MODE, }); const reference = await readSessionReference(sessionDir); const migrationRecord = { runtimeId: randomUUID(), worktreePath: canonicalHomeWorktree, legacyDefault: true, ...(reference.sessionPath ? { sessionPath: reference.sessionPath } : {}), }; if (reference.sessionPath) await restoreRecords([migrationRecord]); else await openRuntime(migrationRecord); await persistWorkspace(); } else if (!loaded.issue) { for (const worktreePath of loaded.manifest.directories) { try { managedWorktrees.add(await realpath(worktreePath)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } const defaultWorktrees = new Set( loaded.manifest.runtimes .filter((record) => record.legacyDefault) .map((record) => record.worktreePath), ); const records = loaded.manifest.runtimes.map((record) => { if (defaultWorktrees.has(record.worktreePath)) return record; defaultWorktrees.add(record.worktreePath); return { ...record, legacyDefault: true }; }); await restoreRecords(records); } if ( !loaded.issue && ![...runtimesByAgentId.values()].some((runtime) => runtime.adapter) ) { await openRuntime({ worktreePath: canonicalHomeWorktree, legacyDefault: true, }); } return this.listAgents()[0]; }, async selectWorktree(worktreePath) { if (stopping) throw new Error("agent registry is stopping"); const canonicalPath = await realpath(worktreePath); const liveRuntimes = [ ...(runtimesByWorktreePath.get(canonicalPath) ?? []), ].filter((runtime) => runtime.agentId); const existing = liveRuntimes.find((runtime) => runtime.legacyDefault) ?? liveRuntimes[0]; if (existing) { if (!existing.legacyDefault) { await setLegacyDefault(existing); await persistWorkspace(); } return publicAgent(existing); } const sessionDir = sessionDirectoryFor(sessionRoot, canonicalPath); await mkdir(sessionDir, { recursive: true, mode: SESSION_DIRECTORY_MODE, }); const reference = await readSessionReference(sessionDir); const opened = await openRuntime({ worktreePath: canonicalPath, legacyDefault: true, ...(reference.sessionPath ? { sessionPath: reference.sessionPath } : {}), }); return publicAgent(getRuntime(opened.runtimeId)); }, async createSessionRuntime(worktreePath) { return openRuntime({ worktreePath }); }, async createQuickRuntime(worktreePath) { return openRuntime({ worktreePath, ephemeral: true }); }, promoteQuickRuntime, async openSessionRuntime(worktreePath, sessionPath) { return openRuntime({ worktreePath, sessionPath }); }, closeSessionRuntime: closeRuntime, async closeQuickRuntime(runtimeId) { const runtime = getRuntime(runtimeId); if (runtime.promoting) { await runtime.promotionPromise; return { runtimeId: runtime.runtimeId, sessionPath: runtime.sessionPath }; } if (!runtime.ephemeral) return { runtimeId: runtime.runtimeId, sessionPath: runtime.sessionPath }; return closeRuntime(runtimeId); }, async getModelCatalog() { const candidates = [...runtimesByAgentId.values()].filter( (runtime) => runtime.adapter && !runtime.stopped && !["closing", "stopped", "failed"].includes(runtime.state), ); let lastError; for (const runtime of candidates) { const adapter = runtime.adapter; try { const response = await enqueue(runtime, () => adapter.send({ type: "get_available_models" }), ); if (response?.success !== true || !Array.isArray(response?.data?.models)) { continue; } lastSuccessfulModelCatalog = response.data.models; return response.data.models; } catch (error) { lastError = error; } } if (lastSuccessfulModelCatalog) return lastSuccessfulModelCatalog; if (lastError) throw lastError; return []; }, listAgents() { return [...runtimesByAgentId.values()] .map(publicAgent) .sort((left, right) => left.worktreePath.localeCompare(right.worktreePath)); }, async listDirectories() { let entries = []; try { entries = await readdir(sessionRoot, { withFileTypes: true }); } catch (error) { if (error?.code !== "ENOENT") throw error; } const references = await Promise.all( entries .filter((entry) => entry.isDirectory()) .map(async (entry) => { const reference = await readSessionReference( path.join(sessionRoot, entry.name), ); return reference.worktreePath; }), ); const paths = new Set([ ...runtimesByWorktreePath.keys(), ...references.filter(Boolean), ]); return [...paths] .map((worktreePath) => { const runtime = [...(runtimesByWorktreePath.get(worktreePath) ?? [])].find( (entry) => entry.agentId, ); return { worktreePath, state: runtime?.state ?? "inactive", isHome: worktreePath === canonicalHomeWorktree, ...(runtime ? { agentId: runtime.agentId } : {}), }; }) .sort((left, right) => left.worktreePath.localeCompare(right.worktreePath)); }, async forgetDirectory(worktreePath) { let canonicalPath = path.normalize(worktreePath); try { canonicalPath = await realpath(worktreePath); } catch (error) { if (error?.code !== "ENOENT") throw error; } if (canonicalPath === canonicalHomeWorktree) throw new Error("cannot forget the home directory"); if ((runtimesByWorktreePath.get(canonicalPath)?.size ?? 0) > 0) throw new Error( "cannot forget a directory while session runtimes are open", ); const sessionDir = sessionDirectoryFor(sessionRoot, canonicalPath); await rm(path.join(sessionDir, SESSION_REFERENCE_FILE), { force: true }); managedWorktrees.delete(canonicalPath); await persistWorkspace(); return { worktreePath: canonicalPath }; }, async listSessions(agentId) { const runtime = getAgent(agentId); return listSessionSummaries(runtime.sessionDir, runtime.sessionPath); }, async listDirectorySessions(worktreePath) { const canonicalPath = await realpath(worktreePath); const sessionDir = sessionDirectoryFor(sessionRoot, canonicalPath); const sessions = await listSessionSummaries(sessionDir); return sessions.map((session) => { const openRuntime = runtimesBySessionPath.get(session.path); return { ...session, isCurrent: false, ...(openRuntime ? { runtimeId: openRuntime.runtimeId } : {}), }; }); }, getWorkspace: workspace, getWorkspaceSummary() { const runtimes = [...runtimesByRuntimeId.values()].filter( (runtime) => !runtime.ephemeral, ); return { bridgeInstanceId, latestSeq: workspaceSequence, openCount: runtimes.length, workingCount: runtimes.filter((runtime) => runtime.state === "streaming") .length, attentionCount: runtimes.filter((runtime) => runtime.attention).length, recoveringCount: runtimes.filter( (runtime) => runtime.state === "recovering", ).length, errorCount: runtimes.filter((runtime) => ["error", "failed"].includes(runtime.state), ).length, directoryCount: new Set(runtimes.map((runtime) => runtime.worktreePath)) .size, resourceWarning: runtimes.length >= RESOURCE_WARNING_COUNT, }; }, async getSessionRuntimeSnapshot(runtimeId) { const runtime = getRuntime(runtimeId); if (!runtime.adapter) return { bridgeInstanceId, latestSeq: workspaceSequence, runtime: publicRuntime(runtime), extensions: [...runtime.extensions.values()], }; return enqueue(runtime, async () => { const adapter = runtime.adapter; const safe = async (command, fallback, { includeError = false } = {}) => { try { return await adapter.send({ type: command }); } catch (error) { return includeError ? { ...fallback, error: String(error) } : fallback; } }; const state = await safe("get_state", { data: {} }); const stats = await safe("get_session_stats", { data: {} }); const transcript = await safe("get_messages", { data: { messages: [] }, }); const commands = await safe("get_commands", { data: { commands: [] } }); const models = await safe( "get_available_models", { data: { models: [] } }, { includeError: true }, ); await updateRuntimeIdentity(runtime, state, { adapter }); return { bridgeInstanceId, latestSeq: workspaceSequence, runtime: publicRuntime(runtime), state, stats, transcript, commands, models, extensions: [...runtime.extensions.values()], }; }); }, eventsAfter(agentId, cursor = 0) { if (!Number.isSafeInteger(cursor) || cursor < 0) throw new TypeError("cursor must be a non-negative integer"); return getAgent(agentId).events.filter((event) => event.seq > cursor); }, subscribe(agentId, cursor, listener) { if (typeof listener !== "function") throw new TypeError("listener must be a function"); const runtime = getAgent(agentId); const events = this.eventsAfter(agentId, cursor); runtime.listeners.add(listener); return { events, unsubscribe: () => runtime.listeners.delete(listener), }; }, workspaceEventsAfter: workspaceReplay, subscribeWorkspace(cursor, listener) { if (typeof listener !== "function") throw new TypeError("listener must be a function"); const replay = workspaceReplay(cursor); workspaceListeners.add(listener); return { ...replay, unsubscribe: () => workspaceListeners.delete(listener), }; }, async waitForRecovery(agentId) { return getAgent(agentId).recoveryPromise; }, async retry(agentId) { const runtime = getAgent(agentId); return enqueue(runtime, async () => { runtime.supervisor.markHealthy(); runtime.recoveryPromise = runtime.supervisor.handleUnexpectedExit(); await runtime.recoveryPromise; return publicAgent(runtime); }); }, async renewSession(agentId) { const runtime = getAgent(agentId); return enqueue(runtime, async () => { if (!runtime.adapter) throw new Error("Pi is not currently running"); if (runtime.state === "streaming" || runtime.queueCount > 0) await runtime.adapter.send({ type: "abort" }); const response = await beginLegacyNewSession(runtime); if (response?.data?.cancelled) throw new Error("Pi cancelled the session renewal"); return publicAgent(runtime); }); }, async restart(agentId) { const runtime = getAgent(agentId); return enqueue(runtime, async () => { const replacedAdapter = runtime.adapter; if (!replacedAdapter) throw new Error("Pi is not currently running"); // Detach the old child before stopping it so late events cannot mutate // presentation state that belongs to the replacement generation. runtime.adapter = undefined; await replacedAdapter.stop(); // Dialog request IDs belong to the terminated child and cannot be // answered by its replacement. runtime.extensions.clear(); runtime.attention = runtime.errorAttention; runtime.supervisor.markHealthy(); runtime.recoveryPromise = runtime.supervisor.handleUnexpectedExit(); const nextAdapter = await runtime.recoveryPromise; if (!nextAdapter) { runtime.state = "failed"; runtime.stateVersion += 1; runtime.error = { code: "restart_failed", message: "Pi did not restart after the recovery attempts were exhausted", }; runtime.errorAttention = true; runtime.attention = true; publishAgent(runtime, "agent_state", { state: runtime.state, error: runtime.error, }); throw new Error(runtime.error.message); } runtime.error = undefined; runtime.errorAttention = false; runtime.attention = runtime.extensions.size > 0; publishAgent(runtime, "agent_state", { state: runtime.state }); publishWorkspace("runtime_recovered", runtime, { runtime: publicRuntime(runtime), }); return publicAgent(runtime); }); }, async route(agentId, operation, payload = {}) { const runtime = getAgent(agentId); if (operation === "abort" && !runtime.promoting) return runtime.adapter.send({ type: "abort" }); return enqueue(runtime, async () => { if (operation === "switch_session") { if (runtime.state === "streaming") throw new Error("cannot switch sessions while Pi is working"); const ownedPath = await ownedSessionPath( runtime.sessionDir, payload.sessionPath, ); reserveSessionPath(ownedPath, runtime); try { const response = await runtime.adapter.send({ type: "switch_session", sessionPath: ownedPath, }); return await refreshAfterLegacySessionChange(runtime, response); } finally { releaseSessionReservation(ownedPath, runtime); } } if (operation === "new_session") { if (runtime.state === "streaming") throw new Error("cannot start a session while Pi is working"); return beginLegacyNewSession(runtime); } const previousState = runtime.state; const previousStateVersion = runtime.stateVersion; const delivery = operation === "submit_prompt" ? previousState === "streaming" ? "follow_up" : "prompt" : undefined; const startsWork = operation === "prompt" || (operation === "submit_prompt" && previousState !== "streaming"); if (startsWork) { runtime.state = "streaming"; if (!runtime.firstMessage && typeof payload.message === "string") runtime.firstMessage = payload.message; } try { if ( operation === "extension_response" && !runtime.extensions.has(payload.requestId) ) throw new Error("extension request is not actionable for this runtime"); const response = await routeCommand( runtime.adapter, operation, payload, previousState, ); if (operation === "set_session_name") { runtime.sessionName = payload.name; await persistWorkspace(); } if (operation === "extension_response") { runtime.extensions.delete(payload.requestId); runtime.attention = runtime.errorAttention || runtime.extensions.size > 0; } return delivery ? { ...response, delivery } : response; } catch (error) { if ( startsWork && runtime.state === "streaming" && runtime.stateVersion === previousStateVersion ) runtime.state = previousState; throw error; } }); }, async stop() { stopping = true; while (pendingOpenOperations.size > 0) await Promise.allSettled(pendingOpenOperations); const runtimes = [...runtimesByRuntimeId.values()]; await Promise.all(runtimes.map((runtime) => runtime.operationQueue)); await Promise.all( runtimes.map(async (runtime) => { runtime.stopped = true; runtime.supervisor?.stop(); await runtime.adapter?.stop().catch(() => {}); runtime.state = "stopped"; return undefined; }), ); }, }; } export const sessionDirectoryPath = sessionDirectoryFor;