fix(agents): a startedAt too large for a float counts as live (Issue #31, review)

json.load reads a huge integer as an int, and math.isfinite raised
OverflowError on it outside any try: conversation_in_use and purge_artifacts
(which must never raise) threw, where 6f4f40a had taken it as live. The
conversion to float now comes first, and an overflow means live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-16 04:05:13 +00:00
1 parent e156179822
commit 60779ba1f8
2 files changed
+15 -7

No files matched your search

@@ -78,12 +78,17 @@ def _session_file_live(data: dict, pid) -> bool:
except OSError: except OSError:
pass pass
# startedAt is epoch ms: a finite positive number. A bool, a string, NaN # startedAt is epoch ms: a finite positive number. A bool, a string, NaN
# (every comparison with it is false) or an infinity is no time at all. # (every comparison with it is false), an infinity or an int too large
# for a float is no time at all.
started = data.get("startedAt") started = data.get("startedAt")
if isinstance(started, bool) or not isinstance(started, (int, float)) \ if isinstance(started, bool) or not isinstance(started, (int, float)):
or not math.isfinite(started) or started <= 0: return True
try:
started = float(started) / 1000.0
except OverflowError:
return True
if not math.isfinite(started) or started <= 0:
return True return True
started = started / 1000.0
# lstart in UTC: a local time is ambiguous in the hour a DST change repeats. # lstart in UTC: a local time is ambiguous in the hour a DST change repeats.
try: try:
out = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True, out = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True,
+6 -3
View File
@@ -472,7 +472,8 @@ def test_liveness_reads_lstart_as_utc(tmp_path, monkeypatch, live):
@pytest.mark.parametrize("started", ["NaN", float("nan"), float("inf"), float("-inf"), "inf", "1760000000000", @pytest.mark.parametrize("started", ["NaN", float("nan"), float("inf"), float("-inf"), "inf", "1760000000000",
"soon", True, False, None, [1], "<missing>"]) "soon", True, False, None, [1], pytest.param(10 ** 400, id="10**400"),
"<missing>"])
def test_an_unusable_started_at_means_live(live, started): def test_an_unusable_started_at_means_live(live, started):
from lib_py.agents.adapters import claude as claude_mod from lib_py.agents.adapters import claude as claude_mod
data = {} if started == "<missing>" else {"startedAt": started} data = {} if started == "<missing>" else {"startedAt": started}
@@ -480,10 +481,12 @@ def test_an_unusable_started_at_means_live(live, started):
assert claude_mod._session_file_live(data, _dead_pid()) is False, "a dead pid holds nothing" assert claude_mod._session_file_live(data, _dead_pid()) is False, "a dead pid holds nothing"
@pytest.mark.parametrize("raw", ["NaN", '"NaN"', "Infinity", "true"]) @pytest.mark.parametrize("raw", ["NaN", '"NaN"', "Infinity", "true",
pytest.param("1" + "0" * 400, id="10**400")])
def test_a_live_holder_with_an_unusable_started_at_blocks_the_purge(home, tmp_path, conv, live, raw): def test_a_live_holder_with_an_unusable_started_at_blocks_the_purge(home, tmp_path, conv, live, raw):
"""json.load turns a bare NaN or Infinity into a float that every """json.load turns a bare NaN or Infinity into a float that every
comparison fails, which once made a live holder look stale.""" comparison fails, which once made a live holder look stale, and a huge
integer into an int no float can hold."""
ws = tmp_path / "ws" ws = tmp_path / "ws"
ws.mkdir() ws.mkdir()
sfile, key = _session_file(home / ".claude", live.pid, conv) sfile, key = _session_file(home / ".claude", live.pid, conv)