When Google returns invalid_grant (or 401 after refresh) on any Calendar call, the integration is marked disconnected (refresh token cleared, last_sync_error set "Google access was revoked or expired — reconnect"), the push endpoint returns 409 {"error":{"code":"google_reconnect_required"}}, and GET /api/v1/integrations/google reports status reconnect_required. Transient 5xx/429 errors are retried (max 2) and then surfaced as retryable without disconnecting. Tests against a fake Google API cover invalid_grant, a 401 after refresh, and a transient 503.
Scope
Failure handling and the status endpoint. The settings screen and plan sync-status UI (task 677261bc) and Feature 17's shopping-list generation are untouched. No new dependency (plain REST over golang.org/x/oauth2).
Points for review
Migration 0015_google_reconnect_required.sql is the riskiest part and needs the closest reading: refresh_token_ct was NOT NULL so it could not be cleared, and the migration rebuilds integrations_google to make it nullable with a CHECK that a row without a token must carry a last_sync_error. Confirm it is correct on a fresh boot and on an upgrade with an existing row, that no data is lost, that applied migrations are untouched, and that it is safe to run against the live production database (this ships in a release that a real household's instance will apply at boot).
The hand-off from the previous task: the developer took the branch where only the refresh token is cleared and the calendar selection is kept, so forgetEvents is deliberately not called and there is no second invalidation path. It argues the invariant still holds because the events remain in the still-selected calendar. Judge that reasoning, and check Disconnect still forgets events and now skips the revoke when there is no token.
invalid_grant and a 401-after-refresh are detected on all three Calendar calls (listing, selection, push), the last_sync_error text is exactly the specified string, and after a refusal later calls answer google_reconnect_required without contacting Google again.
The push now returns 409google_reconnect_required where it previously returned 502 — check nothing else depended on the old code.
Transient 429/5xx are retried at most twice (3 attempts) and then surfaced as a retryable 503 google_unavailable with nothing disconnected or cleared; a non-transient error is not retried.
The new GET /api/v1/integrations/google reports {status, calendar_id, calendar_name, connected_at, last_sync_error} with status not_connected | connected | reconnect_required, and leaks no token material.
Concurrency: the refusal is recorded under the existing account lock so it cannot race a push or a selection change, and the lock discipline is unchanged (one lock).
Redaction still holds everywhere, including Google's raw error bodies.
Test adequacy (the developer reports four mutation checks all caught), maintainability, error handling.
## Task
Maestro task `b8d8da5a-0450-4571-963e-b3dfead24283` — "Token failure handling" (Feature 16, task 4 of 5).
## Done-condition
When Google returns invalid_grant (or 401 after refresh) on any Calendar call, the integration is marked disconnected (refresh token cleared, `last_sync_error` set "Google access was revoked or expired — reconnect"), the push endpoint returns 409 `{"error":{"code":"google_reconnect_required"}}`, and `GET /api/v1/integrations/google` reports status `reconnect_required`. Transient 5xx/429 errors are retried (max 2) and then surfaced as retryable without disconnecting. Tests against a fake Google API cover invalid_grant, a 401 after refresh, and a transient 503.
## Scope
Failure handling and the status endpoint. The settings screen and plan sync-status UI (task `677261bc`) and Feature 17's shopping-list generation are untouched. No new dependency (plain REST over `golang.org/x/oauth2`).
## Points for review
- **Migration `0015_google_reconnect_required.sql` is the riskiest part** and needs the closest reading: `refresh_token_ct` was `NOT NULL` so it could not be cleared, and the migration rebuilds `integrations_google` to make it nullable with a CHECK that a row without a token must carry a `last_sync_error`. Confirm it is correct on a fresh boot **and** on an upgrade with an existing row, that no data is lost, that applied migrations are untouched, and that it is safe to run against the live production database (this ships in a release that a real household's instance will apply at boot).
- **The hand-off from the previous task**: the developer took the branch where only the refresh token is cleared and the **calendar selection is kept**, so `forgetEvents` is deliberately **not** called and there is no second invalidation path. It argues the invariant still holds because the events remain in the still-selected calendar. Judge that reasoning, and check `Disconnect` still forgets events and now skips the revoke when there is no token.
- invalid_grant and a 401-after-refresh are detected on **all three** Calendar calls (listing, selection, push), the `last_sync_error` text is exactly the specified string, and after a refusal later calls answer `google_reconnect_required` without contacting Google again.
- The push now returns **409** `google_reconnect_required` where it previously returned 502 — check nothing else depended on the old code.
- Transient 429/5xx are retried at most twice (3 attempts) and then surfaced as a retryable 503 `google_unavailable` with **nothing** disconnected or cleared; a non-transient error is not retried.
- The new `GET /api/v1/integrations/google` reports `{status, calendar_id, calendar_name, connected_at, last_sync_error}` with status `not_connected | connected | reconnect_required`, and leaks no token material.
- Concurrency: the refusal is recorded under the existing account lock so it cannot race a push or a selection change, and the lock discipline is unchanged (one lock).
- Redaction still holds everywhere, including Google's raw error bodies.
- Test adequacy (the developer reports four mutation checks all caught), maintainability, error handling.
## Evidence (orchestrator-run gate at 1c8092800ab4d8dc69a9a484a1ea4a4e56e75877)
- `gofmt -l .` — empty; `go vet ./...` — clean
- `go test -race -count=1 ./...` — 26 packages ok, 0 failures
- `CGO_ENABLED=0 go build ./...` — ok
- `make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc` — no drift (queries changed and the regenerated code is committed)
- `cd web && npm run typecheck && npm test && npm run build` — PASS, 45 files / 764 tests (no web change)
- `migrations/0015_google_reconnect_required.sql` is present and in order after 0014
Implementation commit: `1c8092800ab4d8dc69a9a484a1ea4a4e56e75877`
When Google refuses the stored grant (invalid_grant on refresh, or HTTP
401 from the Calendar API to the access token just refreshed) on any
Calendar call - listing, choosing or pushing - the connection becomes
reconnect required under the account lock: the refresh token is cleared
and last_sync_error set to "Google access was revoked or expired —
reconnect". The call answers 409 google_reconnect_required, and later
calls answer the same without contacting Google until a reconnect or a
disconnect. The chosen calendar and stored calendar event ids are kept:
the pushed events are still on that calendar, so forgetEvents is not
run.
HTTP 429 and 5xx from the token endpoint or the Calendar API are retried
at most twice with backoff, then answered 503 google_unavailable,
retryable, without clearing or disconnecting anything.
Add GET /api/v1/integrations/google, reporting not_connected, connected
or reconnect_required with the chosen calendar and last_sync_error.
Migration 0015 rebuilds integrations_google so refresh_token_ct can be
cleared, with a CHECK that a row without a token carries the reason.
Maestro task b8d8da5a-0450-4571-963e-b3dfead24283.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A grant refused while no calendar was chosen (for example during calendar
listing) made a later push return no_calendar instead of
google_reconnect_required, sending the household to choose a calendar when
only connecting again helps. grantedConnection now decides the precedence
for every Calendar call that uses the grant (listing, choosing, pushing):
not connected, then a refused grant, then anything else such as no
calendar chosen. It holds the existing cleared-token check, moved out of
accessToken.
Fixes F16-T4-01 for Maestro task b8d8da5a-0450-4571-963e-b3dfead24283.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task
Maestro task
b8d8da5a-0450-4571-963e-b3dfead24283— "Token failure handling" (Feature 16, task 4 of 5).Done-condition
When Google returns invalid_grant (or 401 after refresh) on any Calendar call, the integration is marked disconnected (refresh token cleared,
last_sync_errorset "Google access was revoked or expired — reconnect"), the push endpoint returns 409{"error":{"code":"google_reconnect_required"}}, andGET /api/v1/integrations/googlereports statusreconnect_required. Transient 5xx/429 errors are retried (max 2) and then surfaced as retryable without disconnecting. Tests against a fake Google API cover invalid_grant, a 401 after refresh, and a transient 503.Scope
Failure handling and the status endpoint. The settings screen and plan sync-status UI (task
677261bc) and Feature 17's shopping-list generation are untouched. No new dependency (plain REST overgolang.org/x/oauth2).Points for review
0015_google_reconnect_required.sqlis the riskiest part and needs the closest reading:refresh_token_ctwasNOT NULLso it could not be cleared, and the migration rebuildsintegrations_googleto make it nullable with a CHECK that a row without a token must carry alast_sync_error. Confirm it is correct on a fresh boot and on an upgrade with an existing row, that no data is lost, that applied migrations are untouched, and that it is safe to run against the live production database (this ships in a release that a real household's instance will apply at boot).forgetEventsis deliberately not called and there is no second invalidation path. It argues the invariant still holds because the events remain in the still-selected calendar. Judge that reasoning, and checkDisconnectstill forgets events and now skips the revoke when there is no token.last_sync_errortext is exactly the specified string, and after a refusal later calls answergoogle_reconnect_requiredwithout contacting Google again.google_reconnect_requiredwhere it previously returned 502 — check nothing else depended on the old code.google_unavailablewith nothing disconnected or cleared; a non-transient error is not retried.GET /api/v1/integrations/googlereports{status, calendar_id, calendar_name, connected_at, last_sync_error}with statusnot_connected | connected | reconnect_required, and leaks no token material.Evidence (orchestrator-run gate at
1c8092800a)gofmt -l .— empty;go vet ./...— cleango test -race -count=1 ./...— 26 packages ok, 0 failuresCGO_ENABLED=0 go build ./...— okmake sqlc-check SQLC=$(go env GOPATH)/bin/sqlc— no drift (queries changed and the regenerated code is committed)cd web && npm run typecheck && npm test && npm run build— PASS, 45 files / 764 tests (no web change)migrations/0015_google_reconnect_required.sqlis present and in order after 0014Implementation commit:
1c8092800ab4d8dc69a9a484a1ea4a4e56e75877Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Independent Codex gpt-6-sol review: APPROVED for this exact commit.