diff --git a/acp_adapter/session.py b/acp_adapter/session.py index c124229bec8..bbe34b06789 100644 --- a/acp_adapter/session.py +++ b/acp_adapter/session.py @@ -617,6 +617,10 @@ class SessionManager: _register_task_cwd(session_id, cwd) agent = AIAgent(**kwargs) + # Codex app-server sessions are spawned lazily on the first turn. Stamp + # the ACP workspace onto the agent so the Codex runtime starts from the + # editor/session cwd instead of the Hermes daemon's process cwd. + agent.session_cwd = cwd # ACP stdio transport requires stdout to remain protocol-only JSON-RPC. # Route any incidental human-readable agent output to stderr instead. agent._print_fn = _acp_stderr_print diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index 9928c07878c..e638a194159 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -250,7 +250,9 @@ def run_codex_app_server_turn( # Spawned on first turn, reused across turns, closed at AIAgent # shutdown (see _cleanup hook). if not hasattr(agent, "_codex_session") or agent._codex_session is None: - cwd = getattr(agent, "session_cwd", None) or os.getcwd() + from agent.runtime_cwd import resolve_agent_cwd + + cwd = getattr(agent, "session_cwd", None) or str(resolve_agent_cwd()) # Approval callback: defer to Hermes' standard prompt flow if a # CLI thread has installed one. Gateway / cron contexts get the # codex-side fail-closed default. diff --git a/tests/acp/test_session.py b/tests/acp/test_session.py index 3bfe64a2213..5ff5e08b807 100644 --- a/tests/acp/test_session.py +++ b/tests/acp/test_session.py @@ -77,6 +77,50 @@ class TestCreateSession: def test_get_nonexistent_session_returns_none(self, manager): assert manager.get_session("does-not-exist") is None + def test_make_agent_stamps_session_cwd_for_codex_runtime(self, monkeypatch): + class FakeAgent: + model = "fake-model" + + def __init__(self, **kwargs): + self.kwargs = kwargs + + monkeypatch.setattr("run_agent.AIAgent", FakeAgent) + monkeypatch.setattr( + "acp_adapter.session.load_config", + lambda: { + "model": { + "default": "fake-model", + "provider": "fake-provider", + }, + "mcp_servers": {}, + }, + raising=False, + ) + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: { + "model": { + "default": "fake-model", + "provider": "fake-provider", + }, + "mcp_servers": {}, + }, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + lambda requested=None: { + "provider": requested, + "api_mode": "codex_app_server", + "base_url": "https://example.invalid", + "api_key": "test-key", + }, + ) + monkeypatch.setattr("acp_adapter.session._register_task_cwd", lambda task_id, cwd: None) + + state = SessionManager(db=None).create_session(cwd="/tmp/project") + + assert state.agent.session_cwd == "/tmp/project" + diff --git a/tests/run_agent/test_codex_app_server_integration.py b/tests/run_agent/test_codex_app_server_integration.py index b1de32a3302..7c5ac4f83c7 100644 --- a/tests/run_agent/test_codex_app_server_integration.py +++ b/tests/run_agent/test_codex_app_server_integration.py @@ -293,6 +293,39 @@ class TestRunConversationCodexPath: agent.run_conversation("hi") assert not client_mock.chat.completions.create.called + def test_gateway_terminal_cwd_seeds_codex_thread_cwd(self, monkeypatch, tmp_path): + """Gateway sessions set TERMINAL_CWD without stamping agent.session_cwd. + Codex app-server must still start in that configured workspace instead + of falling back to the Hermes daemon process cwd.""" + from agent.transports.codex_app_server_session import ( + CodexAppServerSession, TurnResult, + ) + + captured: dict[str, str] = {} + + def fake_init(self, **kwargs): + captured["cwd"] = kwargs["cwd"] + self._thread_id = "thread-stub-1" + + def fake_run_turn(self, user_input: str, **kwargs): + return TurnResult( + final_text="ok", + projected_messages=[{"role": "assistant", "content": "ok"}], + turn_id="turn-stub-1", + thread_id="thread-stub-1", + ) + + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init) + monkeypatch.setattr(CodexAppServerSession, "run_turn", fake_run_turn) + + agent = _make_codex_agent() + assert not hasattr(agent, "session_cwd") + with patch.object(agent, "_spawn_background_review", return_value=None): + agent.run_conversation("hi") + + assert captured["cwd"] == str(tmp_path) + class TestReviewForkApiModeDowngrade: """When the parent agent runs on codex_app_server, the background