From d4497fbb5fd625c9072739f96cffd74edfb27b3f Mon Sep 17 00:00:00 2001 From: sunxiaoxue Date: Thu, 27 Aug 2026 12:36:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E9=87=8D=E5=90=AF=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E5=90=8E=E6=81=A2=E5=A4=8D=20worker=20=E5=B0=B1?= =?UTF-8?q?=E7=BB=AA=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/worker-pool.ts | 16 ++++++++- test/restart-worker-ready-lifecycle.test.ts | 36 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 test/restart-worker-ready-lifecycle.test.ts 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')"); + }); +});