From f943b0db30bdb6143a757f8750d306ce34120ad2 Mon Sep 17 00:00:00 2001 From: Godopu Date: Fri, 16 Oct 2026 03:21:50 +0000 Subject: [PATCH] fix(agents): a session file whose startedAt is no finite time is live (Issue #31, Mac review) float("NaN") and a JSON NaN passed the old check, and every comparison with NaN is false, so a live holder looked like a reused pid: the in-use gate let the purge through and the session file was taken as stale. startedAt must now be an int or float (not bool), finite and positive; anything else cannot be told and counts as live. A dead pid is still stale whatever it says. Co-Authored-By: Claude Opus 5.5 --- .../skills/lib_py/agents/adapters/claude.py | 12 ++++++--- tests/test_issue31_purge_residual.py | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.agents/skills/lib_py/agents/adapters/claude.py b/.agents/skills/lib_py/agents/adapters/claude.py index d9e8787..8a3e462 100644 --- a/.agents/skills/lib_py/agents/adapters/claude.py +++ b/.agents/skills/lib_py/agents/adapters/claude.py @@ -1,4 +1,4 @@ -import os, re, json, glob, stat, time, shutil, subprocess +import os, re, json, glob, math, stat, time, shutil, subprocess from typing import Optional, Any from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, conversation_in_use, scan_names, _plausible_id from lib_py.verify_session import workspace_key @@ -77,15 +77,19 @@ def _session_file_live(data: dict, pid) -> bool: return False except OSError: pass + # 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. + started = data.get("startedAt") + if isinstance(started, bool) or not isinstance(started, (int, float)) \ + or not math.isfinite(started) or started <= 0: + return True + started = started / 1000.0 try: out = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True, timeout=5, env=dict(os.environ, LC_ALL="C")).stdout lstart = time.mktime(time.strptime(" ".join(out.split()), "%a %b %d %H:%M:%S %Y")) - started = float(data["startedAt"]) / 1000.0 except Exception: return True - if started <= 0: - return True # startedAt is written 1.7-3.2 s after the process starts; 2 s covers # lstart's one-second resolution. return lstart <= started + 2 diff --git a/tests/test_issue31_purge_residual.py b/tests/test_issue31_purge_residual.py index 74492d4..8efb44f 100644 --- a/tests/test_issue31_purge_residual.py +++ b/tests/test_issue31_purge_residual.py @@ -447,6 +447,33 @@ def test_session_file_liveness_fails_safe(monkeypatch, live): assert claude_mod._session_file_live({"startedAt": hour_ago}, live.pid) is True, "ps unavailable: live" +@pytest.mark.parametrize("started", ["NaN", float("nan"), float("inf"), float("-inf"), "inf", "1760000000000", + "soon", True, False, None, [1], ""]) +def test_an_unusable_started_at_means_live(live, started): + from lib_py.agents.adapters import claude as claude_mod + data = {} if started == "" else {"startedAt": started} + assert claude_mod._session_file_live(data, live.pid) is True + assert claude_mod._session_file_live(data, _dead_pid()) is False, "a dead pid holds nothing" + + +@pytest.mark.parametrize("raw", ["NaN", '"NaN"', "Infinity", "true"]) +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 + comparison fails, which once made a live holder look stale.""" + ws = tmp_path / "ws" + ws.mkdir() + sfile, key = _session_file(home / ".claude", live.pid, conv) + sfile.write_text(f'{{"pid": {live.pid}, "sessionId": "{conv}", "cwd": "/x", "startedAt": {raw}}}') + adapter = get_adapter("claude") + ctx = _ctx(ws, home) + + assert adapter.conversation_in_use(conv, ctx) is True + res = _agents_cli(["in-use", "claude", conv, str(ws)], ws) + assert res.returncode == 4, res.stdout + res.stderr + assert adapter.purge_artifacts(conv, ctx) == [] + assert sfile.exists() and key.exists() + + def test_claude_purge_takes_the_stale_key_with_its_session_file(home, tmp_path, conv, other, live): ws = tmp_path / "ws" ws.mkdir()