diff --git a/.jules/bolt.md b/.jules/bolt.md index d3d39015..3866bbbd 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-06-30 - Optimize DO Queries in Agent +**Learning:** Found N+1 query pattern in `workers/agent/index.ts` where fetching the full thread for agent context was done by iterating over thread metadata using `.map` with `await stub.getEmail(e.id)` inside a `Promise.all`. While `Promise.all` executes concurrently in Node/Workers, executing multiple DO `fetch` calls adds unnecessary roundtrips and DO internal overhead. +**Action:** Replaced the `Promise.all` sequential fetching with `getFullThread(stub, threadId)` (which proxies to the DO `getThreadEmails` query) to collapse N queries into two batched queries on the DO side, significantly reducing overhead for the context-loading step before agent evaluation. diff --git a/workers/agent/index.ts b/workers/agent/index.ts index a651a819..f6af25d1 100644 --- a/workers/agent/index.ts +++ b/workers/agent/index.ts @@ -18,12 +18,13 @@ import { } from "../../shared/folders"; import { isPromptInjection, verifyDraft } from "../lib/ai"; import { + getFullThread, getMailboxStub, stripHtmlToText, textToHtml, } from "../lib/email-helpers"; import { resolveMailboxSettings } from "../lib/mailbox-settings"; -import type { EmailFull, EmailMetadata } from "../lib/schemas"; +import type { EmailFull } from "../lib/schemas"; import { toolDiscardDraft, toolDraftEmail, @@ -433,28 +434,11 @@ export class EmailAgent extends AIChatAgent { } // Load thread for conversation context - const threadEmails = (await stub.getEmails({ - 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: Replace N+1 query pattern with `getFullThread` which fetches all + // thread emails and attachments in two batched queries. + const thread = await getFullThread(stub, emailData.threadId); + if (thread.message_count > 1) { + threadContext = thread.messages .map( (e) => `[${e.date}] ${e.sender} → ${e.recipient} (${e.folder_id}): ${e.body_text.substring(0, 500)}`,