🤖 refactor: convert OAuthFlowManager flow lifecycle to Effect per-flow Scope - #4033
Merged
Conversation
This comment has been minimized.
This comment has been minimized.
Member
Author
|
@codex review |
Member
Author
|
@codex security review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
This comment has been minimized.
This comment has been minimized.
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Sep 1, 2026
This was referenced Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 5 of the progressive Effect migration (first phase of Wave 2, the wave's gating item deferred since #4027): converts
OAuthFlowManagerinternals to Effect with real resource safety. Every registered desktop OAuth flow now owns a per-flowScopewhose release finalizers guarantee cleanup (registration-timeout clear, deferred settlement, loopback-server close) on every termination path — finish, cancel, caller-timeout race, duplicate registration,shutdownAll, and defects. The Promise-based public API is preserved as thinEffect.runPromisefacades, so the three not-yet-converted OAuth services (coderOauthService,codexOauthService,muxGovernorOauthService) and all existing tests work unchanged.Background
Wave 1 (#4022, #4025, #4027, #4028, #4030, #4031, #4032) established the house pattern:
Effect.geninternals, thinrunPromisefacades,handlerGenfor oRPC procedures. #4027 convertedmuxGatewayOauthServicebut explicitly deferred the shared flow-lifecycle manager: its resources (loopbackhttp.Server, registrationsetTimeout, result deferred) were cleaned up via ad-hoctry/catch+ fire-and-forgetvoid closeServer(...), and a defect while resolving the deferred silently skipped the server close. This PR is the acquire/release case that deferral pointed at, and unblocks Phase 6 (batch conversion of the sibling OAuth services).Implementation
Per-flow Scope design —
registercreates aScope.makeUnsafe()per flow and moves ownership of the caller-acquired resources into it via oneEffect.acquireReleaseper resource (a combined acquisition would install its finalizer only after every step succeeded, leaking earlier resources on a later defect — the #4031 Codex P2 lesson). Release runs in reverse acquisition order, preserving the pre-Effectfinishordering: clear registration timeout → settle deferred (waiters unblock before the async close) → close loopback server (awaited).Deferred settlement via finalizer — each
ActiveFlowcarries a mutablefinalResultstaged by the terminating path (finish/cancel/shutdown/replace); the settle finalizer resolves the caller's deferred with it. Settlement is therefore scope-guaranteed rather than an ad-hocresolvecall, with a defensive fallback result so waiters can never hang.Caller-facing timeout race —
waitFormaps toEffect.timeoutoverEffect.promiseon the shared deferred: the local wait timer is fiber-managed (interruption clears it), stays separate from the registration-time timeout, and on any error result runsfinishfor shared cleanup. The cleanup's synchronous bookkeeping (map removal, completed-result recording) runs beforewaitForresolves — exact parity with the old sync prefix — while the async release runs in anEffect.forkDetachfiber, replacing the oldvoid this.finish(...)fire-and-forget with a supervised fiber that survives the caller's completion (verified by a live-runtime probe: detached fibers outlive the parent,runFork/runPromiseexecute synchronously to first suspension, and a throwing finalizer does not skip its siblings).shutdownAll contract — preserved as async (
Promise<void>facade):serviceContainer.disposeawaits it, and loopback-server closes are bounded by the server's force-finish socket handling. It never rejects; release defects are caught (Effect.catchDefect) and logged at debug level, per the startup/shutdown-must-never-crash rule.Effect-native surface —
waitForEffect/cancelEffect/finishEffect/cancelAllEffect/shutdownAllEffectare public (wire-shaped, never-failing — same shape as #4032's Effect surfaces).muxGatewayOauthService's Effect pipeline now yieldsfinishEffectdirectly instead ofEffect.promise(() => …finish(...)), and its registration-timeout callback usesEffect.runFork(finishEffect(...))instead ofvoid finish(...).Not converted to Effect
Deferred— the result deferred's identity is part of the public caller-ownedOAuthFlowEntry(the three unconverted services construct entries withcreateDeferred), so swapping it would break the "existing callers unchanged" contract; revisit when Phase 6 converts entry construction.Validation
oauthFlowManagertests pass byte-identical, plus all OAuth service suites (194 tests: coder/codex/muxGateway/muxGovernor/mcp/copilot/codexOauthAuth) and loopback-server/oauthUtils suites.resolvethrows (the pre-Effect code skipped the close — this test fails on the old implementation), and (2) the detached cleanup fiber completes afterwaitForhas already returned on the timeout path (guards against accidental child-fiber supervision, where the release would be interrupted with the caller).runPromise/runFork,forkDetachoutliving the parent,Effect.timeout+Effect.catchoverEffect.promise).make static-checkgreen.Risks
Low-to-moderate: this is shared lifecycle code under four OAuth login flows (Gateway, Governor, Codex, Coder). The public API, observable ordering (map removal before
finishresolves, deferred settlement before server close, synchronousregister), and error strings are preserved exactly; regressions would surface as leaked loopback listeners, hungwaitForcalls, or unsettled deferreds — all covered by the existing + new suites.Lessons for Phase 6
Phase 6 is the batch conversion of
coderOauthService,codexOauthService,muxGovernorOauthService,copilotOauthService, plus their ~20 router sites. Notes to make it mechanical:waitForEffect/cancelEffect/finishEffect/shutdownAllEffect, so converted service pipelines can yield them directly (seedesktopCallbackPipelineinmuxGatewayOauthServiceas the template), and registration-timeout callbacks should useEffect.runFork(manager.finishEffect(...)).beginFinish's sync-bookkeeping/async-release split is the pattern to reach for wherever a service needs "unregister now, release in background" semantics.startDesktopFlowshould become uninterruptible like the gateway's (🤖 refactor: adopt handlerGen as the oRPC router default and convert gateway OAuth procedures #4032): a client abort between loopback acquisition andregisterwould otherwise leak the server.coderOauthServiceis the outlier: it has extra commit-path liveness checks (has) and multi-step persist/commit finish calls (~10desktopFlows.*sites vs ~5 in the others) — expect most of the Phase 6 effort there.handlerGen(thewaitFor/cancelhandlers for the gateway can join here — the router comment atmuxGatewayOauthalready points at this). PR B:coderOauthServicealone — its commit/persist liveness semantics deserve isolated review, and a combined PR would bury it under the mechanical churn.Generated with
mux• Model:anthropic:claude-fable-5• Thinking:xhigh