diff --git a/web/workspace/src/lib/workspace/memory/conversation-api.test.ts b/web/workspace/src/lib/workspace/memory/conversation-api.test.ts index 76400b932..1e66b60f2 100644 --- a/web/workspace/src/lib/workspace/memory/conversation-api.test.ts +++ b/web/workspace/src/lib/workspace/memory/conversation-api.test.ts @@ -141,6 +141,29 @@ Deno.test("revisit uses the same account-scoped create input then reads authorit ); }); +Deno.test("a full page of maximum-size JSON-escaped text fits the bounded public response reader", async () => { + const text = "x" + "\u0001".repeat(CHAT_LIMITS.maxTextBytes - 1); + const messages = Array.from( + { length: CHAT_LIMITS.maxPageLimit }, + (_, index) => ({ + ...message(index + 1), + text, + }), + ); + const wire = JSON.stringify(page(messages)); + assert(new TextEncoder().encode(wire).length > 2 * 1024 * 1024); + const fetchFn: typeof fetch = () => Promise.resolve(new Response(wire)); + const result = await getConversationPage( + fetchFn, + scope, + conversation.id, + 0, + new AbortController().signal, + ); + assert(result.messages.length === CHAT_LIMITS.maxPageLimit); + assert(result.messages.at(-1)?.text === text); +}); + Deno.test("ACK loss retains exact request bytes for explicit replay and accepted is not a reply", async () => { const saved = new Map(); const storage = { diff --git a/web/workspace/src/lib/workspace/memory/conversation-api.ts b/web/workspace/src/lib/workspace/memory/conversation-api.ts index fb6fcdf37..0361af55f 100644 --- a/web/workspace/src/lib/workspace/memory/conversation-api.ts +++ b/web/workspace/src/lib/workspace/memory/conversation-api.ts @@ -224,7 +224,8 @@ async function request( throw new ConversationApiError(response.status); } try { - return await readBoundedJson(response, 2 * 1024 * 1024); + // 100件×16KiBの有効な本文はJSON escapeで6倍になり得る。 + return await readBoundedJson(response, 16 * 1024 * 1024); } catch { throw new ConversationApiError(null); }