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)
This commit is contained in:
roboomp
2026-07-29 06:29:25 +00:00
committed by can1357
parent fb4393ae16
commit dd4985f31e
4 changed files with 50 additions and 4 deletions
+1 -1
View File
@@ -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 `<parent-artifacts>/local` instead of the clone-nested `<parent-artifacts>/Tan-<id>/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 `<parent-artifacts>/local` instead of the clone-nested `<parent-artifacts>/Tan-<id>/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
@@ -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;
+1 -1
View File
@@ -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;
@@ -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);