Read this first — the truncation is a decoy. zmx ls does truncate a long cmd= to a literal ..., and the first version of this report led with that. It is not the mechanism. Raising zmx's MAX_CMD_LEN or dropping the ellipsis fixes nothing here: a session created by zmx run — which is how graphcode launches every loop — has no cmd= field at all to truncate. Anyone who "fixes" the truncation has not fixed this bug.
What happens
Create any Codex loop and open its pane. The pane spins for 60s (600 tries at 10 Hz), prints graphcode: '<session>' never became ready to attach, exits, and leaves a bare login shell. Meanwhile the codex agent itself is alive and answering its goal headless with clients=0.
Observed on node BC942DA8-DFA7-44D9-A6AA-61FE7D99D3F2 in the 0.1.61 dev build.
Why
ZmxSessionLauncher.daemonReadyCheckCommand adds a second grep for Codex nodes only (aliveCheckCommand, ZmxSessionLauncher.swift:490):
zmx ls | grep -v -e $'\tended=' -e $'\terr=' | grep -q 'name=<session>\t.*cmd=.*codex'
There is no cmd= field on the row it greps:
| Fact |
Source |
run records no command |
main.zig:259 hardcodes .command = null (so does write, :483) |
only attach records one |
main.zig:217 |
ls prints the field only when non-nil |
util.zig:911 |
graphcode launches every loop with zmx run -d |
ZmxSessionLauncher.swift:809, 839, 878, 899, 960 |
Measured over 56 live sessions on one machine: 7 carried any cmd= — the attach-created ones — and 6 of those were truncated. Independently probed on a live Codex loop over 12 seconds: gate half one (name=<session>\t) passes, gate half two never does, the row never grows a cmd=, codex is alive under it throughout, and 5 attach wrappers spin in the until-loop at once.
Blast radius — not only the pane
aliveCheckCommand is the same gate, so for a Codex node it is permanently false everywhere it is asked:
| Call site |
Consequence |
GhosttyTerminalView.swift:328 → waitingAttachCommand |
the reported symptom: 60s spin, then a bare login shell |
ZmxSessionLauncher.swift:1544 local atomic ensure (check || zmx run) |
always takes the zmx run branch — the exact race the comment says it exists to prevent, whose failure mode is "types the entire launch command into the now-live agent's input" |
ZmxSessionLauncher.swift:1088 remote ensure |
same, remotely |
ZmxSessionLauncher.swift:1365 remote send gate (aliveCheck && { send && submit }) |
node send and message edges to a remote Codex loop silently no-op |
Why tests did not catch it
ZmxSessionLauncherTests.swift:224/:233 and AttachedSessionBriefingTests.swift:79 assert the construction of the pattern. Nothing ran it against real zmx ls output — and a synthetic attach-shaped ls line would have passed while the bug survived, since attach sessions are exactly the ones that do carry a cmd=.
Origin and fix direction
The gate came in with #228 ("Prevent duplicate Codex resume launch") to stop the app attaching before the daemon's session was really the Codex one. That intent is real and must be preserved. The readiness signal has to be something graphcode writes itself — a zmx set label, the mechanism already behind presence= and activity= — since nothing about the process's own command line is observable through zmx ls for the sessions that matter.
Repro
- Create any Codex loop.
- Open its pane.
zmx ls | tr '\t' '\n' | grep '^cmd=' — no row for a loop session appears at all.
What happens
Create any Codex loop and open its pane. The pane spins for 60s (600 tries at 10 Hz), prints
graphcode: '<session>' never became ready to attach, exits, and leaves a bare login shell. Meanwhile the codex agent itself is alive and answering its goal headless withclients=0.Observed on node
BC942DA8-DFA7-44D9-A6AA-61FE7D99D3F2in the 0.1.61 dev build.Why
ZmxSessionLauncher.daemonReadyCheckCommandadds a second grep for Codex nodes only (aliveCheckCommand,ZmxSessionLauncher.swift:490):There is no
cmd=field on the row it greps:runrecords no commandmain.zig:259hardcodes.command = null(so doeswrite,:483)attachrecords onemain.zig:217lsprints the field only when non-nilutil.zig:911zmx run -dZmxSessionLauncher.swift:809, 839, 878, 899, 960Measured over 56 live sessions on one machine: 7 carried any
cmd=— the attach-created ones — and 6 of those were truncated. Independently probed on a live Codex loop over 12 seconds: gate half one (name=<session>\t) passes, gate half two never does, the row never grows acmd=, codex is alive under it throughout, and 5 attach wrappers spin in the until-loop at once.Blast radius — not only the pane
aliveCheckCommandis the same gate, so for a Codex node it is permanently false everywhere it is asked:GhosttyTerminalView.swift:328→waitingAttachCommandZmxSessionLauncher.swift:1544local atomic ensure (check || zmx run)zmx runbranch — the exact race the comment says it exists to prevent, whose failure mode is "types the entire launch command into the now-live agent's input"ZmxSessionLauncher.swift:1088remote ensureZmxSessionLauncher.swift:1365remote send gate (aliveCheck && { send && submit })node sendand message edges to a remote Codex loop silently no-opWhy tests did not catch it
ZmxSessionLauncherTests.swift:224/:233andAttachedSessionBriefingTests.swift:79assert the construction of the pattern. Nothing ran it against realzmx lsoutput — and a synthetic attach-shapedlsline would have passed while the bug survived, since attach sessions are exactly the ones that do carry acmd=.Origin and fix direction
The gate came in with #228 ("Prevent duplicate Codex resume launch") to stop the app attaching before the daemon's session was really the Codex one. That intent is real and must be preserved. The readiness signal has to be something graphcode writes itself — a
zmx setlabel, the mechanism already behindpresence=andactivity=— since nothing about the process's own command line is observable throughzmx lsfor the sessions that matter.Repro
zmx ls | tr '\t' '\n' | grep '^cmd='— no row for a loop session appears at all.