fix(task): initialize extension runtime on subagent revival

Both subagent revivers rebuilt the session but never wired the extension
runtime, leaving it pre-init where every action method throws
ExtensionRuntimeNotInitializedError. An extension with a tool_call handler
touching a runtime action then tripped the fail-closed gate in emitToolCall
and blocked every tool, including the hidden yield, so the revived agent
could neither finish nor exit and looped until killed.

Both the warm lifecycle reviver (executor.ts) and the cold persisted
reviver (persisted-revive.ts) now call the shared initializeExtensions
helper on the rebuilt session, restoring runtime actions, onError, and the
session_start event.

Fixes #8824
This commit is contained in:
roboomp
2026-08-19 08:44:44 +00:00
parent d94bdfa1bb
commit 3acc57de8c
4 changed files with 49 additions and 1 deletions
@@ -46,7 +46,7 @@ interface RevivedSessionHandle {
observer: () => IrcWakeObserver | undefined;
}
function createRevivedSession(activeToolNames: string[][]): RevivedSessionHandle {
function createRevivedSession(activeToolNames: string[][], extensionRunner?: unknown): RevivedSessionHandle {
let observer: IrcWakeObserver | undefined;
const session = {
getMountedXdevToolNames: () => [],
@@ -59,6 +59,7 @@ function createRevivedSession(activeToolNames: string[][]): RevivedSessionHandle
},
subscribeRunState: () => () => {},
getLastAssistantMessage: () => undefined,
extensionRunner,
} as unknown as AgentSession;
return { session, observer: () => observer };
}
@@ -129,6 +130,28 @@ afterEach(async () => {
});
describe("persisted subagent revival", () => {
it("initializes the extension runtime on cold revival so tool_call handlers are not fail-closed blocked", async () => {
const cwd = makeTempDir("@pi-revive-ext-init-");
const sessionFile = await createPersistedSession(cwd);
MCPManager.setInstance({ getTools: () => [] } as unknown as MCPManager);
const initialize = vi.fn();
const onError = vi.fn();
const emit = vi.fn(async () => undefined);
const extensionRunner = { initialize, onError, emit };
vi.spyOn(sdkModule, "createAgentSession").mockImplementation(
async () => ({ session: createRevivedSession([], extensionRunner).session }) as CreateAgentSessionResult,
);
const ref = createRef(sessionFile);
const reviver = await createFactory(cwd)(ref);
if (!reviver) throw new Error("Expected a persisted reviver");
await reviver(ref);
expect(initialize).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith({ type: "session_start" });
});
it("cold-revives a restricted contract without loading hostile same-name capabilities", async () => {
const cwd = makeTempDir("@pi-restricted-revive-");
const sessionFile = await createPersistedSession(cwd, true);