Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/api/sessions/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,12 @@ export async function DELETE(
for (const deletedId of [...deletedSessionIds].reverse()) {
if (deletedId === id) continue;
try { await abortSubagent(deletedId); } catch { /* idle or completed */ }
await getRpcSession(deletedId)?.shutdown();
const child = getRpcSession(deletedId);
if (!child?.isRunning()) await child?.shutdown();
}
try { await abortSubagent(id); } catch { /* ordinary session */ }
await getRpcSession(id)?.shutdown();
const target = getRpcSession(id);
if (!target?.isRunning()) await target?.shutdown();
for (const [deletedId, deletedPath] of deletedPaths) {
try {
unlinkSync(deletedPath);
Expand Down
3 changes: 2 additions & 1 deletion lib/rpc-manager.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ test("normal session teardown paths use graceful extension shutdown", async () =
assert.match(replacementShutdownSource, /await this\.shutdown\(\)/);
assert.match(forkSource, /shutdownAfterSessionReplacement\("fork"\)/);
assert.match(cloneSource, /shutdownAfterSessionReplacement\("clone"\)/);
assert.match(deleteRouteSource, /await getRpcSession\(id\)\?\.shutdown\(\)/);
assert.match(deleteRouteSource, /const target = getRpcSession\(id\);\s*if \(!target\?\.isRunning\(\)\) await target\?\.shutdown\(\)/);
assert.match(deleteRouteSource, /const child = getRpcSession\(deletedId\);\s*if \(!child\?\.isRunning\(\)\) await child\?\.shutdown\(\)/);
assert.match(trustRouteSource, /await destroyRpcSessionsForCwd\(result\.cwd\)/);
});

Expand Down
8 changes: 7 additions & 1 deletion lib/rpc-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,9 @@ export class AgentSessionWrapper {
}

case "reload": {
if (this.isSessionRunningForReplacement()) {
throw new Error("Cannot reload while the session is running");
}
if (this.extensionUiAbortController.signal.aborted) {
this.extensionUiAbortController = new AbortController();
}
Expand Down Expand Up @@ -1959,6 +1962,9 @@ export class AgentSessionWrapper {
this.navigateTreeKeepingToolSelection(targetId, { summarize: options?.summarize }),
switchSession: async () => ({ cancelled: true }),
reload: async () => {
if (this.isSessionRunningForReplacement()) {
throw new Error("Cannot reload (extension ctx) while the session is running");
}
this.extensionStatuses.clear();
this.resetExtensionWidgetsForReload();
this.syncProjectTrust();
Expand Down Expand Up @@ -2295,7 +2301,7 @@ export function hasBusyRpcSessionForCwd(cwd: string): boolean {
export async function destroyRpcSessionsForCwd(cwd: string): Promise<number> {
const targetCwd = normalizeRpcCwd(cwd);
const sessions = Array.from(getRegistry().values()).filter(
(session) => normalizeRpcCwd(session.cwd) === targetCwd,
(session) => normalizeRpcCwd(session.cwd) === targetCwd && !session.isRunning(),
);
await Promise.all(sessions.map((session) => session.shutdown()));
return sessions.length;
Expand Down
19 changes: 17 additions & 2 deletions lib/subagent-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { registerSessionLivenessProvider } from "./session-liveness";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import {
createAgentSessionFromServices,
Expand Down Expand Up @@ -540,9 +541,17 @@ export function createSubagentController(
finishQueuedAbort,
);
stored.cancelQueued = queued.cancel;
// A background subagent outlives its parent's turn; keep the parent session from
// being automatically idle-evicted while the run is still active. Registered here,
// after the last call that can throw, so the release below cannot be skipped.
const releaseParentLiveness = registerSessionLivenessProvider({
name: "pi-web-subagent-run",
sessionId: parentSessionId,
isActive: () => getSubagentRuns().get(initialRun.sessionId) === stored,
});
void queued.promise.then(resolveCompletion, (error) => {
resolveCompletion({ ...initialRun, status: "failed", completedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) });
});
}).finally(releaseParentLiveness);

return { run: stored.run, completion: stored.completion };
} catch (error) {
Expand Down Expand Up @@ -663,7 +672,13 @@ export function createSubagentController(
dependencies.invalidateSessionList();
}, finishQueuedAbort);
stored.cancelQueued = queued.cancel;
void queued.promise.then(resolveCompletion, (error) => resolveCompletion({ ...initialRun, status: "failed", completedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) }));
// Keep the resumed run's parent alive past its own turn (see start()).
const releaseParentLiveness = registerSessionLivenessProvider({
name: "pi-web-subagent-run",
sessionId: parentSessionId,
isActive: () => getSubagentRuns().get(request.sessionId) === stored,
});
void queued.promise.then(resolveCompletion, (error) => resolveCompletion({ ...initialRun, status: "failed", completedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) })).finally(releaseParentLiveness);
return { run: stored.run, completion };
}

Expand Down