From 2bc9d4eeb921ff7888e0d7071a9aeb76709bfcdb Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 6 Sep 2026 17:58:24 -0700 Subject: [PATCH] [eric] diagnostics: every envelope names the machine that wrote it (OS, version, chip, cores, memory), the fleet is not one M-series Mac Co-Authored-By: Claude Fable 5.1 --- backend/apps/service/client.py | 2 + backend/apps/service/machine_facts.py | 55 +++++++++++++++++++++++++++ backend/tests/test_service.py | 14 +++++++ 3 files changed, 71 insertions(+) create mode 100644 backend/apps/service/machine_facts.py diff --git a/backend/apps/service/client.py b/backend/apps/service/client.py index fc537fde..68235f8f 100644 --- a/backend/apps/service/client.py +++ b/backend/apps/service/client.py @@ -30,6 +30,7 @@ from uuid import uuid4 import httpx from backend.apps.service import buffer +from backend.apps.service.machine_facts import machine_facts from backend.apps.service.version import APP_VERSION logger = logging.getLogger(__name__) @@ -402,6 +403,7 @@ def submit_diagnostic(diagnostic: dict) -> None: return # The build that wrote an envelope is the first thing a field read needs, and it used to be inferred from which fields were present. diagnostic.setdefault("app_version", APP_VERSION) + diagnostic.setdefault("machine", machine_facts()) try: from backend.apps.service.ring_buffer import snapshot diagnostic["recent_log"] = snapshot() diff --git a/backend/apps/service/machine_facts.py b/backend/apps/service/machine_facts.py new file mode 100644 index 00000000..d19c9879 --- /dev/null +++ b/backend/apps/service/machine_facts.py @@ -0,0 +1,55 @@ +"""Which kind of machine wrote a diagnostic: OS, chip, cores, memory. Every field report used to come +from "a Mac", and every timing verdict from one developer's M-series; the fleet spans a 2017 iMac and +Windows laptops nobody has measured, so the envelope says what it ran on.""" +from __future__ import annotations + +import ctypes +import functools +import os +import platform +import sys + + +def p_memory_bytes() -> int | None: + if sys.platform == "win32": + class MemoryStatus(ctypes.Structure): + _fields_ = [ + ("dwLength", ctypes.c_ulong), + ("dwMemoryLoad", ctypes.c_ulong), + ("ullTotalPhys", ctypes.c_ulonglong), + ("ullAvailPhys", ctypes.c_ulonglong), + ("ullTotalPageFile", ctypes.c_ulonglong), + ("ullAvailPageFile", ctypes.c_ulonglong), + ("ullTotalVirtual", ctypes.c_ulonglong), + ("ullAvailVirtual", ctypes.c_ulonglong), + ("ullAvailExtendedVirtual", ctypes.c_ulonglong), + ] + status = MemoryStatus() + status.dwLength = ctypes.sizeof(MemoryStatus) + kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] + return int(status.ullTotalPhys) if kernel32.GlobalMemoryStatusEx(ctypes.byref(status)) else None + try: + return int(os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")) + except (ValueError, OSError, AttributeError): + return None + + +def p_os_version() -> str: + # Darwin's kernel number (25.6.0) means nothing to a reader; the marketing version does. + if sys.platform == "darwin": + return platform.mac_ver()[0] or platform.release() + if sys.platform == "win32": + return platform.version() + return platform.release() + + +@functools.lru_cache(maxsize=1) +def machine_facts() -> dict[str, object]: + memory = p_memory_bytes() + return { + "os": platform.system(), + "os_version": p_os_version(), + "arch": platform.machine(), + "cpus": os.cpu_count() or 0, + "memory_gb": round(memory / 1024**3, 1) if memory else None, + } diff --git a/backend/tests/test_service.py b/backend/tests/test_service.py index b7dc4f91..bc6b6f1d 100644 --- a/backend/tests/test_service.py +++ b/backend/tests/test_service.py @@ -224,6 +224,20 @@ def test_every_diagnostic_carries_the_app_version(sink): assert APP_VERSION and APP_VERSION != "unknown" +def test_every_diagnostic_says_which_machine_wrote_it(sink): + # Every timing verdict came from one developer's M-series Mac; a field envelope now names its own OS, chip, cores and memory. + import platform + from backend.apps.service.client import submit_diagnostic + submit_diagnostic({"kind": "model_error"}) + _, body = sink[0] + machine = body["d"]["diagnostic"]["machine"] + assert machine["os"] == platform.system() + assert machine["arch"] == platform.machine() + assert machine["cpus"] >= 1 + assert machine["memory_gb"] > 0 + assert machine["os_version"] + + # --- spool ------------------------------------------------------------------- def test_buffer_enqueue_and_drain(tmp_path):