cli: address Bugbot/Copilot review on cell routing fallback · Entire
cli: address Bugbot/Copilot review on cell routing fallback
86a21a8→main·
Soph·1w ago·3 files·+24 added/-9 removed
- Bound currentRepoID's control-plane lookup with a 5s timeout (currentRepoIDTimeout, mirroring expertsCellResolveTimeout): the lookup is best-effort decoration, so a stalled core must not hang recap.
- Case-fold the cluster catalog row's jurisdiction in resolveCellAPIBaseURL: the claim side is already folded, so a differently-cased row misreported ErrNoCellForJurisdiction.
- Rewrite newRecapClient's fallback contract comment to match the implementation: ANY cell-client failure falls back to the data API (expected cases silent, others debug-logged), not just the two named errors.
- Rename runRecap's repoSlug local to repoScope: once cell-routed it holds a repo_id ULID, only the data-API fallback passes a slug.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Sessions
76ad7ee5e8b2View transcript
?\ Multi-cell Fan-out and Cell Routing InfrastructureClaude Code·Fable 5·4 steps
Changes
3
cmd/entire/cli
auth
Mcell_data_api.go+4/-1
Mentireapi_client.go+9
Mrecap.go+11/-8
546 unmodified lines
547
548
549
550
550
551
552
553
554
555
556
546 unmodified lines
var matches []clusterListingRow
sawJurisdiction := false
for _, row := range listing.Clusters {
if row.Jurisdiction != jurisdiction {
// jurisdiction is already a folded lowercase label (resolveJurisdiction);
// fold the catalog row too so a differently-cased row still matches
// instead of misreporting "no cell for jurisdiction".
if !strings.EqualFold(strings.TrimSpace(row.Jurisdiction), jurisdiction) {
continue
}
sawJurisdiction = true
Mcmd/entire/cli/auth/cell_data_api.go+4/-1
4 unmodified lines
5
6
7
8
9
10
11
2 unmodified lines
14
15
16
17
18
19
20
21
22
23
24
45 unmodified lines
70
71
72
73
74
75
76
77
78
4 unmodified lines
"errors"
"io"
"strings"
"time"
"github.com/entireio/cli/cmd/entire/cli/api"
"github.com/entireio/cli/cmd/entire/cli/auth"
2 unmodified lines
"github.com/entireio/cli/internal/coreapi"
// currentRepoIDTimeout bounds currentRepoID's control-plane lookup. The lookup
// is best-effort decoration (recap degrades to personal-only without it), so a
// stalled core must not hang the command — mirror expertsCellResolveTimeout.
const currentRepoIDTimeout = 5 * time.Second
// runAuthenticatedActivityAPI runs fn with an authenticated client for the
// activity/recap surface. It prefers the caller's home entire-api cell (the same
// shared client the experts commands use), which serves the /me/* endpoints
45 unmodified lines
// no extra resolution is needed. Any failure returns "" — recap then shows the
// personal side only rather than erroring.
func currentRepoID(ctx context.Context) string {
ctx, cancel := context.WithTimeout(ctx, currentRepoIDTimeout)
defer cancel()
forge, owner, repo, err := gitremote.ResolveRemoteRepo(ctx, "origin")
if err != nil {
return ""
Mcmd/entire/cli/entireapi_client.go+9
122 unmodified lines
123
124
125
126
126
127
128
129
13 unmodified lines
143
144
145
146
146
147
148
149
1 unmodified line
151
152
153
154
154
155
156
157
158
159
160
160
161
162
163
30 unmodified lines
194
195
196
197
198
199
200
197
198
199
200
201
202
203
204
205
206
122 unmodified lines
if err != nil {
return err
}
client, repoSlug, err := newRecapClient(ctx, f.insecureHTTP)
client, repoScope, err := newRecapClient(ctx, f.insecureHTTP)
if err != nil {
if errors.Is(err, api.ErrInsecureHTTP) {
fmt.Fprintf(errW, "ENTIRE_API_BASE_URL is set to an insecure http:// URL (%s). Use https:// for production, or pass --insecure-http-auth for local dev.\n", api.BaseURL())
}
13 unmodified lines
// actually scoped (a repo query was sent), so an unscoped recap isn't
// mislabelled as scoped to the current repo.
repoName := ""
if repoSlug != "" {
if repoScope != "" {
repoName = currentRepoSlug(ctx)
}
if f.useTUI(interactive.IsTerminalWriter(w), interactive.CanPromptInteractively(), IsAccessibleMode()) {
1 unmodified line
Range: rangeKey,
View: mode,
Agent: f.agentName(),
Repo: repoSlug,
Repo: repoScope,
RepoName: repoName,
Color: color,
})
}
start, end := rangeKey.Bounds(time.Now())
resp, err := recap.FetchMeRecap(ctx, client, start, end, repoSlug, 0)
resp, err := recap.FetchMeRecap(ctx, client, start, end, repoScope, 0)
if err != nil {
return handleRecapFetchError(errW, err)
}
30 unmodified lines
// the data API (which addresses them by name). Empty when the current repo
// can't be resolved — recap then shows the personal side only.
//
// It prefers the caller's home entire-api cell (the shared client), falling back
// to the data API when the region has no cell yet (ErrNoCellForJurisdiction) or
// the caller isn't logged in — recap tolerates the latter, rendering and letting
// the server answer 401 rather than hard-failing. Every other failure surfaces.
// It prefers the caller's home entire-api cell (the shared client) and falls
// back to the data API on ANY cell-client failure — the cell path is a
// best-effort upgrade, so a cell problem must never break a command that worked
// before it existed. Expected fallbacks (region has no cell yet, not logged in)
// are silent; unexpected ones are debug-logged (logCellClientFallback). Only
// failures of the data-API path itself surface — except ErrNotLoggedIn, which
// recap tolerates, rendering and letting the server answer 401.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, string, error) {
// Best-effort upgrade: on any cell failure fall back to the data API path
// below, which tolerates a missing login (renders, lets the server 401) so