Fix Codex Remote thread writer conflicts
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
+5
-2
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user