@@ -19,9 +19,11 @@ import {
1919 type PolicySkipReason ,
2020} from "./policy" ;
2121import {
22+ cancelQueuedStorageWorkerSpawns ,
2223 drainStorageWorkers ,
2324 registerStorageWorker ,
2425 terminateStorageWorker ,
26+ withStorageWorkerSpawnGate ,
2527} from "./worker-lifecycle" ;
2628
2729export type PolicyJobStatus = "idle" | "running" ;
@@ -148,10 +150,27 @@ export function resetStorageCleanupPolicyJobForTests(): void {
148150 * worker still exiting at that moment trips a Bun-internal assertion on Windows
149151 * and takes the whole run down, so a suite that spawns workers must be able to
150152 * wait for them rather than fire-and-forget.
153+ *
154+ * Do not route through the sync reset's `void terminate(...)`: that used to
155+ * deregister the worker before the thread exited, so a follow-up drain saw an
156+ * empty registry and returned while Windows was still reclaiming the thread.
151157 */
152158export async function resetStorageCleanupPolicyJobForTestsAsync ( ) : Promise < void > {
153- resetStorageCleanupPolicyJobForTests ( ) ;
154- await drainStorageWorkers ( ) ;
159+ disownActiveRun ( ) ;
160+ cancelQueuedStorageWorkerSpawns ( ) ;
161+ const worker = activeWorker ;
162+ activeWorker = null ;
163+ inflight = null ;
164+ testHooks = null ;
165+ state = { status : "idle" } ;
166+ // Join the worker before releasing the mutation slot so a concurrent run
167+ // cannot acquire CODEX_HOME while the aborted thread is still mutating.
168+ try {
169+ if ( worker ) await terminateStorageWorker ( worker ) ;
170+ await drainStorageWorkers ( ) ;
171+ } finally {
172+ releaseHeldMutationSlot ( ) ;
173+ }
155174}
156175
157176/** Terminate an in-flight worker during process shutdown. */
@@ -161,6 +180,8 @@ export function abortStorageCleanupPolicyJob(): void {
161180 void terminateStorageWorker ( activeWorker ) ;
162181 activeWorker = null ;
163182 }
183+ // Sync path cannot join; prefer abortStorageCleanupPolicyJobAsync when the
184+ // caller can await so the mutation slot stays held until the thread exits.
164185 releaseHeldMutationSlot ( ) ;
165186 if ( state . status === "running" ) {
166187 state = {
@@ -177,6 +198,37 @@ export function abortStorageCleanupPolicyJob(): void {
177198 inflight = null ;
178199}
179200
201+ /**
202+ * Shutdown path that joins worker threads. Prefer this over the sync abort when
203+ * the caller can await (server drain, test teardown) so Windows isolate reclaim
204+ * does not race a still-exiting storage worker.
205+ */
206+ export async function abortStorageCleanupPolicyJobAsync ( ) : Promise < void > {
207+ disownActiveRun ( ) ;
208+ cancelQueuedStorageWorkerSpawns ( ) ;
209+ const worker = activeWorker ;
210+ activeWorker = null ;
211+ if ( state . status === "running" ) {
212+ state = {
213+ ...state ,
214+ status : "idle" ,
215+ finishedAt : Date . now ( ) ,
216+ lastError : "aborted" ,
217+ lastOutcome : {
218+ ok : false ,
219+ error : "evaluation_failed" ,
220+ } ,
221+ } ;
222+ }
223+ inflight = null ;
224+ try {
225+ if ( worker ) await terminateStorageWorker ( worker ) ;
226+ await drainStorageWorkers ( ) ;
227+ } finally {
228+ releaseHeldMutationSlot ( ) ;
229+ }
230+ }
231+
180232function outcomeFromResult ( result : PolicyRunResult ) : PolicyJobOutcome {
181233 return {
182234 ok : result . ok ,
@@ -232,22 +284,13 @@ function applyMutationBusy(): void {
232284}
233285
234286function runInWorker ( opts : RequestPolicyRunOptions & { blockMs ?: number } ) : Promise < PolicyRunResult > {
235- return new Promise ( ( resolve , reject ) => {
287+ return withStorageWorkerSpawnGate ( ( ) => new Promise < PolicyRunResult > ( ( resolve , reject ) => {
236288 const requestId = crypto . randomUUID ( ) ;
237289 let settled = false ;
238290 const worker = new Worker ( new URL ( "./policy-worker.ts" , import . meta. url ) . href ) ;
239291 registerStorageWorker ( worker ) ;
240292 activeWorker = worker ;
241293
242- const timer = setTimeout ( ( ) => {
243- if ( settled ) return ;
244- settled = true ;
245- cancelActiveRun = null ;
246- void terminateStorageWorker ( worker ) ;
247- if ( activeWorker === worker ) activeWorker = null ;
248- reject ( new Error ( "storage_cleanup_worker_timeout" ) ) ;
249- } , WORKER_TIMEOUT_MS ) ;
250-
251294 const finish = ( fn : ( ) => void ) => {
252295 if ( settled ) return ;
253296 settled = true ;
@@ -256,10 +299,15 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
256299 if ( activeWorker === worker ) activeWorker = null ;
257300 // Settle the caller only after the thread is actually gone, so a suite
258301 // that awaits its request cannot reach the next test file with a worker
259- // still exiting behind it.
302+ // still exiting behind it. Also keeps executeJob's finally from releasing
303+ // the mutation slot while the thread may still mutate CODEX_HOME.
260304 void terminateStorageWorker ( worker ) . then ( fn , fn ) ;
261305 } ;
262306
307+ const timer = setTimeout ( ( ) => {
308+ finish ( ( ) => reject ( new Error ( "storage_cleanup_worker_timeout" ) ) ) ;
309+ } , WORKER_TIMEOUT_MS ) ;
310+
263311 cancelActiveRun = ( ) => {
264312 finish ( ( ) => reject ( new Error ( "aborted" ) ) ) ;
265313 } ;
@@ -296,7 +344,7 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
296344 ...( process . env . OPENCODEX_HOME ? { OPENCODEX_HOME : process . env . OPENCODEX_HOME } : { } ) ,
297345 } ,
298346 } ) ;
299- } ) ;
347+ } ) ) ;
300348}
301349
302350async function executeJob ( opts : RequestPolicyRunOptions ) : Promise < void > {
0 commit comments