Problem
Every open of /agents fires three live external API calls in parallel:
- Slack (
get_slack_messages in backend/app/routers/gateway.py): conversations.list → 5× conversations.history → users.info lookups
- Gmail (
get_gmail_messages): messages.list + up to 10× messages.get(format=full), with inline OAuth refresh
- GitHub (
get_github_activity): /user + /notifications + /search/issues + /users/{login}/received_events
Aggregate perceived latency ~2–5s on every page open. Nothing is cached — frontend calls in app/src/lib/api.ts:127-140 go straight through to the backend, and the backend goes straight to the external API.
Goal
Subsequent loads within the cache TTL return in <100ms. Cold start still pays the external-API cost once.
Approach
In-memory TTL cache on the backend (module-level dict, ~180s TTL). Cache-aside pattern wired into all three feed handlers. Production-grade equivalent would be Redis (shared across uvicorn workers, survives restarts) + SWR/React Query on the client; in-memory dict here has the same architectural shape so it's a clean swap to Redis later when multi-worker / quota pressure arrives.
Prerequisite refactor: extract the bodies of get_slack_messages, get_gmail_messages, get_github_activity into pure async functions in backend/app/services/feed_fetchers.py so the cache and (future) background poller can both call them.
Files
- NEW
backend/app/services/signal_feed_cache.py (~50 lines)
- NEW
backend/app/services/feed_fetchers.py (extracted from gateway.py)
- MOD
backend/app/routers/gateway.py — handlers become cache-aside wrappers; DELETE /<service>/disconnect clears that user's cache entry
- NEW
backend/tests/test_signal_feed_cache.py — TTL behavior, expiry, clear
Acceptance
Out of scope
- Redis migration (for multi-worker / quota pressure later)
- Client-side localStorage cache (server cache already <100ms; revisit if true 0ms first-paint becomes a goal)
Related
Sub-issue: auto-refresh the cache every 120s so the feed stays current without manual reload. Filed separately.
Problem
Every open of
/agentsfires three live external API calls in parallel:get_slack_messagesinbackend/app/routers/gateway.py):conversations.list→ 5×conversations.history→users.infolookupsget_gmail_messages):messages.list+ up to 10×messages.get(format=full), with inline OAuth refreshget_github_activity):/user+/notifications+/search/issues+/users/{login}/received_eventsAggregate perceived latency ~2–5s on every page open. Nothing is cached — frontend calls in
app/src/lib/api.ts:127-140go straight through to the backend, and the backend goes straight to the external API.Goal
Subsequent loads within the cache TTL return in <100ms. Cold start still pays the external-API cost once.
Approach
In-memory TTL cache on the backend (module-level dict, ~180s TTL). Cache-aside pattern wired into all three feed handlers. Production-grade equivalent would be Redis (shared across uvicorn workers, survives restarts) + SWR/React Query on the client; in-memory dict here has the same architectural shape so it's a clean swap to Redis later when multi-worker / quota pressure arrives.
Prerequisite refactor: extract the bodies of
get_slack_messages,get_gmail_messages,get_github_activityinto pure async functions inbackend/app/services/feed_fetchers.pyso the cache and (future) background poller can both call them.Files
backend/app/services/signal_feed_cache.py(~50 lines)backend/app/services/feed_fetchers.py(extracted from gateway.py)backend/app/routers/gateway.py— handlers become cache-aside wrappers;DELETE /<service>/disconnectclears that user's cache entrybackend/tests/test_signal_feed_cache.py— TTL behavior, expiry, clearAcceptance
pytestclean; new cache tests passOut of scope
Related
Sub-issue: auto-refresh the cache every 120s so the feed stays current without manual reload. Filed separately.