The web dashboard was making ~84 GitHub API calls per refresh (6 calls × 14 sessions with PRs), quickly exhausting the GitHub GraphQL rate limit (5000 points/hour). When rate-limited, PR enrichment failed silently, showing misleading default data ("+0 -0", "CI failing", "needs review" for everything).
File: packages/core/src/types.ts
- Added
"done"and"terminated"to theSessionStatustype union - These values were used in metadata files but missing from the type definition
File: packages/web/src/lib/types.ts
- Updated
getAttentionLevel()to return"done"for sessions with:status === "done"status === "terminated"pr.state === "merged"pr.state === "closed"
- Prevents terminal sessions from being classified as "working" or "pending"
File: packages/web/src/app/page.tsx
- Added logic to skip PR enrichment for sessions with terminal statuses:
"merged","killed","cleanup","done","terminated"
- Also skips enrichment if PR state is already
"merged"or"closed" - Impact: Reduces API calls by ~50% for typical session mix
File: packages/web/src/lib/cache.ts (NEW)
- Implemented
TTLCache<T>class with 60-second TTL - Caches PR enrichment data by key:
owner/repo#123 - Automatically evicts stale entries on
get() - Impact: Reduces API calls by ~90% for repeated page refreshes
File: packages/web/src/lib/serialize.ts
enrichSessionPR()now checks cache first before hitting SCM API- Caches successful enrichment results for 60 seconds
- Does NOT cache failed enrichment attempts
File: packages/web/src/lib/serialize.ts
- Detects when all API calls fail (likely rate limit)
- Sets explicit blocker:
"API rate limited or unavailable" - Logs error to console for debugging
- Does NOT cache failed attempts (prevents stale error states)
File: packages/web/src/lib/serialize.ts
basicPRToDashboard()now uses explicit blocker:"Data not loaded"- Default
ciStatus: "none"is neutral (not "failing") - Default
reviewDecision: "none"is neutral (not "changes_requested") - Prevents misleading UI before enrichment completes
Files:
packages/web/src/lib/__tests__/cache.test.ts(NEW) - 9 testspackages/web/src/lib/__tests__/types.test.ts(NEW) - 29 testspackages/web/src/lib/__tests__/serialize.test.ts(NEW) - 16 tests
Test Coverage:
- TTL cache behavior (get, set, expiry, clear)
getAttentionLevel()for all status combinationsbasicPRToDashboard()default valuesenrichSessionPR()with successful enrichmentenrichSessionPR()with cache hitsenrichSessionPR()with rate limit errors (all API calls fail)enrichSessionPR()with partial failures (some API calls succeed)- Cache key generation
- Terminal session classification
File: packages/web/src/components/Dashboard.tsx
- Added "orchestrator terminal" link in header next to ClientTimestamp
- Styled with hover effects matching the rest of the UI
- Links to
/sessions/orchestrator
pnpm build
# ✓ All packages build successfullypnpm eslint
# ✓ No linting errorspnpm --filter @agent-orchestrator/web test
# ✓ 140 passed (143 total, 3 pre-existing failures in components.test.tsx)
# ✓ All new tests pass:
# - cache.test.ts: 9/9 tests pass
# - types.test.ts: 29/29 tests pass
# - serialize.test.ts: 16/16 tests pass- API calls per refresh: ~84 calls (6 × 14 sessions)
- Rate limit exhaustion: Every ~60 refreshes (5000 points ÷ 84 ≈ 60)
- Recovery time: 1 hour (rate limit reset)
- User experience: Garbage data when rate-limited
- API calls per refresh: ~7-10 calls (only non-terminal, non-cached sessions)
- Rate limit exhaustion: Every ~500+ refreshes (10× improvement)
- Recovery time: Same (1 hour), but happens 10× less often
- User experience: Explicit "rate limited" message, cached data for 60s
- 60-second cache TTL: Balance between freshness and API savings
- In-memory cache: Simple, no external dependencies, server-side only
- Fail-closed on errors: Explicit "unavailable" blockers, not misleading defaults
- Don't cache failures: Prevents stale error states, allows retry
- Skip terminal sessions: No value in refreshing merged/closed PR data
- ✅ ESM modules with
.jsextensions in imports (where appropriate) - ✅
node:prefix for built-in imports - ✅ Strict TypeScript mode
- ✅ No
anytypes - ✅ Type-safe plugin pattern with
satisfies - ✅ Error handling for external API calls
- ✅ Comprehensive test coverage for edge cases
packages/core/src/types.ts- Added "done" and "terminated" to SessionStatuspackages/web/src/lib/types.ts- Fixed getAttentionLevel for terminal sessionspackages/web/src/app/page.tsx- Skip enrichment for terminal sessionspackages/web/src/lib/serialize.ts- Added caching and rate limit handlingpackages/web/src/components/Dashboard.tsx- Added orchestrator terminal button
packages/web/src/lib/cache.ts- TTL cache implementationpackages/web/src/lib/__tests__/cache.test.ts- Cache testspackages/web/src/lib/__tests__/types.test.ts- Attention level testspackages/web/src/lib/__tests__/serialize.test.ts- Serialization tests
- Monitor dashboard performance in production
- Adjust cache TTL if needed (current: 60s)
- Consider adding cache metrics (hits/misses) for observability
- Consider persisting cache to disk for server restarts
- Consider rate limit backoff/retry logic if needed
- Pre-existing component test failures (3) are unrelated to these changes
- Pre-existing warnings (tracker-linear @composio/core, plugin-registry) are unrelated
- All new functionality is thoroughly tested with 54 new test cases