fix(agents): compare a claude process's lstart in UTC (Issue #31, Mac review)

time.mktime read ps's local lstart, which is ambiguous in the hour a DST
change repeats: the start could come out an hour off and a live holder be
taken for a reused pid (or the reverse). ps now runs with TZ=UTC0, which BSD
ps and procps both honour, and calendar.timegm reads it. No /proc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-16 03:23:17 +00:00
1 parent f943b0db30
commit af8ed397f6
2 files changed
+28 -3

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):