fix(issue-31): a claude purge removes every path the session owns, reports what is left, and waits for the process; the stop docs say a purge removes the row #59

Merged
Godopu merged 26 commits from fix/issue-31 into main 2026-10-17 16:25:21 +00:00
2 changed files with 28 additions and 3 deletions
Showing only changes of commit af8ed397f6 - Show all commits

No files matched your search

@@ -1,4 +1,4 @@
import os, re, json, glob, math, stat, time, shutil, subprocess import os, re, json, glob, math, stat, time, shutil, calendar, subprocess
from typing import Optional, Any from typing import Optional, Any
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, conversation_in_use, scan_names, _plausible_id from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, conversation_in_use, scan_names, _plausible_id
from lib_py.verify_session import workspace_key from lib_py.verify_session import workspace_key
@@ -84,10 +84,11 @@ def _session_file_live(data: dict, pid) -> bool:
or not math.isfinite(started) or started <= 0: or not math.isfinite(started) or started <= 0:
return True return True
started = started / 1000.0 started = started / 1000.0
# 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,
timeout=5, env=dict(os.environ, LC_ALL="C")).stdout timeout=5, env=dict(os.environ, LC_ALL="C", TZ="UTC0")).stdout
lstart = time.mktime(time.strptime(" ".join(out.split()), "%a %b %d %H:%M:%S %Y")) lstart = calendar.timegm(time.strptime(" ".join(out.split()), "%a %b %d %H:%M:%S %Y"))
except Exception: except Exception:
return True return True
# startedAt is written 1.7-3.2 s after the process starts; 2 s covers # startedAt is written 1.7-3.2 s after the process starts; 2 s covers
+24
View File
@@ -447,6 +447,30 @@ 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" assert claude_mod._session_file_live({"startedAt": hour_ago}, live.pid) is True, "ps unavailable: live"
def test_liveness_reads_lstart_as_utc(tmp_path, monkeypatch, live):
"""2026-11-01 01:30 happens twice in EST5EDT. A process that started at
the second one (06:30 UTC) shows lstart 01:30 to a local-time ps; read as
the first (05:30 UTC) it predates a session file written at 05:45. ps
runs with TZ=UTC0 instead, which BSD ps and procps both honour."""
import calendar
from lib_py.agents.adapters import claude as claude_mod
fake = tmp_path / "fakebin"
fake.mkdir()
(fake / "ps").write_text('#!/bin/sh\nif [ "$TZ" = UTC0 ]; then echo "Sun Nov 1 06:30:00 2026"; '
'else echo "Sun Nov 1 01:30:00 2026"; fi\n')
(fake / "ps").chmod(0o755)
utc = lambda hhmmss: calendar.timegm(time.strptime(f"2026-11-01 {hhmmss}", "%Y-%m-%d %H:%M:%S")) * 1000 # noqa: E731
try:
with monkeypatch.context() as m:
m.setenv("PATH", f"{fake}:{os.environ['PATH']}")
m.setenv("TZ", "EST5EDT,M3.2.0,M11.1.0")
time.tzset()
assert claude_mod._session_file_live({"startedAt": utc("05:45:00")}, live.pid) is False, "reused pid"
assert claude_mod._session_file_live({"startedAt": utc("06:30:02")}, live.pid) is True
finally:
time.tzset()
@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], "<missing>"])
def test_an_unusable_started_at_means_live(live, started): def test_an_unusable_started_at_means_live(live, started):