From a7d61d812e9a8985f14567e7f5302e79961cc938 Mon Sep 17 00:00:00 2001 From: mfowler Date: Sun, 23 Aug 2026 02:39:00 +0000 Subject: [PATCH] Fix Codex Remote thread writer conflicts --- README.md | 8 ++++++-- agents.example.toml | 1 + agents.py | 9 +++++++-- tests/smoke_codex.sh | 6 ++++-- tests/test_unit.py | 7 +++++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4692692..6165248 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ limit_re = "usage limit|limit reached" [backend.codex] # Codex TUI, exposed through Codex Remote Control bin = "codex" preamble = "{bin} remote-control start --json >/dev/null" +remote_addr = "unix://" # connect the TUI to that daemon (one shared thread writer) flags = "--dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --no-alt-screen" supports_resume = false # thread-ID persistence is not implemented by this harness prompt_delivery = "arg" @@ -203,11 +204,14 @@ prompt_delivery = "exec" # {kickoff}=prompt file, {session}=session name, For an `"arg"` backend the flag *templates* are configurable (so you can point at a non-claude CLI): `resume_flag` (default `--resume '{id}'`), `model_flag` (default `--model '{model}'`), -`remote_control_flag` (default `--remote-control '{session}'`). A backend that sets `process_name` +`remote_control_flag` (default `--remote-control '{session}'`), and `remote_addr_flag` (default +`--remote '{addr}'`). A backend that sets `process_name` participates in backend-mismatch healing; one that doesn't (e.g. `demo`) never does. An optional `preamble` runs first and must succeed before the TUI starts; it supports `{bin}`, `{session}`, `{model}`, and `{dir}` templates. The Codex backend uses it to idempotently start the -shared Remote Control daemon before launching each tmux-hosted Codex TUI. +shared Remote Control daemon, then uses `remote_addr = "unix://"` to connect each tmux-hosted Codex +TUI to that daemon. This keeps the app-server as the sole thread-store writer while local and +Remote clients share the session. For an unattended TUI with a deterministic first-run gate, `startup_prompt_re` opts into one screen check after `startup_prompt_delay` seconds; on a match, the harness types the explicitly configured `startup_prompt_response` and `submit_key`. Codex uses this to trust the agent's diff --git a/agents.example.toml b/agents.example.toml index c74a68f..aede060 100644 --- a/agents.example.toml +++ b/agents.example.toml @@ -53,6 +53,7 @@ fatal_re = "redacted_thinking|blocks cannot be modified|cannot be modified" [backend.codex] # Codex TUI + the shared Remote Control app-server daemon bin = "codex" preamble = "{bin} remote-control start --json >/dev/null" +remote_addr = "unix://" # make the TUI a client of the daemon; it must not own a second writer flags = "--dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --no-alt-screen" supports_resume = false # the harness does not yet persist Codex thread IDs prompt_delivery = "arg" diff --git a/agents.py b/agents.py index 5da8314..9159748 100755 --- a/agents.py +++ b/agents.py @@ -349,6 +349,11 @@ def start_agent(cfg, agent, *, force=False): if backend.get("remote_control"): parts.append(_render_template(backend.get("remote_control_flag", "--remote-control '{session}'"), {"session": session})) + if backend.get("remote_addr"): + parts.append(_render_template(backend.get("remote_addr_flag", "--remote '{addr}'"), { + "addr": backend["remote_addr"], "session": session, "model": model, + "dir": shlex.quote(cwd), + })) if model: parts.append(_render_template(backend.get("model_flag", "--model '{model}'"), {"model": model})) if backend.get("flags"): @@ -356,8 +361,8 @@ def start_agent(cfg, agent, *, force=False): parts.append(f"\"$(cat '{kf}')\"") cmd = " ".join(p for p in parts if p) # Some interactive CLIs need an idempotent service prepared before their TUI starts. - # Codex Remote Control is one such service: `remote-control start` ensures the shared - # app-server daemon is enrolled and connected, then the pane runs the ordinary Codex TUI. + # The Codex backend starts the shared app-server here, while remote_addr above makes the + # pane's TUI a client of that daemon so both local and Remote UIs share one thread writer. if backend.get("preamble"): preamble = _render_template(backend["preamble"], { "bin": backend["bin"], "session": session, "model": model, diff --git a/tests/smoke_codex.sh b/tests/smoke_codex.sh index 17a1c7c..efeeca0 100755 --- a/tests/smoke_codex.sh +++ b/tests/smoke_codex.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Isolated live smoke of the Codex backend, driven entirely through agents.py. -# It starts one uniquely-named tmux session, verifies Remote Control and the Codex TUI, then -# removes only that session. The shared Remote Control daemon intentionally remains available. +# It starts one uniquely-named tmux session, verifies Remote Control and a daemon-connected Codex +# TUI, then removes only that session. The shared Remote Control daemon intentionally remains +# available. set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" @@ -54,6 +55,7 @@ watch = "none" [backend.codex] bin = "$CODEX_BIN" preamble = "{bin} remote-control start --json >/dev/null" +remote_addr = "unix://" flags = "--dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --no-alt-screen" supports_resume = false prompt_delivery = "arg" diff --git a/tests/test_unit.py b/tests/test_unit.py index e91954b..d37e7e3 100755 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -77,6 +77,7 @@ limit_re = "usage limit|limit reached" [backend.codex] bin = "codex" preamble = "{bin} remote-control start --json >/dev/null" +remote_addr = "unix://" flags = "--dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --no-alt-screen" supports_resume = false prompt_delivery = "arg" @@ -260,7 +261,7 @@ class TestExampleConfig(unittest.TestCase): self.assertIn(be, cfg["backends"], f"backend {be} missing from example") self.assertEqual(len(agents.phases(cfg)), 2) - def test_codex_launch_prepares_remote_control_then_starts_tui(self): + def test_codex_launch_prepares_remote_control_then_connects_tui_to_daemon(self): tmp = tempfile.mkdtemp(prefix="aotest-ut-codex-") old_alive, old_new, old_log = agents.session_alive, agents.new_session, agents.log old_capture, old_run = agents.capture_pane, agents._run @@ -277,7 +278,9 @@ class TestExampleConfig(unittest.TestCase): self.assertEqual(len(launched), 1) cmd = launched[0] self.assertTrue(cmd.startswith( - "codex remote-control start --json >/dev/null && codex ")) + "codex remote-control start --json >/dev/null && " + "codex --remote 'unix://' --model 'gpt-test' ")) + self.assertEqual(cmd.count("--remote 'unix://'"), 1) self.assertIn("--model 'gpt-test'", cmd) self.assertIn( "--dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --no-alt-screen",