Skip to content

[macOS 26.803] SSH-connected host cannot resume some threads locally; orphan app-server blocks Remote pairing with 409 #37969

Open

Description

@whenwillupdate

What version of the ChatGPT/Codex desktop app are you using?

Both Macs:

  • ChatGPT desktop: 26.803.61601
  • Build: 6396
  • Bundled Codex CLI/app-server: 0.147.0-alpha.6.5

What platform are you using?

Both machines:

  • macOS 27.0 (26A5406e)
  • Apple Silicon (arm64)

Topology:

  • Controller Mac: uses ChatGPT Desktop > Settings > Connections > SSH to connect to the other Mac.
  • Target Mac: also runs ChatGPT Desktop locally and should be directly available through QR-based Remote Control.
  • Both apps are signed in to the same ChatGPT account/workspace.
  • A legacy ChatGPT Classic app is installed on the target, but it and its background helper were not running while this reproduced.

What issue are you seeing?

A Mac that has been used as a ChatGPT Desktop built-in SSH target can be left with an SSH-started codex app-server that conflicts with the target Mac's local ChatGPT Desktop app.

The failure has two related user-visible symptoms:

  1. Thread access becomes frontend-dependent.
    • Threads previously opened locally on the target can be resumed through built-in SSH after the target desktop app exits.
    • Some threads opened or resumed through built-in SSH cannot later be opened/resumed from the target's local desktop app while the SSH-created app-server remains.
    • This feels like the SSH runtime retains ownership or path/runtime context after the controller UI disconnects.
  2. Direct Remote Control setup on the target is blocked.
    • The target first showed:
      • Unable to enable Remote Control. Make sure only one ChatGPT instance is running.
    • A later attempt showed:
      • Unable to get pairing code: Remote control failed to connect.
    • Target-side logs repeatedly reported:
WARN codex_app_server_transport::transport::remote_control::websocket
failed to connect to app-server remote control websocket:
HTTP error: 409 Conflict
body: {"detail":"Remote app server already online"}
reconnect_attempt=<n>

During the failure, the target had both a desktop-owned app-server and a separate background Unix-socket app-server:

/Applications/ChatGPT.app/.../codex app-server --listen stdio://
~/.local/bin/codex app-server --listen unix://

The Unix-socket process had PPID 1. A sanitized codex doctor --json summary for it reported:

background server is running
mode: ephemeral
pid file: missing
settings: missing
control socket: ~/.codex/app-server-control/app-server-control.sock

The local state and log databases passed integrity checks. This does not look like transcript/database corruption.

Observed sequence

This is the observed sequence; I have not yet reduced it to a fully deterministic single-command reproducer.

  1. On the target Mac, open and use several project threads in the local ChatGPT desktop app.
  2. Quit the target's local ChatGPT desktop app.
  3. From the controller Mac, add/enable the target under Settings > Connections > SSH.
  4. Open/resume project threads on the target through the built-in SSH connection.
  5. Disconnect SSH and quit ChatGPT Desktop on the controller.
  6. Relaunch ChatGPT Desktop locally on the target.
  7. Attempt to open/resume threads that were used over SSH and enable Control this Mac / obtain a QR pairing code.
  8. Some SSH-used threads fail to open locally, and Remote Control setup/reconnect logs repeated 409 Remote app server already online.
  9. Inspecting the target shows an SSH/background app-server --listen unix:// alongside the local desktop app-server.

Recovery that worked temporarily

  1. Fully quit ChatGPT Desktop on the controller Mac, not only close its window.
  2. Disconnect the built-in SSH connection.
  3. Terminate the exact SSH-owned proxy/app-server on the target.
  4. Restart ChatGPT Desktop on the target.

After this cleanup, the target desktop could take ownership again and QR Remote setup could connect. The conflict can recur when an SSH/background app-server reappears.

Expected behavior

  • Disconnecting or quitting a ChatGPT Desktop built-in SSH connection should cleanly stop its SSH proxy and ephemeral remote app-server, unless a managed daemon is intentionally configured.
  • If a remote app-server already exists, the target's local desktop app should safely attach/take over or show which connection owns it and offer a supported disconnect action.
  • A thread should remain openable from the target's local desktop app after it was opened through built-in SSH.
  • QR Remote pairing should not be blocked indefinitely by a ghost/stale app-server connection.
  • codex doctor should identify the owning frontend/session for a live Unix-socket app-server instead of reporting an ephemeral server with no PID/settings metadata.

Related issues

The distinct part here is the same macOS target also running ChatGPT Desktop locally: SSH-created runtime ownership makes thread access asymmetric and blocks direct QR Remote pairing on the current 26.803 build.

User feedback: SSH-owned Linux thread cannot continue locally

  • Type: External — @koume3003, September 9.
  • User reports (translated from Japanese): Opening a Linux workstation project from Mac Desktop over SSH leaves the same thread “ロック中” (“locked”) in the workstation's local Desktop, preventing local continuation.
  • The reporter attributes this to the SSH-started app-server holding the writer lock. While the remote connection remains active, stopping that server triggers automatic reconnection and lock reacquisition. They therefore use separate local/remote threads, losing continuity of conversation and work.
  • Request: A shared app-server for local Desktop, CLI and SSH clients, or explicit lock release/handoff so the same thread can continue locally.
  • Relationship: Related SSH/local ownership feedback, not a confirmed duplicate or common root cause. This report does not establish an orphaned server after disconnect, and supplies no build, diagnostics or evidence of history deletion.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    app-serverIssues involving app server protocol or interfaces
    on Aug 11, 2026
  2. whenwillupdate commented on Aug 11, 2026

    @whenwillupdate
    Author

    Additional controlled comparison after testing a third Mac as the SSH target:

    • All three Macs use the same ChatGPT desktop build: 26.803.61601 (build 6396).
    • All three run macOS 27.0 (26A5406e) on arm64.
    • Two affected targets use SSH-resolved Codex CLI/app-server 0.147.0-alpha.6.5.
    • A third target that does not reproduce the local/SSH thread conflict uses SSH-resolved Codex CLI/app-server 0.144.1.

    The working target also has both:

    local ChatGPT desktop app-server
    incoming SSH app-server --listen unix://
    

    at the same time, and can move between local desktop and built-in SSH without the reported thread-access failure. Therefore, local Desktop + incoming SSH app-server coexistence is not inherently invalid.

    The strongest differentiator observed so far is the SSH target runtime:

    affected targets: 0.147.0-alpha.6.5
    working control:  0.144.1
    

    On the affected target, codex doctor --json reports the Unix-socket background server as mode: ephemeral with missing PID/settings files, while Remote Control logs continue to return repeated 409 Remote app server already online.

    This narrows the suspected regression/incompatibility window to the target-side CLI/app-server update between 0.144.1 and 0.147.0-alpha.6.5, or to stale enrollment/identity state retained during that upgrade.

    A macOS “Allow in Background” setting is enabled on one affected target, but the system UI currently says the app is not running in the background. More importantly, that toggle would not own or terminate an SSH-started ~/.local/bin/codex app-server --listen unix://. It should not be treated as the root cause without a separate controlled test.

    No versions were changed during this comparison; the working 0.144.1 target is being kept as a negative control.

  3. pyffcwj-hash commented on Aug 14, 2026

    @pyffcwj-hash

    Additional regression detail from my reproduction:

    This behavior only started after the latest Codex update. Before that update, I did not see the SSH-side Codex process remain behind after closing Desktop / IDE / CLI and disconnecting SSH.

    The timing also appears correlated with the newer single-active-client / single-owner session validation behavior, where the same thread/session is prevented from being actively opened by more than one Codex frontend at a time.

    I cannot confirm that this ownership validation is the root cause, but the observed failure is consistent with a stale owner not being released when an SSH-created Codex process survives its controlling client:

    SSH client opens/resumes thread
            ↓
    thread gains active owner / writer
            ↓
    Desktop / IDE / CLI closes
    SSH disconnects
            ↓
    remote Codex process remains alive unexpectedly
            ↓
    owner / writer state is not released
            ↓
    subsequent SSH client or local Codex on target
    cannot load/resume the same session normally
    

    The important control remains local-only use:

    Local Codex usage
      -> close Desktop / IDE / CLI
      -> Codex process exits normally
      -> session ownership is released
      -> session can be reopened normally
    

    So this looks like a regression introduced by the recent session single-owner enforcement interacting with SSH process cleanup/lifecycle rather than a general problem with the session data itself.

    In other words, the uniqueness check is useful, but if an SSH-spawned process becomes stale, that protection can turn the leaked process into a persistent session lock affecting both remote SSH access and direct local access on the target machine.

  4. HeGuanyuan commented on Sep 4, 2026

    @HeGuanyuan

    Additional reproduction on two Apple Silicon Macs with the SSH-resolved codex-cli 0.153.2.

    In both directions:

    1. Desktop A opened a thread located on Mac B through the built-in SSH connection.
    2. The turn completed, the task was no longer open in the controller UI, and the controller Desktop was later disconnected/closed.
    3. On the target, the SSH-started codex app-server --listen unix://... survived under PPID 1. No codex app-server proxy remained connected.
    4. lsof showed that orphan still owned the per-thread writer lock, while the target's local Desktop reported "This is open in another app."
    5. Terminating only that exact orphan released the writer, after which the target Desktop could open the thread normally.

    This indicates that a completed turn or closed UI is not a sufficient lifecycle boundary for writer ownership: ownership follows the loaded app-server, and SSH teardown can leave that server orphaned.

    It also makes a safe unattended workaround difficult. The public API exposes loaded/runtime state but not subscriber presence (#35676), and there is no atomic per-thread unload operation (#21889).

    More concretely, when the Desktop-created SSH proxy transport closes, app-server must remove every subscription owned by that connection. Once a thread has no subscribers and no active work, it must release the writer lock within a bounded time. Ephemeral SSH proxy/app-server processes should additionally use a lease or heartbeat so an orphaned transport cannot retain ownership indefinitely.

  5. VishalJ99 commented on Sep 16, 2026

    @VishalJ99

    Additional observation on macOS with the same built-in SSH topology, affecting two separate threads. In this case the target Mac's local desktop runtime retained ownership and blocked the SSH runtime from opening the thread.

    Observed sequence

    1. A thread on the target Mac was open in that Mac's desktop app.
    2. From a second Mac, attempting to open the same thread through the desktop app's built-in SSH connection failed.
    3. The target had both a desktop-bundled Codex process and a separate SSH-facing app-server. Process/file inspection showed the desktop-bundled process still had the affected thread's rollout JSONL open. The SSH-facing app-server logged:
    failed to initialize thread persistence: thread-store conflict: thread <redacted-thread-id> already has an active writer
    
    1. Restarting the target Mac's desktop app released the conflict; the same thread could then be opened through the SSH connection.

    This is an observed incident sequence, not a fresh minimal reproduction. The open rollout descriptor supports the ownership diagnosis alongside the conflict log and recovery, but is not by itself proof of which lock was held. This report does not establish that the target desktop was idle, that its writer was orphaned, or that the separate Remote Control pairing/HTTP 409 symptom occurred.

    Target versions checked after recovery

    • macOS: 26.4.1
    • ChatGPT desktop app: 26.908.70816
    • Desktop-bundled Codex: 0.154.0-alpha.6.2
    • Codex resolved by the SSH shell: 0.153.4

    These are the installed versions read after recovery; the exact pre-restart running build was not captured. A version mismatch is present, but this observation does not prove that it caused the conflict.

    Expected behavior

    The desktop and built-in SSH connection should provide a supported way to access the same thread: attach to its existing owner, offer an explicit safe ownership handoff, or at least show its history with an actionable ownership message. Switching access routes should not require restarting the target desktop app. Single-writer protection should remain intact.

    This appears to corroborate the thread-access portion of this issue and the ownership behavior in #37403 and #37450. #43253 provides a related read-only fallback in the TUI; the affected surface here is the desktop app over built-in SSH.

    Machine names, user names, thread IDs, task contents, and local paths have been omitted.

  6. ricklove commented on Sep 23, 2026

    @ricklove

    Workaround I found which an agent can use to get control form either side:


    We verified a manual workaround for switching task ownership between the local Mac desktop and a remote desktop app connected over SSH (Windows app in our case).

    Disconnecting the remote desktop alone was insufficient: its background app-server remained running and retained the tasks. Stopping that server let the Mac desktop resume them. Conversely, terminating the Mac desktop let the remote desktop resume them.

    Switch to the Mac desktop

    1. Disconnect the remote desktop’s SSH connection first. Otherwise, it may automatically restart the background server.
    2. On the Mac, stop the managed daemon:
      codex app-server daemon stop
    3. Check for a remaining background server:
      ps -axo pid,ppid,command | grep '[c]odex.*app-server'
      In our setup, an SSH-created app-server --listen unix:// sometimes remained or respawned. If present, terminate that specific PID:
      kill -TERM <background-server-pid>
    4. Open the Mac desktop and resume the tasks.

    Switch to the remote SSH desktop

    1. Quit the Mac desktop. In our installation, AppleScript quit displayed a confirmation, so we used this over SSH:
      pkill -TERM -f '^/Applications/ChatGPT.app/Contents/MacOS/ChatGPT$'
      Adjust the executable path if your app differs.
    2. Verify the Mac desktop and its child app-server have exited. Leave the background daemon running.
    3. Connect from the remote desktop over SSH and open the tasks. In our testing, it started a background server automatically when needed.

    This interrupts work in the server being stopped; resume it from the destination desktop. It provides manual ownership transfer, not simultaneous access. We tested with bundled CLI/app-server 0.155.0-alpha.9.2.

  7. Staphylococcus commented on Sep 27, 2026

    @Staphylococcus

    Additional verified occurrence on a Linux host with two Codex Desktop SSH clients. This adds a repeated lock-reacquisition case: stopping the current lock holder was only a temporary release, because another remote client started a replacement backend and acquired the same task again.

    Environment

    • Host: Linux (NixOS), running Codex Desktop locally.
    • Installed Linux Desktop package: 26.915.31945.
    • Host CLI: codex-cli 0.156.1; the active local Desktop app-server executable was also from the 0.156.1 package.
    • Remote Desktop clients: one macOS and one Linux machine, both connected to that host over SSH. Their exact app/runtime versions were not collected.

    Observed sequence and evidence

    This is a diagnosed incident, not a fresh minimal reproduction.

    1. A task could not be continued in the host's local Desktop. Its latest recorded turn was completed; the local backend reported notLoaded.
    2. On the host, the task's writer-lock inode was matched to a live kernel FLOCK ADVISORY WRITE entry and the owning process's file descriptor. The owner was an SSH-started codex -c features.code_mode_host=true app-server --listen unix://, orphaned under PID 1.
    3. After verifying that exact owner and sending it SIGTERM, the process exited and both the lock file and matching kernel lock disappeared. Delayed checks initially confirmed the release.
    4. With the remote Desktop apps still running, a new SSH-started backend from the other client acquired the same task's writer lock. Local access remained blocked.
    5. Terminating that replacement produced another temporary release, followed by a new backend associated with the first remote client acquiring the lock again.

    These were successive live owners, verified from kernel locks and open file descriptors, rather than an abandoned lock filename or simultaneous writers.

    Recovery verified

    We fully exited Codex Desktop on both remote clients, verified their desktop processes were gone, then terminated the remaining verified SSH-owned backend on the host.

    The host's local Desktop subsequently acquired the task's writer lock, reported the task as idle, and retained ownership throughout a 30-second check. No lock files were manually deleted and no task history was edited.

    Expected behavior

    Please provide a supported per-task ownership release/handoff for an idle task, identify the owning client in the lock error, and prevent automatic reconnect from immediately undoing that handoff. Single-writer protection should remain intact.

    Switching back to the computer hosting a completed task should not require inspecting kernel locks and closing entire Desktop apps on multiple other computers.

    The backend respawn aspect also resembles #24542. The exact reconnect/resume call path was not traced, and this incident did not test the separate Remote Control pairing/HTTP 409 symptom.

    All machine/user names, network addresses, task IDs, private paths, repository details, and conversation contents have been omitted.

  8. jinwukong commented on Sep 28, 2026

    @jinwukong

    Additional diagnosed occurrence on macOS, with a recovery that did not require terminating either app-server. This also exposes a narrow CLI recovery gap related to #24542.

    Environment

    • macOS 27.0, Apple Silicon.
    • Desktop app 26.924.22138 (build 11645); installed bundled Codex binary 0.158.0-alpha.2.1.
    • SSH-started Unix-socket app-server: 0.157.1, reported through its control interface. Standalone CLI: 0.157.1.
    • iOS client in request logs: codex_chatgpt_ios_remote, version 1.2026.258.

    The bundled binary version was read after recovery; the Desktop process was not restarted to establish its exact running build. The version difference is not established as the cause. This is an observed incident, not a fresh minimal reproduction.

    Correlated evidence

    The Desktop app-server and an SSH-started app-server --listen unix:// shared the same Remote installation_id, server_id, and environment_id. Their persisted enrollment rows had different client keys:

    app_server_client_name remote_control_enabled before After recovery
    Codex Desktop 1 1
    Empty string, SSH/background server 1 0

    The Desktop process had the affected thread's rollout and thread-writer-locks/<thread-id>.lock open. Meanwhile, the iOS resume request was handled by the SSH app-server, whose log contained:

    app_server.client_name="codex_chatgpt_ios_remote"
    rpc.method="thread/resume"
    thread-store conflict: thread <thread-id> already has an active writer
    

    The Desktop process was simultaneously failing to connect its Remote websocket with:

    HTTP error: 409 Conflict
    {"detail":"Remote app server already online"}
    

    The diagnosis relies on the correlated request/process logs, open descriptors, enrollment identity, and recovery result—not merely the existence of a lock file. Kernel lock ownership was not independently inspected on macOS.

    Recovery and its limits

    1. Connected to the existing SSH app-server control socket, initialized the protocol client, and confirmed thread/loaded/list returned an empty list.

    2. Tried codex app-server daemon disable-remote-control; it rejected the operation:

      Error: app server is running but is not managed by codex app-server daemon
      
    3. Sent the native remoteControl/disable RPC with {} to that same SSH app-server. Its response and subsequent remoteControl/status/read both reported disabled.

    4. Verified that the background enrollment's persisted flag changed to 0, while the Desktop enrollment remained 1.

    5. At 2026-09-28 13:47:24 UTC, the background Remote connection was disabled. At 13:47:43 UTC, the original Desktop process transitioned from Errored to Connected; the device API then reported the host online.

    Both app-server processes remained alive. No lock files, transcripts, or enrollments were deleted; no pairing was recreated; no Desktop restart or task-stop operation was performed.

    This verifies restoration of the Desktop Remote connection. Reopening the thread on the phone, an actual SSH workflow after the change, and behavior across a machine restart were not independently verified. It is not evidence that all cross-client writer handoff problems are solved.

    Narrow improvement opportunity

    Provide a supported, explicitly targeted Remote disable/recovery operation for a reachable SSH-started app-server that is not managed by the daemon lifecycle. It should be possible to release that server's Remote registration without stopping its process, deleting history, or forcibly stealing thread ownership.

    At inspected main commit c0d26949be4144c751894ae96e28d3db2208b764:

    • Daemon::set_remote_control still rejects a reachable unmanaged server.
    • The existing CLI RPC helper sends ephemeral: true. Simply removing the unmanaged-server check would therefore not necessarily provide the durable behavior observed above; managed launch settings and unmanaged enrollment preferences have different persistence responsibilities.
    • The native durable disable implementation persists false before disabling, and its database update is scoped by account, websocket URL, and client key.

    Useful regression coverage would distinguish durable versus ephemeral disable on unmanaged listeners, preserve the Desktop enrollment's enabled state, and ensure recovery does not terminate existing processes or tasks. Broader automatic takeover or writer handoff still needs an owner-aware design; this observation does not establish that automatic takeover is safe.

  9. anothaDev commented on Oct 6, 2026

    @anothaDev

    Additional verified occurrence on Linux using ChatGPT Desktop locally on a host and ChatGPT Desktop on a laptop connected to that host through built-in SSH. The same conversation became blocked in both switching directions.

    Environment and scope

    • Linux host and laptop; the SSH app-server reported Arch Linux, x86_64.
    • Laptop Desktop client version recorded in the failed resume request: 26.915.31945.
    • Host Desktop client version observed in contemporaneous app-server logs: 26.930.41038.
    • SSH-owned app-server version confirmed through its executable path and protocol initialization: 0.158.0.
    • These observations span October 5–6, 2026. The user reports that switching between these access routes previously worked seamlessly. We have not identified the first bad version, and the version difference is not established as the cause.

    This is a diagnosed recurring incident, not a fresh minimal reproduction.

    Verified evidence

    1. The laptop could reach the host over SSH and read the affected conversation's metadata/history, but thread/resume failed:

      method=thread/resume
      errorCode=-32600
      thread <redacted-thread-id> already has an active writer
      

      The host logged the corresponding persistence failure:

      Failed to create session: thread-store conflict:
      thread <redacted-thread-id> already has an active writer
      
    2. During the first incident, matching the affected thread's writer-lock inode against /proc/locks identified a live exclusive FLOCK owned by the host Desktop's bundled app-server. This blocked the laptop's SSH-facing runtime.

    3. During the recurrence, the direction was reversed: the host's local Desktop could not open the conversation. The same inode-to-kernel-lock check identified the SSH-started codex ... app-server --listen unix:// as its live writer-lock owner.

    4. We connected to that existing Unix control socket and verified its peer PID matched the lock holder. A metadata-only thread/read reported the conversation as idle; its latest persisted turn was completed.

    These were live process-owned locks, not merely leftover lock filenames. The evidence does not establish that the SSH process was orphaned or that all frontend subscriptions had ended.

    Targeted recovery verified

    For the idle conversation in the second incident:

    1. Sent thread/archive to the verified owning SSH app-server, followed immediately by thread/unarchive for the same conversation.
    2. Verified that the conversation was unarchived, its history file existed, its stored history-item count was unchanged, and its matching kernel writer lock was gone.
    3. The user then reopened the conversation successfully in the host's local Desktop.

    Neither app-server was terminated. No lock files were manually deleted, no conversation database rows were directly edited, and no conversation content was posted here. This was a recovery for a verified idle conversation, not a proposal to bypass single-writer protection or interrupt active work.

    Expected behavior

    Switching between the host Desktop and its built-in SSH access route should attach to the current owner or provide a safe, explicit per-conversation handoff. A completed, idle conversation should not require an archive/unarchive cycle or process termination just to continue on the other computer.

    This corroborates the thread-access/ownership portion of this issue. It does not establish the underlying regression commit or prove that the separate Remote Control pairing/HTTP 409 symptom has the same cause.

    Privacy: machine names, usernames, network addresses, conversation identifiers and contents, project names, private paths, account identifiers, and credentials have been omitted. Log excerpts above are reduced and redacted.

  10. unthingable commented on Oct 17, 2026

    @unthingable

    Additional macOS-to-macOS Desktop-over-SSH reproduction (2026-10-17):

    • I opened the same thread from another Mac's ChatGPT Desktop via SSH. After closing that remote app and disconnecting it, the host Mac's local Desktop still showed: This is open in another app.
    • On the host, I found a PPID-1 process running: codex -c features.code_mode_host=true app-server --listen unix://. Its local log database associated it with the affected thread.
    • I sent SIGTERM to that exact process. It exited, and after reopening the thread locally, Codex reported it as idle; I regained access.

    I did not capture the Desktop or CLI versions at the time. I have omitted the thread ID, machine names, filesystem paths, and conversation content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingremote

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions