From d97d48827fba06f2bb2da9e695dc888920bb7755 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 28 Jun 2026 16:39:12 +0000 Subject: [PATCH] fix(coding-agent): ACP factory re-resolves TITLE_SYSTEM.md per session cwd createAcpSessionFactory spreads baseOptions into every per-session createAgentSession call. baseOptions.titleSystemPrompt was resolved once at launch from main.ts, so opening session/new for a client- supplied workspace different from the launch cwd would carry the launch repo's TITLE_SYSTEM.md into the new session and bias replan title refreshes against the wrong policy. The factory now calls discoverTitleSystemPromptFile(cwd) + resolvePromptInput per session and passes the result after the spread, overriding the launch-time value. When the target workspace has no override, undefined wins and the bundled prompt is used as expected. Regression test asserts that a per-session cwd with a project-level TITLE_SYSTEM.md beats the launch-cwd prompt threaded through baseOptions. --- packages/coding-agent/CHANGELOG.md | 2 +- packages/coding-agent/src/main.ts | 8 +++ .../test/acp-mcp-isolation.test.ts | 62 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a8672a1eb..b602821fa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed replan-driven session title refresh (`title.refreshOnReplan`, triggered after `todo init`) ignoring the user's `TITLE_SYSTEM.md` override and silently falling back to the bundled prompt — most visible in Plan Mode, where early todo init replans would overwrite the auto title against the configured policy. `AgentSession` now owns the resolved title prompt (threaded through `CreateAgentSessionOptions.titleSystemPrompt`) and both first-input titling and replan refresh read from the same source ([#3734](https://github.com/can1357/oh-my-pi/issues/3734)). +- Fixed replan-driven session title refresh (`title.refreshOnReplan`, triggered after `todo init`) ignoring the user's `TITLE_SYSTEM.md` override and silently falling back to the bundled prompt — most visible in Plan Mode, where early todo init replans would overwrite the auto title against the configured policy. `AgentSession` now owns the resolved title prompt (threaded through `CreateAgentSessionOptions.titleSystemPrompt`), the ACP per-`session/new` factory re-resolves `TITLE_SYSTEM.md` against each client-supplied workspace cwd, and both first-input titling and replan refresh read from the same source ([#3734](https://github.com/can1357/oh-my-pi/issues/3734)). ## [16.2.3] - 2026-06-28 diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 77f1abe58..b4ab33f8c 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -361,6 +361,13 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess const nextSettings = await args.settings.cloneForCwd(cwd); const nextSessionManager = SessionManager.create(cwd, args.sessionDir); const agentId = `acp:${nextSessionManager.getSessionId()}`; + // `baseOptions.titleSystemPrompt` is resolved from the launch cwd; an ACP + // host can open `session/new` for any client-supplied workspace, so + // re-discover `TITLE_SYSTEM.md` against THIS session's `cwd` to keep the + // replan-driven title refresh consistent with the target project's + // policy (PR #3736 follow-up). + const titleSystemPromptSource = discoverTitleSystemPromptFile(cwd); + const titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt"); const { session: nextSession } = await args.createSession({ ...args.baseOptions, cwd, @@ -371,6 +378,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess agentId, hasUI: false, enableMCP: false, + titleSystemPrompt, }); if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) { args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey); diff --git a/packages/coding-agent/test/acp-mcp-isolation.test.ts b/packages/coding-agent/test/acp-mcp-isolation.test.ts index 1b1d68b20..e99c32149 100644 --- a/packages/coding-agent/test/acp-mcp-isolation.test.ts +++ b/packages/coding-agent/test/acp-mcp-isolation.test.ts @@ -75,3 +75,65 @@ describe("createAcpSessionFactory MCP isolation (issue #1234)", () => { } }); }); + +describe("createAcpSessionFactory TITLE_SYSTEM.md per-cwd resolution (PR #3736)", () => { + it("re-resolves the title prompt for the per-session cwd instead of inheriting the launch cwd's override", async () => { + const tempDir = TempDir.createSync("@pi-acp-title-prompt-"); + let authStorage: AuthStorage | undefined; + try { + authStorage = await AuthStorage.create(tempDir.join("auth.db")); + const modelRegistry = new ModelRegistry(authStorage); + const settings = Settings.isolated({}); + + const projectDir = tempDir.join("project"); + await Bun.write(`${projectDir}/.omp/TITLE_SYSTEM.md`, "Project-specific title policy."); + + const fakeSession = {} as AgentSession; + const captured: CreateAgentSessionOptions[] = []; + const createSession = async (options: CreateAgentSessionOptions): Promise => { + captured.push(options); + return { + session: fakeSession, + extensionsResult: { + extensions: [], + errors: [], + runner: undefined, + } as unknown as CreateAgentSessionResult["extensionsResult"], + setToolUIContext: () => {}, + eventBus: { + emit: () => {}, + on: () => () => {}, + off: () => {}, + } as unknown as CreateAgentSessionResult["eventBus"], + }; + }; + + // baseOptions carries the LAUNCH cwd's prompt; the factory must + // override it with the per-session cwd's `TITLE_SYSTEM.md`. + const factory = createAcpSessionFactory({ + baseOptions: { + titleSystemPrompt: "Launch-cwd policy that must not leak.", + } as CreateAgentSessionOptions, + settings, + sessionDir: tempDir.join("sessions"), + authStorage, + modelRegistry, + parsedArgs: {}, + rawArgs: [], + createSession, + }); + + await factory(projectDir); + + expect(captured).toHaveLength(1); + expect(captured[0].titleSystemPrompt).toBe("Project-specific title policy."); + } finally { + try { + authStorage?.close(); + } finally { + await Bun.sleep(0); + await tempDir.remove(); + } + } + }); +});