diff --git a/src/core/worker-pool.ts b/src/core/worker-pool.ts index ac3fc6ea8..0ab1ad21a 100644 --- a/src/core/worker-pool.ts +++ b/src/core/worker-pool.ts @@ -10750,7 +10750,21 @@ function setupWorkerHandlers( logger.warn(`[${t}] Ignored restart_result from stale worker generation`); break; } - restartCoordinator.resolve(ds.session.sessionId, msg.attemptId, msg.status); + const restartSettled = restartCoordinator.resolve( + ds.session.sessionId, + msg.attemptId, + msg.status, + ); + // requestSessionRestart() fences the live generation by clearing + // workerReady before asking the worker to respawn its CLI in place. An + // in-worker respawn does not emit the process-level `ready` message + // again, so the matching successful receipt is the authoritative edge + // that must release that fence. Without this assignment the terminal + // can be prompt-ready/idle while relay and fork remain permanently + // blocked as `worker_busy`. + if (restartSettled && msg.status === 'succeeded') { + ds.workerReady = true; + } break; } diff --git a/test/restart-worker-ready-lifecycle.test.ts b/test/restart-worker-ready-lifecycle.test.ts new file mode 100644 index 000000000..6bd7b2d13 --- /dev/null +++ b/test/restart-worker-ready-lifecycle.test.ts @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const workerPoolSource = readFileSync( + new URL('../src/core/worker-pool.ts', import.meta.url), + 'utf8', +); + +describe('in-worker restart lifecycle fence', () => { + it('releases workerReady only after the current restart succeeds', () => { + const start = workerPoolSource.indexOf("case 'restart_result':"); + const end = workerPoolSource.indexOf("case 'cli_session_id':", start); + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + + const branch = workerPoolSource.slice(start, end); + const staleWorkerGuard = branch.indexOf('if (ds.worker !== worker)'); + const resolve = branch.indexOf('restartCoordinator.resolve('); + const successGuard = branch.indexOf("restartSettled && msg.status === 'succeeded'"); + const releaseFence = branch.indexOf('ds.workerReady = true;'); + + expect(staleWorkerGuard).toBeGreaterThanOrEqual(0); + expect(resolve).toBeGreaterThan(staleWorkerGuard); + expect(successGuard).toBeGreaterThan(resolve); + expect(releaseFence).toBeGreaterThan(successGuard); + }); + + it('keeps failed or stale restart receipts from releasing workerReady', () => { + const start = workerPoolSource.indexOf("case 'restart_result':"); + const end = workerPoolSource.indexOf("case 'cli_session_id':", start); + const branch = workerPoolSource.slice(start, end); + + expect(branch.match(/ds\.workerReady = true;/g)).toHaveLength(1); + expect(branch).toContain("if (restartSettled && msg.status === 'succeeded')"); + }); +});