agmsg version: v1.2.3-36-g6b382d8 (integration/terminal-driver-v1)
Host: macOS, bash 3.2 / zsh interactive login shell
Codex: codex-cli via nvm, delivery mode monitor
Summary
spawn.sh launches Codex through the real CLI binary rather than the monitor
shim, so the session comes up without --remote. It then owns its own thread,
and codex-bridge-launcher.sh cannot attach to that thread — it retries every
few seconds, forever. The spawned seat never receives a message.
Observed on a freshly spawned seat: 21 run/codex-bridge-rate.* files from a
single launcher pid at roughly 4-second intervals, and the bridge log filling
with one line per attempt:
codex-bridge: thread/resume failed: thread <id> already has an active writer
The seat's argv is the real binary, with no --remote:
node /path/to/nvm/versions/node/v23.7.0/bin/codex $agmsg actas <name>
A working seat, started by hand from an interactive shell, carries it:
node /path/to/nvm/versions/node/v23.7.0/bin/codex resume --remote ws://127.0.0.1:<port> <thread>
Why it happens
--remote is added only by the monitor shim — codex-monitor.sh:247 and
:250. It appears nowhere in scripts/drivers/types/codex/type.conf.
The shim is installed as a shell function, which is deliberate (#193
"Prefer shell function setup for Codex monitor shim"):
codex () { "$HOME/.agents/skills/agmsg/scripts/drivers/types/codex/codex-shim.sh" "$@"; }
A shell function lives in the interactive shell's rc and is not exported.
spawn.sh:237 resolves the CLI with CLI_PATH="$(command -v "$CLI_BIN_EXE")",
and spawn.sh is not interactive, so the function is not defined there and
command -v returns the real binary. Measured, as a pair:
interactive shell command -v codex -> codex (the function)
non-interactive command -v codex -> /path/to/nvm/.../bin/codex (the binary)
exported? BASH_FUNC_codex in the environment -> 0
So every spawn of a Codex role bypasses the shim. This is not limited to a
role's first spawn: the resolution has nothing to do with whether a
role-session record exists.
Why it looks like it works
A seat that an operator restarts inside its pane goes through the
interactive shell, where the function is defined, so that launch does get
--remote and the bridge attaches. The seat then behaves normally, and the
failure reads as a transient that "fixed itself on restart".
Not the same as
Reproduction
- From a non-interactive context,
spawn.sh codex <name> --project <dir>.
ps -o args= -p <the new codex pid> — the argv is the real binary path with
no --remote.
ls run/codex-bridge-rate.* grows by one file every few seconds, and
run/codex-bridge.<team>.<name>.log repeats already has an active writer.
Suggested direction
The shim ships in the skill at a known path
(scripts/drivers/types/codex/codex-shim.sh), so the launcher can name it
instead of depending on PATH and shell-function resolution. Whether that belongs
in the type manifest's cli= — which spawn.sh:224 already describes as
trusted manifest data — or in a per-type launcher step, is an open question that
touches the other types.
Whatever the shape: closing this should not leave a path that silently falls
back to the real binary. A silent fallback reproduces exactly this bug, and its
symptom is a seat that looks spawned and never speaks.
agmsg version:
v1.2.3-36-g6b382d8(integration/terminal-driver-v1)Host: macOS, bash 3.2 / zsh interactive login shell
Codex: codex-cli via nvm, delivery mode
monitorSummary
spawn.shlaunches Codex through the real CLI binary rather than the monitorshim, so the session comes up without
--remote. It then owns its own thread,and
codex-bridge-launcher.shcannot attach to that thread — it retries everyfew seconds, forever. The spawned seat never receives a message.
Observed on a freshly spawned seat: 21
run/codex-bridge-rate.*files from asingle launcher pid at roughly 4-second intervals, and the bridge log filling
with one line per attempt:
The seat's argv is the real binary, with no
--remote:A working seat, started by hand from an interactive shell, carries it:
Why it happens
--remoteis added only by the monitor shim —codex-monitor.sh:247and:250. It appears nowhere inscripts/drivers/types/codex/type.conf.The shim is installed as a shell function, which is deliberate (#193
"Prefer shell function setup for Codex monitor shim"):
A shell function lives in the interactive shell's rc and is not exported.
spawn.sh:237resolves the CLI withCLI_PATH="$(command -v "$CLI_BIN_EXE")",and
spawn.shis not interactive, so the function is not defined there andcommand -vreturns the real binary. Measured, as a pair:So every
spawnof a Codex role bypasses the shim. This is not limited to arole's first spawn: the resolution has nothing to do with whether a
role-session record exists.
Why it looks like it works
A seat that an operator restarts inside its pane goes through the
interactive shell, where the function is defined, so that launch does get
--remoteand the bridge attaches. The seat then behaves normally, and thefailure reads as a transient that "fixed itself on restart".
Not the same as
codexopens a fresh thread while the bridge delivers to the recorded seat, and the seat can never follow #959 / fix(codex): open the TUI on the role's recorded thread, and let the bridge attach to it #960 — there the shim does run and the TUI opens a fresh threadwhile the bridge delivers to the recorded one; messages are consumed and
marked read invisibly. Here the shim does not run at all, and the bridge never
attaches, so nothing is consumed.
bridge-request file pinned to a dead port, failing with
ECONNREFUSED. Thisone fails with
already has an active writeragainst a live thread.Reproduction
spawn.sh codex <name> --project <dir>.ps -o args= -p <the new codex pid>— the argv is the real binary path withno
--remote.ls run/codex-bridge-rate.*grows by one file every few seconds, andrun/codex-bridge.<team>.<name>.logrepeatsalready has an active writer.Suggested direction
The shim ships in the skill at a known path
(
scripts/drivers/types/codex/codex-shim.sh), so the launcher can name itinstead of depending on PATH and shell-function resolution. Whether that belongs
in the type manifest's
cli=— whichspawn.sh:224already describes astrusted manifest data — or in a per-type launcher step, is an open question that
touches the other types.
Whatever the shape: closing this should not leave a path that silently falls
back to the real binary. A silent fallback reproduces exactly this bug, and its
symptom is a seat that looks spawned and never speaks.