[macOS][regression] Desktop cannot resume Remote Control / CLI thread: `already has an active writer` after latest update

Open 💬 34 comments Opened Aug 7, 2026 by xkun1

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

  1. Keep Codex Remote Control enabled on the Mac.
  2. Use ChatGPT mobile Remote Control to open/continue a Codex CLI thread.
  3. Later, on the same Mac, try to resume/open that same thread from the Desktop client.
  4. 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 writer conflict.
  • 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.

View original on GitHub ↗

34 Comments

imaximov · 20 days ago

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.

leadingproblemsolver · 20 days ago

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:

thread_id
lease_id
owner_runtime_id
owner_surface = desktop | cli | remote
acquired_at
last_heartbeat_at
lease_epoch
state = active | releasing | released | expired
release_reason

Then handoff can be a compare-and-swap operation rather than implicit process lifetime:

  1. Desktop owns epoch N.
  2. Remote/CLI requests handoff.
  3. Desktop quiesces writes and records its final durable cursor/turn.
  4. Lease N is released.
  5. New owner acquires epoch N+1.
  6. Any late write carrying epoch N is rejected as stale.

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, versus stale 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.

IA21 · 19 days ago

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.

kerenkxs2 · 18 days ago

This error is happening to me too. Codex resume is basically useless now.

Eslsamu · 17 days ago

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:

  1. Opening the ChatGPT/Codex desktop app with remote control enabled, or
  2. Running:
codex features enable remote_control
codex remote-control

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:

Error loading messages: Codex server returned an error.

At the same time, the codex remote-control process logs:

ERROR codex_core::session::session: failed to initialize thread persistence:
thread-store conflict: thread <thread-id> already has an active writer

ERROR codex_core::session: Failed to create session:
thread-store conflict: thread <thread-id> already has an active writer

This is deterministic:

  • Thread not open in CLI → works remotely.
  • Open the thread in CLI → remote/mobile access fails with already has an active writer.
  • Exit that CLI thread → it becomes remotely accessible again.

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.

xiaoguan002 · 17 days ago

I reproduced the same regression in a Desktop-over-SSH workflow on macOS and captured the lock ownership and recovery path.

Environment

  • Local Desktop build: 26.803.61601
  • Remote macOS host
  • Remote Desktop bundled runtime observed: 0.147.0-alpha.6.5
  • Managed standalone app-server after recovery: 0.147.0
  • Thread size: approximately 47 MB

Reproduction

  1. A thread on the remote Mac had already completed its latest turn.
  2. The remote ChatGPT Desktop app remained running with that thread previously opened.
  3. Opening the same remote thread from another Mac through its saved SSH host produced:

``
thread <redacted> already has an active writer
``

  1. A read-only thread/read request succeeded, but the automatic thread/resume performed by Desktop failed with JSON-RPC -32600.

Lock evidence

On the remote host, lsof showed the Desktop-bundled codex app-server child holding both:

  • the thread's rollout JSONL; and
  • ~/.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:

thread/resume errorCode=null
maybe_resume_success
resumeState=resumed
streamRole=owner

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/read still works while another writer owns the thread, Desktop could also provide a visible Open read-only fallback instead of automatically attempting thread/resume and failing.

YoungPhlo · 17 days ago

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:

  • attach to or forward controls to the existing local writer when that owner is reachable;
  • otherwise offer an explicit read-only transcript view instead of automatically attempting resume;
  • show actionable owner information in conflicts: surface, runtime, PID or endpoint where appropriate, acquisition time, and whether a handoff is possible;
  • support an explicit quiesced handoff: flush the current writer, release its lease, then allow the new surface to acquire it.

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 = none setting 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:

Ubuntu 24.04.3 LTS
Linux 6.17.0-35-generic, x86_64
24 CPU cores, 54 GiB RAM
Standalone Codex CLI: 0.147.0
Node.js: v24.8.0
vigneshgurusamy · 17 days ago

I can reproduce the same issue on Linux.

Environment:

  • Ubuntu 24.04.3 LTS
  • Codex CLI/app-server: 0.147.0
  • Remote Control enabled
  • ChatGPT mobile app on Android

Behavior:

  • Projects and session lists load correctly.
  • Completed/inactive sessions load their conversations.
  • Active/running sessions appear in the list, but opening them on mobile fails with:

Error loading messages: Codex server returned an error.

  • After the CLI releases the active session, the conversation becomes accessible remotely.

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.

johnolusetire · 16 days ago

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

Eslsamu · 16 days ago
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:

~/.codex/thread-writer-locks/

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:

  • Remote Control / iPhone can access and interact with the thread again.
  • Trying to open that same thread in the CLI then fails with the same already has an active writer error, this time on the CLI side.

So the behavior appears to be:

  • CLI owns thread → Remote Control cannot access it.
  • Delete lock / Remote Control takes ownership → CLI cannot access it.
  • Close whichever process currently owns the thread → the other side can access it.

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.

Andingfangt · 16 days ago

I can reproduce a related version of this issue on Linux with Codex CLI 0.147.0.

My setup:

  • Linux
  • Codex CLI 0.147.0
  • codex remote-control running as a long-lived app-server under a systemd user service
  • ChatGPT mobile connected to that Remote Control host

``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.

Psiphonc · 16 days ago

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-control process as the second surface.

Environment

  • macOS, Apple Silicon
  • Standalone CLI: codex-cli 0.147.0
  • Zed integration runtime: codex-cli 0.147.0
  • Zed launches Codex through this process chain:

``text
Zed
-> npm exec @agentclientprotocol/codex-acp
-> codex-acp
-> codex app-server
``

The affected rollout metadata identifies the threads as:

source=vscode
originator=zed
cli_version=0.147.0

Reproduction

  1. Start Zed with the Codex ACP integration enabled.
  2. Open or resume several Codex threads in Zed.
  3. Leave Zed running. At least one affected thread had already emitted task_complete; two others were still usable from Zed.
  4. Start a separate foreground Remote Control process:

``sh
codex remote-control
``

  1. When Remote Control attempts to initialize those existing Zed-owned threads, it repeatedly logs:

```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

lsof showed that all three affected thread locks were held by the same long-lived Zed child app-server process:

codex <zed-app-server-pid> ... ~/.codex/thread-writer-locks/<thread-a>.lock
codex <zed-app-server-pid> ... ~/.codex/thread-writer-locks/<thread-b>.lock
codex <zed-app-server-pid> ... ~/.codex/thread-writer-locks/<thread-c>.lock

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-control started first, and the first active writer error 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-server exhibits 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 .lock files is not a safe fix because the original process is still alive and actively owns the rollout files.

unixapple · 16 days ago

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

VirgilMing · 16 days ago

Adding a data point: msstore Desktop over its built-in SSH connection to remote, with a negative control that makes the scope fairly clear.

Setup

  • Local: msstore Codex Desktop
  • Connects to remote via "SSH connection" provided in Codex Desktop
  • This function demands that remote machine runs a codex-cli on a sh
  • Remote: also msstore Codex Desktop;
  • its user works only in msstore codex and never actually works in codex-cli
  • remote msstore desktop and cli use same CODEX_HOME

Regression

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

  1. Remote Codex Desktop runs with thread A open/owned there.
  2. Via the SSH connection, other remote threads (B, C, etc.) can still be opened by my Codex Desktop immediately while remote Codex Desktop remains running.
  3. I attempt to open the exact same remote thread A in my Codex Desktop.
  4. It is now rejected with the localized UI message:

``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_HOME exclusivity 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:

Desktop(thread A) + SSH CLI(thread B)  -> works
Desktop(thread A) + SSH CLI(thread A)  -> active-writer rejection
cimpapa · 15 days ago

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

pasunboneleve · 15 days ago

Reproduced on Fedora 44 with CLI 0.147.0, ChatGPT Desktop 26.803.81509 (bundled Codex 0.147.0-alpha.6.6), and iOS 1.2026.209.

Deterministic behavior:

  • Active CLI thread → iOS reports “Error loading messages.”
  • Exit CLI → the same thread loads immediately.
  • Active GUI-created thread → iOS works normally.

Host logs show iOS thread/resume failing with:

thread-store conflict: thread <id> already has an active writer

This confirms a cross-process CLI/Remote writer-handoff problem, not session corruption or Linux-specific behavior.

JG-devo · 15 days ago

Independent reproduction with process-level evidence on macOS. This matches the Remote/CLI case described above and also reproduces the reverse ownership direction.

Environment

  • macOS 15.7.7 (24G720), arm64
  • Standalone npm CLI: codex-cli 0.147.0
  • ChatGPT Desktop: 26.803.61601
  • Desktop-bundled Codex: 0.147.0-alpha.6.5
  • VS Code extension-bundled Codex: 0.147.0-alpha.6.5
  • iOS Remote client identified in host logs: 1.2026.209

Live CLI -> iOS Remote failure

  1. Resume a local thread in a normal VS Code terminal using the standalone CLI.
  2. Keep the CLI process and thread running.
  3. Open the same thread from ChatGPT iOS Remote through the connected Desktop host.

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 deterministic thread/resume failures:

rpc.method="thread/resume"
app_server.client_name="codex_chatgpt_ios_remote"
app_server.client_version="1.2026.209"

Failed to create session:
thread-store conflict: thread <redacted> already has an active writer

lsof showed that ~/.codex/thread-writer-locks/<redacted>.lock was open by the healthy standalone codex resume process. 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:

  1. Open a previously idle local thread from iOS Remote.
  2. The Desktop-bundled app-server acquires that thread's writer lock.
  3. A subsequent standalone codex resume for the same thread fails with already has an active writer.
  4. Fully quitting Desktop releases the ownership and the CLI resume immediately succeeds.

The Desktop frontend also logged Conversation state not found for 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.

artemohanjanyan · 14 days ago

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

  • macOS 26.5.2 (25F84), arm64
  • Codex CLI and Remote daemon: 0.147.0
  • Host started with codex remote-control start
  • Client: ChatGPT Remote on Android

Deterministic behavior

  1. Start or resume a thread in a normal Codex terminal TUI and leave it open.
  2. Open the same thread from Android Remote.
  3. Remote fails to load/connect to that thread.
  4. Exit the local TUI cleanly.
  5. The same thread can then be opened through Remote.
  6. Threads created through Remote, or existing threads not open in a TUI, work normally.

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/resume through the daemon Unix socket, after which the daemon tries to initialize a second persistent session:

app_server.client_name="codex_chatgpt_android_remote"
rpc.method="thread/resume"

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 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-control daemon. No lock files were deleted during diagnosis.

leandronatalcoral · 11 days ago

Reproduced on Linux with the VS Code extension → ChatGPT Desktop Remote → iPhone path.

Environment

  • Pop!_OS 24.04, x86_64
  • ChatGPT Desktop package: 26.810.52044
  • VS Code Codex client reports: 26.810.52044
  • iOS Remote client reports: 1.2026.209

Behavior

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:

Error loading messages: The Codex server returned an error.
This task couldn't reconnect.

At the same time, the Desktop host logs show the iOS client attempting thread/resume:

rpc.method="thread/resume"
app_server.client_name="codex_chatgpt_ios_remote"
Failed to create session:
thread-store conflict: thread <redacted> already has an active writer

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/resume instead 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.

shleder · 11 days ago

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:

pipx install codex-rescue
codex-rescue sessions
codex-rescue doctor --latest

A healthy/no-damage diagnosis is valuable evidence here. codex-rescue does 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

cryptocore-ask · 10 days ago

Additional deterministic reproduction on macOS, specifically covering ChatGPT Desktop <-> VS Code in both directions and iOS Remote after the latest desktop update.

Environment

  • macOS 26.2, Apple Silicon
  • ChatGPT Desktop: 26.810.52044
  • Desktop bundled Codex: codex-cli 0.148.0-alpha.9
  • VS Code Codex extension: 26.810.52044
  • VS Code extension bundled Codex: codex-cli 0.148.0-alpha.9
  • Previous Desktop release observed earlier on the same day: 26.727.51351

Bidirectional reproduction

  1. Open or resume a thread in the VS Code Codex extension.
  2. Open the same thread in ChatGPT Desktop or through iOS Remote.
  3. Desktop fails thread/resume with JSON-RPC -32600 and thread <redacted> already has an active writer; iOS shows the generic message Error loading messages: The Codex server returned an error.
  4. Reverse the ownership: open the thread in ChatGPT Desktop first, then open it in VS Code.
  5. VS Code repeatedly reports that the thread is already open/running elsewhere and cannot display it.

This also happens for a thread whose latest turn is already completed: merely restoring/viewing the thread assigns the client streamRole=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:

  • Desktop 26.727.51351: 0 occurrences of already has an active writer
  • After relaunch/update to 26.810.52044: successive app sessions recorded 2, 22, and 34 occurrences

Remote transport itself was healthy: remoteControl/enable succeeded and the host connection reached state=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.

Estefan-cmyk · 10 days ago

I can reproduce a closely related Desktop-only failure on macOS.

Environment:

  • macOS 26.5.2
  • ChatGPT Desktop 26.810.50856 (build 6644)
  • Bundled Codex: codex-cli 0.148.0-alpha.9

Observed behavior:

  1. Open an existing pinned local task with completed history.
  2. The conversation viewer flickers and remains blank. The app may report that the task is active elsewhere even though no second window is intentionally open.
  3. Fully quit and restart the Desktop app.
  4. The same task still cannot be displayed.

The app logs repeatedly record:

thread/resume failed (code -32600): thread <redacted> already has an active writer
source=thread_hydration

This repeats continuously while the viewer flickers. A read-only thread/read succeeds, 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.

rellaqaq · 9 days ago

Adding a deterministic macOS archive/delete variant of this active-writer lifecycle issue.

Environment

  • Apple Silicon macOS
  • ChatGPT Desktop bundled Codex: 0.148.0-alpha.15
  • Standalone Unix app-server / CLI used by the owner's SSH workflow: 0.147.0
  • Remote control is disabled on the standalone server; both runtimes intentionally share the same local Codex history.

Reproduction

  1. Open a local thread in ChatGPT Desktop, complete the latest turn, and leave Desktop running.
  2. Hours later, send the shipped archive operation for that exact UUID through the standalone Unix app-server:

``text
codex archive --remote unix:// <uuid>
Error: failed to archive session
``

  1. Durable state remains healthy and unarchived: the authoritative SQLite row and rollout agree, PRAGMA quick_check is ok, and there is no parent/child spawn edge.
  2. lsof shows 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
``

  1. Fully quit ChatGPT Desktop through its normal application lifecycle. The rollout handle disappears while the SSH app-server and executor remain alive.
  2. Repeat the identical first-party archive command. It succeeds immediately. The row changes to archived and the rollout moves under archived_sessions.
  3. codex delete --remote unix:// --force <uuid> then succeeds, removes the row and rollout, preserves every unrelated active thread ID, leaves zero dangling edges, and keeps quick_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.

opringle · 9 days ago

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.

anandx · 9 days ago

We found a working workaround (but please fix this 🙏🏾)

We hit the same already has an active writer (code -32600) failure with thread/archive as well as thread/resume, verified on the Desktop-bundled Codex runtime 0.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 -32600 error would also make recovery much clearer.

sean-hyperion · 8 days ago

Additional macOS reproduction — persistent daemon re-acquires desktop-owned thread locks after restart

Env: macOS 26.5 (Apple Silicon); ChatGPT Desktop 26.810.x with bundled Codex 0.148.0-alpha.x; standalone codex-cli 0.147.0 running codex app-server --remote-control as a daemon; CLI dispatch via codex exec resume <thread>.

Repro:

  1. Open thread T in the desktop app.
  2. codex exec resume T "..." from a terminal → thread/resume failed: thread T already has an active writer (code -32600).
  3. codex app-server daemon restart → the new daemon PID immediately re-acquires ~/.codex/thread-writer-locks/T.lock (lsof shows the daemon holding it), so the CLI still cannot resume.
  4. Quit the desktop app entirely → lock released within seconds; CLI resume works.

Also observed: a CLI exec resume killed 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.

MarlinDiary · 5 days ago

Adding a controlled macOS → iOS Remote reproduction that separates the relay-owner collision from the per-thread writer conflict.

Environment

  • macOS, Apple Silicon
  • ChatGPT Desktop 26.818.41509 (build 6962)
  • Desktop-bundled Codex 0.149.0-alpha.4.1
  • Managed standalone Codex 0.149.0
  • Same iPhone, ChatGPT account, workspace, Mac, and Project throughout the comparison

Stage 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:

{"detail":"Remote app server already online"}

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:

  • Two existing tasks that were not loaded by Desktop opened and resumed successfully from iOS Remote.
  • A different task that was currently open/loaded by Desktop remained inaccessible from the same iPhone and showed the generic Error loading messages UI.
  • The host-side failure for that task was:
thread-store conflict: thread <redacted> already has an active writer

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.

andybergon · 4 days ago

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 (bundled codex-cli 0.149.0-alpha.4.1). T3 Code, which runs its own codex app-server --listen stdio:// from the npm @openai/codex 0.149.0.

What happened:

  1. A thread created by T3 Code (originator: t3code_desktop in the rollout) was visible in ChatGPT Desktop's thread list and I opened it there.
  2. Every later send from T3 Code failed at thread/resume with thread <id> already has an active writer.
  3. lsof ~/.codex/thread-writer-locks/<id>.lock showed 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.
  4. Closing the thread tab in Desktop did not release the lock. Quitting ChatGPT Desktop did, and T3's own app-server took the lock and resumed fine.

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.

o-henry · 4 days ago

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 resume can fail during TUI bootstrap with:

Failed to resume session ...
thread/resume failed: thread <thread-id> already has an active writer (code -32600)

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.

darshanbib · 3 days ago

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 reports already 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:

The prototype adds an under-development, default-off yield_thread_writer_after_turn feature. When enabled, it:

  1. Releases durable writer ownership only after a root turn reaches a terminal state.
  2. Reacquires ownership immediately before that process submits its next turn/start.
  3. Reconstructs model context from canonical, paginated rollout history after reacquisition.
  4. Replays completed turns missed by the reconnecting client through the existing v2 turn/started, item/completed, and turn/completed notifications 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:

  • End-to-end TUI <-> Desktop/Remote handoff exercised locally.
  • Focused cross-process completed_turn_can_yield_writer_between_app_server_processes integration test passes for A -> B -> A.
  • The test verifies missed user and assistant items are replayed and included in reconstructed model context.
  • codex-thread-store: 226 passed.
  • codex-app-server-protocol: 289 passed, 1 skipped.
  • Scoped fix/format checks and cargo build -p codex-cli passed.

Environment: Codex CLI 0.149.0-alpha.4.1, macOS 26.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.

YoungPhlo · 2 days ago

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.

nanggo · 2 days ago

Confirming a closely related macOS failure with the new TUI /app handoff. 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

  • macOS 26.6.2, arm64
  • Codex Desktop 26.820.60940 (build 7119)
  • Desktop-bundled Codex: 0.150.0-alpha.8
  • Current standalone CLI: 0.149.1
  • Long-lived standalone daemon still running: 0.149.0

Reproduction

  1. Resume an existing thread in the standalone TUI through the Unix app-server daemon.
  2. Run /app from that TUI.
  3. The TUI prints:

``text
Opened this session in the Desktop app.
``

It then returns to the shell prompt.

  1. Desktop opens the task but shows:

``text
This is open in another app
Close it there to continue here.
``

Retry does not help.

Read-only process evidence

  • No interactive TUI remains, but a parentless long-lived process is still running as codex app-server --listen unix:// (PPID 1; alive for more than four days).
  • lsof shows that exact daemon still holding both the affected rollout JSONL and its thread-writer-locks/<redacted>.lock file.
  • The rollout's latest lifecycle event is task_complete; there is no running turn to protect.
  • Desktop is using a different bundled app-server --listen stdio:// process.
  • The daemon executable is from standalone 0.149.0, even though standalone/current is now 0.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 /app reports success without transferring or releasing ownership.

Expected behavior

/app should 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.

shleder · 1 day ago

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 scan enumerates what is still there without touching any lock, so you can confirm your data survived while waiting for the ownership fix.

whenwillupdate · 22 hours ago

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:

codex-cli 0.144.1

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:

  1. On each SSH target host, make sure codex --version resolves to the same known-good standalone CLI version.
  2. Start/resume the Remote SSH task through ChatGPT/Codex's built-in SSH connection, but let the remote host's pinned CLI/runtime be the execution-side runtime.
  3. Avoid mixing multiple active runtimes on the same thread during recovery. If Desktop/VS Code/Remote has already taken the writer, fully quit the owning surface before resuming from the pinned runtime.

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 writer conflict. 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.