@@ -19,6 +19,7 @@ import { serverErrorSpanMiddleware } from "./http/server-error-span"
1919import { v2WorkerUnavailableResponse } from "./http/v2-worker-unavailable"
2020import { API_CORS_RESPONSE_HEADERS , apiCorsPreflightResponse } from "./http/api-cors"
2121import { persistSession , preloadSession , type SessionsBinding } from "./mcp/lib/session-store"
22+ import { makeRecoverablePromiseMemo } from "./platform/recoverable-promise-memo"
2223import { classifyWorkerQueue } from "./queue-dispatch"
2324
2425const WorkerFileSystemLive = FileSystem . layerNoop ( { } )
@@ -137,16 +138,7 @@ const buildHandler = async () => {
137138// Memoized via the build promise so concurrent first requests share one build.
138139// A rejected build is cleared after those callers observe it, allowing a later
139140// request to recover instead of pinning the isolate to a rejected promise.
140- let handlerPromise : ReturnType < typeof buildHandler > | undefined
141- const getHandler = ( ) : ReturnType < typeof buildHandler > => {
142- if ( handlerPromise !== undefined ) return handlerPromise
143- const pending = buildHandler ( )
144- handlerPromise = pending
145- void pending . catch ( ( ) => {
146- if ( handlerPromise === pending ) handlerPromise = undefined
147- } )
148- return pending
149- }
141+ const handlerMemo = makeRecoverablePromiseMemo ( buildHandler )
150142
151143// RPC has no HttpApi request to construct the application services for it, so
152144// it gets a sibling isolate-wide ManagedRuntime. Its headless service graph
@@ -156,7 +148,7 @@ const buildRpcRuntime = async (env: Record<string, unknown>) => {
156148 import ( "./runtime/mcp-service-graph" ) ,
157149 import ( "@/platform/DatabasePgLive" ) ,
158150 ] )
159- return ManagedRuntime . make (
151+ const runtime = ManagedRuntime . make (
160152 InvestigationServicesLive . pipe (
161153 Layer . provideMerge ( WorkerPlatformLive ) ,
162154 Layer . provideMerge ( layerPg ) ,
@@ -165,18 +157,19 @@ const buildRpcRuntime = async (env: Record<string, unknown>) => {
165157 Layer . provideMerge ( WorkerConfigProviderLayer ) ,
166158 ) ,
167159 )
160+ try {
161+ // ManagedRuntime also acquires lazily and retains a failed build fiber.
162+ // Acquire before resolving the recoverable outer promise so a later RPC
163+ // can construct a fresh runtime after an initialization failure.
164+ await runtime . context ( )
165+ return runtime
166+ } catch ( error ) {
167+ await runtime . dispose ( )
168+ throw error
169+ }
168170}
169171
170- let rpcRuntimePromise : ReturnType < typeof buildRpcRuntime > | undefined
171- const getRpcRuntime = ( env : Record < string , unknown > ) : ReturnType < typeof buildRpcRuntime > => {
172- if ( rpcRuntimePromise !== undefined ) return rpcRuntimePromise
173- const pending = buildRpcRuntime ( env )
174- rpcRuntimePromise = pending
175- void pending . catch ( ( ) => {
176- if ( rpcRuntimePromise === pending ) rpcRuntimePromise = undefined
177- } )
178- return pending
179- }
172+ const rpcRuntimeMemo = makeRecoverablePromiseMemo ( buildRpcRuntime )
180173
181174type InternalRpcMethod = "listMcpTools" | "callMcpTool" | "submitDiagnosis"
182175
@@ -207,7 +200,7 @@ const runInternalRpc = async (
207200 ctx : ExecutionContext ,
208201) => {
209202 const [ runtime , { callMcpToolRpc, listMcpToolsRpc, submitDiagnosisRpc } ] = await Promise . all ( [
210- getRpcRuntime ( env ) ,
203+ rpcRuntimeMemo . get ( env ) ,
211204 import ( "./internal-rpc" ) ,
212205 ] )
213206 let exit : Exit . Exit < unknown , unknown >
@@ -326,7 +319,7 @@ const handle = async (
326319 // Start the expensive cold handler build and the independent KV read before
327320 // buffering an MCP body. Warm requests resolve both promises immediately;
328321 // cold MCP requests hide module evaluation and KV latency behind body I/O.
329- const pendingHandler = getHandler ( )
322+ const pendingHandler = handlerMemo . get ( )
330323 const pendingSession = kv && reqSid ? preloadSession ( kv , reqSid ) : undefined
331324
332325 // MCP diagnostics: buffer the body so we can peek the JSON-RPC method/id
@@ -350,10 +343,19 @@ const handle = async (
350343 }
351344
352345 try {
353- const { handler } = pendingSession
346+ const built = pendingSession
354347 ? ( await Promise . all ( [ pendingHandler , pendingSession ] ) ) [ 0 ]
355348 : await pendingHandler
356- const response = await handler ( forwardRequest , HandlerContext )
349+ let response : Response
350+ try {
351+ response = await built . handler ( forwardRequest , HandlerContext )
352+ } catch ( error ) {
353+ // `toWebHandler` acquires lazily and pins a rejected inner build.
354+ // Evict only the exact wrapper used by this request so the next real
355+ // request can rebuild it; overlapping failures cannot clear a retry.
356+ if ( handlerMemo . evict ( pendingHandler ) ) await built . dispose ( )
357+ throw error
358+ }
357359 if ( kv && isMcp ) {
358360 const resSid = response . headers . get ( "mcp-session-id" )
359361 // Only persist when the server issued a new session — i.e. on
0 commit comments