From 13b1077d3e4462b52aa7cf25a9d9b7069f633b2b Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 28 Jul 2026 17:03:01 +0000 Subject: [PATCH] fix(session): reset announced xd mounts on transcript replace The announced-mount baseline persisted across /new, switchSession, and branch, which replace agent.state.messages but only clear session-scoped tool state. A device announced in the old transcript stayed in the cache, so reconnecting it into the fresh history was filtered as already known and never announced, leaving the new conversation unaware of the device. Reset the announced baseline (and any undelivered pending delta) from #clearSessionScopedToolState, so the next notice re-seeds from the new transcript and a reconnecting device announces again. Fixes #6921 (cherry picked from commit d06dde02b9de4aacacd7aea5ee51edc7e524e3fb) --- packages/coding-agent/CHANGELOG.md | 2 +- .../coding-agent/src/session/agent-session.ts | 1 + .../coding-agent/src/session/session-tools.ts | 14 ++++++++ .../agent-session-tool-rebuild-skip.test.ts | 36 +++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 525dbdeb5..b3a219abc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed `xd://` mount notices re-announcing already-known devices on session resume / host reconnect: the notice was diff-gated only against the in-memory mount set, which reset each resume, so reconnecting MCP/RPC-host devices re-spliced a redundant developer message into history and busted the provider prompt-cache prefix (re-billing the whole suffix at full price on metered providers). Notices now carry a structured `{ added, removed }` payload and are gated against the devices persisted history already announced—including legacy rendered notices from before the structured payload—so a resume that re-establishes the same inventory emits nothing ([#6921](https://github.com/can1357/oh-my-pi/issues/6921)). +- Fixed `xd://` mount notices re-announcing already-known devices on session resume / host reconnect: the notice was diff-gated only against the in-memory mount set, which reset each resume, so reconnecting MCP/RPC-host devices re-spliced a redundant developer message into history and busted the provider prompt-cache prefix (re-billing the whole suffix at full price on metered providers). Notices now carry a structured `{ added, removed }` payload and are gated against the devices persisted history already announced—including legacy rendered notices from before the structured payload—so a resume that re-establishes the same inventory emits nothing. The announced baseline is reset when the transcript is replaced (`/new`, `switchSession`, `branch`), so a device reconnecting into the fresh history announces again ([#6921](https://github.com/can1357/oh-my-pi/issues/6921)). ## [17.1.8] - 2026-07-28 diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index d2eb9e249..adf7efa2a 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -4337,6 +4337,7 @@ export class AgentSession { this.agent.clearDeferredToolDirectives(); this.#toolChoiceQueue.clear(); this.#tools.clearAcpPermissionDecisions(); + this.#tools.resetAnnouncedMounts(); } /** diff --git a/packages/coding-agent/src/session/session-tools.ts b/packages/coding-agent/src/session/session-tools.ts index 7be29d5c8..157a83e3f 100644 --- a/packages/coding-agent/src/session/session-tools.ts +++ b/packages/coding-agent/src/session/session-tools.ts @@ -694,6 +694,20 @@ export class SessionTools { this.#host.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev"); } + /** + * Forget mount-notice tracking for a replaced transcript. Called when session + * history is swapped wholesale (`/new`, `switchSession`, `branch`): the base + * system prompt is rebuilt from the current tool set, so the previous + * transcript's announced baseline and any undelivered delta no longer apply. + * The next notice re-seeds from the new transcript, and a device reconnecting + * into it announces again. + */ + resetAnnouncedMounts(): void { + this.#announcedMounts.clear(); + this.#announcedMountsSeeded = false; + this.#pendingXdevMountDelta = undefined; + } + /** * Seed {@link #announcedMounts} from persisted mount notices the first time a * notice is consumed. On resume the in-memory mount set is rebuilt from diff --git a/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts b/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts index ff8414f1a..b4b561a76 100644 --- a/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts +++ b/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts @@ -997,6 +997,42 @@ These tools became available: expect(fetchText).not.toContain("xd://mcp__nucleus_search"); }); + it("re-announces a device after the transcript is replaced by /new", async () => { + const xdev = createTestXdevState(); + const { session } = newSession(async toolNames => `tools:${toolNames.join(",")}`, { + xdev, + responses: [{ content: ["ok"] }, { content: ["ok"] }], + }); + const search = createMcpCustomTool("mcp__nucleus_search", "nucleus", "search", "Search nucleus"); + + // Announce the device in the original transcript. + await session.refreshMCPTools([search]); + await session.prompt("hello"); + expect( + session.agent.state.messages.filter( + message => message.role === "custom" && message.customType === "xdev-mount-notice", + ), + ).toHaveLength(1); + + // /new swaps in a fresh transcript that no longer carries the notice. A + // resume/reconnect rebuilds the mount set from scratch, so model the device + // dropping out across the boundary. + await session.newSession(); + xdev.mountedNames.clear(); + + // The same device reconnects into the new transcript: because the announced + // baseline was reset with the transcript, it must announce again (otherwise + // the new conversation never learns the device is available). + await session.refreshMCPTools([search]); + await session.prompt("world"); + const newTranscriptNotices = session.agent.state.messages.filter( + (message): message is CustomMessage => message.role === "custom" && message.customType === "xdev-mount-notice", + ); + expect(newTranscriptNotices).toHaveLength(1); + const text = typeof newTranscriptNotices[0].content === "string" ? newTranscriptNotices[0].content : ""; + expect(text).toContain("xd://mcp__nucleus_search"); + }); + it("keeps xd:// mount deltas model-visible without rendering them during quiet startup", async () => { const { session, contexts } = newSession(async toolNames => `tools:${toolNames.join(",")}`, { xdev: createTestXdevState(),