fix(session): migrated legacy xd mount notices on resume
Replayed the stable added and removed inventory sections from legacy xdev-mount-notice content when structured details are absent. This keeps the first post-upgrade resume from re-announcing devices that persisted history already introduced. Covered both structured and legacy resume histories, including removed devices and inline docs that must not be interpreted as inventory. Fixes #6921 (cherry picked from commit 634a4c2de75f99e219f408c56bed83c04fe1290a)
This commit is contained in:
@@ -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, 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 ([#6921](https://github.com/can1357/oh-my-pi/issues/6921)).
|
||||
|
||||
## [17.1.8] - 2026-07-28
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Agent, AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
|
||||
import { isRecord, logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
|
||||
import { reset as resetCapabilities } from "../capability";
|
||||
import type { ModelRegistry } from "../config/model-registry";
|
||||
import { formatModelString } from "../config/model-resolver";
|
||||
@@ -705,10 +705,45 @@ export class SessionTools {
|
||||
this.#announcedMountsSeeded = true;
|
||||
for (const message of this.#host.agent.state.messages) {
|
||||
if (message.role !== "custom" || message.customType !== XDEV_MOUNT_NOTICE_MESSAGE_TYPE) continue;
|
||||
const details = message.details as XdevMountNoticeDetails | undefined;
|
||||
if (!details) continue;
|
||||
for (const name of details.added ?? []) this.#announcedMounts.add(name);
|
||||
for (const name of details.removed ?? []) this.#announcedMounts.delete(name);
|
||||
const details = message.details;
|
||||
if (
|
||||
isRecord(details) &&
|
||||
Array.isArray(details.added) &&
|
||||
details.added.every(name => typeof name === "string") &&
|
||||
Array.isArray(details.removed) &&
|
||||
details.removed.every(name => typeof name === "string")
|
||||
) {
|
||||
for (const name of details.added) this.#announcedMounts.add(name);
|
||||
for (const name of details.removed) this.#announcedMounts.delete(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Releases before structured notice details persisted only the rendered
|
||||
// prompt. Replay its two stable inventory sections so the first resume
|
||||
// after upgrading does not re-announce every dynamic device once.
|
||||
if (typeof message.content !== "string") continue;
|
||||
let section: "added" | "removed" | undefined;
|
||||
for (const line of message.content.split("\n")) {
|
||||
if (line === "These tools became available:") {
|
||||
section = "added";
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("No longer mounted")) {
|
||||
section = "removed";
|
||||
continue;
|
||||
}
|
||||
if (line === "Configured inline device docs:" || line === "</system-notice>") break;
|
||||
if (line.startsWith("Read `xd://<tool>`")) {
|
||||
section = undefined;
|
||||
continue;
|
||||
}
|
||||
if (!section) continue;
|
||||
const match = /^- xd:\/\/(\S+?)(?:\s+—|$)/.exec(line);
|
||||
const name = match?.[1];
|
||||
if (!name) continue;
|
||||
if (section === "added") this.#announcedMounts.add(name);
|
||||
else this.#announcedMounts.delete(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -928,21 +928,45 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => {
|
||||
expect(notices[0]).not.toContain("No longer mounted");
|
||||
});
|
||||
|
||||
it("does not re-announce devices a resumed session already announced in history", async () => {
|
||||
it.each([
|
||||
{
|
||||
priorNotice: {
|
||||
role: "custom",
|
||||
customType: "xdev-mount-notice",
|
||||
content: "The xd:// device inventory changed.\n\nxd://mcp__nucleus_search became available.",
|
||||
details: { added: ["mcp__nucleus_search"], removed: [] },
|
||||
attribution: "agent",
|
||||
display: false,
|
||||
timestamp: 1,
|
||||
} satisfies AgentMessage,
|
||||
},
|
||||
{
|
||||
priorNotice: {
|
||||
role: "custom",
|
||||
customType: "xdev-mount-notice",
|
||||
content: `<system-notice>
|
||||
The xd:// device inventory changed.
|
||||
These tools became available:
|
||||
- xd://mcp__nucleus_search — Search nucleus
|
||||
- xd://mcp__retired — Retired device
|
||||
Read \`xd://<tool>\` for docs + JSON schema before first use; write the JSON args object to \`xd://<tool>\` to execute.
|
||||
No longer mounted (writes to these devices will fail):
|
||||
- xd://mcp__retired
|
||||
Configured inline device docs:
|
||||
These tools became available:
|
||||
- xd://mcp__nucleus_fetch — This is inline documentation, not an inventory entry.
|
||||
</system-notice>`,
|
||||
attribution: "agent",
|
||||
display: false,
|
||||
timestamp: 1,
|
||||
} satisfies AgentMessage,
|
||||
},
|
||||
])("does not re-announce devices a resumed session already announced in history", async ({ priorNotice }) => {
|
||||
// Model a process resume / host reconnect: persisted history already carries
|
||||
// a mount notice for mcp__nucleus_search, but the fresh in-memory mount set
|
||||
// starts empty. When the device reconnects, the notice must NOT re-splice a
|
||||
// redundant developer message — doing so busts the provider prompt-cache
|
||||
// prefix and re-bills the whole suffix on metered providers.
|
||||
const priorNotice: AgentMessage = {
|
||||
role: "custom",
|
||||
customType: "xdev-mount-notice",
|
||||
content: "The xd:// device inventory changed.\n\nxd://mcp__nucleus_search became available.",
|
||||
details: { added: ["mcp__nucleus_search"], removed: [] },
|
||||
attribution: "agent",
|
||||
display: false,
|
||||
timestamp: Date.now() - 1000,
|
||||
};
|
||||
const { session, contexts } = newSession(async toolNames => `tools:${toolNames.join(",")}`, {
|
||||
xdev: createTestXdevState(),
|
||||
responses: [{ content: ["ok"] }, { content: ["ok"] }],
|
||||
|
||||
Reference in New Issue
Block a user