fix: refuse ambiguous Codex session-id capture

When more than one rollout matches the project cwd within the capture
window, return None and warn instead of guessing the earliest. The worst
case is now a kept placeholder id, never a wrong one. Part of the P2
capture-race fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:43:52 -04:00
parent 1b79530214
commit f6a90e8f4d
2 changed files with 23 additions and 43 deletions
+14 -1
View File
@@ -1,9 +1,12 @@
import json
import logging
from datetime import datetime
from pathlib import Path
from hqt.harnesses.base import HarnessConfigurator, SpawnConfig
log = logging.getLogger(__name__)
class CodexConfigurator(HarnessConfigurator):
name = "codex"
@@ -73,5 +76,15 @@ class CodexConfigurator(HarnessConfigurator):
candidates.append((started_at, payload["id"]))
except (json.JSONDecodeError, KeyError, OSError, TypeError):
continue
candidates.sort(key=lambda t: t[0])
if len(candidates) > 1:
# Ambiguous: more than one Codex rollout matches this cwd + time
# window (e.g. a second Codex started in the same project). Guessing
# risks storing the wrong conversation id, so keep the placeholder.
log.warning(
"capture_session_id: %d rollouts match cwd=%s since=%s; refusing to guess",
len(candidates),
project_path,
since,
)
return None
return candidates[0][1] if candidates else None
+9 -42
View File
@@ -1,7 +1,6 @@
import json
import os
import time
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import patch
@@ -184,57 +183,25 @@ def test_codex_capture_accepts_file_after_since(tmp_path):
assert result == "new-id"
def test_codex_capture_first_post_since_cwd_match_wins(tmp_path):
"""Among multiple post-since rollouts, the one started nearest since wins."""
def test_codex_capture_ambiguous_returns_none(tmp_path):
"""Two rollouts match cwd + since window -> refuse to guess, return None."""
sessions_dir = tmp_path / ".codex" / "sessions" / "dir"
sessions_dir.mkdir(parents=True)
rollout_older = sessions_dir / "rollout-older.jsonl"
rollout_newer = sessions_dir / "rollout-newer.jsonl"
_write_rollout(rollout_older, "older-id", "/projects/foo")
_write_rollout(rollout_newer, "newer-id", "/projects/foo")
rollout_a = sessions_dir / "rollout-a.jsonl"
rollout_b = sessions_dir / "rollout-b.jsonl"
_write_rollout(rollout_a, "id-a", "/projects/foo")
_write_rollout(rollout_b, "id-b", "/projects/foo")
now = time.time()
since = now - 120
os.utime(rollout_older, (now - 30, now - 30))
os.utime(rollout_newer, (now - 10, now - 10))
os.utime(rollout_a, (now - 30, now - 30))
os.utime(rollout_b, (now - 10, now - 10))
c = CodexConfigurator()
with patch("hqt.harnesses.configurators.codex.Path.home", return_value=tmp_path):
result = c.capture_session_id(Path("/projects/foo"), since)
assert result == "older-id"
def test_codex_capture_prefers_started_nearest_since_over_newest_file(tmp_path):
"""Late capture should match the Codex session spawned at since, not a later same-cwd session."""
sessions_dir = tmp_path / ".codex" / "sessions" / "dir"
sessions_dir.mkdir(parents=True)
target = sessions_dir / "rollout-target.jsonl"
unrelated_later = sessions_dir / "rollout-later.jsonl"
since = datetime(2026, 6, 10, 21, 9, 15, tzinfo=UTC).timestamp()
_write_rollout(
target,
"target-id",
"/projects/foo",
timestamp="2026-06-10T21:09:16.000Z",
)
_write_rollout(
unrelated_later,
"later-id",
"/projects/foo",
timestamp="2026-06-10T21:45:45.000Z",
)
now = time.time()
os.utime(target, (now - 60, now - 60))
os.utime(unrelated_later, (now, now))
c = CodexConfigurator()
with patch("hqt.harnesses.configurators.codex.Path.home", return_value=tmp_path):
result = c.capture_session_id(Path("/projects/foo"), since)
assert result == "target-id"
assert result is None
def test_codex_capture_skips_non_matching_cwd_picks_older_matching(tmp_path):