From 4db744079083b7428595af51f301f2d1f7e938bb Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Sat, 8 Aug 2026 08:38:47 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20durable=20evolution=20count=20=E2=80=94?= =?UTF-8?q?=20include=20valid=20disk=20logs=20after=20daemon=20restart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- emrg/server/daemon.py | 28 +++++++++++++++++++++++++--- tests/test_ws_e2e.py | 3 ++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 099a5f20..e4f7e4fb 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -287,23 +287,45 @@ async def serve(self) -> None: pass def _evolution_count(self) -> int: - """Total completed evolution cycles across scheduler handlers. + """Total completed evolution cycles across scheduler handlers + disk. The daemon's own ``self.evolutions`` list is a legacy from the pre-scheduler BackgroundThread design (#95) and is never appended; the scheduler's handlers own the real per-cycle logs. Aggregate from the scheduler, falling back to the legacy list only when the scheduler is unavailable (e.g. test harnesses mock it away). + + The scheduler's in-memory count resets to 0 on daemon restart, while + the ``evolution-*.json`` log files persist — so also count valid log + files on disk and return the max. This keeps the GUI growth card / + evolution toast consistent with the ``recent`` list (which reads the + same files) across restarts instead of showing 0. """ + in_memory = 0 sched = getattr(self, "_scheduler", None) if sched is not None: try: total = sched.total_evolutions() if isinstance(total, int): - return total + in_memory = total except Exception: pass - return len(self.evolutions) + else: + in_memory = len(self.evolutions) + + disk = 0 + try: + logs_dir = config_dir() / "logs" + for f in logs_dir.glob("evolution-*.json"): + try: + data = json.loads(f.read_text(encoding="utf-8")) + if data.get("timestamp"): + disk += 1 + except (json.JSONDecodeError, OSError): + continue # corrupt/partial write — don't count + except OSError: + pass + return max(in_memory, disk) async def _handle_client(self, ws) -> None: """Handle a single WebSocket client connection. diff --git a/tests/test_ws_e2e.py b/tests/test_ws_e2e.py index 3099e1d7..dc08e3f1 100644 --- a/tests/test_ws_e2e.py +++ b/tests/test_ws_e2e.py @@ -524,7 +524,7 @@ async def _test(): frame = await asyncio.wait_for(ws.recv(), timeout=5) resp = json.loads(frame) assert resp["type"] == "evolution_summary" - assert resp["count"] == 0 # in-memory evolutions empty in test harness + assert resp["count"] == 2 # valid disk logs counted (corrupt skipped) # newest first (reverse lexicographic = chronological) assert [r["timestamp"] for r in resp["recent"]] == [ "2026-08-06T10:00:00", "2026-08-06T09:00:00"] @@ -552,6 +552,7 @@ async def _test(): frame = await asyncio.wait_for(ws.recv(), timeout=5) resp = json.loads(frame) assert resp["type"] == "evolution_summary" + assert resp["count"] == 25, f"count must include all valid disk logs, got {resp['count']}" assert len(resp["recent"]) == 20, f"limit must clamp to 20, got {len(resp['recent'])}" # newest-first: first entry is the highest timestamp assert resp["recent"][0]["timestamp"] == "2026-08-06T00:24:00"