perf(webui): strip agent-internal state and dedupe snapshot fetches

Thread state carried ~1.9MB of internal runtime data
(_quickjs_snapshot_payload, skills_metadata) that no WebUI code reads,
and opening a conversation fired three independent fetches of it. The
BFF now strips those keys (1.9MB -> ~15KB) and getConversation shares
one in-flight request across concurrent callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m4
2026-07-31 09:21:35 +08:00
parent bd1ef8abf5
commit a171db23f8
4 changed files with 89 additions and 6 deletions
+53
View File
@@ -21,3 +21,56 @@ describe("conversation API errors", () => {
);
});
});
describe("getConversation request dedupe", () => {
it("shares one in-flight fetch across concurrent callers", async () => {
let resolveFetch: ((response: Response) => void) | null = null;
const fetchMock = vi.fn().mockImplementation(
() =>
new Promise<Response>((resolve) => {
resolveFetch = resolve;
})
);
vi.stubGlobal("fetch", fetchMock);
const first = getConversation("thread-a");
const second = getConversation("thread-a");
expect(fetchMock).toHaveBeenCalledTimes(1);
resolveFetch!(
new Response(JSON.stringify({ thread: {}, state: {} }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
await expect(first).resolves.toEqual({ thread: {}, state: {} });
await expect(second).resolves.toEqual({ thread: {}, state: {} });
// Once settled, the next call fetches again (no stale caching).
fetchMock.mockResolvedValue(
new Response(JSON.stringify({ thread: {}, state: {} }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
await getConversation("thread-a");
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it("does not dedupe failures into a poisoned entry", async () => {
const fetchMock = vi
.fn()
.mockResolvedValue(
new Response(JSON.stringify({ error: "boom" }), { status: 500 })
);
vi.stubGlobal("fetch", fetchMock);
await expect(getConversation("thread-b")).rejects.toMatchObject({
status: 500,
});
await expect(getConversation("thread-b")).rejects.toMatchObject({
status: 500,
});
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
+18 -6
View File
@@ -72,14 +72,26 @@ export async function createConversation(): Promise<{ threadId: string }> {
return body;
}
export async function getConversation(
// Opening a conversation fires several effects that all need the same
// snapshot; share one in-flight request instead of fetching 3x.
const inflightConversations = new Map<string, Promise<ConversationSnapshot>>();
export function getConversation(
threadId: string
): Promise<ConversationSnapshot> {
return readJson<ConversationSnapshot>(
await fetch(`/api/conversations/${encodeURIComponent(threadId)}`, {
cache: "no-store",
})
);
const inflight = inflightConversations.get(threadId);
if (inflight) return inflight;
const request = fetch(`/api/conversations/${encodeURIComponent(threadId)}`, {
cache: "no-store",
}).then((response) => readJson<ConversationSnapshot>(response));
inflightConversations.set(threadId, request);
const cleanup = () => {
if (inflightConversations.get(threadId) === request) {
inflightConversations.delete(threadId);
}
};
request.then(cleanup, cleanup);
return request;
}
export async function patchConversation(
@@ -41,6 +41,20 @@ describe("conversation response isolation", () => {
).toEqual({ values: { messages: [{ content: "visible" }] } });
});
it("strips internal agent state that the browser never reads", () => {
// These keys dominate the thread-state payload (1.9MB of 1.9MB) and no
// WebUI code path references them; forwarding them is pure parse cost.
expect(
sanitizeConversationData({
values: {
_quickjs_snapshot_payload: { snapshot: "x".repeat(100) },
skills_metadata: [{ name: "skill-a" }],
messages: [{ content: "visible" }],
},
})
).toEqual({ values: { messages: [{ content: "visible" }] } });
});
it("requires all durable metadata fields to match the registry", () => {
assertThreadMatchesScope(
"thread-a",
+4
View File
@@ -8,6 +8,10 @@ const INTERNAL_STATE_KEYS = new Set([
"workspace_dir",
"workspace_root",
"workspace_backend",
// Agent-internal runtime state. Together these are ~1.9MB of a typical
// thread-state payload and no WebUI code path reads them.
"_quickjs_snapshot_payload",
"skills_metadata",
]);
function isRecord(value: unknown): value is Record<string, unknown> {