Repository navigation
[macOS 26.803] SSH-connected host cannot resume some threads locally; orphan app-server blocks Remote pairing with 409 #37969
Description
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:
- 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.
- 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:
- The target first showed:
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.
- On the target Mac, open and use several project threads in the local ChatGPT desktop app.
- Quit the target's local ChatGPT desktop app.
- From the controller Mac, add/enable the target under Settings > Connections > SSH.
- Open/resume project threads on the target through the built-in SSH connection.
- Disconnect SSH and quit ChatGPT Desktop on the controller.
- Relaunch ChatGPT Desktop locally on the target.
- Attempt to open/resume threads that were used over SSH and enable Control this Mac / obtain a QR pairing code.
- Some SSH-used threads fail to open locally, and Remote Control setup/reconnect logs repeated
409 Remote app server already online. - Inspecting the target shows an SSH/background
app-server --listen unix://alongside the local desktop app-server.
Recovery that worked temporarily
- Fully quit ChatGPT Desktop on the controller Mac, not only close its window.
- Disconnect the built-in SSH connection.
- Terminate the exact SSH-owned proxy/app-server on the target.
- 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 doctorshould 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
- Codex Desktop remote proxy respawns unmanaged app-server and blocks daemon bootstrap #24542 — Desktop SSH proxy can keep an unmanaged app-server alive/respawn it.
- Idle SSH remote threads fail after restarting connection with
AbsolutePathBuf deserialized without a base path#31983 — idle SSH remote threads fail after restarting the connection. - remote-control 409 stale enrollment needs supported takeover or repair path #28862 — stale Remote Control enrollment/app-server returns 409 and lacks a supported takeover path.
- Codex-created launchctl restart job becomes a keepalive loop that repeatedly terminates ChatGPT #32321 — an orphaned desktop app-server can keep the Remote Control WebSocket and cause 409.
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
github-actions commentedon Aug 11, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
already has an active writerafter latest update #37403Powered by Codex Action
whenwillupdate commentedon Aug 11, 2026
Additional controlled comparison after testing a third Mac as the SSH target:
26.803.61601(build6396).27.0(26A5406e) on arm64.0.147.0-alpha.6.5.0.144.1.The working target also has both:
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:
On the affected target,
codex doctor --jsonreports the Unix-socket background server asmode: ephemeralwith missing PID/settings files, while Remote Control logs continue to return repeated409 Remote app server already online.This narrows the suspected regression/incompatibility window to the target-side CLI/app-server update between
0.144.1and0.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.1target is being kept as a negative control.pyffcwj-hash commentedon Aug 14, 2026
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:
The important control remains local-only use:
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.
HeGuanyuan commentedon Sep 4, 2026
Additional reproduction on two Apple Silicon Macs with the SSH-resolved
codex-cli 0.153.2.In both directions:
codex app-server --listen unix://...survived under PPID 1. Nocodex app-server proxyremained connected.lsofshowed that orphan still owned the per-thread writer lock, while the target's local Desktop reported "This is open in another app."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.
VishalJ99 commentedon Sep 16, 2026
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
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
26.4.126.908.708160.154.0-alpha.6.20.153.4These 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.
ricklove commentedon Sep 23, 2026
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
app-server --listen unix://sometimes remained or respawned. If present, terminate that specific PID:Switch to the remote SSH desktop
pkill -TERM -f '^/Applications/ChatGPT.app/Contents/MacOS/ChatGPT$'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.Staphylococcus commentedon Sep 27, 2026
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
26.915.31945.codex-cli 0.156.1; the active local Desktop app-server executable was also from the0.156.1package.Observed sequence and evidence
This is a diagnosed incident, not a fresh minimal reproduction.
notLoaded.FLOCK ADVISORY WRITEentry and the owning process's file descriptor. The owner was an SSH-startedcodex -c features.code_mode_host=true app-server --listen unix://, orphaned under PID 1.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.
jinwukong commentedon Sep 28, 2026
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
26.924.22138(build11645); installed bundled Codex binary0.158.0-alpha.2.1.0.157.1, reported through its control interface. Standalone CLI:0.157.1.codex_chatgpt_ios_remote, version1.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 Remoteinstallation_id,server_id, andenvironment_id. Their persisted enrollment rows had different client keys:app_server_client_nameremote_control_enabledbeforeCodex DesktopThe Desktop process had the affected thread's rollout and
thread-writer-locks/<thread-id>.lockopen. Meanwhile, the iOS resume request was handled by the SSH app-server, whose log contained:The Desktop process was simultaneously failing to connect its Remote websocket with:
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
Connected to the existing SSH app-server control socket, initialized the protocol client, and confirmed
thread/loaded/listreturned an empty list.Tried
codex app-server daemon disable-remote-control; it rejected the operation:Sent the native
remoteControl/disableRPC with{}to that same SSH app-server. Its response and subsequentremoteControl/status/readboth reporteddisabled.Verified that the background enrollment's persisted flag changed to
0, while the Desktop enrollment remained1.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
ErroredtoConnected; 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_controlstill rejects a reachable unmanaged server.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.falsebefore 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.
anothaDev commentedon Oct 6, 2026
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
26.915.31945.26.930.41038.0.158.0.This is a diagnosed recurring incident, not a fresh minimal reproduction.
Verified evidence
The laptop could reach the host over SSH and read the affected conversation's metadata/history, but
thread/resumefailed:The host logged the corresponding persistence failure:
During the first incident, matching the affected thread's writer-lock inode against
/proc/locksidentified a live exclusiveFLOCKowned by the host Desktop's bundled app-server. This blocked the laptop's SSH-facing runtime.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.We connected to that existing Unix control socket and verified its peer PID matched the lock holder. A metadata-only
thread/readreported the conversation asidle; its latest persisted turn wascompleted.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:
thread/archiveto the verified owning SSH app-server, followed immediately bythread/unarchivefor the same conversation.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.
unthingable commentedon Oct 17, 2026
Additional macOS-to-macOS Desktop-over-SSH reproduction (2026-10-17):
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.