Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
30 changes: 7 additions & 23 deletions workers/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -433,28 +434,11 @@ export class EmailAgent extends AIChatAgent<any> {
}

// 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)}`,
Expand Down
Loading