diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 099a5f2..e4f7e4f 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 3099e1d..dc08e3f 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"