Skip to content

Commit 94e31d0

Browse files
committed
revert(pdf-server): drop viewAnnotationIds — wrong about source of truth
The viewer's annotationMap is baseline (loadBaselineAnnotations from the PDF) + diff restored from localStorage + manual edits + model adds. viewAnnotationIds only tracks the last category, so the warning fires for any update to an annotation the model didn't itself add — including the entire restored state. That's not "best-effort" miss rate, it's structurally wrong about where the data lives. Model would learn to ignore the warning. The viewer-side log.error on the silent skip (mcp-app.ts:4251) is the right layer for this.
1 parent bf17628 commit 94e31d0

2 files changed

Lines changed: 1 addition & 84 deletions

File tree

examples/pdf-server/server.test.ts

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,57 +1482,5 @@ describe("interact tool", () => {
14821482
await client.close();
14831483
await server.close();
14841484
}, 15_000);
1485-
1486-
it("update_annotations warns about ids never seen by add_annotations", async () => {
1487-
// The viewer's processCommands does `if (!existing) continue` for
1488-
// unknown ids — silent no-op. After a failed add_annotations batch the
1489-
// model would update_annotations with confidence and get "Queued" back
1490-
// while nothing rendered. Track ids server-side to surface the gap.
1491-
const { server, client } = await connect();
1492-
const uuid = "update-unknown-id";
1493-
1494-
// add_annotations / update_annotations don't gate on viewsPolled
1495-
// (only get_screenshot/get_text do) — they just enqueue and return.
1496-
const add = await client.callTool({
1497-
name: "interact",
1498-
arguments: {
1499-
viewUUID: uuid,
1500-
action: "add_annotations",
1501-
annotations: [
1502-
{
1503-
id: "stamp-a",
1504-
type: "stamp",
1505-
page: 1,
1506-
x: 100,
1507-
y: 100,
1508-
label: "A",
1509-
},
1510-
],
1511-
},
1512-
});
1513-
expect(add.isError).toBeFalsy();
1514-
1515-
const upd = await client.callTool({
1516-
name: "interact",
1517-
arguments: {
1518-
viewUUID: uuid,
1519-
action: "update_annotations",
1520-
annotations: [
1521-
{ id: "stamp-a", x: 200 },
1522-
{ id: "stamp-b", x: 200 },
1523-
],
1524-
},
1525-
});
1526-
// Not an error — user might have drawn stamp-b manually in the iframe.
1527-
// But the model needs to know it might be shouting into the void.
1528-
expect(upd.isError).toBeFalsy();
1529-
const updText = (upd.content as Array<{ text: string }>)[0].text;
1530-
expect(updText).toContain("WARNING");
1531-
expect(updText).toContain("stamp-b");
1532-
expect(updText).not.toContain("stamp-a");
1533-
1534-
await client.close();
1535-
await server.close();
1536-
}, 15_000);
15371485
});
15381486
});

examples/pdf-server/server.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,6 @@ export const viewSourcePaths = new Map<string, string>();
334334
/** Valid form field names per viewer UUID (populated during display_pdf) */
335335
const viewFieldNames = new Map<string, Set<string>>();
336336

337-
/**
338-
* Annotation ids the model has added per view, used to warn when
339-
* update_annotations targets an id we never saw add_annotations for.
340-
* Best-effort: doesn't see manual edits in the iframe, so the warning
341-
* is "did you mean…" not a hard error.
342-
*/
343-
const viewAnnotationIds = new Map<string, Set<string>>();
344-
345337
/** Detailed form field info per viewer UUID (populated during display_pdf) */
346338
const viewFieldInfo = new Map<string, FormFieldInfo[]>();
347339

@@ -384,7 +376,6 @@ function pruneStaleQueues(): void {
384376
commandQueues.delete(uuid);
385377
viewFieldNames.delete(uuid);
386378
viewFieldInfo.delete(uuid);
387-
viewAnnotationIds.delete(uuid);
388379
viewsPolled.delete(uuid);
389380
viewSourcePaths.delete(uuid);
390381
stopFileWatch(uuid);
@@ -1957,17 +1948,9 @@ URL: ${normalized}`,
19571948
{ type: "add_annotations" }
19581949
>["annotations"],
19591950
});
1960-
if (!viewAnnotationIds.has(uuid)) {
1961-
viewAnnotationIds.set(uuid, new Set());
1962-
}
1963-
for (const a of annotations) {
1964-
// Schema is Record<string, any> so id is `any` — coerce, but skip
1965-
// nullish so we don't store "undefined"/"null" as known ids.
1966-
if (a.id != null) viewAnnotationIds.get(uuid)!.add(String(a.id));
1967-
}
19681951
description = `add ${annotations.length} annotation(s)`;
19691952
break;
1970-
case "update_annotations": {
1953+
case "update_annotations":
19711954
if (!annotations || annotations.length === 0)
19721955
return {
19731956
content: [
@@ -1978,16 +1961,6 @@ URL: ${normalized}`,
19781961
],
19791962
isError: true,
19801963
};
1981-
// The viewer silently skips updates for unknown ids (mcp-app.ts:
1982-
// `if (!existing) continue`). If a prior add_annotations batch
1983-
// failed mid-way, the model gets a happy "Queued" here while
1984-
// nothing renders. Surface what we can — but only as a warning,
1985-
// since the user may have drawn the annotation manually.
1986-
const known = viewAnnotationIds.get(uuid);
1987-
const unknown = annotations
1988-
.filter((a) => a.id != null)
1989-
.map((a) => String(a.id))
1990-
.filter((id) => !known?.has(id));
19911964
enqueueCommand(uuid, {
19921965
type: "update_annotations",
19931966
annotations: annotations as Extract<
@@ -1996,11 +1969,7 @@ URL: ${normalized}`,
19961969
>["annotations"],
19971970
});
19981971
description = `update ${annotations.length} annotation(s)`;
1999-
if (unknown.length > 0) {
2000-
description += ` — WARNING: id(s) [${unknown.join(", ")}] not seen in any add_annotations call for this view; viewer may silently no-op`;
2001-
}
20021972
break;
2003-
}
20041973
case "remove_annotations":
20051974
if (!ids || ids.length === 0)
20061975
return {

0 commit comments

Comments
 (0)