🤖 refactor: adopt handlerGen as the oRPC router default and convert gateway OAuth procedures - #4032
Merged
Merged
Conversation
…ment handlerGen convention
This comment has been minimized.
This comment has been minimized.
Member
Author
|
@codex review |
Member
Author
|
@codex security review |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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
Final phase of the progressive Effect migration roadmap: makes
handlerGenthe documented default for future oRPC procedures, converts the two remaining router procedures whose backing service is already Effect-native (muxGateway.getAccountStatus,muxGatewayOauth.startDesktopFlow), and delivers the streamManager placement decision plus a migration completion audit (below). Deliberately progressive, not wholesale: procedures backed by Promise services are left untouched — the convention is to convert the service surface first, never to wrap Promises in Effect at the router.Background
Phases 0–3 landed in #4022 (spike/effectBridge), #4025 (memory), #4027 (retryManager + gateway OAuth internals), #4028 (providerService), #4030 (providerModelFactory), #4031 (heartbeat/idle workers on Schedule+Scope).
router.tshas ~315 handler sites; 11 already rodehandlerGen(7memory.*, 4providers.*mutations). This PR audits the remaining 300+, converts exactly the ones whose backing pipelines already exist as Effect, and encodes the go-forward convention as a module doc inrouter.ts.Implementation
muxGatewayOauthService: the Effect pipelines from 🤖 refactor: convert retryManager and muxGatewayOauthService internals to Effect #4027 were private behindEffect.runPromisefacades. They are now exposed as wire-shaped public Effect methods (matching theproviderService.setConfigEffecthouse pattern):getAccountStatusEffect()— left interruptible: the balance fetch is a pure read, and the session-expired credential clear is a single best-effort promise that runs to completion even if the fiber is interrupted while awaiting it (JS promises are not cancelled by fiber interruption).startDesktopFlowEffect()— wrapped inEffect.uninterruptible(mirrorsasAtomicMutationin providerService): a client abort between loopback-server acquisition anddesktopFlows.registerwould otherwise leak the server with nothing left to close it.runPromisewrappers) so the existing service tests stay byte-identical; the router no longer calls them.router.ts: the two procedures ridehandlerGen; a module doc codifies the convention (handlerGen default for new unary procedures; plain handlers only for Promise-backed services pending conversion, event-iterator subscriptions, and trivial sync reads; audit abort-atomicity before converting mutations). No lint rule: no cheap existing rule expresses "async handler in this one file is suspect" without flagging the ~280 legitimately deferred sites, and building lint infrastructure is out of scope.Conversion audit
Converted here (2):
muxGateway.getAccountStatusmuxGatewayOauthService(Effect since #4027)muxGatewayOauth.startDesktopFlowmuxGatewayOauthServiceEffect.uninterruptible— prevents loopback-server leak on client abortAlready on handlerGen (11):
memory.list/read/save/delete/setPinned/consolidationStatus/consolidate(#4025),providers.addCustomProvider/removeCustomProvider/setProviderConfig/setModels(#4028). Total after this PR: 13.Audited and deferred (with reasons):
muxGatewayOauth.waitForDesktopFlow/cancelDesktopFlowOAuthFlowManager(promise-native deferred registry)providers.list/getConfigproviderServicesync readsproviders.updateRoutePreferencesConfig(Promise)subscribe*,onChange,onConfigChanged, terminal/chat streams)handlerGencannot produce event iterators; blocked on an Effect Stream bridge (existing backlog item from #4025)config.*(19 sites)Config(Promise)projects.idleCompaction.get/set,workspace.heartbeat.setprojectService/workspaceServicesettings storescodexOauth/copilotOauth/coderOauth/muxGovernorOauth(~18 sites)workspace.*(~47),projects.*(~23),mcp*(~26),terminal,analytics,backup,update, remaining (~200 total)streamManager placement decision
Recommendation: defer wholesale conversion; migrate by seams, starting with the two lifecycle seams below. (Analysis of the 5,281-line file, informed by the #4031 lesson that
runSync(Scope.close(...))only composes when fibers suspend on clock timers.)Why wholesale conversion is wrong right now:
AbortControllermismatch. streamManager sits on AI SDK v5streamText, cancelled via WebAbortSignal(per-stream controllers allocated instartStream, polled everyfullStreamiteration). Interrupting a fiber does not cancel the SDK network stream; every one of the ~30 abort touchpoints would need dual-cancellation glue (Scope finalizer →abort()and signal → interrupt).fullStreamconsumption loop (processStreamWithCleanup, ~750 lines) suspends on network I/O and tool execution — exactly the case where 🤖 refactor: convert periodic-worker scheduling to Effect Schedule + Scope #4031 showed synchronousScope.closecannot work. Teardown must berunPromise-based with stopped-flag latching, otherwise late chunks race new streams in the same workspace slot and can corruptpartial.json.WorkspaceStreamInfocarries 30+ interconnected fields (parts accumulation, step tracker, usage accumulators, fallback chains, pending tool buffers, throttle timers) mutated across four phases; a single-pass rewrite would touch hundreds of transitions at once.TurnEngineEventSinkpushes toAIService/AgentSession/IPC; bridging to EffectStream/Hubforces cross-layer churn in three consumers.Proposed seam map for incremental follow-up (in order):
createTempDirForStream/cleanupStreamTempDir)Effect.acquireReleasein a per-stream ScopeschedulePartialWrite/flushPartialWrite)setTimeout+ promise chainingEffect.sleep+ interrupt), same template as idleDispatcher (#4031)partialWriteTimernot pinnedcategorizeError,isResponseIdLost)SetSchema.TaggedErrorclassification pipelines;Reffor the registryisResponseIdLostasserted directly; one test pinscreateStreamResultvia castrecordSessionUsage,resolveTotalUsageForStreamEnd)Effect.genpipelinestokenTrackerfield (re-type as marker per #4031 lesson if swapped)Seams 1–2 are the #4031 patterns verbatim and are safe first steps; the outer stream engine (reader loop, retry/fallback chains, event sink) should convert last, if ever, and only after seams shrink it.
Migration completion state
Effect-native today:
memoryOperations/memoryMeta(#4025),retryManager+muxGatewayOauthService(#4027, public Effect surface as of this PR),providerServicemutations (#4028),providerModelFactory(#4030),heartbeatService/idleCompactionService/idleDispatcher(#4031). Router: 13/~315 sites on handlerGen; every remaining site is either a subscription (Stream bridge backlog) or backed by a Promise service.Suggested future order (value ÷ risk):
waitForDesktopFlow/cancelDesktopFlowplus the four sibling OAuth services (~20 router sites) as mechanical batches.memory.onChangebacklog item.Validation
make static-checkgreen;bun test src/node/services/muxGatewayOauthService.test.ts src/node/orpc/effectBridge.test.ts src/node/orpc/router.test.ts— 23 pass, 0 fail, tests unchanged.runPromiseover the same Effects the router now yields), covering the session-expired credential-clear path and desktop-flow start/callback/exchange.Risks
Low. Wire contracts, schemas, and service behavior are unchanged; the two converted procedures execute the same Effect pipelines as before, now directly on the oRPC fiber instead of behind
runPromise. The one intentional semantic change: client aborts can now interruptgetAccountStatusmid-fetch (previously it always ran to completion) — safe for a read; the credential-clear write is single-promise atomic.startDesktopFlowis explicitly uninterruptible, so its abort behavior is identical to before.Generated with
xum• Model:anthropic:claude-fable-5• Thinking:xhigh• Cost:$0.00