Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ jobs:
tests/test_doctor_install_freshness.py
tests/test_file_lock.py
tests/test_file_lock_cross_process.py
tests/test_host_thread_activity.py
tests/control_plane/test_effect_runtime_integration.py
tests/control_plane/test_shadow_observation_retirement.py
tests/test_python_install_owner_platform.py
Expand Down
7 changes: 5 additions & 2 deletions loopx/codex_app_thread_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def codex_homes(env: Mapping[str, str] | None = None, user_home: Path | None = N
``LOOPX_CODEX_HOMES`` (``os.pathsep``-separated) is used verbatim when set.
Otherwise ``CODEX_HOME``, ``~/.codex`` and sibling ``~/.codex-*`` homes are
searched, because Codex App installs can each keep their own home.
Relative entries use the process working directory when their store is read.
"""

env = os.environ if env is None else env
Expand Down Expand Up @@ -108,14 +109,16 @@ def _query_threads(uri: str, thread_ids: list[str]) -> dict[str, tuple[Any, Any]


def _thread_rows(db: Path, thread_ids: list[str]) -> dict[str, tuple[Any, Any]]:
# Configured homes may be relative; SQLite file URIs require an absolute path.
uri = db.absolute().as_uri()
try:
return _query_threads(f"{db.as_uri()}?mode=ro", thread_ids)
return _query_threads(f"{uri}?mode=ro", thread_ids)
except sqlite3.OperationalError:
# A WAL store whose -shm is absent cannot be read with mode=ro.
# Without a -wal file the main file is the whole store.
if Path(f"{db}-wal").exists():
raise
return _query_threads(f"{db.as_uri()}?immutable=1", thread_ids)
return _query_threads(f"{uri}?immutable=1", thread_ids)


def current_codex_execution_identity(
Expand Down
19 changes: 19 additions & 0 deletions tests/test_host_thread_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ def _observe(home: CodexHome, *thread_ids: str) -> dict[str, HostThreadActivity]
return observe_codex_threads(thread_ids, homes=[home.root])


@pytest.mark.parametrize("configuration", ["explicit", "CODEX_HOME", "LOOPX_CODEX_HOMES"])
def test_relative_codex_home_recovers_activity_without_writing_the_store(
tmp_path, monkeypatch, configuration,
):
selected = CodexHome(tmp_path / "project # 中文" / ".codex")
rollout = selected.thread("relative-thread", [_event(T2, "task_complete")])
before = {path: path.read_bytes() for path in (selected.db, rollout)}
monkeypatch.chdir(tmp_path)
relative = selected.root.relative_to(tmp_path)
homes = [relative] if configuration == "explicit" else codex_homes(
env={configuration: str(relative)}, user_home=tmp_path / "synthetic-user",
)
assert homes[0] == relative
result = observe_codex_threads(["relative-thread"], homes=homes)["relative-thread"]
assert result.state is HostThreadState.IDLE
assert result.last_turn_ended_at == T2
assert all(path.read_bytes() == content for path, content in before.items())


def test_current_execution_reads_the_bound_home_only_and_recovers_without_preferences(home, tmp_path):
other = CodexHome(tmp_path / ".codex-other")
meta = {"timestamp": T0, "type": "session_meta", "payload": {"id": "t", "model_provider": "openai"}}
Expand Down
Loading