fix: release finalized Pi runtimes

This commit is contained in:
2026-07-21 15:39:34 +02:00
parent 4c9424f1e3
commit 019f6b282e
2 changed files with 56 additions and 7 deletions
+15 -7
View File
@@ -111,13 +111,21 @@ export function sessionRoutes(
}
}
d.hub.publish(id, e.type, e);
if (
e.type === "system_event"
&& e.event === "agent_end"
&& d.mgr.get(id) !== rt
&& boundRuntimes.get(id) === rt
) {
boundRuntimes.delete(id);
if (e.type === "system_event" && e.event === "agent_end") {
if (d.mgr.get(id) !== rt && boundRuntimes.get(id) === rt) {
boundRuntimes.delete(id);
} else if (typeof runner.sessionShow === "function") {
void withSessionLifecycle(id, async () => {
if (d.mgr.get(id) !== rt || boundRuntimes.get(id) !== rt) return;
const manifest = await runner.sessionShow(id, workspace);
if (manifest?.status !== "finalized") return;
if (d.mgr.teardownIfCurrent(id, rt) && boundRuntimes.get(id) === rt) {
boundRuntimes.delete(id);
}
}).catch((error: unknown) => {
console.error(`[session:${id}] terminal runtime cleanup failed:`, error);
});
}
}
});
} catch (error) {
+41
View File
@@ -1304,6 +1304,47 @@ test("an unexpectedly exited runtime publishes its terminal sequence then releas
]);
});
test("agent_end releases the Pi runtime after the session was finalized", async () => {
let listener: ((event: any) => void) | undefined;
const bridge = {
onClientEvent: (next: (event: any) => void) => { listener = next; },
emitClientEvent: (event: any) => listener?.(event),
};
const runtime = { bridge };
let current: any;
let teardownCalls = 0;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
mgr: {
get: () => current,
createFor: () => { current = runtime; return runtime; },
configure: async () => {},
start: () => {},
teardownIfCurrent: (_id: string, expected: any) => {
if (current !== expected) return false;
teardownCalls += 1;
current = undefined;
return true;
},
} as any,
thtRunner: {
sessionNew: async () => ({ id: "s1" }),
sessionShow: async () => ({ status: "finalized" }),
searchPack: async () => {},
failSession: async () => {},
} as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
getSettings: () => ({ workspace: "local" }) as any,
});
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
await new Promise((resolve) => setImmediate(resolve));
bridge.emitClientEvent({ type: "system_event", event: "agent_end" });
await new Promise((resolve) => setImmediate(resolve));
expect(teardownCalls).toBe(1);
expect(current).toBeUndefined();
});
test("POST resume tears down a created runtime when bridge binding fails", async () => {
const delivered: string[] = [];
const actualHub = new SseHub();