fix: enforce one Pi runtime per user

This commit is contained in:
2026-07-21 16:13:17 +02:00
parent a040c083cc
commit 00761ae2ca
4 changed files with 125 additions and 19 deletions
+17 -1
View File
@@ -12,6 +12,7 @@ export interface SessionRuntime {
rpc: RpcClient;
bridge: SessionBridge;
child: ChildProcessWithoutNullStreams;
ownerKey?: string;
}
export interface RuntimeOptions {
@@ -102,6 +103,15 @@ export class PiProcessManager {
get(id: string): SessionRuntime | undefined { return this.runtimes.get(id); }
teardownForPrincipal(principal: PrincipalContext): string[] {
const ownerKey = `${principal.issuer}\0${principal.subject}`;
const stopped: string[] = [];
for (const [id, runtime] of [...this.runtimes.entries()]) {
if (runtime.ownerKey === ownerKey && this.teardownIfCurrent(id, runtime)) stopped.push(id);
}
return stopped;
}
/** Spawn and register a runtime synchronously, without starting a model turn. */
createFor(sessionId: string, o: RuntimeOptions = {}): SessionRuntime {
// A duplicate start must never tear down a live session: that used to send
@@ -110,6 +120,7 @@ export class PiProcessManager {
if (existing) {
throw new Error(`session runtime already active: ${sessionId}`);
}
if (o.principal) this.teardownForPrincipal(o.principal);
if (this.runtimes.size >= this.cfg.maxPiProcesses) {
throw new Error("max Pi processes reached");
}
@@ -120,7 +131,12 @@ export class PiProcessManager {
try {
const rpc = new RpcClient(child);
const bridge = new SessionBridge(rpc);
const runtime: SessionRuntime = { rpc, bridge, child };
const runtime: SessionRuntime = {
rpc,
bridge,
child,
ownerKey: o.principal ? `${o.principal.issuer}\0${o.principal.subject}` : undefined,
};
rt = runtime;
bridge.beginTurn();
this.runtimes.set(sessionId, runtime);
+8 -9
View File
@@ -97,14 +97,6 @@ export function sessionRoutes(
return true;
};
const releaseFinalizedRuntimes = async (): Promise<void> => {
await Promise.all([...boundRuntimes.entries()].map(([id, rt]) =>
withSessionLifecycle(id, () => releaseIfFinalized(id, rt)).catch((error: unknown) => {
console.error(`[session:${id}] stale runtime cleanup failed:`, error);
}),
));
};
const bindRuntime = (
id: string, rt: ReturnType<PiProcessManager["createFor"]>, runner: any, workspace?: string,
) => {
@@ -208,7 +200,10 @@ export function sessionRoutes(
let s: Settings;
try { s = await d.getSettings(principal); } catch { return storageFailure(reply); }
const runner = runnerFor(principal);
await releaseFinalizedRuntimes();
// A persisted session is resumable without keeping Pi alive. New work replaces every
// runtime owned by this principal, while runtimes belonging to other users remain intact.
// Optional chaining preserves the deliberately narrow manager stubs used by route tests.
for (const id of d.mgr.teardownForPrincipal?.(principal) ?? []) boundRuntimes.delete(id);
const ensure = await d.readiness.ensure(s.workspace ?? "", principal);
if (!ensure.ok) return reply.code(503).send({ error: READINESS_FAILURE_MESSAGE });
// Local-only: verify the DWH is reachable BEFORE creating the session, so a dropped
@@ -388,6 +383,10 @@ export function sessionRoutes(
}
}
for (const stoppedId of d.mgr.teardownForPrincipal?.(principal) ?? []) {
boundRuntimes.delete(stoppedId);
}
let rt: ReturnType<PiProcessManager["createFor"]> | undefined;
try {
if (current) {