Files
pi-gui/src/bridge/agent-registry.js
T
alex 0949c6f0d7 refactor!: remove redundant compatibility layers
Use native browser and Tauri behavior instead of custom selector, dialog, and window-state implementations. Remove obsolete bridge, relay, asset, and dependency paths to keep the supported surface small.

BREAKING CHANGE: The Noctalia relay no longer accepts --agent, and the Tauri host no longer exposes the legacy load_agent command.
2026-08-31 17:35:07 +02:00

1819 lines
57 KiB
JavaScript

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;