fix: return the era-parsed tool, and hide aggregate reports in paginated mode (#1909)

Copilot round 8.

The era contract validated with the wire schema but returned the raw input,
so the wire schema's normalization was discarded — both era schemas are
z.object, which strips unknown top-level fields as the strict codec does, so
salvaged tools could carry fields the strict path drops. The parsed output is
now what gets normalized and returned. This also simplifies the bridge: issues
come straight from the wire parse with their paths intact, so the custom-issue
forwarding (and its early describeIssue application) is gone.

The malformed-entry report is written by the aggregate walk's salvage, but in
paginated mode the tools/prompts/resources panels render the paged stores,
which neither write nor clear it — leaving a stale warning above a freshly
fetched page it does not describe, with indices into an aggregate the user is
no longer looking at. Those reports are now suppressed in paginated mode;
resource templates keep theirs, having no paged store. Suppressing rather than
adding per-page salvage: pointing at the wrong entry costs more than staying
quiet, and per-page salvage is a feature, not a display fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXCSrnSBV7vZfxycG5Z6E
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-15 20:08:03 -04:00
commit 7db807b0de

View file

@ -197,6 +197,29 @@ describe("salvage era item contract (#1909)", () => {
);
});
it("strips unknown fields the way the strict codec does", async () => {
// The contract validates through a wire schema; returning the ORIGINAL
// value rather than the parsed one would leave salvaged tools carrying
// fields the strict path drops, so the two paths would hand the UI
// differently-shaped tools for the same server.
const strayTool = {
name: "stray",
inputSchema: { type: "object" },
notAToolField: "should not survive",
};
const parsed = toolItemSchemaForEra(false).safeParse(strayTool);
expect(parsed.success).toBe(true);
expect(parsed.success && parsed.data).not.toHaveProperty("notAToolField");
// And the strict path agrees — this is the shape being matched, not an
// invented rule.
const server = await startToolsServer([strayTool], false);
stop = server.stop;
const connected = await connect(server.url, "legacy");
const { tools } = await connected.listAllTools();
expect(tools[0]).not.toHaveProperty("notAToolField");
});
it("the era contract matches the strict path on both divergences", () => {
// Legacy: rejected, exactly as the strict parse above rejected it.
expect(