You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CODER-003 — (Optional) Aggregate builds endpoint across all repos (one call) #79
The panel lists builds for all repos in an instance. Rather than the frontend making one /deployments call per repo (N calls, N GitHub round-trips), provide a single aggregate endpoint.
Acceptance Criteria
GET /v1/instances/:id/coding/builds returns { builds: Array<{ repoId, repoName, available, run: Run | null }> } — the latest run per repo (drill-down to full history stays on the per-repo /deployments).
Fans out to GitHub with bounded concurrency; a single repo failing degrades to available:false for that repo, never fails the whole response.
Owner-scoped; only repos with a githubRepo are queried.
Technical Notes
File: workers/api/src/routes/coding.ts. Reuse the per-repo GitHub fetch helper from CODER-001; Promise.allSettled over the instance's repos.
Trade-off: simplest frontend (1 call) vs. an extra endpoint. If CODER-004 ships against per-repo calls first, this is a pure latency optimization — keep it optional.
KV repo registry per instance: repo-registry:{instanceId} → [{ repoId, githubRepo }], written on repo add/remove (D1 remains source of truth; KV is the fan-out index).
Fan-out with per-repo conditional requests./builds iterates the registry and fetches each repo's latest run with If-None-Match/If-Modified-Since using the per-repo ETag stored in KV. Unchanged repos return 304 → 0 rate-limit cost, served from the KV cache. Bounded concurrency (~6) via Promise.allSettled; one repo failing → available:false for that repo only.
Aggregated response{ builds: [{ repoId, repoName, available, run }] }.
Rate-limit tiers. Only active-tier repos are fetched live each cycle; idle-tier repos are served straight from KV (or refreshed on the 5–10 min idle cadence). So N repos ≠ N live GitHub calls per refresh — this is what keeps us well under 5,000/hr at scale.
Auth. GitHub App installation token per owner (installationTokenForOwner), server-side only, encrypted cache, never browser-exposed.
Decision recorded: Repo registry strategy (Option D — Hybrid)
Decision: Option D — Hybrid auto-registry with auto-archive.
Auto-add on first open — a repo joins the registry the first time it's opened in the Coder agent; no manual registration step.
Auto-archive after inactivity — repos with no activity for X days are auto-archived. Default suggestion 30 days, but treat X as a configurable value (TBD) — not hard-coded.
Archived = visible but not polled — archived repos still appear in the build-history UI, but drop out of the fan-out/poll set → zero rate-limit cost.
Manual reactivation — an archived repo can be un-archived / reactivated when reopened in the agent.
Chosen over:
A (registry grows forever) — unbounded fan-out / rate-limit cost.
B (drop from registry on session end) — loses build history.
C (manual add/remove) — too much friction.
Implementation note: this maps onto the KV repo registry from the ticket body — add an archived flag (+ lastActivityAt) per registry entry; the aggregate /builds fan-out iterates only non-archived entries, while the UI still reads archived entries from KV/cached history.
Decision recorded: Active threshold triggers (dual-signal)
Decision: Dual-trigger "active" mode — BOTH signals count.
What makes a repo "active" (either signal is sufficient):
Active conversation — the user is currently having a conversation with the Coder agent for that repo.
Active agent work — an agent is currently executing a task on that repo.
What makes a repo go fully idle / halt updates:
No active conversation AND no agent work AND no UI open for that repo.
When ALL three conditions are inactive, polling stops completely — no updates at all (not even slow polling).
This is stronger than "slow polling" for idle — it's a complete halt.
Polling tiers that result:
Active (conversation running OR agent running): high-frequency polling (e.g. every 30s) — exact value TBD.
Passive (UI open/visible, but no conversation and no agent): low-frequency polling (e.g. every 5 min) — exact value TBD.
Halted (UI not open, no conversation, no agent): zero polling — completely stopped.
Implementation note. The frontend tracks three signals:
Conversation activity (new message sent / response being received)
Agent run state (from existing runState in repoStatuses)
UI visibility (Page Visibility API or tab-focus detection)
All three feed a single tier computation that drives the polling interval. When the tab/UI is hidden AND no agent is running AND no conversation is active, the interval is set to null / cleared entirely — not just slowed.
Applies to: CODER-001 (build history), CODER-003 (multi-repo registry), CODER-006 (aggregate status endpoint), and CODER-007 (WebSocket push — the server-side push cadence should respect the same tiers).
GET /v1/instances/:id/coding/builds (workers/api/src/routes/coding.ts) — one call for the whole Build Status panel instead of N per-repo /deployments requests.
Returns { builds: Array<{ repoId, repoName, available, run: Run | null }> } — the latest run per repo (full history stays on per-repo /deployments).
Bounded concurrency (6) fan-out to keep well under GitHub's rate limit even with many repos; Promise.allSettled so a single repo failing degrades to available:falsefor that repo only, never failing the whole response.
Owner-scoped (requireOwned); only GitHub-connected repos are queried. Non-GitHub/local repos are still listed as available:false (not hidden) so the panel can show them; the instance's repo order is preserved.
Refactor/DRY: extracted githubHeaders() + mapWorkflowRun() + latestRunFor() and reused them in both /builds and /deployments; the singular /deployment route is left untouched.
Note on the decision comments above (KV repo-registry, per-repo ETag fan-out, Option D auto-archive, active/passive/halted polling tiers): those are refinements that pay off with the frontend polling loop and at higher repo counts. The endpoint here delivers the acceptance criteria (single aggregate call, bounded concurrency, per-repo graceful degradation); the registry/archive/tier layer is best landed with CODER-004's polling loop (which decides cadence) rather than speculatively now — flagging so it isn't lost.
tsc --noEmit clean. Next: CODER-004 (#80) — the Builds panel UI that consumes this.
Activity
serge-ivo commentedon Aug 1, 2026
Architectural clarification (CODER-003)
Multi-repo data model (concrete).
repo-registry:{instanceId}→[{ repoId, githubRepo }], written on repo add/remove (D1 remains source of truth; KV is the fan-out index)./buildsiterates the registry and fetches each repo's latest run withIf-None-Match/If-Modified-Sinceusing the per-repoETagstored in KV. Unchanged repos return304→ 0 rate-limit cost, served from the KV cache. Bounded concurrency (~6) viaPromise.allSettled; one repo failing →available:falsefor that repo only.{ builds: [{ repoId, repoName, available, run }] }.Rate-limit tiers. Only active-tier repos are fetched live each cycle; idle-tier repos are served straight from KV (or refreshed on the 5–10 min idle cadence). So N repos ≠ N live GitHub calls per refresh — this is what keeps us well under 5,000/hr at scale.
Auth. GitHub App installation token per owner (
installationTokenForOwner), server-side only, encrypted cache, never browser-exposed.serge-ivo commentedon Aug 1, 2026
Decision recorded: Repo registry strategy (Option D — Hybrid)
Decision: Option D — Hybrid auto-registry with auto-archive.
Chosen over:
Implementation note: this maps onto the KV repo registry from the ticket body — add an
archivedflag (+lastActivityAt) per registry entry; the aggregate/buildsfan-out iterates only non-archived entries, while the UI still reads archived entries from KV/cached history.serge-ivo commentedon Aug 1, 2026
Decision recorded: Active threshold triggers (dual-signal)
Decision: Dual-trigger "active" mode — BOTH signals count.
What makes a repo "active" (either signal is sufficient):
What makes a repo go fully idle / halt updates:
Polling tiers that result:
Implementation note. The frontend tracks three signals:
runStateinrepoStatuses)All three feed a single tier computation that drives the polling interval. When the tab/UI is hidden AND no agent is running AND no conversation is active, the interval is set to
null/ cleared entirely — not just slowed.Applies to: CODER-001 (build history), CODER-003 (multi-repo registry), CODER-006 (aggregate status endpoint), and CODER-007 (WebSocket push — the server-side push cadence should respect the same tiers).
feat(coder): aggregate /builds endpoint across all repos (CODER-003, #79
serge-ivo commentedon Aug 2, 2026
Shipped in
6015ba7— closingGET /v1/instances/:id/coding/builds(workers/api/src/routes/coding.ts) — one call for the whole Build Status panel instead of N per-repo/deploymentsrequests.{ builds: Array<{ repoId, repoName, available, run: Run | null }> }— the latest run per repo (full history stays on per-repo/deployments).Promise.allSettledso a single repo failing degrades toavailable:falsefor that repo only, never failing the whole response.requireOwned); only GitHub-connected repos are queried. Non-GitHub/local repos are still listed asavailable:false(not hidden) so the panel can show them; the instance's repo order is preserved.githubHeaders()+mapWorkflowRun()+latestRunFor()and reused them in both/buildsand/deployments; the singular/deploymentroute is left untouched.Note on the decision comments above (KV repo-registry, per-repo ETag fan-out, Option D auto-archive, active/passive/halted polling tiers): those are refinements that pay off with the frontend polling loop and at higher repo counts. The endpoint here delivers the acceptance criteria (single aggregate call, bounded concurrency, per-repo graceful degradation); the registry/archive/tier layer is best landed with CODER-004's polling loop (which decides cadence) rather than speculatively now — flagging so it isn't lost.
tsc --noEmitclean. Next: CODER-004 (#80) — the Builds panel UI that consumes this.