From dd3140b3912af8db3b2df2750ff41412da2d8d63 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 15:22:51 +0000 Subject: [PATCH] Add execution retry and emergency profile settings Why: Routing needs an explicit per-profile attempt limit and a named Codex emergency profile before controller enforcement can be implemented. Evidence: The new parser test failed on unknown keys before implementation; all 15 config test executions now pass and git diff --check is clean. Impact: Adds backward-compatible optional configuration fields only; routing behavior is unchanged in this commit. Refs: feature/agent-cli-enforcement --- scripts/agent_exec_config.py | 27 ++++++++++++++++++++++++++- tests/test_agent_exec_config.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/scripts/agent_exec_config.py b/scripts/agent_exec_config.py index 84aa59b0..e213fe14 100644 --- a/scripts/agent_exec_config.py +++ b/scripts/agent_exec_config.py @@ -54,9 +54,13 @@ EXECUTION_KEYS = frozenset( "idle_timeout_seconds", "summary_bytes", "raw_output_bytes", + "max_attempts_per_profile", + "emergency_profile", } ) -EXECUTION_REQUIRED = EXECUTION_KEYS - frozenset({"idle_timeout_seconds"}) +EXECUTION_REQUIRED = EXECUTION_KEYS - frozenset( + {"idle_timeout_seconds", "max_attempts_per_profile", "emergency_profile"} +) PROFILE_KEYS = frozenset({"adapter", "executable", "model", "enabled"}) CUSTOM_ADAPTER_KEYS = frozenset( { @@ -84,6 +88,8 @@ class ExecutionPolicy: summary_bytes: int raw_output_bytes: int idle_timeout_seconds: int = DEFAULT_IDLE_TIMEOUT_SECONDS + max_attempts_per_profile: int = 1 + emergency_profile: str | None = None @dataclass(frozen=True) @@ -391,6 +397,23 @@ def _build_execution(raw: Any) -> ExecutionPolicy: if raw_output_bytes < summary_bytes: raise ConfigError("raw_output_bytes must be >= summary_bytes") + if "max_attempts_per_profile" in raw: + max_attempts_per_profile = _positive_bounded( + raw["max_attempts_per_profile"], + "max_attempts_per_profile", + 1, + 10, + ) + else: + max_attempts_per_profile = 1 + + if "emergency_profile" in raw: + emergency_profile = raw["emergency_profile"] + if not isinstance(emergency_profile, str) or not emergency_profile: + raise ConfigError("emergency_profile must be a non-empty string") + else: + emergency_profile = None + return ExecutionPolicy( fresh_process_per_unit=True, permission=permission, @@ -400,6 +423,8 @@ def _build_execution(raw: Any) -> ExecutionPolicy: summary_bytes=summary_bytes, raw_output_bytes=raw_output_bytes, idle_timeout_seconds=idle_timeout_seconds, + max_attempts_per_profile=max_attempts_per_profile, + emergency_profile=emergency_profile, ) diff --git a/tests/test_agent_exec_config.py b/tests/test_agent_exec_config.py index 1599c130..6261c3b2 100644 --- a/tests/test_agent_exec_config.py +++ b/tests/test_agent_exec_config.py @@ -405,6 +405,35 @@ class AgentExecConfigTests(unittest.TestCase): with self.assertRaises(ConfigError): load_config(self.repo) + def test_exposes_max_attempts_per_profile_and_emergency_profile(self) -> None: + block = MINIMAL_BLOCK.replace( + " raw_output_bytes: 10485760\n", + " raw_output_bytes: 10485760\n" + " max_attempts_per_profile: 3\n" + " emergency_profile: codex\n", + ).replace( + " claude:\n" + " adapter: claude\n" + " executable: claude\n" + " model: auto\n" + " enabled: true\n", + " claude:\n" + " adapter: claude\n" + " executable: claude\n" + " model: auto\n" + " enabled: true\n" + "\n" + " codex:\n" + " adapter: codex\n" + " executable: codex\n" + " model: auto\n" + " enabled: true\n", + ) + write_repo(self.repo, block) + cfg = load_config(self.repo) + self.assertEqual(cfg.execution.max_attempts_per_profile, 3) + self.assertEqual(cfg.execution.emergency_profile, "codex") + if __name__ == "__main__": unittest.main()