From beee702d27c5f69b32a1c5effe29e131013ccce4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 05:27:10 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20N+1=20query=20ov?= =?UTF-8?q?erhead=20in=20thread=20context=20fetching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the sequential loop using `Promise.all(threadEmails.map(...))` to fetch each thread message individually via `stub.getEmail` with a single DO batch call to `getFullThread(stub, emailData.threadId)`. This eliminates N+1 query latency for large threads checked against prompt injections. Co-authored-by: schmug <38227427+schmug@users.noreply.github.com> --- .jules/bolt.md | 3 +++ workers/agent/index.ts | 23 +++++------------------ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index d3d39015..5ac660fb 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -24,3 +24,6 @@ ## 2026-06-29 - Optimize D1 Queries by Batching **Learning:** Found N+1 query pattern on the hub stats page (`hub/src/routes/admin/stats.ts`) when iterating over the list of inbound peers to query `events_pulled_24h` for each peer sequentially using `.first()`. This introduces significant roundtrip latency and resource overhead as the number of peers grows. D1 supports batching queries, which can collapse N queries into a single HTTP roundtrip. D1's returned `batch` types can be peculiar, particularly when running against tests using Miniflare or test shims where row result structure might differ across platforms. **Action:** Replaced sequential queries in `Promise.all` with `db.batch()` to combine the multiple `db.prepare(...).bind(...)` calls into a single execution, and normalized the output structure parsing. This significantly reduces N+1 overhead and improves performance scaling as the peer count increases. +## 2026-07-28 - Optimize N+1 Query in Agent Thread Context Parsing +**Learning:** In `workers/agent/index.ts`, when preparing the thread context to check for prompt injections and auto-trigger agent workflows, the codebase was mapping over `threadEmails` (retrieved via `stub.getEmails`) and calling `stub.getEmail` inside a `Promise.all` for each email in the thread to get the full body. This caused an N+1 query issue for large threads, generating many separate roundtrips to the Durable Object. +**Action:** Replaced the `Promise.all` loop with a single call to the existing helper function `getFullThread(stub, emailData.threadId)`. This leverages the DO `getThreadEmails` RPC method, which runs a single combined DO SQL query (batching the fetches), significantly reducing latency and compute overhead for deep conversation threads. diff --git a/workers/agent/index.ts b/workers/agent/index.ts index a651a819..e0421adc 100644 --- a/workers/agent/index.ts +++ b/workers/agent/index.ts @@ -18,6 +18,7 @@ import { } from "../../shared/folders"; import { isPromptInjection, verifyDraft } from "../lib/ai"; import { + getFullThread, getMailboxStub, stripHtmlToText, textToHtml, @@ -437,24 +438,10 @@ export class EmailAgent extends AIChatAgent { thread_id: emailData.threadId, })) as EmailMetadata[]; if (threadEmails.length > 1) { - const fullThread = await Promise.all( - threadEmails.map(async (e) => { - const full = (await stub.getEmail(e.id)) as EmailFull | null; - const text = full?.body ? stripHtmlToText(full.body) : ""; - return { - id: e.id, - sender: e.sender, - recipient: e.recipient, - subject: e.subject, - date: e.date, - folder_id: e.folder_id, - body_text: text, - }; - }), - ); - // ⚡ Bolt: Optimize sorting by replacing `new Date(b.date).getTime()` with `Date.parse(b.date)` - fullThread.sort((a, b) => Date.parse(a.date) - Date.parse(b.date)); - threadContext = fullThread + // ⚡ Bolt: Eliminate N+1 query overhead by replacing Promise.all mapping of `stub.getEmail` + // with a single call to `getFullThread`, which fetches the entire thread context in one DO roundtrip. + const fullThreadData = await getFullThread(stub, emailData.threadId); + threadContext = fullThreadData.messages .map( (e) => `[${e.date}] ${e.sender} → ${e.recipient} (${e.folder_id}): ${e.body_text.substring(0, 500)}`,