From 7ffb99d779e976c6d665912a6dc5afba495d0e84 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:08:48 +0400 Subject: [PATCH] fix(codex): hardcode exec flags (#75) --- readme.md | 2 ++ src/takopi/runners/codex.py | 45 ++++++++++++++++++++++++++++--------- tests/test_exec_runner.py | 23 ++++++++++++++++--- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index c4c26ff..fa4023b 100644 --- a/readme.md +++ b/readme.md @@ -68,6 +68,8 @@ voice_transcription = true [codex] # optional: profile from ~/.codex/config.toml profile = "takopi" +# optional: extra codex CLI args (exec flags are managed by Takopi) +# extra_args = ["-c", "notify=[]"] [claude] model = "sonnet" diff --git a/src/takopi/runners/codex.py b/src/takopi/runners/codex.py index 4e9061f..1fb02bc 100644 --- a/src/takopi/runners/codex.py +++ b/src/takopi/runners/codex.py @@ -25,18 +25,29 @@ _RECONNECTING_RE = re.compile( r"^Reconnecting\.{3}\s*(?P\d+)/(?P\d+)\s*$", re.IGNORECASE, ) -_EXEC_ONLY_FLAGS = {"--skip-git-repo-check"} +_EXEC_ONLY_FLAGS = { + "--skip-git-repo-check", + "--json", + "--output-schema", + "--output-last-message", + "--color", + "-o", +} +_EXEC_ONLY_PREFIXES = ( + "--output-schema=", + "--output-last-message=", + "--color=", +) -def _split_exec_flags(extra_args: list[str]) -> tuple[list[str], list[str]]: - base_args: list[str] = [] - exec_args: list[str] = [] +def _find_exec_only_flag(extra_args: list[str]) -> str | None: for arg in extra_args: if arg in _EXEC_ONLY_FLAGS: - exec_args.append(arg) - else: - base_args.append(arg) - return base_args, exec_args + return arg + for prefix in _EXEC_ONLY_PREFIXES: + if arg.startswith(prefix): + return arg + return None def _parse_reconnect_message(message: str) -> tuple[int, int] | None: @@ -409,8 +420,13 @@ class CodexRunner(ResumeTokenMixin, JsonlSubprocessRunner): state: Any, ) -> list[str]: _ = prompt, state - base_args, exec_args = _split_exec_flags(self.extra_args) - args = [*base_args, "exec", *exec_args, "--json"] + args = [ + *self.extra_args, + "exec", + "--json", + "--skip-git-repo-check", + "--color=never", + ] if resume: args.extend(["resume", resume.value, "-"]) else: @@ -585,7 +601,7 @@ def build_runner(config: EngineConfig, config_path: Path) -> Runner: extra_args_value = config.get("extra_args") if extra_args_value is None: - extra_args = ["-c", "notify=[]", "--skip-git-repo-check"] + extra_args = ["-c", "notify=[]"] elif isinstance(extra_args_value, list) and all( isinstance(item, str) for item in extra_args_value ): @@ -595,6 +611,13 @@ def build_runner(config: EngineConfig, config_path: Path) -> Runner: f"Invalid `codex.extra_args` in {config_path}; expected a list of strings." ) + exec_only_flag = _find_exec_only_flag(extra_args) + if exec_only_flag: + raise ConfigError( + f"Invalid `codex.extra_args` in {config_path}; exec-only flag " + f"{exec_only_flag!r} is managed by Takopi." + ) + title = "Codex" profile_value = config.get("profile") if profile_value: diff --git a/tests/test_exec_runner.py b/tests/test_exec_runner.py index bebe564..3cb6e02 100644 --- a/tests/test_exec_runner.py +++ b/tests/test_exec_runner.py @@ -12,7 +12,7 @@ from takopi.model import ( StartedEvent, TakopiEvent, ) -from takopi.runners.codex import CodexRunner +from takopi.runners.codex import CodexRunner, _find_exec_only_flag CODEX_ENGINE = EngineId("codex") @@ -131,7 +131,7 @@ async def test_run_allows_parallel_different_sessions() -> None: def test_codex_exec_flags_after_exec() -> None: runner = CodexRunner( codex_cmd="codex", - extra_args=["-c", "notify=[]", "--skip-git-repo-check"], + extra_args=["-c", "notify=[]"], ) state = runner.new_state("hi", None) args = runner.build_args("hi", None, state=state) @@ -139,12 +139,29 @@ def test_codex_exec_flags_after_exec() -> None: "-c", "notify=[]", "exec", - "--skip-git-repo-check", "--json", + "--skip-git-repo-check", + "--color=never", "-", ] +@pytest.mark.parametrize( + ("extra_args", "expected"), + [ + ([], None), + (["-c", "notify=[]"], None), + (["--skip-git-repo-check"], "--skip-git-repo-check"), + (["--color=never"], "--color=never"), + (["--output-schema", "schema.json"], "--output-schema"), + (["--output-last-message=out.txt"], "--output-last-message=out.txt"), + (["-o", "out.txt"], "-o"), + ], +) +def test_find_exec_only_flag(extra_args: list[str], expected: str | None) -> None: + assert _find_exec_only_flag(extra_args) == expected + + @pytest.mark.anyio async def test_run_serializes_new_session_after_session_is_known( tmp_path, monkeypatch