Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-plants-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-copilot-delegate": patch
---

Skip delayed orphan PID registration after a Copilot task has already reached a terminal state.
9 changes: 9 additions & 0 deletions src/runtime/task-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export type CreateTaskInput = Omit<SpawnCopilotResult, 'taskId'> & {
}

const tasks = new Map<string, TaskState>()
const TERMINAL_STATUSES = new Set<TaskState['status']>([
'complete',
'failed',
'cancelled',
])

export function createTask(
input: CreateTaskInput,
Expand All @@ -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,
Expand Down
39 changes: 39 additions & 0 deletions tests/task-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down