From 97359454bb9ad6207279b89effd59292eb906e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E5=9F=BA=E9=AD=81?= <1412414664@qq.com> Date: Wed, 8 Jul 2026 10:05:49 +0800 Subject: [PATCH] fix(runtime): skip stale pid registration --- .changeset/quiet-plants-train.md | 5 ++++ src/runtime/task-registry.ts | 9 ++++++++ tests/task-registry.test.ts | 39 ++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 .changeset/quiet-plants-train.md diff --git a/.changeset/quiet-plants-train.md b/.changeset/quiet-plants-train.md new file mode 100644 index 0000000..3111d83 --- /dev/null +++ b/.changeset/quiet-plants-train.md @@ -0,0 +1,5 @@ +--- +"opencode-copilot-delegate": patch +--- + +Skip delayed orphan PID registration after a Copilot task has already reached a terminal state. diff --git a/src/runtime/task-registry.ts b/src/runtime/task-registry.ts index dcf5048..1ff8eea 100644 --- a/src/runtime/task-registry.ts +++ b/src/runtime/task-registry.ts @@ -47,6 +47,11 @@ export type CreateTaskInput = Omit & { } const tasks = new Map() +const TERMINAL_STATUSES = new Set([ + 'complete', + 'failed', + 'cancelled', +]) export function createTask( input: CreateTaskInput, @@ -65,6 +70,10 @@ export function createTask( if (task.pid > 0 && pidFilePath) { getPidIdentity(task.pid) .then((identity) => { + if (TERMINAL_STATUSES.has(task.status)) { + return + } + if (identity) { appendPidEntry( pidFilePath, diff --git a/tests/task-registry.test.ts b/tests/task-registry.test.ts index 8544b65..1d0618c 100644 --- a/tests/task-registry.test.ts +++ b/tests/task-registry.test.ts @@ -227,6 +227,45 @@ describe('task registry PID-file hooks', () => { expect(() => readFileSync(pidFilePath, 'utf-8')).toThrow() }) + it('does not append to the PID file after a terminal transition wins the spawn race', async () => { + const dir = mkdtempSync(join(tmpdir(), 'task-registry-')) + tempPaths.push(dir) + const pidFilePath = join(dir, 'orphans.pids') + + const child = Bun.spawn({ cmd: ['sleep', '30'] }) + childHandles.push(child) + const abortController = new AbortController() + + const task = createTask( + { + parentSessionID: 'session-fast-terminal', + pid: child.pid, + startedAt: Date.now(), + status: 'running', + args: ['-c', 'sleep 30'], + cwd: process.cwd(), + stdoutLineBuffer: '', + events: [], + child, + completionPromise: child.exited.then(() => undefined), + abortController, + pidFilePath, + }, + undefined, + ) + + setStatus(task, 'complete', { pidFilePath }) + + const staleEntryAppeared = await waitUntil( + () => + existsSync(pidFilePath) && + readFileSync(pidFilePath, 'utf-8').includes(`${child.pid}\t`), + { timeoutMs: 300, intervalMs: 25 }, + ) + + expect(staleEntryAppeared).toBe(false) + }) + it('skips PID-file work when pid <= 0', async () => { // Given a task with pid === 0 (sentinel for "no subprocess yet"). // The createTask guard `task.pid > 0 && pidFilePath` must short-circuit