Voice server permanent deadlock when pid file is deleted while server process survives (bind-loser exits before writing pid)
まだ誰も着手していません。
- 主要言語
- Shell
- スター
- 11.2k
- フォーク
- 1.9k
- 平均マージ
- 14時間 16分
- マージ済み PR(30日)
- 6
説明
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.
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
app.js の欠落または古い pid-file パスから始め、voice-server.js の bind-loser 処理と比較します。サーバーと named pipe が稼働したままの状態で pid-file を削除して、障害を再現します。完了の条件は、後続の voice 呼び出しが再接続するか、有効な liveness 情報を復元し、spawn の試行を繰り返した後にタイムアウトしないことです。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- javascript
- 領域
- backend, cli, operating-systems
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 活発
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 64/100