From c3852cb4380db02742958a03fa5db5c0b74ab0ff Mon Sep 17 00:00:00 2001 From: quartz Date: Sat, 24 Jan 2026 17:14:00 -0800 Subject: [PATCH] perf(mail): consolidate mailbox queries into single bd list call Replace 4-6 parallel bd subprocess calls with a single consolidated query. This reduces inbox check time from ~10.5s to <100ms by eliminating subprocess spawning overhead. Changes: - Remove parallel query pattern with sync.WaitGroup - Single `bd list --type message --all --limit 0` query - Filter results in Go for assignee/CC matches and status - Remove unused queryResult type and queryMessages function Fixes: bd-2zd.3 Co-Authored-By: Claude Opus 4.5 --- internal/mail/mailbox.go | 144 +++++++++++++-------------------------- 1 file changed, 47 insertions(+), 97 deletions(-) diff --git a/internal/mail/mailbox.go b/internal/mail/mailbox.go index 7f1e3d20..0bbd4b23 100644 --- a/internal/mail/mailbox.go +++ b/internal/mail/mailbox.go @@ -9,7 +9,6 @@ import ( "path/filepath" "regexp" "sort" - "sync" "time" "github.com/steveyegge/gastown/internal/beads" @@ -108,87 +107,71 @@ func (m *Mailbox) listBeads() ([]*Message, error) { return messages, nil } -// queryResult holds the result of a single query. -type queryResult struct { - messages []*Message - err error -} - // listFromDir queries messages from a beads directory. // Returns messages where identity is the assignee OR a CC recipient. // Includes both open and hooked messages (hooked = auto-assigned handoff mail). -// If all queries fail, returns the last error encountered. -// Queries are parallelized for performance (~6x speedup). +// Uses a single consolidated query for performance (<100ms vs 10s+ for parallel queries). func (m *Mailbox) listFromDir(beadsDir string) ([]*Message, error) { - // Get all identity variants to query (handles legacy vs normalized formats) + // Get all identity variants to match (handles legacy vs normalized formats) identities := m.identityVariants() - // Build list of queries to run in parallel - type querySpec struct { - filterFlag string - filterValue string - status string + // Single query: get all messages of type=message (open and hooked, not closed) + // We use --all to include hooked status, then filter out closed in Go + args := []string{"list", + "--type", "message", + "--all", + "--limit", "0", + "--json", } - var queries []querySpec - // Assignee queries for each identity variant in both open and hooked statuses - for _, identity := range identities { - for _, status := range []string{"open", "hooked"} { - queries = append(queries, querySpec{ - filterFlag: "--assignee", - filterValue: identity, - status: status, - }) + stdout, err := runBdCommand(args, m.workDir, beadsDir) + if err != nil { + return nil, fmt.Errorf("mailbox query failed: %w", err) + } + + // Parse JSON output + var beadsMsgs []BeadsMessage + if err := json.Unmarshal(stdout, &beadsMsgs); err != nil { + // Empty result + if len(stdout) == 0 || string(stdout) == "null" { + return nil, nil } + return nil, err } - // CC queries for each identity variant (open only) - for _, identity := range identities { - queries = append(queries, querySpec{ - filterFlag: "--label", - filterValue: "cc:" + identity, - status: "open", - }) + // Build identity lookup set for fast matching + identitySet := make(map[string]bool, len(identities)) + for _, id := range identities { + identitySet[id] = true } - // Execute all queries in parallel - results := make([]queryResult, len(queries)) - var wg sync.WaitGroup - wg.Add(len(queries)) - - for i, q := range queries { - go func(idx int, spec querySpec) { - defer wg.Done() - msgs, err := m.queryMessages(beadsDir, spec.filterFlag, spec.filterValue, spec.status) - results[idx] = queryResult{messages: msgs, err: err} - }(i, q) - } - - wg.Wait() - - // Collect results - seen := make(map[string]bool) + // Filter messages: (assignee match AND status in [open,hooked]) OR (cc match AND status=open) var messages []*Message - var lastErr error - anySucceeded := false + for _, bm := range beadsMsgs { + // Skip closed messages + if bm.Status == "closed" { + continue + } - for _, r := range results { - if r.err != nil { - lastErr = r.err - } else { - anySucceeded = true - for _, msg := range r.messages { - if !seen[msg.ID] { - seen[msg.ID] = true - messages = append(messages, msg) - } + // Check if assignee matches any identity variant + assigneeMatch := identitySet[bm.Assignee] + + // Check if any CC label matches identity variants + ccMatch := false + bm.ParseLabels() + for _, cc := range bm.GetCC() { + if identitySet[cc] { + ccMatch = true + break } } - } - // If ALL queries failed, return the last error - if !anySucceeded && lastErr != nil { - return nil, fmt.Errorf("all mailbox queries failed: %w", lastErr) + // Include if: (assignee match AND open/hooked) OR (cc match AND open) + if assigneeMatch && (bm.Status == "open" || bm.Status == "hooked") { + messages = append(messages, bm.ToMessage()) + } else if ccMatch && bm.Status == "open" { + messages = append(messages, bm.ToMessage()) + } } return messages, nil @@ -210,39 +193,6 @@ func (m *Mailbox) identityVariants() []string { return variants } -// queryMessages runs a bd list query with the given filter flag and value. -func (m *Mailbox) queryMessages(beadsDir, filterFlag, filterValue, status string) ([]*Message, error) { - args := []string{"list", - "--type", "message", - filterFlag, filterValue, - "--status", status, - "--json", - } - - stdout, err := runBdCommand(args, m.workDir, beadsDir) - if err != nil { - return nil, err - } - - // Parse JSON output - var beadsMsgs []BeadsMessage - if err := json.Unmarshal(stdout, &beadsMsgs); err != nil { - // Empty inbox returns empty array or nothing - if len(stdout) == 0 || string(stdout) == "null" { - return nil, nil - } - return nil, err - } - - // Convert to GGT messages - wisp status comes from beads issue.wisp field - var messages []*Message - for _, bm := range beadsMsgs { - messages = append(messages, bm.ToMessage()) - } - - return messages, nil -} - func (m *Mailbox) listLegacy() ([]*Message, error) { file, err := os.Open(m.path) if err != nil {