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 <noreply@anthropic.com>
This commit is contained in:
quartz
2026-01-24 17:14:00 -08:00
committed by John Ogle
parent 2f893f0ad3
commit c3852cb438

View File

@@ -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 {