feat(bridge): establish local pi status bridge

Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
This commit is contained in:
2026-07-27 14:51:45 +02:00
commit b7fea83ed6
91 changed files with 15029 additions and 0 deletions
+492
View File
@@ -0,0 +1,492 @@
import { createHash, randomUUID } from "node:crypto";
import {
chmod,
mkdir,
readdir,
readFile,
realpath,
stat,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { startPiRpcAdapter } from "./pi-rpc-adapter.js";
import { createRecoverySupervisor } from "./recovery-supervisor.js";
const SESSION_DIRECTORY_MODE = 0o700;
const SESSION_REFERENCE_FILE = "bridge-agent.json";
const DEFAULT_EVENT_LIMIT = 1_000;
export class UnknownAgentError extends Error {
constructor(agentId) {
super(`unknown agent: ${agentId}`);
this.name = "UnknownAgentError";
}
}
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,
},
);
}
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) {
const entries = await readdir(sessionDir, { withFileTypes: true });
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));
}
async function listDirectoryCatalog(sessionRoot, agentsByPath) {
let entries;
try {
entries = await readdir(sessionRoot, { withFileTypes: true });
} catch (error) {
if (error?.code === "ENOENT") return [];
throw error;
}
const references = await Promise.all(
entries
.filter((entry) => entry.isDirectory())
.map(async (entry) => {
const reference = await readSessionReference(
path.join(sessionRoot, entry.name),
);
if (!reference.worktreePath) return undefined;
return path.basename(
sessionDirectoryFor(sessionRoot, reference.worktreePath),
) === entry.name
? reference.worktreePath
: undefined;
}),
);
const paths = new Set([
...agentsByPath.keys(),
...references.filter(Boolean),
]);
return [...paths]
.map((worktreePath) => {
const agent = agentsByPath.get(worktreePath);
return {
worktreePath,
state: agent?.state ?? "inactive",
...(agent ? { agentId: agent.id } : {}),
};
})
.sort((left, right) => left.worktreePath.localeCompare(right.worktreePath));
}
function publicAgent(agent) {
return {
id: agent.id,
worktreePath: agent.worktreePath,
sessionDir: agent.sessionDir,
state: agent.state,
};
}
function routeCommand(adapter, operation, payload = {}, agentState) {
switch (operation) {
case "prompt":
case "steer":
case "follow_up":
return adapter.send({ type: operation, message: payload.message });
case "submit_prompt":
return adapter.send({
type: agentState === "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 "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 "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,
eventLimit = DEFAULT_EVENT_LIMIT,
maxRecoveryAttempts,
recoveryDelayForAttempt,
sleep,
}) {
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");
const agentsByPath = new Map();
const agentsById = new Map();
const inFlight = new Map();
async function ensureAgent(worktreePath) {
if (typeof worktreePath !== "string" || !path.isAbsolute(worktreePath))
throw new TypeError("worktreePath must be an absolute path");
const canonicalPath = await realpath(worktreePath);
const existing = agentsByPath.get(canonicalPath);
if (existing) return publicAgent(existing);
const creating = inFlight.get(canonicalPath);
if (creating) return creating;
const creation = (async () => {
const sessionDir = sessionDirectoryFor(sessionRoot, canonicalPath);
await mkdir(sessionDir, {
recursive: true,
mode: SESSION_DIRECTORY_MODE,
});
await chmod(sessionDir, SESSION_DIRECTORY_MODE);
const reference = await readSessionReference(sessionDir);
const agent = {
id: `agent-${randomUUID()}`,
worktreePath: canonicalPath,
sessionDir,
sessionPath: reference.sessionPath,
state: "idle",
events: [],
listeners: new Set(),
nextSequence: 1,
adapter: undefined,
stopped: false,
recoveryPromise: undefined,
supervisor: undefined,
};
const publish = (type, data) => {
const event = {
version: "v1",
seq: agent.nextSequence++,
type,
agentId: agent.id,
data,
};
agent.events.push(event);
if (agent.events.length > eventLimit) agent.events.shift();
for (const listener of agent.listeners) listener(event);
};
const launch = async () => {
const adapter = startAdapter({
cwd: canonicalPath,
sessionDir,
...(agent.sessionPath ? { sessionPath: agent.sessionPath } : {}),
onEvent: (event) => {
if (
event.type === "agent_state" &&
typeof event.data?.state === "string"
) {
agent.state = event.data.state;
if (event.data.state === "idle") agent.supervisor.markHealthy();
}
publish(event.type, event.data);
},
onError: (error) => {
agent.state = "error";
publish("agent_state", {
state: "error",
error: { code: error.code ?? "unknown", message: error.message },
});
if (
!agent.stopped &&
["child_exited", "child_error"].includes(error.code)
) {
agent.recoveryPromise = agent.supervisor
.handleUnexpectedExit()
.then((adapterAfterRecovery) => {
if (agent.stopped) return undefined;
agent.state = adapterAfterRecovery ? "idle" : "failed";
publish("agent_state", { state: agent.state });
return adapterAfterRecovery;
});
}
},
});
agent.adapter = adapter;
const state = await adapter.send({ type: "get_state" });
agent.state = state?.data?.isStreaming ? "streaming" : "idle";
const sessionPath = state?.data?.sessionFile;
if (typeof sessionPath === "string" && path.isAbsolute(sessionPath)) {
agent.sessionPath = sessionPath;
await persistSessionReference(sessionDir, sessionPath, canonicalPath);
}
return adapter;
};
agent.supervisor = createRecoverySupervisor({
start: launch,
...(maxRecoveryAttempts ? { maxAttempts: maxRecoveryAttempts } : {}),
...(recoveryDelayForAttempt
? { delayForAttempt: recoveryDelayForAttempt }
: {}),
...(sleep ? { sleep } : {}),
onState: (recovery) => publish("recovery", recovery),
});
await launch();
agentsByPath.set(canonicalPath, agent);
agentsById.set(agent.id, agent);
return publicAgent(agent);
})();
inFlight.set(canonicalPath, creation);
try {
return await creation;
} finally {
inFlight.delete(canonicalPath);
}
}
function getAgent(agentId) {
const agent = agentsById.get(agentId);
if (!agent) throw new UnknownAgentError(agentId);
return agent;
}
async function refreshAgentSessionReference(agent) {
const state = await agent.adapter.send({ type: "get_state" });
const sessionPath = state?.data?.sessionFile;
if (typeof sessionPath === "string" && path.isAbsolute(sessionPath)) {
agent.sessionPath = sessionPath;
await persistSessionReference(
agent.sessionDir,
sessionPath,
agent.worktreePath,
);
}
}
async function ownedSessionPath(agent, sessionPath) {
const [resolvedSessionDir, resolvedSessionPath] = await Promise.all([
realpath(agent.sessionDir),
realpath(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 switchSession(agent, sessionPath) {
if (agent.state === "streaming")
throw new Error("cannot switch sessions while Pi is working");
const ownedPath = await ownedSessionPath(agent, sessionPath);
const response = await agent.adapter.send({
type: "switch_session",
sessionPath: ownedPath,
});
if (!response?.data?.cancelled) await refreshAgentSessionReference(agent);
return response;
}
async function newSession(agent) {
if (agent.state === "streaming")
throw new Error("cannot start a session while Pi is working");
const response = await agent.adapter.send({ type: "new_session" });
if (!response?.data?.cancelled) await refreshAgentSessionReference(agent);
return response;
}
return {
async start() {
return ensureAgent(homeWorktree);
},
async selectWorktree(worktreePath) {
return ensureAgent(worktreePath);
},
listAgents() {
return [...agentsById.values()]
.map(publicAgent)
.sort((left, right) =>
left.worktreePath.localeCompare(right.worktreePath),
);
},
async listDirectories() {
return listDirectoryCatalog(sessionRoot, agentsByPath);
},
async listSessions(agentId) {
const agent = getAgent(agentId);
return listSessionSummaries(agent.sessionDir, agent.sessionPath);
},
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 agent = getAgent(agentId);
const events = this.eventsAfter(agentId, cursor);
agent.listeners.add(listener);
return {
events,
unsubscribe: () => agent.listeners.delete(listener),
};
},
async waitForRecovery(agentId) {
return getAgent(agentId).recoveryPromise;
},
async retry(agentId) {
const agent = getAgent(agentId);
agent.supervisor.markHealthy();
agent.recoveryPromise = agent.supervisor.handleUnexpectedExit();
await agent.recoveryPromise;
return publicAgent(agent);
},
async restart(agentId) {
const agent = getAgent(agentId);
await agent.adapter.stop();
return this.retry(agentId);
},
async route(agentId, operation, payload) {
const agent = getAgent(agentId);
if (operation === "switch_session")
return switchSession(agent, payload.sessionPath);
if (operation === "new_session") return newSession(agent);
return routeCommand(agent.adapter, operation, payload, agent.state);
},
async stop() {
await Promise.all(
[...agentsById.values()].map(async (agent) => {
agent.stopped = true;
agent.supervisor.stop();
await agent.adapter.stop();
agent.state = "stopped";
}),
);
},
};
}
export const sessionDirectoryMode = SESSION_DIRECTORY_MODE;