[macOS][regression] Desktop cannot resume Remote Control / CLI thread: `already has an active writer` after latest update
Summary
After updating the ChatGPT Desktop client on macOS on August 7, 2026, an existing workflow that previously worked stopped working:
- During off-hours, I use ChatGPT mobile Remote Control to continue a Codex CLI thread on my Mac.
- During the day, I open the same thread in the Desktop client and continue from there.
After the latest Desktop update, the Desktop/CLI handoff fails because the thread is reported as already having an active writer.
This appears to be a regression in thread ownership / writer handoff between the Desktop-bundled app-server/runtime and the CLI/Remote Control runtime.
Environment
- Platform: macOS (iMac)
- Standalone CLI:
``text``
codex-cli 0.147.0
- ChatGPT Desktop bundled Codex runtime:
``text``
/Applications/ChatGPT.app/Contents/Resources/codex --version
codex-cli 0.147.0-alpha.6.5
- The issue started immediately after installing the latest ChatGPT Desktop update on August 7, 2026.
- The same Remote Control / CLI / Desktop workflow worked before that update.
Steps to reproduce
- Keep Codex Remote Control enabled on the Mac.
- Use ChatGPT mobile Remote Control to open/continue a Codex CLI thread.
- Later, on the same Mac, try to resume/open that same thread from the Desktop client.
- Observe that the thread cannot be resumed because another writer is considered active.
Actual behavior
The resume attempt fails with an error like:
Failed to resume session from ~/.codex/sessions/YYYY/MM/DD/rollout-...jsonl:
thread/resume failed during TUI bootstrap:
thread/resume failed: thread <redacted-thread-id> already has an active writer (code -32600)
A key observation:
- With the Desktop client running, the thread hits the
already has an active writerconflict. - If I fully quit the Desktop client, the same session can immediately be opened/resumed normally from the standalone CLI.
The session JSONL itself therefore appears intact; the problem looks like writer ownership/lifecycle rather than session corruption.
Expected behavior
Remote Control is intended to let the same work continue across mobile and desktop usage. A thread used through Remote Control / CLI should be able to hand off cleanly to Desktop when the user returns to the Mac.
Expected flow:
Mobile Remote Control / CLI thread
↓
user stops actively using mobile
↓
Desktop resumes the same thread
↓
ownership/writer handoff succeeds
The user should not need to disable Remote Control, kill app-server processes, fork the conversation, or create a new thread just to switch between mobile and Desktop.
Impact
This breaks a core daily workflow:
- Desktop during work hours
- Mobile Remote Control after work
- Same Codex thread/context across both surfaces
Because Remote Control needs to remain enabled, simply turning it off is not a practical workaround.
Regression evidence
The behavior changed immediately after the latest Desktop update. No CLI configuration or session files were changed at the time.
The standalone CLI and Desktop-bundled runtime are different builds:
Standalone: 0.147.0
Desktop bundled: 0.147.0-alpha.6.5
This may be related to stricter single-writer enforcement without a corresponding Desktop ↔ Remote Control ownership handoff.
Privacy note
The local username, exact session path, thread ID, repository/project names, and conversation contents are intentionally omitted. I can provide additional sanitized diagnostics if maintainers request them.
34 Comments
Reproduced on Windows 11 with the same versions (Desktop bundled codex-cli 0.147.0-alpha.6.5), and it does not require Remote Control — merely viewing a thread in the Desktop app takes its writer and never releases it:
Open thread X in the Desktop app → codex exec resume X "hi" → thread-store conflict: thread X already has an active writer
Switch to a different thread in the app (X no longer displayed) → still conflict (waiting several minutes doesn't help)
Close the app window, tray process still running → still conflict
Fully quit the app (tray → exit) → exec resume X succeeds immediately
So the writer appears to be held per-opened-thread for the lifetime of the app process, rather than released on navigation away. Same conclusion as OP: ownership/lifecycle issue, session file intact.
Impact on Windows is worse than macOS: there is no attach alternative — codex app-server daemon start / codex remote-control start both fail with codex app-server daemon lifecycle is only supported on Unix platforms, and the Desktop app exposes no control socket (default ~/.codex/app-server-control/app-server-control.sock absent), so external automation (exec/SDK resumeThread) has no way to share the app's writer and must fully restart the app between turns.
The reproduction suggests the missing primitive is not just "release the writer sooner"; it is an explicit, inspectable writer lease with fencing semantics.
If thread ownership is represented only by "some runtime has an active writer", handoff bugs are hard to distinguish from stale ownership. I would persist something like:
Then handoff can be a compare-and-swap operation rather than implicit process lifetime:
The
lease_epoch/fencing token matters. A timeout alone can otherwise create the opposite failure: Desktop looks dead, CLI takes ownership, then the old writer wakes and both believe they own the thread.For UX/debugging, the conflict should expose who owns the lease and whether it is live:
held by Desktop pid/runtime X, heartbeat 1.2s ago, versusstale lease last seen 12m ago. That makes "fully quit Desktop" a diagnosable lease-lifecycle bug rather than the only recovery mechanism.This is adjacent to the operational-state model I have been building in https://github.com/leadingproblemsolver/living-context-engine: durable state is useful only when ownership/status claims retain provenance and can be reconciled against current reality. LCE itself does not implement writer leasing, but the same principle applies here: make the ownership claim a first-class record with evidence and lifecycle, not an implicit inference from an open process/thread.
Same issue on Fedora 43. If vs code with codex extension is running, codex resume in cli will exit with this error even if the chat is not ongoing in vs code, even if the codex pane in vs code is hidden. It only works when I fully close vs code. This is a bad regression, I keep an eye on token usage through the codex cli app because the vs code extension /status command is useless, it does not tell me anything besides session ID. I need to query /status in codex cli from time to time to keep track of token consumption. I cannot exit vs code each time I need to check that.
This error is happening to me too. Codex resume is basically useless now.
I'm seeing what appears to be the same underlying writer-ownership regression, but in a slightly different and very reproducible Remote Control scenario on macOS.
Impact / severity
For my workflow, this currently makes Remote Control effectively unusable.
The primary reason I use Remote Control is to leave active Codex CLI threads running on my development machine and then monitor or steer those same live threads from my iPhone when I'm away from the computer.
At the moment, Remote Control only works for threads that are not open in the CLI. As soon as a thread is actually active in the CLI — i.e. exactly the thread I would want to control remotely — the mobile client can no longer load it.
So while the Remote Control server itself is reachable and inactive threads work, the core live-session use case is completely blocked.
Scenario
I start Codex normally in the CLI and then expose the machine to Remote Control, either by:
I can then see my local Codex threads from the ChatGPT iPhone app.
Behavior
Threads that exist locally but are not currently open in a Codex CLI process work correctly from the iPhone app. I can open them, see their messages, and interact with them.
However, any thread that is currently open/running in the CLI fails when opened from the iPhone app.
The iPhone shows:
At the same time, the
codex remote-controlprocess logs:This is deterministic:
already has an active writer.Expected behavior
Remote Control should be able to attach to / proxy the existing live CLI session rather than attempting to become a second persistent writer.
The main use case for Remote Control is leaving an active CLI Codex session running on the development machine and then viewing or steering that same live session from another device.
Requiring the CLI session to be closed before Remote Control can access it defeats that workflow entirely. In practice, this means Remote Control is currently unusable for controlling active CLI work, even though inactive threads remain accessible.
This occurs regardless of whether Remote Control is started through the desktop app or directly with
codex remote-control.I reproduced the same regression in a Desktop-over-SSH workflow on macOS and captured the lock ownership and recovery path.
Environment
26.803.616010.147.0-alpha.6.50.147.0Reproduction
``
``thread <redacted> already has an active writer
thread/readrequest succeeded, but the automaticthread/resumeperformed by Desktop failed with JSON-RPC-32600.Lock evidence
On the remote host,
lsofshowed the Desktop-bundledcodex app-serverchild holding both:~/.codex/thread-writer-locks/<thread-id>.lock.Killing only that child was not sufficient: the parent ChatGPT process immediately respawned it and the new child reacquired the same writer lock. Navigating away from the thread also did not release ownership.
Fully quitting the remote ChatGPT Desktop process released the lock. A managed standalone app-server then resumed the thread successfully from the SSH client. The local Desktop log showed:
The first successful resume took about 70 seconds because the rollout was large, but the session data was intact.
Conclusion / workaround
This confirms a writer-lifecycle/handoff problem rather than session corruption or an SSH transport failure. The current reliable workaround is to fully quit the Desktop process that owns the thread, then resume through the other client.
Because
thread/readstill works while another writer owns the thread, Desktop could also provide a visible Open read-only fallback instead of automatically attemptingthread/resumeand failing.Adding a maintainer oriented framing: the single writer invariant itself makes sense. The rollout JSONL is canonical and the SQLite thread history is derived from it, so two independent writers could corrupt ordering, compaction, or projection state.
The missing primitive is a usable ownership lifecycle. When a local runtime already owns a thread, a second surface should:
That preserves the safety property while avoiding the current requirement to fully quit Desktop or another host process just to inspect or continue the same thread.
A global
history.persistence = nonesetting would not address this error and would trade away the resume/fork/rollback benefits of durable thread history.The useful control here is writer ownership and handoff.
Specs:
I can reproduce the same issue on Linux.
Environment:
Behavior:
Error loading messages: Codex server returned an error.The host app-server log repeatedly reports:
failed to initialize thread persistence:
thread-store conflict: thread [REDACTED] already has an active writer
Failed to create session:
thread-store conflict: thread [REDACTED] already has an active writer
The affected rollout file exists and contains valid JSONL. This appears to be a writer-ownership or handoff problem where Remote Control tries to create a second writer instead of attaching to the already-running CLI session.
I don't know if this is the same error i had earlier but when i got this, i basically went to the .codex directory and deleted the lock file associated with chat i wanted to resume. never had this issue until last week.
This was on the VS Code Server Extension on a remote Linux machine
ls ~/.codex
archived_sessions config.toml history.jsonl logs_2.sqlite memories_1.sqlite session_index.jsonl thread-writer-locks
attachments config.toml.backup-20260729-180008 installation_id logs_2.sqlite-shm models_cache.json sessions tmp
auth.json db-backup-20260727 ipc logs_2.sqlite-wal plugins shell_snapshots version.json
cache goals_1.sqlite log memories rules skills
ls ~/.codex/thread-writer-locks/
019ef4ae-30da-7ac0-9a2f-f421eb61610c.lock 019ff2cd-fe3d-7443-896f-31a87daf5f2e.lock
I tested the lock-file workaround mentioned above.
Deleting the corresponding file from:
does make the thread accessible through Remote Control again.
However, this does not solve the underlying issue. It simply changes which process owns the thread.
After deleting the lock and opening the thread remotely:
already has an active writererror, this time on the CLI side.So the behavior appears to be:
This means the lock-file deletion is only a workaround for handing the thread over from one client to another. It does not allow the intended Remote Control workflow of keeping a CLI thread running while viewing/steering that same live thread from the app.
That distinction is important for severity: for anyone using Remote Control specifically to control active CLI sessions while away from their machine, the feature is currently effectively unusable.
It also suggests this isn't simply a stale-lock cleanup issue. The current single-writer behavior seems fundamentally incompatible with simultaneous CLI + Remote Control access to the same thread, unless Remote Control is intended to attach/proxy the existing writer rather than create another one.
I’m honestly surprised this isn’t one of the highest-priority Codex issues right now and that it has remained broken for several days. Remote Control is effectively unusable across the board for what I’d consider its most important use case: keeping an active CLI session running and then viewing or steering that same thread remotely from another device.
I can reproduce a related version of this issue on Linux with Codex CLI 0.147.0.
My setup:
codex remote-controlrunning as a long-lived app-server under a systemd user service``
text
``thread/resume failed: thread <id> already has an active writer (code -32600)
As a temporary workaround, I connected the CLI TUI to the same app-server process already used by Remote Control, instead of starting a second app-server:
codex --remote "unix:///tmp/codex-rc-XXXXXX/rc.sock"
Resuming can also be done through that shared endpoint:
codex --remote "unix:///tmp/codex-rc-XXXXXX/rc.sock" resume <thread-id>
The socket path is generated dynamically by foreground codex remote-control, so I use a small wrapper to discover the live socket owned by the systemd service before launching the CLI.
With both ChatGPT mobile and the CLI connected to the same app-server process, I no longer encounter the cross-process active writer conflict in this workflow.
This suggests that the conflict is between separate app-server owners rather than between multiple UI clients connected to one app-server. It is only a workaround: a supported shared endpoint or explicit writer handoff between Remote Control, Desktop, and CLI would still be preferable.
I can reproduce the same writer-ownership/handoff failure on macOS with Zed's Codex ACP integration as the existing owner and a standalone
codex remote-controlprocess as the second surface.Environment
codex-cli 0.147.0codex-cli 0.147.0``
text
``Zed
-> npm exec @agentclientprotocol/codex-acp
-> codex-acp
-> codex app-server
The affected rollout metadata identifies the threads as:
Reproduction
task_complete; two others were still usable from Zed.``
sh
``codex remote-control
```text
ERROR codex_core::session::session: failed to initialize thread persistence:
thread-store conflict: thread <redacted> already has an active writer
ERROR codex_core::session: Failed to create session:
thread-store conflict: thread <redacted> already has an active writer
```
Lock-owner evidence
lsofshowed that all three affected thread locks were held by the same long-lived Zed child app-server process:The same process also had the corresponding rollout JSONL files open. Two rollout files continued receiving events from Zed after the Remote Control errors, confirming that these were real live owners rather than orphaned lock files.
The third thread had completed its last turn the previous day but its writer was still retained by the Zed app-server. This app-server had remained alive since Zed was started roughly two days earlier.
The timing also lines up with the second-owner attempt:
codex remote-controlstarted first, and the firstactive writererror appeared about 37 seconds later while the Zed app-server was still holding the locks.Interpretation
This extends the issue beyond Desktop/CLI handoff: a third-party IDE surface using
codex-acp -> codex app-serverexhibits the same behavior.The single-writer guard is working, but Remote Control starts another app-server owner and attempts to initialize persistence for threads already owned by Zed. There is no automatic attach/proxy path to the existing Zed app-server, and Zed's app-server also retains writers for previously opened/completed threads for its full process lifetime.
Expected behavior would be for Remote Control to attach/proxy to the reachable existing owner, or perform an explicit quiesced handoff, rather than attempting to become a second persistence writer. Deleting the
.lockfiles is not a safe fix because the original process is still alive and actively owns the rollout files.Face the same issue when i leave the codex cli without active turns, and in my developping apps I got notfication and attach to the thread(not resuming just review, via websocket over the app's in app tailscale connectivity to the codex app server i launched) and i got the same error
seems introduced after host codex cli updated to 0.147
Adding a data point:
msstoreDesktop over its built-in SSH connection to remote, with a negative control that makes the scope fairly clear.Setup
msstoreCodex Desktopcodex-clion ashmsstoreCodex Desktop;msstorecodex and never actually works incodex-climsstoredesktop and cli use sameCODEX_HOMERegression
Before the recent update, I could read (and even write; I took care to avoid write race conditions; I agree that single-write model is the right way) remote session thread from my Codex desktop, with remote Codex desktop up and running.
This is a part of my cooperation workflow as remote user didn't have to send lengthy responses by his Codex via other tools; I could just read them myself without asking him for redundant operations.
Now the same workflow is blocked.
Reproduction / negative control
``
text
``此任务正在其他位置运行。
请先在那里关闭会话,然后在此处重试。
i.e. “This task is active elsewhere. Close it there and try again here.”
So this is not a global Desktop/CLI, account, or
CODEX_HOMEexclusivity problem. The conflict is specifically per-thread.Why this matters
The secondary SSH client does not need to become a second writer. A read-only/spectator access to the live thread while Desktop remains the sole writer can still be useful.
The new single-writer invariant makes sense for persistence safety, but the current client behavior appears to route “open this thread” through
thread/resume, which blocks even observation when another app-server already owns the writer.A visible Open read-only / spectator fallback (or attaching the secondary client to the existing writer-owning app-server) would preserve the single-writer invariant while restoring the pre-update monitoring workflow.
This also gives a simple A/B control on the same running Desktop process:
Resolving the issue by downgrading the Codex CLI
npm install -g @openai/codex@0.144.6
It works on my mac, Codex Desktop version 26.803.81509
Reproduced on Fedora 44 with CLI
0.147.0, ChatGPT Desktop26.803.81509(bundled Codex0.147.0-alpha.6.6), and iOS1.2026.209.Deterministic behavior:
Host logs show iOS
thread/resumefailing with:This confirms a cross-process CLI/Remote writer-handoff problem, not session corruption or Linux-specific behavior.
Independent reproduction with process-level evidence on macOS. This matches the Remote/CLI case described above and also reproduces the reverse ownership direction.
Environment
codex-cli 0.147.026.803.616010.147.0-alpha.6.50.147.0-alpha.6.51.2026.209Live CLI -> iOS Remote failure
The Desktop Remote transport was healthy: the host app-server websocket reached
Connected, and the current Remote enrollment was enabled. Nevertheless, opening the live thread from iOS produced two deterministicthread/resumefailures:lsofshowed that~/.codex/thread-writer-locks/<redacted>.lockwas open by the healthy standalonecodex resumeprocess. The CLI continued functioning normally, so this was a real live owner, not an orphaned marker or stale process.The iOS UI surfaced the failure as “This task couldn’t reconnect” / an error loading messages.
Reverse direction
The reverse case also reproduces:
codex resumefor the same thread fails withalready has an active writer.The Desktop frontend also logged
Conversation state not foundfor the remotely opened thread shortly after the Desktop app-server acquired its lock, suggesting a secondary UI/app-server state synchronization problem.Pairing negative control
There had previously been a separate broken Remote enrollment returning HTTP 404
Remote environment not found. Removing/re-pairing the connection created a new enrollment and restored a connected Remote websocket. The writer conflict still reproduced after that successful re-pair, so it is not caused by stale QR pairing, authentication, or host reachability.Interpretation
This appears to be the 0.147 single-writer invariant working as intended at the storage layer, while the Remote open/resume path is not writer-aware. iOS Remote attempts to initialize a second persistent session instead of attaching/proxying to the existing live owner (or falling back to a read-only spectator view).
No lock files were deleted and no running process was stopped during diagnosis. Deleting the pathname while the original process holds the open inode would be unsafe because it could permit a second lock inode and concurrent writers.
Independent reproduction on macOS using CLI-only Remote Control. The ChatGPT desktop app is not used in this workflow, so this variant does not involve a Desktop-owned writer.
Environment
0.147.0codex remote-control startDeterministic behavior
The Remote websocket is connected and the project/thread list is available, ruling out host reachability, authentication, and pairing failures.
The CLI daemon logs show that Android Remote sends
thread/resumethrough the daemon Unix socket, after which the daemon tries to initialize a second persistent session:The existing writer is the healthy, still-running terminal TUI, not an orphaned lock. This therefore appears to be the same missing attach/proxy or explicit handoff path described above, but specifically between two standalone CLI 0.147.0 processes: the TUI and
codex remote-controldaemon. No lock files were deleted during diagnosis.Reproduced on Linux with the VS Code extension → ChatGPT Desktop Remote → iPhone path.
Environment
26.810.5204426.810.520441.2026.209Behavior
Remote pairing and transport are healthy: the Desktop host refreshes its Remote token and reaches a connected Remote websocket. The iPhone can list the local VS Code threads, but opening a thread that is currently owned by the VS Code Codex client fails before any messages are displayed.
The iPhone UI reports:
At the same time, the Desktop host logs show the iOS client attempting
thread/resume:This repeated for two different VS Code-owned threads. Pairing, authentication, and host reachability therefore do not appear to be the cause.
Impact
This blocks even read-only monitoring. A user cannot view messages or follow the progress of an active VS Code Codex task from the phone, which is the primary Remote use case. The mobile client appears to request a new persistent writer through
thread/resumeinstead of attaching/proxying to the existing VS Code-owned session or falling back to a spectator/read-only view.Privacy
Thread IDs, account and installation IDs, computer/user names, project/repository names, local paths, conversation titles/content, screenshots, and complete logs are intentionally omitted.
Your report is a useful negative/control case for
codex-rescue: the rollout appears intact and the failure looks like active-writer ownership/lifecycle, not file corruption. I’m field-testing the tool and want it to say that conservatively rather than invent a recovery problem.If the affected local thread is still available, could you run:
A healthy/no-damage diagnosis is valuable evidence here.
codex-rescuedoes not fix writer ownership or Remote Control handoff; it only inspects durable local session evidence and keeps source rollouts read-only.Please share sanitized output, versions and exit codes only—no raw JSONL/SQLite, prompts, credentials, thread IDs, or unredacted paths. Repo: https://github.com/shleder/codex-rescue
Additional deterministic reproduction on macOS, specifically covering ChatGPT Desktop <-> VS Code in both directions and iOS Remote after the latest desktop update.
Environment
26.810.52044codex-cli 0.148.0-alpha.926.810.52044codex-cli 0.148.0-alpha.926.727.51351Bidirectional reproduction
thread/resumewith JSON-RPC-32600andthread <redacted> already has an active writer; iOS shows the generic messageError loading messages: The Codex server returned an error.This also happens for a thread whose latest turn is already
completed: merely restoring/viewing the thread assigns the clientstreamRole=owner. Restarting the Mac does not help when both applications restore their previous windows and immediately reacquire ownership.Regression correlation from local app logs
On the same machine and same day:
26.727.51351: 0 occurrences ofalready has an active writer26.810.52044: successive app sessions recorded 2, 22, and 34 occurrencesRemote transport itself was healthy:
remoteControl/enablesucceeded and the host connection reachedstate=connected. This rules out pairing, authentication, host reachability, and ordinary network failure.Expected behavior
A second UI surface should attach/follow the reachable owner, provide a read-only transcript fallback, or perform an explicit handoff. Opening a completed or active thread in one supported client should not make it inaccessible from every other supported client, especially iOS Remote.
Privacy note: thread IDs, account/installation IDs, computer and user names, repository/project names, local paths, conversation content, screenshots, and full logs are intentionally omitted. Sanitized excerpts can be provided if maintainers need them.
I can reproduce a closely related Desktop-only failure on macOS.
Environment:
Observed behavior:
The app logs repeatedly record:
This repeats continuously while the viewer flickers. A read-only
thread/readsucceeds, confirming that the session content is intact. Forking the affected task to a new thread ID also succeeds; the fork is idle and its full completed history displays normally.So in this case the stale writer survives a full app restart for the original thread, while a fork is an effective recovery workaround. No conversation text, local paths, or thread IDs are included here.
Adding a deterministic macOS archive/delete variant of this active-writer lifecycle issue.
Environment
0.148.0-alpha.150.147.0Reproduction
``
text
``codex archive --remote unix:// <uuid>
Error: failed to archive session
PRAGMA quick_checkisok, and there is no parent/child spawn edge.lsofshows only the Desktop-bundled app-server holding that thread's rollout. The standalone server log repeatedly records:``
text
``thread-store conflict: thread <redacted> already has an active writer
archived_sessions.codex delete --remote unix:// --force <uuid>then succeeds, removes the row and rollout, preserves every unrelated active thread ID, leaves zero dangling edges, and keepsquick_check=ok.This demonstrates that the single-writer guard is protecting healthy storage, while Desktop retains a completed thread's writer long enough to block not only resume/Remote access but also the shipped archive/delete lifecycle from another supported app-server. The current CLI collapses the typed conflict into generic
failed to archive session, so the user gets neither owner attribution nor an actionable handoff/release path.Expected: release completed/non-viewed thread writers after the documented idle lifecycle, route the archive request to the live local owner, or surface a typed conflict that identifies the owning surface and the safe action. No lock file, SQLite row, or rollout was edited directly; no conversation content, full UUID, username, project path, or raw log is included.
Cross-linking a VS Code → iOS Remote controlled version bisect from #38219:
https://github.com/openai/codex/issues/38219#issuecomment-5333449089
This produced the same thread/resume / already has an active writer failure:
26.810.50856 / CLI 0.148.0-alpha.9: confirmed failing
26.727.40816 / CLI 0.146.0-alpha.9.2: confirmed working after reloading every VS Code window
Linking it here so the version-boundary evidence remains visible if #38219 is closed as a duplicate.
We found a working workaround (but please fix this 🙏🏾)
We hit the same
already has an active writer (code -32600)failure withthread/archiveas well asthread/resume, verified on the Desktop-bundled Codex runtime0.147.0-alpha.6.5.Our mitigation is to queue the archive, follow the thread’s runtime state over the local Desktop IPC bus, and retry once the owner reports idle. If our own private app-server retains a completed thread, we recycle that server after a short grace period because there is no unload/release RPC.
This improves the UX but is not a real ownership fix; we have seen writers survive the terminal idle event. A supported
thread/release/thread/unload, automatic release after terminal idle, or a safe takeover API would remove these workarounds. Including owner identity and last activity in the-32600error would also make recovery much clearer.Additional macOS reproduction — persistent daemon re-acquires desktop-owned thread locks after restart
Env: macOS 26.5 (Apple Silicon); ChatGPT Desktop
26.810.xwith bundled Codex0.148.0-alpha.x; standalonecodex-cli 0.147.0runningcodex app-server --remote-controlas a daemon; CLI dispatch viacodex exec resume <thread>.Repro:
codex exec resume T "..."from a terminal →thread/resume failed: thread T already has an active writer (code -32600).codex app-server daemon restart→ the new daemon PID immediately re-acquires~/.codex/thread-writer-locks/T.lock(lsofshows the daemon holding it), so the CLI still cannot resume.Also observed: a CLI
exec resumekilled mid-turn by a network drop left the lock held until the daemon was restarted.Impact: with the app open, none of our long-lived coordinator threads can be driven from the CLI; we now keep the app closed and drive threads exclusively from the CLI.
Same request as others here: a supported shared endpoint or an explicit writer handoff between Desktop / Remote Control / CLI, and a documented lock lease so a dead writer doesn't strand the thread.
Adding a controlled macOS → iOS Remote reproduction that separates the relay-owner collision from the per-thread writer conflict.
Environment
26.818.41509(build6962)0.149.0-alpha.4.10.149.0Stage 1: duplicate app-server control
Initially both the Desktop-bundled app-server and a managed standalone app-server were running. The standalone Remote transport reported repeated HTTP 409 responses with:
Stopping the standalone daemon removed this duplicate relay-owner condition. This part matches #39547.
Stage 2: clean Desktop-only A/B comparison
After restarting Desktop and verifying that only the Desktop-bundled app-server remained:
Error loading messagesUI.The good and bad tasks were compared under the same connection and Project. Project assignment/migration did not predict the result. Transcript size also did not predict the result: a much larger task loaded successfully while the smaller Desktop-loaded task failed. Both legacy and paginated history modes were present among the controls.
The strongest discriminator was whether Desktop had loaded the target task and retained its writer. Navigating away from a task was not a reliable release mechanism; restarting Desktop and leaving the target task unopened released it for iOS Remote.
Interpretation
The 409 relay-owner collision is one failure mode, but it does not explain the clean Desktop-only result. With a single Desktop app-server, iOS Remote still appears to issue a resume path that conflicts with the Desktop-owned writer instead of attaching to the existing in-process thread, using a read-only spectator path, or performing an explicit writer handoff.
No session files, lock files, or SQLite records were edited during the comparison. Task IDs, account identifiers, local paths, Project names, and conversation contents are omitted.
Another variant: the Desktop-bundled app-server also grabs writer locks on threads that belong to a third-party app-server client, and keeps them until the app is quit.
Environment: macOS 26.6, Apple Silicon. ChatGPT Desktop
26.818.41509(bundledcodex-cli 0.149.0-alpha.4.1). T3 Code, which runs its owncodex app-server --listen stdio://from the npm@openai/codex0.149.0.What happened:
originator: t3code_desktopin the rollout) was visible in ChatGPT Desktop's thread list and I opened it there.thread/resumewiththread <id> already has an active writer.lsof ~/.codex/thread-writer-locks/<id>.lockshowed the Desktop app-server process (/Applications/ChatGPT.app/Contents/Resources/codex ... app-server) holding it, along with locks for seven other threads. The lock mtime was two minutes before the last T3 write to the rollout.So opening a thread in Desktop is enough to take ownership, and only quitting the app gives it back. For non-OpenAI clients the failure is a raw stack trace, which is why T3 is adding a fork-on-conflict fallback in pingdotgg/t3code#7414. A way to release a writer lock without quitting the app, or a Desktop that doesn't acquire the lock just for viewing, would make this recoverable.
Confirming this writer-ownership regression on macOS as well. After using a thread in Codex Desktop, attempting to continue it from the CLI with
codex resumecan fail during TUI bootstrap with:This is especially disruptive when combined with the current Desktop composer/session regression (
{"value":{},"source":"owned","hooks":[],"promises":[]}), because users are pushed to CLI as the fallback and then cannot resume the same thread while Desktop still owns the writer.Fully quitting Desktop is a workaround, but that is not a viable handoff model. Please expose/release thread ownership correctly when Desktop is no longer actively writing, or provide an explicit Desktop→CLI handoff/attach path. A maintainer status update, affected-version range, and target fix build would be appreciated because this currently blocks normal cross-surface workflows.
I reproduced this exact writer-lifecycle problem on macOS with the TUI, ChatGPT Desktop, and iOS Remote. The thread is healthy and appears in Remote, but while another Codex process owns it, mobile shows
Error loading messages; resuming in the opposite direction reportsalready has an active writer (code -32600).I built and exercised a local prototype that fixes the serial handoff workflow while retaining the single-writer invariant:
rust-v0.149.0-alpha.4.1The prototype adds an under-development, default-off
yield_thread_writer_after_turnfeature. When enabled, it:turn/start.turn/started,item/completed, andturn/completednotifications before starting the new turn, so its transcript catches up.This does not permit concurrent writers, merge divergent turns, or use last-write-wins semantics. An in-progress turn retains ownership; the supported behavior is A -> B -> A serialization after terminal turns.
Validation:
completed_turn_can_yield_writer_between_app_server_processesintegration test passes for A -> B -> A.codex-thread-store: 226 passed.codex-app-server-protocol: 289 passed, 1 skipped.cargo build -p codex-clipassed.Environment: Codex CLI
0.149.0-alpha.4.1, macOS26.4(25E246), ChatGPT Desktop, and ChatGPT iOS Remote.The repository does not currently accept external code PRs, so I am sharing the working prototype here as implementation evidence and for maintainers to reuse or adapt. I can provide additional logs, screenshots, or a formatted patch if useful.
Something I've been thinking about lately relative to this issue is: why couldn't desktop and CLI be synced? Aren't we running this on top of a server, what prevents us from using one portal with the other being updated in real time? As if you're looking into a window instead of restricting us to this weird, opaque locking mechanism...
It looks like this has been implemented recently. Using one chat thread I can send a prompt from the CLI and watch thinking traces, commands, summaries in the Codex app on my macbook, my workstation, my phone - all seeing the ~same thing update in real time.
(
Codex desktop 26.818.61809,codex-cli 0.149.1,ChatGPT iOS v1.2026.224)Not sure which contributor['s agent] did this, but thank you. Sincerely.
Confirming a closely related macOS failure with the new TUI
/apphandoff. In this case the TUI reports a successful handoff and exits, but the standalone Unix app-server keeps the thread writer, so Desktop immediately blocks on the same thread.Environment
26.6.2, arm6426.820.60940(build7119)0.150.0-alpha.80.149.10.149.0Reproduction
/appfrom that TUI.``
text
``Opened this session in the Desktop app.
It then returns to the shell prompt.
``
text
``This is open in another app
Close it there to continue here.
Retrydoes not help.Read-only process evidence
codex app-server --listen unix://(PPID 1; alive for more than four days).lsofshows that exact daemon still holding both the affected rollout JSONL and itsthread-writer-locks/<redacted>.lockfile.task_complete; there is no running turn to protect.app-server --listen stdio://process.0.149.0, even thoughstandalone/currentis now0.149.1, so an upgrade did not recycle the persistent daemon. This may be a contributing lifecycle/version-skew factor, but the directly observed failure is that/appreports success without transferring or releasing ownership.Expected behavior
/appshould be an actual handoff: either Desktop attaches to the existing daemon/thread, or the old app-server releases/transfers the writer after the TUI detaches. It should not report success and exit while leaving Desktop unable to continue. A safe Take over here action or owner/process identification would also make the failure recoverable.No lock, rollout, or SQLite state was modified during this diagnosis. Identifiers, local paths, and conversation content are omitted.
This writer-lock family keeps growing across Desktop/CLI/iOS handoffs. One practical note for people whose threads became unreachable: the rollout files usually survive fully intact on disk even when every resume path refuses them.
npx --yes @shleddy/vetto rescue --json scanenumerates what is still there without touching any lock, so you can confirm your data survived while waiting for the ownership fix.Adding a workaround that resolved this class of failure for us. The affected setup is ChatGPT/Codex Desktop using its built-in SSH Remote path; the key was to pin the standalone Codex CLI on the remote execution host to a known-good older version, while allowing the ChatGPT/Codex client app itself to continue updating.
In our working setup, the remote macOS hosts use:
The Desktop client can be newer; what mattered was keeping the CLI/runtime that actually runs on the SSH target consistent and older than the regressed runtime family.
Operationally:
codex --versionresolves to the same known-good standalone CLI version.This is only a workaround, not a product fix. It does not solve the underlying missing follower/read-only attach or explicit handoff behavior, and running another newer app-server against the same thread can still reproduce the
already has an active writerconflict. But for the built-in SSH Remote workflow, pinning the remote host CLI to the older known-good version removed the immediate inability to continue the affected sessions for us.