Voice server permanent deadlock when pid file is deleted while server process survives (bind-loser exits before writing pid)
Nobody has claimed this yet.
- Dominant language
- Shell
- Stars
- 11.2k
- Forks
- 1.9k
- Avg merge
- 14h 16m
- Merged PRs (30d)
- 6
Description
Summary
Voice server enters a permanent deadlock ("Failed to connect to or spawn the voice server after 3 attempts: voice server did not write a valid pid file within 8000ms") whenever its pid file is deleted (e.g. by OS temp-directory cleanup) while the server process itself is still alive and holding the named pipe.
Root cause (traced in shipped 1.0.84-1 win32-x64 build)
Voice IPC uses two independent records of server liveness that can desync:
- A Windows named pipe
\\.\pipe\copilot-voice-<hash>(kernel object, survives independently) - A pid file at
%TEMP%\copilot-voice\<hash>.pid(the client's only source of truth)
Both <hash> values are derived deterministically: sha256(cliVersion + "|" + sha256(USERPROFILE).slice(0,8)).slice(0,8).
Sequence that produces the deadlock:
- A server starts, wins the pipe, writes its pid file. It keeps running (idle-shutdown only arms once its connected-client count reaches 0; clients that die without a clean disconnect can keep that count above 0 indefinitely, so a server can persist for days).
- The pid file is removed independently of the process (observed cause: Windows temp-directory cleanup). The pipe is untouched — it's a kernel object, not a file.
app.jsclient logic reads the pid file to decide whether to connect or spawn:
A missing pid file skips the connect branch entirely, even thoughlet p = readPid(t.pidFile); if (p !== null && isAlive(p)) { connect(t.address) /* ... */ } // else: falls straight through to spawning a new server — never attempts connect(t.address) directlyt.addressis already known deterministically and the pipe is live and accepting connections.- The client spawns a new server. The new process computes the same pipe name, tries to bind, and loses to the still-running original.
- In
voice-server.js, the losing branch exits before ever writing a pid file:S === "lost" && (log("bind-loser..."), await i.dispose(), process.exit(id)) // id = 10 // ... pid file write (Lo(t.pidFile, process.pid)) is further down, never reached here - The client waits the full pid-file timeout (
$_n = 8e3, 8000ms) for a file that can never appear, then retries (WJe = [0,100,500], 3 attempts total), then throws the error above.
Every retry repeats step 4-6 identically — this is a permanent deadlock, not a transient failure. It only resolves if something external kills the original server or manually restores the pid file.
Suggested fixes (either alone fixes it)
- In the client (
app.js), when the pid file is missing/stale, attemptconnect(t.address)directly before spawning — the address is already computed deterministically, so this costs nothing and immediately closes the race. - In the server (
voice-server.js), onbind-loser, write the winning pid (which is discoverable from the bind failure) to the pid file before exiting, instead of exiting silently with no pid file at all.
Either change means a deleted pid file can never produce an unrecoverable state as long as the real owning process is still alive.
Environment
- Copilot CLI 1.0.84-1, win32-x64
- Reproduced by manually deleting
%TEMP%\copilot-voice\<hash>.pidwhile a previously-started voice server process was still running; the next voice-triggering CLI invocation failed with the exact reported error, deterministically, on every retry.
Workaround used
Killing the orphaned server process (identified via its named pipe and boot-time server log) releases the pipe, allowing a fresh spawn to bind and write its pid file normally. A local sessionStart hook now detects this exact signature (dead/missing pid file + live matching pipe + exactly one candidate copilot.exe --prefer-version <version> process) and self-heals by writing the correct pid before the CLI ever attempts to connect.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in app.js at the missing or stale pid-file path, then compare it with the bind-loser handling in voice-server.js. Reproduce the failure by deleting the pid file while the server and named pipe remain alive. Done means a subsequent voice invocation reconnects or restores valid liveness information instead of timing out after repeated spawn attempts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- backend, cli, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100