
Resolving Codex CLI and Desktop Thread Lock Conflicts
Learn why transitioning between the Codex CLI and Desktop app can trigger thread writer lock errors caused by the managed app server daemon's 30-minute grace period. We explore workarounds like selective daemon stops, chat forking, and the unconfirmed --no-daemon flag, alongside fixes and bugs from recent 0.164.0 and 0.165.0 alpha releases.
Chapter 1
The Lock That Outlives Your Terminal
Ethan Park
Okay, quick one before you close that terminal. Thanks to Jellypod to help make this daily show a reality. Today is about a bug that bites anyone who bounces between the Codex CLI and the Desktop app on one machine. It builds on 0.164.0, which we covered a while back for inline model and permission switching and for saved thread settings on resume.
Maya
Here's the symptom, from issue 57004 on the Codex GitHub. A user on macOS, CLI 0.164.0, desktop build 26.1021.31345, finishes a turn. The CLI prints "Worked" and there's no task running. They exit. They open the same conversation in the Desktop app and get a banner that says, in English, "Open in another application. Close the conversation there to continue here." The reporter's was localized in Chinese. The only buttons are Retry and Fork chat.
Ethan Park
And the CLI's exit message points them the wrong way. It says to run codex agents, select the task, and press x. The reporter did that repeatedly. The task already showed as Ready, and the saved last turn was completed, with runtime status reporting idle. Stop is built for cancelling a turn in progress. There's no turn in progress, so pressing x on a Ready task does nothing. That's the reporter's reading of the behavior, and it matches what they saw.
Maya
So who actually holds the conversation? The reporter ran lsof and found a specific process id with a lock file open under the dot codex directory, in a folder called thread writer locks, one file per thread id. That process was the managed app server daemon, running out of the 0.164.0 package under app server daemon releases. The Desktop app had its own separate app server process. So the CLI exited, but the shared daemon kept the writer lock, and Desktop's single writer check says no.
Ethan Park
Why does the daemon hold on? The reporter cites the official unsubscribe documentation, which describes a 30 minute grace period with no subscribers and no activity before the thread unloads. They're careful here, and we should be too. They rejoined the idle thread over a local socket, called thread unsubscribe, and got back "unsubscribed." About 40 seconds later the thread was still in the loaded list. That's consistent with delayed unload. It doesn't prove the timer is broken, and it doesn't prove nobody else was subscribed. The complaint is that there's no usable handoff while you wait.
Chapter 2
Workarounds, Caveats, and Smaller Fixes
Maya
So what do you do today? Option one, the blunt one. Issue 56797, a Windows report about a remote disconnect, says the local Desktop only unblocked after codex app server daemon stop. That works because it kills the process holding the lock. But the daemon is shared. In the 57004 report, other conversations were running under it, and the reporter deliberately didn't restart it for that reason. Stopping it interrupts everything, including subagents and background work. Check codex agents first, and only stop it if nothing else matters.
Ethan Park
Option two is Fork chat. It gets you a working conversation in Desktop right away, but it's a new conversation. You lose the identity of the original thread, so anything that refers to the old one won't follow you. Fine for a quick follow up, awkward for long running work.
Maya
Option three is prevention, and I want to flag the confidence level. The notes we're working from suggest launching the CLI with the no daemon flag when you know you're handing off to Desktop. The idea is an embedded server that dies with the terminal, so the lock goes with it. We haven't seen that confirmed in the issue itself, so test it on a throwaway thread first. Whichever you try, the check is the same: after you exit, run lsof on the thread's lock file and see whether a daemon process still has it open.
Ethan Park
On a real fix, the reporter links pull request 56378, described as a validated patch for an explicit unsubscribe writer handoff. It's still a proposal as of the report. What they're asking for is a release operation scoped to one conversation, separate from stopping a turn, so an idle writer can be handed over without touching the daemon or anything else. Nothing is assigned and nothing is merged in what we've seen, so don't plan around it landing next week.
Maya
Now the smaller items. Issue 56850, reported on 0.165.0 alpha 1, hits people with custom providers using the responses wire API, like a llama.cpp style local server. If a long function call, say a big heredoc in exec command, runs past the output budget, the stream ends as incomplete with reason max output tokens. Codex leaves the truncated call in history and replays it on every later request. Strict servers that validate the arguments as JSON then return a 400, and the thread stays stuck. The reporter's workaround was having their bridge send an explicit max output tokens at the server's maximum.
Ethan Park
Another one in the app server area, issue 56781. Archiving a thread also archives its spawned subagent threads, but unarchive restores only the parent. If you rely on subagents, list the children after unarchiving and don't assume they came back.
Maya
And two things that did ship in 0.164.0. Config overrides passed with the c or config flag are now preserved when they appear at more than one subcommand level, so they stop getting silently dropped. And sandbox checks were hardened for case variant metadata paths on macOS, plus replaceable symlinks used to locate executables.
Ethan Park
The practical takeaway: if you end a CLI session and plan to continue in Desktop, check codex agents for other live work before you reach for the daemon stop command. If the shared daemon is busy, fork or wait out the grace period.