From dd4985f31e424a3bf12e6d19368fcbaeeea7c28e Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 29 Jul 2026 06:29:25 +0000 Subject: [PATCH] fix(coding-agent): scoped subagent local root overrides Keep subagent localProtocolOptions on their ToolSession instead of installing them as the process-global LocalProtocolHandler override. No-context URL consumers therefore retain the active top-level session's mapping while tan and task subagents continue to resolve through their caller context. Add SDK regression coverage proving subagent creation preserves an existing global mapping. Fixes #6971 (cherry picked from commit a02eef174b03036dc960c842a0901d22333ad9cd) --- packages/coding-agent/CHANGELOG.md | 2 +- .../src/internal-urls/local-protocol.ts | 5 ++- packages/coding-agent/src/sdk.ts | 2 +- .../test/sdk-session-isolation.test.ts | 45 +++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0d5a81dfb..e267b6085 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed `/tan` agents being unable to read parent-session `local://` attachments (pasted files, generated references). `TanCommandController` now threads the parent session's `localProtocolOptions` into the tan clone, so `local://` resolves against `/local` instead of the clone-nested `/Tan-/local` root — matching the task-subagent path ([#6971](https://github.com/can1357/oh-my-pi/issues/6971)). +- Fixed `/tan` agents being unable to read parent-session `local://` attachments (pasted files, generated references). `TanCommandController` now threads the parent session's `localProtocolOptions` into the tan clone, so `local://` resolves against `/local` instead of the clone-nested `/Tan-/local` root. Subagent local mappings now remain session-bound rather than replacing the process-global override used by active-session suggestions and links ([#6971](https://github.com/can1357/oh-my-pi/issues/6971)). ## [17.1.8] - 2026-07-28 diff --git a/packages/coding-agent/src/internal-urls/local-protocol.ts b/packages/coding-agent/src/internal-urls/local-protocol.ts index 27a1bd752..a20724390 100644 --- a/packages/coding-agent/src/internal-urls/local-protocol.ts +++ b/packages/coding-agent/src/internal-urls/local-protocol.ts @@ -390,8 +390,9 @@ export class LocalProtocolHandler implements ProtocolHandler { /** * Install a process-global override that wins over the AgentRegistry-based - * derivation. Used by SDK consumers that wire `localProtocolOptions` on - * `createAgentSession` and by subagents that share their parent's root. + * derivation. Used by top-level SDK consumers that wire + * `localProtocolOptions` on `createAgentSession`; subagents keep their + * inherited mapping session-bound. */ static setOverride(value: LocalProtocolOptions | undefined): void { LocalProtocolHandler.#override = value; diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index afe318051..efcd398b8 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1772,7 +1772,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} getArtifactsDir, getSessionId: () => sessionManager.getSessionId?.() ?? null, }; - if (options.localProtocolOptions) { + if (options.localProtocolOptions && !options.parentTaskPrefix) { LocalProtocolHandler.setOverride(options.localProtocolOptions); } toolSession.getArtifactsDir = getArtifactsDir; diff --git a/packages/coding-agent/test/sdk-session-isolation.test.ts b/packages/coding-agent/test/sdk-session-isolation.test.ts index 832dfff5a..2850af256 100644 --- a/packages/coding-agent/test/sdk-session-isolation.test.ts +++ b/packages/coding-agent/test/sdk-session-isolation.test.ts @@ -7,6 +7,7 @@ import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; import type { Rule } from "@oh-my-pi/pi-coding-agent/capability/rule"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; +import { LocalProtocolHandler } from "@oh-my-pi/pi-coding-agent/internal-urls/local-protocol"; import { AgentLifecycleManager } from "@oh-my-pi/pi-coding-agent/registry/agent-lifecycle"; import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry"; import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk"; @@ -109,6 +110,7 @@ describe("createAgentSession session storage isolation", () => { afterEach(async () => { vi.restoreAllMocks(); + LocalProtocolHandler.resetOverrideForTests(); for (const tempDir of tempDirs.splice(0)) { removeSyncWithRetries(tempDir); } @@ -147,6 +149,49 @@ describe("createAgentSession session storage isolation", () => { await session.dispose(); } }); + it("keeps subagent local:// mappings from replacing the process-global override", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `pi-sdk-local-override-${Snowflake.next()}-`)); + tempDirs.push(tempDir); + const cwd = path.join(tempDir, "project"); + fs.mkdirSync(cwd, { recursive: true }); + const globalOptions = { + getArtifactsDir: () => path.join(tempDir, "active-artifacts"), + getSessionId: () => "active-session", + }; + const subagentOptions = { + getArtifactsDir: () => path.join(tempDir, "parent-artifacts"), + getSessionId: () => "parent-session", + }; + LocalProtocolHandler.setOverride(globalOptions); + + const { session } = await createAgentSession({ + cwd, + agentDir: path.join(tempDir, "agent"), + modelRegistry: sharedModelRegistry, + settings: Settings.isolated(), + disableExtensionDiscovery: true, + skills: [], + contextFiles: [], + promptTemplates: [], + slashCommands: [], + toolNames: [], + enableMCP: false, + enableLsp: false, + agentRegistry: new AgentRegistry(), + agentId: "Tan-local-override-test", + agentDisplayName: "tan", + parentTaskPrefix: "Tan-local-override-test", + parentAgentId: "Main", + localProtocolOptions: subagentOptions, + }); + + try { + expect(LocalProtocolHandler.resolveOptions()).toBe(globalOptions); + } finally { + await session.dispose(); + } + }); + it("does not replace a newer registry generation when creation expected the id to be absent", async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `pi-sdk-generation-cas-${Snowflake.next()}-`)); tempDirs.push(tempDir);