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:
@@ -13,6 +13,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed Claude Code marketplace plugins ignoring the `enabledPlugins` switch in `~/.claude/settings.json` and `.claude/settings(.local).json`: a plugin turned off for a project no longer loads there, and a local-scope install enabled for a project loads even when its recorded `projectPath` is a different directory
|
||||
- Fixed revived subagents (warm lifecycle reviver and cold persisted reviver) rebuilding the session without initializing the extension runtime, leaving every runtime action throwing `ExtensionRuntimeNotInitializedError`. An extension with a `tool_call` handler that touched a runtime action (e.g. `appendEntry`) 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 revivers now call the shared `initializeExtensions` helper, restoring runtime actions, `onError`, and the `session_start` event ([#8824](https://github.com/can1357/oh-my-pi/issues/8824)).
|
||||
- Fixed task and eval subagents discovering newly added agent definitions while resolving their role aliases from stale startup settings. Subagent preflight now atomically reloads persisted settings before agent discovery while preserving live runtime overrides.
|
||||
- Fixed images returned by tools mounted under `xd://` rendering only as file links instead of inline terminal graphics.
|
||||
- Resume Cursor idle-stall turns after completed MCP/todo tool results. The watchdog already closes the Connect stream, so unmarked blocks no longer need the `exec-resolved` marker to continue.
|
||||
|
||||
@@ -35,6 +35,7 @@ import type { HindsightSessionState } from "../hindsight/state";
|
||||
import type { LocalProtocolOptions } from "../internal-urls";
|
||||
import type { MCPManager } from "../mcp/manager";
|
||||
import type { MnemopiSessionState } from "../mnemopi/state";
|
||||
import { initializeExtensions } from "../modes/runtime-init";
|
||||
import subagentAsyncPendingTemplate from "../prompts/system/subagent-async-pending.md" with { type: "text" };
|
||||
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
||||
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
||||
@@ -3167,6 +3168,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
const { session: revived } = await createAgentSession(
|
||||
buildSubagentSessionOptions(reopened, expectedAgentRef),
|
||||
);
|
||||
// Re-run the executor's extension wiring on the rebuilt session.
|
||||
// Skipping it leaves the runner pre-init, so a `tool_call` handler
|
||||
// touching a runtime action trips the fail-closed gate and blocks
|
||||
// every tool (including `yield`) in the revived agent (issue #8824).
|
||||
await initializeExtensions(revived, {
|
||||
reportSendError: (action, err) =>
|
||||
logger.error("Extension send failed", { action, error: err.message }),
|
||||
reportRuntimeError: err =>
|
||||
logger.error("Extension error", { path: err.extensionPath, error: err.error }),
|
||||
});
|
||||
AgentRegistry.global().syncSessionStatus(id, revived);
|
||||
installIrcWakeTurnMonitor(revived);
|
||||
return revived;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import type { ModelRegistry } from "../config/model-registry";
|
||||
import { formatModelRoleAlias } from "../config/model-roles";
|
||||
import type { Settings } from "../config/settings";
|
||||
import { MCPManager } from "../mcp/manager";
|
||||
import { initializeExtensions } from "../modes/runtime-init";
|
||||
import type { PersistedSubagentReviverFactory } from "../registry/agent-lifecycle";
|
||||
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
||||
import { createAgentSession } from "../sdk";
|
||||
@@ -153,6 +155,17 @@ export function createPersistedSubagentReviverFactory(
|
||||
// `alwaysInclude` can re-add non-defaultInactive extension/custom tools
|
||||
// the original run didn't carry. Unknown/missing names are ignored.
|
||||
await session.setActiveToolsByName([...init.tools, ...session.getMountedXdevToolNames()]);
|
||||
// Wire the extension runtime exactly as the live executor does. Without
|
||||
// this the runner stays pre-init, every action method throws
|
||||
// `ExtensionRuntimeNotInitializedError`, and a `tool_call` handler that
|
||||
// touches a runtime action trips the fail-closed gate in `emitToolCall`,
|
||||
// blocking every tool — including the hidden `yield` — in the revived
|
||||
// agent. `session_start` also re-runs so extensions restore per-session
|
||||
// state (issue #8824).
|
||||
await initializeExtensions(session, {
|
||||
reportSendError: (action, err) => logger.error("Extension send failed", { action, error: err.message }),
|
||||
reportRuntimeError: err => logger.error("Extension error", { path: err.extensionPath, error: err.error }),
|
||||
});
|
||||
// Cold revives must drive registry status themselves — createAgentSession
|
||||
// doesn't wire this generically (the live path does it in the executor).
|
||||
// The internal run-state signal precedes deferrable public `agent_end`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user