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 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e0361cf43
commit
f943b0db30
2 files changed
+35
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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], "<missing>"])
|
||||
def test_an_unusable_started_at_means_live(live, started):
|
||||
from lib_py.agents.adapters import claude as claude_mod
|
||||
data = {} if started == "<missing>" 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()
|
||||
|
||||
Reference in new issue
Block a user