ListPeers misses live uds sessions — updatePidFile is non-atomic read-modify-write
Symptom
ListPeers returns sessions in the bridge-only list even when they have a live uds socket in /tmp/cc-socks/. Affected sessions are unreachable via uds: and the only fallback (bridge:) triggers a Remote Control permission dialog.
Root cause
updatePidFile in src/utils/concurrentSessions.ts (observed at sha fae5242, version 2.1.89-dev.20260330) does a non-atomic read-modify-write:
async function updatePidFile(patch) {
let pidFile = join(getSessionsDir(), `${process.pid}.json`);
let data = jsonParse(await readFile(pidFile, "utf8"));
await writeFile(pidFile, jsonStringify({ ...data, ...patch }));
}
Two failure modes observed
1. Lost-update race — Concurrent updatePidFile calls (e.g., updateSessionName racing updateSessionActivity) both read stale data; the second write clobbers the first. Sessions lose their name field → ListPeers drops them from the named uds list.
2. Null-byte truncation — Sessions that crash mid-writeFile leave files ending ...sock",<20-24 null bytes>. Observed on 3 of 13 session files:
| File | Size | Nulls | Valid bytes | Missing fields |
|---|---|---|---|---|
| 66766.json | 241 | 24 | 217 | name, status, updatedAt, bridgeSessionId |
| 73234.json | 233 | 20 | 213 | same |
| 512175.json | 230 | 21 | 209 | same |
All three truncate immediately after messagingSocketPath's trailing comma. Possibly Bun's writeFile pre-sizes (fallocate/ftruncate) before filling, and the crash lands between.
Corroborating evidence
Nudge's ~/.claude/slack_sessions/*.json has the same failure pattern — slack_sync_debug.log shows repeated "Error reading session file .../hook_*.json: Expecting value: line 1 column 1 (char 0)". Same non-atomic writer pattern likely.
Fix
Atomic tmp+rename — saveSessionIndex in the same file already does this correctly:
async function updatePidFile(patch) {
let pidFile = join(getSessionsDir(), `${process.pid}.json`);
let tmp = `${pidFile}.${Date.now()}.tmp`;
let data = jsonParse(await readFile(pidFile, "utf8"));
await writeFile(tmp, jsonStringify({ ...data, ...patch }));
await rename(tmp, pidFile); // atomic on same fs
}
This fixes the null-byte case. The lost-update race additionally needs either in-process serialization (a mutex around updatePidFile since it's always the same pid) or ListPeers robustness (see below).
Secondary
ListPeers should tolerate parse failures — fall back to /proc/<pid>/cwd when the JSON is bad:
for s in /tmp/cc-socks/*.sock; do
pid=${s##*/}; pid=${pid%.sock}
readlink /proc/$pid/cwd
done
This is what users currently do manually to discover sessions ListPeers misses.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗