From af8ed397f6586677eee2bc1661e686b813e77a92 Mon Sep 17 00:00:00 2001 From: Godopu Date: Fri, 16 Oct 2026 03:23:17 +0000 Subject: [PATCH] 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 --- .../skills/lib_py/agents/adapters/claude.py | 7 +++--- tests/test_issue31_purge_residual.py | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.agents/skills/lib_py/agents/adapters/claude.py b/.agents/skills/lib_py/agents/adapters/claude.py index 8a3e462..af29fcb 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, math, stat, time, shutil, subprocess +import os, re, json, glob, math, stat, time, shutil, calendar, 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 @@ -84,10 +84,11 @@ def _session_file_live(data: dict, pid) -> bool: or not math.isfinite(started) or started <= 0: return True started = started / 1000.0 + # lstart in UTC: a local time is ambiguous in the hour a DST change repeats. 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")) + timeout=5, env=dict(os.environ, LC_ALL="C", TZ="UTC0")).stdout + lstart = calendar.timegm(time.strptime(" ".join(out.split()), "%a %b %d %H:%M:%S %Y")) except Exception: return True # startedAt is written 1.7-3.2 s after the process starts; 2 s covers diff --git a/tests/test_issue31_purge_residual.py b/tests/test_issue31_purge_residual.py index 8efb44f..945be49 100644 --- a/tests/test_issue31_purge_residual.py +++ b/tests/test_issue31_purge_residual.py @@ -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" +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", "soon", True, False, None, [1], ""]) def test_an_unusable_started_at_means_live(live, started):