Summary
After completing a provider OAuth flow in the browser (user sees "Provider account connected"), Junior re-prompts for authorization on the next turn. The browser showed success, but subsequent Sentry API calls trigger another auth link.
Confirmed facts
- The OAuth callback saves the token synchronously before returning the success HTML. If the token write or parse fails, the browser shows a failure page — not success. So the token IS in the store when the browser renders success.
- Re-prompting requires a fresh
auth_required signal from the sandbox egress layer. That signal fires when the credential broker throws CredentialUnavailableError (token missing/expired) or when an upstream API returns 401.
- When
auth_required fires via maybeHandleAuthSignal, startAuthorizationPause is always called with unlinkExistingProvider: true. This calls unlinkProvider, which deletes the stored OAuth token before sending a new auth link.
Most likely root cause
- User completes Sentry OAuth → callback saves token → browser shows success.
- Turn resumes (or user sends a new message starting a fresh turn).
- Agent uses Sentry tool → sandbox egress injects the OAuth token → calls Sentry API.
- Sentry API returns 401 (scope gap, org/project mismatch, token audience issue, or similar).
- Egress records
auth_required signal for the sentry provider.
- Auth orchestration calls
startAuthorizationPause("sentry", { unlinkExistingProvider: true }).
unlinkProvider deletes the freshly stored Sentry token from the user token store.
- New Sentry OAuth link sent → user sees another auth prompt.
- Loop repeats.
This means every time the Sentry API rejects the token (for any reason), the system does a destructive re-auth that wipes a potentially-valid credential rather than surfacing an actionable error.
Supporting bugs
1. Silent ResumeTurnBusyError swallow (oauth-callback.ts)
When the conversation lock is contended, ResumeTurnBusyError is caught and silently discarded with no log and no retry. The original paused turn is never resumed; the session stays stuck in awaiting_resume.
2. pendingAuth not cleared on session abandon (reply-executor.ts)
When a new user message arrives while a session is in awaiting_resume with resumeReason === "auth", the old session is abandoned and the turn is marked closed — but conversation.processing.pendingAuth is not cleared. The stale pending-auth record persists until the next successful credential issuance.
3. Related: JUNIOR-40 — GitHub OAuth refresh with HTTP 200 error body
In the GitHub plugin, refreshUserAccessToken calls github.com/login/oauth/access_token for token rotation. GitHub returns HTTP 200 for some error conditions. In edge cases where oauthErrorCode evaluates falsy (empty error field, non-JSON URL-encoded response), the check !response.ok || errorCode passes, and parseOAuthTokenResponse throws "OAuth token response missing access_token". Surfaces as an unhandled crash in the GitHub sandbox egress. See JUNIOR-40 — 33 events, 5 users, ongoing.
Alternative root causes to rule out
Before closing with the hypothesis above, confirm that Rahul's trace doesn't show:
- Token store miss immediately after callback — would indicate user ID / provider key mismatch between callback write (
stored.userId) and broker read (credentialUserSubjectId)
- Credential not injected — would indicate the Sentry CLI request host doesn't match
plugin.yaml domains (sentry.io, us.sentry.io, de.sentry.io)
- No upstream 401 before re-prompt — would point to pending-auth/session-reuse logic as root cause
Fix direction
Primary: Stop unconditionally deleting provider credentials on every auth_required signal. Differentiate:
- Token-store miss / no credential available → re-auth makes sense
- Upstream 401 after credential injection → likely scope/org mismatch; surface a specific error; do not unlink until the token is confirmed invalid or user explicitly requests reconnect
Secondary:
- Log
ResumeTurnBusyError instead of silently swallowing it; consider a retry mechanism
- Clear
pendingAuth when abandoning an awaiting_resume auth session
- Add structured audit logs (provider, user, session, token-store hit/miss, upstream status) to make re-auth loops diagnosable
Evidence
- Reported in
#proj-junior
- Related Sentry event: JUNIOR-40 — GitHub OAuth token refresh crash, ongoing
Summary
After completing a provider OAuth flow in the browser (user sees "Provider account connected"), Junior re-prompts for authorization on the next turn. The browser showed success, but subsequent Sentry API calls trigger another auth link.
Confirmed facts
auth_requiredsignal from the sandbox egress layer. That signal fires when the credential broker throwsCredentialUnavailableError(token missing/expired) or when an upstream API returns 401.auth_requiredfires viamaybeHandleAuthSignal,startAuthorizationPauseis always called withunlinkExistingProvider: true. This callsunlinkProvider, which deletes the stored OAuth token before sending a new auth link.Most likely root cause
auth_requiredsignal for the sentry provider.startAuthorizationPause("sentry", { unlinkExistingProvider: true }).unlinkProviderdeletes the freshly stored Sentry token from the user token store.This means every time the Sentry API rejects the token (for any reason), the system does a destructive re-auth that wipes a potentially-valid credential rather than surfacing an actionable error.
Supporting bugs
1. Silent
ResumeTurnBusyErrorswallow (oauth-callback.ts)When the conversation lock is contended,
ResumeTurnBusyErroris caught and silently discarded with no log and no retry. The original paused turn is never resumed; the session stays stuck inawaiting_resume.2.
pendingAuthnot cleared on session abandon (reply-executor.ts)When a new user message arrives while a session is in
awaiting_resumewithresumeReason === "auth", the old session is abandoned and the turn is marked closed — butconversation.processing.pendingAuthis not cleared. The stale pending-auth record persists until the next successful credential issuance.3. Related: JUNIOR-40 — GitHub OAuth refresh with HTTP 200 error body
In the GitHub plugin,
refreshUserAccessTokencallsgithub.com/login/oauth/access_tokenfor token rotation. GitHub returns HTTP 200 for some error conditions. In edge cases whereoauthErrorCodeevaluates falsy (emptyerrorfield, non-JSON URL-encoded response), the check!response.ok || errorCodepasses, andparseOAuthTokenResponsethrows "OAuth token response missing access_token". Surfaces as an unhandled crash in the GitHub sandbox egress. See JUNIOR-40 — 33 events, 5 users, ongoing.Alternative root causes to rule out
Before closing with the hypothesis above, confirm that Rahul's trace doesn't show:
stored.userId) and broker read (credentialUserSubjectId)plugin.yamldomains (sentry.io,us.sentry.io,de.sentry.io)Fix direction
Primary: Stop unconditionally deleting provider credentials on every
auth_requiredsignal. Differentiate:Secondary:
ResumeTurnBusyErrorinstead of silently swallowing it; consider a retry mechanismpendingAuthwhen abandoning anawaiting_resumeauth sessionEvidence
#proj-junior