Voice server permanent deadlock when pid file is deleted while server process survives (bind-loser exits before writing pid)
还没有人认领这个 Issue。
- 主要语言
- Shell
- 星标
- 11.2k
- 派生
- 1.9k
- 平均合并
- 14 小时 16 分钟
- 30 天内合并 PR
- 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 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
从 app.js 中缺失或过期的 pid-file 路径开始,然后将其与 voice-server.js 中对 bind-loser 的处理进行比较。删除 pid-file,同时保持 server 和 named pipe 运行,以重现该故障。完成的标准是:后续的 voice 调用能够重新连接或恢复有效的 liveness 信息,而不是在反复尝试 spawn 后超时。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- javascript
- 领域
- backend, cli, operating-systems
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 活跃
- 描述清晰度
- 描述清楚
- 新手友好度
- 64/100