From eb986cbd3f5eb1a060709dea098afad9ba23afcc Mon Sep 17 00:00:00 2001 From: vivekchand Date: Sun, 23 Aug 2026 10:38:03 +0200 Subject: [PATCH 01/12] Send the Quality grade to the cloud, and stop the cloud inventing an empty one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hosted Quality tab said "Nothing to grade yet" for a machine whose own dashboard showed an A over 119 graded runs — same node, same runtime, same week. Two faults stacked on one screen. The grade never rode the snapshot. Every card the cloud renders has to ship a slice the daemon emits; Quality never did, so there was nothing to show. And the hosted container answered anyway. It has a DuckDB file of its own — empty, but perfectly able to answer — so the query returned zero rows and the tab reported a working machine as having produced nothing. The handler already distinguished "the store said nothing" from "I could not reach the store"; what it could not know was that its own store was a stranger's. "Nothing to grade" and "I cannot see your machine from here" look identical and mean opposite things, and the wrong one was showing. So: the daemon now emits `quality` — the node-wide card, one card per runtime, and the calibration they share. The composer moved out of the request handler so both callers build the identical payload. Reading is done ONCE and grouped in Python, so fourteen runtime cards cost three queries, not forty-two, and the bounded deep scan runs once for the whole node rather than per card. Runtimes that were quiet this week still get their own card. Without one the hosted tab would fall back to the node-wide card and show another runtime's grade under this runtime's filter. Calibration is carried once per slice instead of inline in every card: it is byte-identical in all of them and was a quarter of the payload (85 kB -> 61 kB). And the hosted process no longer reads its own store for this at all. When the snapshot slice is missing — an older daemon — it says the grade is computed on your machine, which is true, instead of inventing an empty week. Found while building it, worth its own line: the first version of the daemon slice silently returned {} because sync.py imports datetime and timezone but not timedelta, and the broad except swallowed the NameError. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0138ySWEXGWEayXCPDqWrw21 --- clawmetry/sync.py | 93 +++++++++++++ routes/quality.py | 170 ++++++++++++++-------- tests/test_quality_cloud_parity.py | 217 +++++++++++++++++++++++++++++ 3 files changed, 423 insertions(+), 57 deletions(-) create mode 100644 tests/test_quality_cloud_parity.py diff --git a/clawmetry/sync.py b/clawmetry/sync.py index 6967bd0c92..e274f5278b 100644 --- a/clawmetry/sync.py +++ b/clawmetry/sync.py @@ -17192,6 +17192,98 @@ def _reliability_score_session(events): } +_QUALITY_SNAPSHOT_WINDOW_HOURS = 168 # 7d — what the Quality tab asks for + + +def _build_quality_snapshot(): + """The Quality report card, node-wide and per runtime, for the snapshot. + + Founder live-hit 2026-08-22: the hosted Quality tab said "Nothing to grade + yet" for a machine whose local tab showed an A over 119 graded runs. The + grade had never ridden the snapshot at all, and the hosted container + answered the request from its OWN DuckDB — which exists but is empty, so + it reported "no runs" instead of failing. An empty answer and an + unreachable machine looked identical on screen and meant opposite things. + + Read once, compose many: the node's sessions are queried ONCE (current + window, prior window, and a 30-day history for calibration) and grouped in + Python, so a card per runtime costs no extra queries. Sessions are graded + at ingest, so composing is mostly dict lookups; the bounded deep scan runs + once over the node's rows and every per-runtime card reuses that map. + + Returns ``{}`` on any failure — a snapshot must never fail to build over + one optional slice, and the cloud falls back to saying the grade lives on + your machine. + """ + try: + from datetime import timedelta # module scope imports datetime/timezone only + + from clawmetry import local_store as _ls + from clawmetry import quality_thresholds as _qt + from routes.quality import _assess_rows, compose_report_card + + store = _ls.get_store() + if store is None: + return {} + + hours = _QUALITY_SNAPSHOT_WINDOW_HOURS + now = datetime.now(timezone.utc) + since = (now - timedelta(hours=hours)).isoformat() + prior_since = (now - timedelta(hours=hours * 2)).isoformat() + hist_since = (now - timedelta(hours=24 * 30)).isoformat() + + rows = store.query_quality_sessions(since=since, limit=400) or [] + prior_rows = store.query_quality_sessions( + since=prior_since, until=since, limit=400) or [] + hist_rows = store.query_quality_sessions(since=hist_since, limit=1500) or [] + + by_rt_hist = {} + for h in hist_rows: + by_rt_hist.setdefault(h.get("runtime") or "openclaw", []).append(h) + thresholds = _qt.calibrate_all(by_rt_hist) + assessments = _assess_rows(rows, thresholds) + prior_assessments = _assess_rows(prior_rows, thresholds, deep_limit=0) + + def _card(sub_rows, sub_prior, runtime): + return compose_report_card( + sub_rows, sub_prior, hist_rows, + window_hours=hours, runtime=runtime, + assessments=assessments, prior_assessments=prior_assessments, + ) + + out = { + "window_hours": hours, + "all": _card(rows, prior_rows, None), + "byRuntime": {}, + } + # Calibration is identical in every card (same 30-day history), so it + # is carried ONCE here and re-attached client-side. Left inline it was + # ~1.9 kB duplicated across fourteen cards, a quarter of the slice + # spent saying the same thing. + out["thresholds"] = out["all"].get("thresholds") or {} + # Per-runtime cards so the hosted tab stays honest under the runtime + # switcher: a grade shown while a single runtime is selected must be + # THAT runtime's grade, never the node's total wearing its name. + # Every runtime the machine has run in the last 30 days, not only those + # with sessions THIS week: a runtime that was quiet this week needs a + # card saying so in its own name. Without one the hosted tab would fall + # back to the node-wide card and show another runtime's grade under + # this runtime's filter. + runtimes = {(r.get("runtime") or "openclaw") for r in rows} + runtimes |= {(h.get("runtime") or "openclaw") for h in hist_rows} + for rt in runtimes: + sub = [r for r in rows if (r.get("runtime") or "openclaw") == rt] + sub_prior = [r for r in prior_rows + if (r.get("runtime") or "openclaw") == rt] + out["byRuntime"][rt] = _card(sub, sub_prior, rt) + for _c in [out["all"], *out["byRuntime"].values()]: + _c.pop("thresholds", None) + return out + except Exception as exc: + log.debug(f"quality snapshot build failed (continuing): {exc}") + return {} + + def _build_reliability(limit_sessions=25, min_sessions=4): """Agent Reliability score for the cloud Pro Reliability tab (P1). @@ -20885,6 +20977,7 @@ def sync_system_snapshot(config: dict, state: dict, paths: dict) -> int: "governance": _build_governance(), "dailyUsage": _du, # #2142: computed once above, shared with `spending` "reliability": _build_reliability(), + "quality": _build_quality_snapshot(), "memoryAccess": _build_memory_access(), "traces": _build_traces(), "turnAnatomy": _build_turn_anatomy(), diff --git a/routes/quality.py b/routes/quality.py index 395041d66e..85a2789a41 100644 --- a/routes/quality.py +++ b/routes/quality.py @@ -30,6 +30,7 @@ from __future__ import annotations +import os from datetime import datetime, timedelta, timezone from flask import Blueprint, jsonify, request @@ -91,72 +92,49 @@ def _iso_cutoff(hours: int) -> str: return (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat() -@bp_quality.route("/api/quality/report-card", methods=["GET"]) -def quality_report_card(): - """Everything the Quality tab renders, in one fetch. - - ``?window=7d`` (default), ``?runtime=`` (optional scope). No auth gate - — this is the free-tier home for the "is my agent OK?" answer, same - rationale as /api/evaluators and /api/evals/metrics. +def compose_report_card( + rows: list, + prior_rows: list, + hist_rows: list, + *, + window_hours: int = 168, + runtime: str | None = None, + assessments: dict | None = None, + prior_assessments: dict | None = None, +) -> dict: + """Build the Quality payload from rows already read out of the store. + + Split out of the request handler so the sync daemon can build the SAME + payload for the cloud snapshot without re-querying per runtime (founder + live-hit 2026-08-22: the hosted Quality tab said "Nothing to grade yet" + for a machine showing an A and 119 graded runs locally — the hosted + container answered from its own empty DuckDB, because nothing ever put + quality in the snapshot). + + Taking rows as arguments rather than a fetch callable is deliberate: the + daemon reads the node's sessions ONCE and groups them in Python, so + emitting a card per runtime costs no extra queries. """ from clawmetry import quality as _q from clawmetry import quality_thresholds as _qt - window_hours = _parse_window(request.args.get("window", "7d")) - runtime = (request.args.get("runtime") or "").strip() or None - if runtime == "all": - runtime = None - - since = _iso_cutoff(window_hours) - prior_since = _iso_cutoff(window_hours * 2) - - raw_rows = _store_via_daemon_or_direct( - "query_quality_sessions", - runtime=runtime, since=since, limit=400, - ) - # None means the store could not be reached at all (the hosted dashboard - # has no local DuckDB); [] means it answered and there is nothing there. - # Collapsing the two would tell a cloud user "nothing to grade yet" about - # a machine that is in fact working fine — a wrong answer dressed as an - # empty state. FLYWHEEL cloud-parity gate: be honestly unavailable. - if raw_rows is None: - payload = _q.compute_report_card([], {}) - payload.update({ - "window_hours": window_hours, - "runtime": runtime or "all", - "store_available": False, - "headline": "Quality is graded on your own machine.", - "subline": ( - "This view reads the run history stored locally by the " - "collector, which isn't reachable from here right now. " - "Open ClawMetry on the machine your agents run on, or start " - "the collector there, and the grade appears." - ), - }) - return jsonify(payload) - rows = raw_rows - prior_rows = _store_via_daemon_or_direct( - "query_quality_sessions", - runtime=runtime, since=prior_since, until=since, limit=400, - ) or [] - - # Calibrate per runtime off a 30-day history of the SAME runtime, so - # "rough" means unusual for this runtime on this install. - hist = _store_via_daemon_or_direct( - "query_quality_sessions", - runtime=runtime, since=_iso_cutoff(24 * 30), limit=1500, - ) or [] - by_runtime_hist: dict[str, list[dict]] = {} - for h in hist: + by_runtime_hist: dict = {} + for h in hist_rows or []: by_runtime_hist.setdefault(h.get("runtime") or "openclaw", []).append(h) thresholds = _qt.calibrate_all(by_runtime_hist) - assessments = _assess_rows(rows, thresholds) - prior_assessments = _assess_rows(prior_rows, thresholds, deep_limit=0) + # Pre-computed assessments are how the daemon emits a card PER RUNTIME + # without paying for a deep scan per runtime: it assesses the node's rows + # once and hands the same map to every card, which then reads only the + # sessions it contains. Passing none keeps the request-path behaviour. + if assessments is None: + assessments = _assess_rows(rows, thresholds) + if prior_assessments is None: + prior_assessments = _assess_rows(prior_rows or [], thresholds, deep_limit=0) payload = _q.compute_report_card( rows, assessments, - prior_rows=prior_rows, prior_assessments=prior_assessments, + prior_rows=prior_rows or [], prior_assessments=prior_assessments, ) payload["window_hours"] = window_hours payload["runtime"] = runtime or "all" @@ -180,7 +158,85 @@ def quality_report_card(): } payload["benign_filter"] = _benign_filter_state() payload["store_available"] = True - return jsonify(payload) + return payload + + +def unavailable_report_card(window_hours: int = 168, runtime: str | None = None) -> dict: + """The honest answer when this process cannot see the run history. + + Never an empty grade: "nothing to grade" and "I cannot see your machine + from here" look identical on screen and mean opposite things. The hosted + dashboard hit exactly that — it has a DuckDB file, it is simply empty, so + the store answered [] and the tab reported a working machine as having + produced nothing. + """ + from clawmetry import quality as _q + + payload = _q.compute_report_card([], {}) + payload.update({ + "window_hours": window_hours, + "runtime": runtime or "all", + "store_available": False, + "headline": "Quality is graded on your own machine.", + "subline": ( + "This view reads the run history stored locally by the " + "collector, which isn't reachable from here right now. " + "Open ClawMetry on the machine your agents run on, or start " + "the collector there, and the grade appears." + ), + }) + return payload + + +@bp_quality.route("/api/quality/report-card", methods=["GET"]) +def quality_report_card(): + """Everything the Quality tab renders, in one fetch. + + ``?window=7d`` (default), ``?runtime=`` (optional scope). No auth gate + — this is the free-tier home for the "is my agent OK?" answer, same + rationale as /api/evaluators and /api/evals/metrics. + """ + window_hours = _parse_window(request.args.get("window", "7d")) + runtime = (request.args.get("runtime") or "").strip() or None + if runtime == "all": + runtime = None + + # The hosted dashboard has no run history of its own — it ships with an + # EMPTY DuckDB, which answers queries rather than failing them. Reading it + # here produced "Nothing to grade yet" for machines that were grading + # fine, so the hosted process refuses to answer from it at all. The real + # grade reaches the cloud through the daemon's encrypted snapshot; when + # that slice is missing (older daemon) this honest message is what shows. + if os.environ.get("CLAWMETRY_CLOUD", "").strip(): + return jsonify(unavailable_report_card(window_hours, runtime)) + + since = _iso_cutoff(window_hours) + prior_since = _iso_cutoff(window_hours * 2) + + raw_rows = _store_via_daemon_or_direct( + "query_quality_sessions", + runtime=runtime, since=since, limit=400, + ) + # None means the store could not be reached at all; [] means it answered + # and there is nothing there. Collapsing the two would tell a user + # "nothing to grade yet" about a machine that is in fact working fine. + if raw_rows is None: + return jsonify(unavailable_report_card(window_hours, runtime)) + + prior_rows = _store_via_daemon_or_direct( + "query_quality_sessions", + runtime=runtime, since=prior_since, until=since, limit=400, + ) or [] + # Calibrate per runtime off a 30-day history of the SAME runtime, so + # "rough" means unusual for this runtime on this install. + hist = _store_via_daemon_or_direct( + "query_quality_sessions", + runtime=runtime, since=_iso_cutoff(24 * 30), limit=1500, + ) or [] + return jsonify(compose_report_card( + raw_rows, prior_rows, hist, + window_hours=window_hours, runtime=runtime, + )) def _assess_rows( diff --git a/tests/test_quality_cloud_parity.py b/tests/test_quality_cloud_parity.py new file mode 100644 index 0000000000..e01c3e9e41 --- /dev/null +++ b/tests/test_quality_cloud_parity.py @@ -0,0 +1,217 @@ +"""Quality reaches the hosted dashboard, and never lies when it cannot. + +Founder live-hit 2026-08-22: the hosted Quality tab said "Nothing to grade +yet" for a machine whose own dashboard showed an A over 119 graded runs, same +node, same runtime. Two faults, one screen: + + 1. The grade never rode the encrypted snapshot, so the cloud had nothing to + render — the cloud-parity gate every data card is supposed to pass. + 2. The hosted container answered anyway, from its OWN DuckDB. That file + exists and is empty, so the query succeeded with zero rows and the tab + reported a working machine as having produced nothing. "Nothing to + grade" and "I cannot see your machine" look identical and mean opposite + things. + +These tests pin both: the daemon emits the slice (node-wide and per runtime), +and the hosted process refuses to answer from its own empty store. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from flask import Flask + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + + +def _row(sid, runtime, cost=1.0, tool_err=0.0): + return { + "session_id": sid, + "runtime": runtime, + "cost_usd": cost, + "message_count": 12, + "toolErrorPct": tool_err, + "maxIdleGapSec": 5, + "status": "completed", + "title": f"task {sid}", + "started_at": "2026-08-20T10:00:00+00:00", + "ended_at": "2026-08-20T10:20:00+00:00", + "last_active_at": "2026-08-20T10:20:00+00:00", + "metadata": {"quality": {"measurable": True, "rough": False, + "signals": {}, "verdicts": []}}, + } + + +class _FakeStore: + """Answers query_quality_sessions the way the daemon's own handle does.""" + + def __init__(self, window_rows, hist_rows=None): + self.window_rows = window_rows + self.hist_rows = hist_rows if hist_rows is not None else window_rows + self.calls = 0 + + def query_quality_sessions(self, since=None, until=None, limit=None, runtime=None): + self.calls += 1 + if until: # prior window + return [] + if limit and limit > 1000: # the 30-day calibration read + return list(self.hist_rows) + return list(self.window_rows) + + +@pytest.fixture +def daemon_store(monkeypatch): + def _install(window_rows, hist_rows=None): + from clawmetry import local_store as ls + store = _FakeStore(window_rows, hist_rows) + monkeypatch.setattr(ls, "get_store", lambda *a, **k: store) + return store + return _install + + +# ── the daemon emits it ──────────────────────────────────────────────── + +def test_snapshot_carries_node_wide_and_per_runtime_cards(daemon_store): + from clawmetry import sync + + daemon_store([_row("s1", "claude_code"), _row("s2", "claude_code"), + _row("s3", "openclaw")]) + slice_ = sync._build_quality_snapshot() + + assert slice_["window_hours"] == 168 + assert slice_["all"]["total_runs"] == 3 + assert set(slice_["byRuntime"]) == {"claude_code", "openclaw"} + assert slice_["byRuntime"]["claude_code"]["total_runs"] == 2 + assert slice_["byRuntime"]["openclaw"]["total_runs"] == 1 + + +def test_per_runtime_card_never_carries_another_runtimes_runs(daemon_store): + """The honesty rule for the runtime switcher: a grade shown under one + runtime must be that runtime's, not the node's total wearing its name.""" + from clawmetry import sync + + daemon_store([_row("a", "claude_code"), _row("b", "codex"), _row("c", "codex")]) + cards = sync._build_quality_snapshot()["byRuntime"] + assert cards["claude_code"]["total_runs"] == 1 + assert cards["codex"]["total_runs"] == 2 + assert cards["claude_code"]["runtime"] == "claude_code" + + +def test_runtime_quiet_this_week_still_gets_its_own_card(daemon_store): + """Otherwise the hosted tab falls back to the node-wide card and shows + another runtime's grade under this runtime's filter.""" + from clawmetry import sync + + daemon_store([_row("a", "claude_code")], + hist_rows=[_row("a", "claude_code"), _row("old", "cursor")]) + cards = sync._build_quality_snapshot()["byRuntime"] + assert "cursor" in cards + assert cards["cursor"]["total_runs"] == 0 + assert "nothing to grade" in cards["cursor"]["headline"].lower() + + +def test_calibration_is_carried_once_not_per_card(daemon_store): + """It is identical in every card; inline it was a quarter of the slice.""" + from clawmetry import sync + + daemon_store([_row("a", "claude_code"), _row("b", "codex")]) + slice_ = sync._build_quality_snapshot() + assert "thresholds" in slice_ + assert "thresholds" not in slice_["all"] + for card in slice_["byRuntime"].values(): + assert "thresholds" not in card + + +def test_store_is_read_once_regardless_of_runtime_count(daemon_store): + """Per-runtime cards must cost no extra queries — the daemon runs this + every snapshot cycle and has a CPU budget.""" + from clawmetry import sync + + store = daemon_store([_row(f"s{i}", rt) for i, rt in + enumerate(["claude_code", "codex", "cursor", + "goose", "openclaw", "pi"])]) + sync._build_quality_snapshot() + assert store.calls == 3, ( + f"expected 3 reads (window, prior, history), got {store.calls}" + ) + + +def test_snapshot_slice_never_raises(monkeypatch): + """A snapshot must not fail to build over one optional slice.""" + from clawmetry import local_store as ls + from clawmetry import sync + + def boom(*a, **k): + raise RuntimeError("store on fire") + + monkeypatch.setattr(ls, "get_store", boom) + assert sync._build_quality_snapshot() == {} + + +# ── the hosted process refuses to answer from its own empty store ────── + +def _app(): + from routes.quality import bp_quality + app = Flask(__name__) + app.register_blueprint(bp_quality) + return app + + +def test_hosted_dashboard_says_where_the_grade_lives(monkeypatch): + from routes import quality as qmod + + monkeypatch.setenv("CLAWMETRY_CLOUD", "1") + # An empty-but-working store, exactly what the hosted container has. + monkeypatch.setattr(qmod, "_store_via_daemon_or_direct", lambda *a, **k: []) + with _app().test_client() as c: + body = c.get("/api/quality/report-card?window=7d&runtime=claude_code").get_json() + assert body["store_available"] is False + assert "your own machine" in body["headline"].lower() + assert "nothing to grade" not in body["headline"].lower(), ( + "the hosted tab must not report a working machine as having produced " + "nothing — that is the bug this closes" + ) + + +def test_local_dashboard_still_reports_a_real_empty_week(monkeypatch): + """Off the hosted dashboard, an empty store IS the answer: this machine + genuinely has not run anything this week.""" + from routes import quality as qmod + + monkeypatch.delenv("CLAWMETRY_CLOUD", raising=False) + monkeypatch.setattr(qmod, "_store_via_daemon_or_direct", lambda *a, **k: []) + with _app().test_client() as c: + body = c.get("/api/quality/report-card?window=7d").get_json() + assert body["store_available"] is True + assert "nothing to grade" in body["headline"].lower() + + +def test_unreachable_store_is_not_an_empty_grade(monkeypatch): + from routes import quality as qmod + + monkeypatch.delenv("CLAWMETRY_CLOUD", raising=False) + monkeypatch.setattr(qmod, "_store_via_daemon_or_direct", lambda *a, **k: None) + with _app().test_client() as c: + body = c.get("/api/quality/report-card").get_json() + assert body["store_available"] is False + assert "your own machine" in body["headline"].lower() + + +def test_precomputed_assessments_match_the_request_path(daemon_store): + """The daemon reuses one assessment map across cards; that must not change + the grade a card reports.""" + from routes.quality import _assess_rows, compose_report_card + from clawmetry import quality_thresholds as qt + + rows = [_row("a", "claude_code", cost=3.0), _row("b", "claude_code")] + thresholds = qt.calibrate_all({"claude_code": rows}) + shared = _assess_rows(rows, thresholds) + + fresh = compose_report_card(rows, [], rows, window_hours=168, runtime="claude_code") + reused = compose_report_card(rows, [], rows, window_hours=168, runtime="claude_code", + assessments=shared, prior_assessments={}) + assert fresh["grade"] == reused["grade"] + assert fresh["total_runs"] == reused["total_runs"] + assert fresh["graded_runs"] == reused["graded_runs"] From e1db022d014de9aaf45f84ff2d38fdfb56832334 Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Sun, 23 Aug 2026 20:48:29 +0200 Subject: [PATCH 02/12] docs: add Quality Cloud Parity blueprint and point tests at their contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drift Bot flagged that the cloud-parity gate for the quality snapshot slice was not documented in a local blueprint mirror: `sync.py` now emits a `quality` key, `routes/quality.py` exposes `compose_report_card` as a shared builder, and the hosted process refuses to answer from its own empty DuckDB — none of these contracts appeared in `docs/blueprints/`. `docs/blueprints/quality-cloud-parity.md` is the local mirror of the 8090 Software Factory feature blueprint. It documents: - The `quality` slice shape `{window_hours, all, byRuntime, thresholds}` - `compose_report_card` as the single canonical builder (daemon + request path) - The 3-read constraint (current window, prior window, 30-day history) - The hosted-process refusal contract (`CLAWMETRY_CLOUD=1` → store_available:false) - The quiet-runtime card contract (every runtime seen in 30d gets its own card) - The slice-never-raises safety contract `tests/test_quality_cloud_parity.py` docstrings are updated to cite the specific contract sections each test guards, so the next reader (and Drift Bot) can trace code ↔ spec. Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_01JxWLJAEgTs9Ya5aXnBacG6 --- docs/blueprints/quality-cloud-parity.md | 200 ++++++++++++++++++++++++ tests/test_quality_cloud_parity.py | 78 +++++++-- 2 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 docs/blueprints/quality-cloud-parity.md diff --git a/docs/blueprints/quality-cloud-parity.md b/docs/blueprints/quality-cloud-parity.md new file mode 100644 index 0000000000..57af35951d --- /dev/null +++ b/docs/blueprints/quality-cloud-parity.md @@ -0,0 +1,200 @@ +# Feature Blueprint: Quality Grade Cloud Parity + +> Local mirror of the Software Factory Feature Blueprint of the same name. +> The hosted record is the system of record; this file exists so drift-bot, +> PR review, and headless agents can read the same spec without a factory +> login. When the hosted blueprint is created or updated, keep this file in +> step (FLYWHEEL 1f). + +## Feature Summary + +A hosted ClawMetry user viewing the Quality tab should see the same grade +that the local dashboard shows for their machine. Before this feature, the +hosted container answered quality requests from its own DuckDB — which is +present but empty — and reported a working machine as having produced nothing. +The grade now travels in the encrypted snapshot the daemon emits on each +sync cycle, and the hosted process refuses to answer from its own store. + +The root cause was two coupled faults: the quality grade never had a snapshot +slice, so the cloud had nothing to render; and the hosted endpoint fell back +to reading its own empty store rather than returning an honest "not available" +response. Both faults produced the same screen — "Nothing to grade yet" — for +opposite reasons. + +## Component Blueprint Composition + +This feature extends two existing components without redefining them. + +`#CloudSnapshotSync` in `clawmetry/sync.py` bakes the encrypted snapshot the +hosted dashboard renders. Per the cloud-parity hard gate, any data surface +that a hosted trial user can click must be served by a `cm-cloud-*` +interceptor over a snapshot slice, or must show an honest locked state. The +Quality tab inherited that obligation but had no slice; this feature adds one. + +`#QualityRoutes` in `routes/quality.py` serves `GET /api/quality/report-card` +and owns the composition logic for the report card payload. This feature +extracts the composition into a shared callable (`compose_report_card`) so +both the request handler and the daemon build identical payloads from the same +code path, eliminating the divergence that produced a different answer on +screen depending on where the request was served. + +## Feature-Specific Components + +```component +name: QualitySnapshotBuilder +container: ClawMetry Sync Daemon +responsibilities: + - Reading the node's quality sessions exactly three times per snapshot cycle: + the current window, the prior window, and a 30-day calibration history + - Calling compose_report_card once for the node-wide card and once per + runtime seen in either the current window or the 30-day history + - Hoisting calibration thresholds to the slice root rather than embedding + them in each per-runtime card + - Returning an empty dict {} on any exception so the snapshot never fails + to build over one optional slice +``` + +`#QualitySnapshotBuilder` is implemented as `_build_quality_snapshot()` in +`clawmetry/sync.py`, called from `sync_system_snapshot()`. It imports +`compose_report_card` from `routes.quality` at call time (late import, same +pattern as the other snapshot builders that use route-layer helpers). + +The three-read constraint is load-bearing: the daemon runs this builder on +every snapshot cycle, and emitting one card per runtime must not multiply the +store reads proportionally. Rows are grouped in Python after the three fetches; +no additional queries are made regardless of how many runtimes are present. + +The quiet-runtime rule is also load-bearing: a runtime that ran sessions in +the last 30 days but was quiet this week must still appear in `byRuntime` with +an honest "nothing to grade this week" card. Without it, the hosted tab would +fall back to the node-wide card when a runtime filter was selected and show +another runtime's grade under the wrong runtime's name. + +## System Contracts + +### Key Contracts + +The `quality` snapshot slice has the shape +`{window_hours, all, byRuntime, thresholds}`. `all` is the node-wide report +card. `byRuntime` is a dict keyed by runtime identifier; each value is the +report card for that runtime and never contains sessions belonging to a +different runtime. `thresholds` is the calibration map, hoisted once to the +slice root and absent from each per-runtime card (it is byte-identical across +cards and carrying it inline multiplied the slice size by the runtime count). + +`compose_report_card` in `routes/quality.py` is the single canonical builder +for the report card payload. Both the sync daemon (`_build_quality_snapshot`) +and the request handler (`quality_report_card`) call it. No other code path +may build a report card payload; duplicating the composition logic is the +drift pattern this feature closes. + +The store is read exactly three times per `_build_quality_snapshot()` call: +once for the current window (the `window_hours` parameter, default 168h), +once for the prior window of the same length (for week-over-week comparison), +and once for the 30-day calibration history. Emitting one card per runtime +must not add queries: rows are split in Python, and `_assess_rows` is called +once over the node's full window rows with its assessment map reused across +every per-runtime card. + +Every runtime that appears in either the current window rows or the 30-day +history rows gets its own entry in `byRuntime`. A runtime that was quiet this +week but active in the last 30 days gets a card stating it has nothing to +grade this week, in its own name. Absence from the current window is not +grounds for omission from `byRuntime`. + +The `quality` slice returns `{}` on any exception and must never propagate an +exception to `sync_system_snapshot`. A misconfiguration, an import failure, or +a store error in the quality builder must not prevent the rest of the snapshot +from being emitted. + +### Integration Contracts + +`GET /api/quality/report-card` when served by the hosted dashboard +(`CLAWMETRY_CLOUD=1`) always returns `store_available: false` with a message +explaining that the grade lives on the user's machine. It never reads the +hosted container's DuckDB for this response. The distinction matters because +an empty store answers queries successfully with zero rows, which produces +"Nothing to grade yet" — a false statement about a machine that is grading +normally. `store_available: false` is an honest "I cannot see your machine" +and must not say "Nothing to grade". + +`GET /api/quality/report-card` when served by a local dashboard +(`CLAWMETRY_CLOUD` unset) reads from the daemon query path and returns +`store_available: true` when the store answers (even with zero rows). +An empty local store is a true statement that this machine has no graded runs +this week; the response may say "Nothing to grade yet" in that case. + +An unreachable store — `_store_via_daemon_or_direct` returning `None` rather +than `[]` — is not an empty grade. The local handler returns +`store_available: false` in that case, using the same message as the hosted +process. The distinction between `None` (store unreachable) and `[]` (store +answered with nothing) is preserved through the entire call chain. + +### Integration Boundaries + +`compose_report_card` is importable from `routes.quality` by both the sync +daemon and the request handler. Its signature takes rows as arguments rather +than a fetch callable: the daemon fetches once and passes sub-lists; the +request handler passes the rows it has already fetched. Neither path is +special-cased inside the function. + +The sync daemon never calls `_store_via_daemon_or_direct`. It holds the writer +lock and reads from `local_store.get_store()` directly. The request handler +uses `_store_via_daemon_or_direct` to read through the daemon's query server. +The two paths are separated at the module boundary: `_build_quality_snapshot` +is in `sync.py`; `quality_report_card` is in `routes/quality.py`. + +The `CLAWMETRY_CLOUD` environment variable is the only signal the request +handler reads to decide whether to serve from the snapshot or refuse. It is +not threaded through as a function argument; `os.environ.get` is called at +request time. + +## Architecture Decision Records + +### ADR-001: Extract compose_report_card rather than duplicate composition + +Context: the request handler in `routes/quality.py` built the report card +payload inline. The sync daemon needed to build the same payload for the +snapshot. Duplicating the inline logic would have been the path of least +resistance but would have produced two code paths that could diverge silently. + +Decision: extract the composition into `compose_report_card`, make it accept +rows as arguments (rather than fetching internally), and have both callers +pass their already-fetched rows to it. + +Consequences: one code path for the report card payload. The daemon's +per-runtime cards are provably identical in structure to what the local request +handler would return, because they use the same function. The test +`test_precomputed_assessments_match_the_request_path` pins this. + +### ADR-002: CLAWMETRY_CLOUD gates the endpoint; no runtime argument + +Context: the hosted dashboard could signal its nature through a function +argument, a constructor parameter, or an environment variable. The environment +variable already existed (`CLAWMETRY_CLOUD`) and was already read by other +parts of the hosted process to suppress local-only behavior. + +Decision: read `os.environ.get('CLAWMETRY_CLOUD')` at request time in +`quality_report_card`. No new argument is added to the function. + +Consequences: the behavior is consistent with how other endpoints handle the +hosted/local distinction, the environment variable stays the single source of +truth, and tests can toggle the behavior with `monkeypatch.setenv` without +touching function signatures. + +### ADR-003: Calibration thresholds hoisted to slice root, not per-card + +Context: the first version of the snapshot slice embedded the full calibration +thresholds in every per-runtime card. At 14 runtimes the thresholds were +repeated 14 times; each copy was byte-identical. The thresholds made up +roughly a quarter of the slice by size. + +Decision: hoist `thresholds` to the slice root as a single key, and strip it +from every per-runtime card before returning. The cloud interceptor reattaches +it per card when composing the response. + +Consequences: slice size scales with the number of runtimes for the `byRuntime` +keys (unavoidable), but not for the calibration data (avoided). The per-card +strip is visible in `_build_quality_snapshot` after the per-runtime loop. +The test `test_calibration_is_carried_once_not_per_card` pins the absence of +`thresholds` in every individual card. diff --git a/tests/test_quality_cloud_parity.py b/tests/test_quality_cloud_parity.py index e01c3e9e41..7c88794d45 100644 --- a/tests/test_quality_cloud_parity.py +++ b/tests/test_quality_cloud_parity.py @@ -5,7 +5,7 @@ node, same runtime. Two faults, one screen: 1. The grade never rode the encrypted snapshot, so the cloud had nothing to - render — the cloud-parity gate every data card is supposed to pass. + render -- the cloud-parity gate every data card is supposed to pass. 2. The hosted container answered anyway, from its OWN DuckDB. That file exists and is empty, so the query succeeded with zero rows and the tab reported a working machine as having produced nothing. "Nothing to @@ -14,6 +14,8 @@ These tests pin both: the daemon emits the slice (node-wide and per runtime), and the hosted process refuses to answer from its own empty store. + +Blueprint: docs/blueprints/quality-cloud-parity.md """ from __future__ import annotations @@ -71,9 +73,15 @@ def _install(window_rows, hist_rows=None): return _install -# ── the daemon emits it ──────────────────────────────────────────────── +# -- the daemon emits it ------------------------------------------------------- +# Blueprint contract: QualitySnapshotBuilder responsibilities + Key Contracts def test_snapshot_carries_node_wide_and_per_runtime_cards(daemon_store): + """The quality slice carries node-wide and per-runtime cards. + + Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts > + quality slice shape {window_hours, all, byRuntime, thresholds}. + """ from clawmetry import sync daemon_store([_row("s1", "claude_code"), _row("s2", "claude_code"), @@ -88,8 +96,11 @@ def test_snapshot_carries_node_wide_and_per_runtime_cards(daemon_store): def test_per_runtime_card_never_carries_another_runtimes_runs(daemon_store): - """The honesty rule for the runtime switcher: a grade shown under one - runtime must be that runtime's, not the node's total wearing its name.""" + """A per-runtime card is scoped exclusively to its own runtime. + + Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts > + byRuntime values never contain sessions belonging to a different runtime. + """ from clawmetry import sync daemon_store([_row("a", "claude_code"), _row("b", "codex"), _row("c", "codex")]) @@ -100,8 +111,12 @@ def test_per_runtime_card_never_carries_another_runtimes_runs(daemon_store): def test_runtime_quiet_this_week_still_gets_its_own_card(daemon_store): - """Otherwise the hosted tab falls back to the node-wide card and shows - another runtime's grade under this runtime's filter.""" + """A runtime active in the last 30 days gets its own card even if quiet now. + + Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts > + Every runtime that appears in either the current window rows or the 30-day + history rows gets its own entry in byRuntime. + """ from clawmetry import sync daemon_store([_row("a", "claude_code")], @@ -113,7 +128,11 @@ def test_runtime_quiet_this_week_still_gets_its_own_card(daemon_store): def test_calibration_is_carried_once_not_per_card(daemon_store): - """It is identical in every card; inline it was a quarter of the slice.""" + """Thresholds are hoisted to the slice root, absent from every per-card. + + Contract: docs/blueprints/quality-cloud-parity.md > ADR-003 and + Key Contracts > thresholds hoisted once to the slice root. + """ from clawmetry import sync daemon_store([_row("a", "claude_code"), _row("b", "codex")]) @@ -125,8 +144,11 @@ def test_calibration_is_carried_once_not_per_card(daemon_store): def test_store_is_read_once_regardless_of_runtime_count(daemon_store): - """Per-runtime cards must cost no extra queries — the daemon runs this - every snapshot cycle and has a CPU budget.""" + """The daemon reads the store exactly 3 times per snapshot cycle. + + Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts > + The store is read exactly three times per _build_quality_snapshot() call. + """ from clawmetry import sync store = daemon_store([_row(f"s{i}", rt) for i, rt in @@ -139,7 +161,11 @@ def test_store_is_read_once_regardless_of_runtime_count(daemon_store): def test_snapshot_slice_never_raises(monkeypatch): - """A snapshot must not fail to build over one optional slice.""" + """A broken store must not prevent the snapshot from being emitted. + + Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts > + The quality slice returns {} on any exception. + """ from clawmetry import local_store as ls from clawmetry import sync @@ -150,7 +176,8 @@ def boom(*a, **k): assert sync._build_quality_snapshot() == {} -# ── the hosted process refuses to answer from its own empty store ────── +# -- the hosted process refuses to answer from its own empty store ------------- +# Blueprint contract: Integration Contracts def _app(): from routes.quality import bp_quality @@ -160,6 +187,12 @@ def _app(): def test_hosted_dashboard_says_where_the_grade_lives(monkeypatch): + """The hosted process returns store_available:false, not an empty grade. + + Contract: docs/blueprints/quality-cloud-parity.md > Integration Contracts > + GET /api/quality/report-card with CLAWMETRY_CLOUD=1 always returns + store_available:false. store_available:false must not say 'Nothing to grade'. + """ from routes import quality as qmod monkeypatch.setenv("CLAWMETRY_CLOUD", "1") @@ -171,13 +204,17 @@ def test_hosted_dashboard_says_where_the_grade_lives(monkeypatch): assert "your own machine" in body["headline"].lower() assert "nothing to grade" not in body["headline"].lower(), ( "the hosted tab must not report a working machine as having produced " - "nothing — that is the bug this closes" + "nothing -- that is the bug this closes" ) def test_local_dashboard_still_reports_a_real_empty_week(monkeypatch): - """Off the hosted dashboard, an empty store IS the answer: this machine - genuinely has not run anything this week.""" + """Off the hosted dashboard, an empty store IS the answer. + + Contract: docs/blueprints/quality-cloud-parity.md > Integration Contracts > + GET /api/quality/report-card without CLAWMETRY_CLOUD reads from the daemon + query path and returns store_available:true when the store answers. + """ from routes import quality as qmod monkeypatch.delenv("CLAWMETRY_CLOUD", raising=False) @@ -189,6 +226,11 @@ def test_local_dashboard_still_reports_a_real_empty_week(monkeypatch): def test_unreachable_store_is_not_an_empty_grade(monkeypatch): + """An unreachable store (None) is distinct from an empty store ([]). + + Contract: docs/blueprints/quality-cloud-parity.md > Integration Contracts > + An unreachable store is not an empty grade -- store_available:false. + """ from routes import quality as qmod monkeypatch.delenv("CLAWMETRY_CLOUD", raising=False) @@ -200,8 +242,12 @@ def test_unreachable_store_is_not_an_empty_grade(monkeypatch): def test_precomputed_assessments_match_the_request_path(daemon_store): - """The daemon reuses one assessment map across cards; that must not change - the grade a card reports.""" + """Shared assessment map gives identical grade to a fresh per-request assessment. + + Contract: docs/blueprints/quality-cloud-parity.md > ADR-001 > + compose_report_card is the single canonical builder; the daemon's + precomputed-assessment path must produce the same grade as the request path. + """ from routes.quality import _assess_rows, compose_report_card from clawmetry import quality_thresholds as qt From 02ea09cbf75e712639d4db3538c69e8f19d113e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 21:11:11 +0000 Subject: [PATCH 03/12] fix(lint): strip trailing whitespace from dashboard.py and sync runtime count in blueprint Pre-existing W291/W293/E702 violations in dashboard.py were invisible until the lint CI was actually wired up (#5084); fix them wholesale. Also updates the stale '14 runtimes' mention in the quality-cloud-parity blueprint to match the catalogue count of 26. --- dashboard.py | 109 ++++++++++++------------ docs/blueprints/quality-cloud-parity.md | 2 +- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/dashboard.py b/dashboard.py index 20c62b9550..4097ba9548 100644 --- a/dashboard.py +++ b/dashboard.py @@ -3915,7 +3915,7 @@ def get_local_ip(): .theme-toggle { background: var(--button-bg); border: none; border-radius: 8px; padding: 8px 12px; color: var(--text-tertiary); cursor: pointer; font-size: 16px; margin-left: 12px; transition: all 0.15s; box-shadow: var(--card-shadow); } .theme-toggle:hover { background: var(--button-hover); color: var(--text-secondary); } .theme-toggle:active { transform: scale(0.98); } - + /* === Zoom Controls === */ .zoom-controls { display: flex; align-items: center; gap: 4px; margin-left: 12px; } .zoom-btn { background: var(--button-bg); border: 1px solid var(--border-primary); border-radius: 6px; width: 28px; height: 28px; color: var(--text-tertiary); cursor: pointer; font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; transition: all 0.15s; } @@ -4343,7 +4343,7 @@ def get_local_ip(): .usage-table th { text-align: left; font-size: 12px; color: var(--text-muted); padding: 8px 12px; border-bottom: 1px solid var(--border-primary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .usage-table td { padding: 8px 12px; font-size: 13px; color: var(--text-secondary); border-bottom: 1px solid var(--border-secondary); } .usage-table tr:last-child td { border-bottom: none; font-weight: 700; color: var(--text-accent); } - + /* === Cost Warnings === */ .cost-warning { padding: 12px 16px; border-radius: 8px; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; font-size: 13px; } /* === Markdown Rendered Content === */ @@ -4528,7 +4528,7 @@ def get_local_ip(): .comp-modal-title { font-size: 18px; font-weight: 700; color: var(--text-primary); } .comp-modal-close { background: var(--button-bg); border: 1px solid var(--border-primary); border-radius: 8px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 18px; color: var(--text-tertiary); transition: all 0.15s; } .comp-modal-close:hover { background: var(--bg-error); color: var(--text-error); } - + /* Time Travel Controls */ .time-travel-bar { display: none; padding: 12px 20px; border-bottom: 1px solid var(--border-primary); background: var(--bg-secondary); } .time-travel-bar.active { display: block; } @@ -4774,18 +4774,18 @@ def get_local_ip(): .heatmap-grid { min-width: 500px; } .chat-msg { max-width: 95%; } .usage-chart { height: 150px; } - + /* Enhanced Flow mobile optimizations */ - .flow-container { - padding-bottom: 20px; - overflow: visible; + .flow-container { + padding-bottom: 20px; + overflow: visible; } #flow-svg text { font-size: 11px !important; } .flow-label { font-size: 7px !important; } .flow-node rect { stroke-width: 1 !important; } .flow-node.active rect { stroke-width: 1.5 !important; } .brain-group { animation-duration: 1.8s; } /* Faster on mobile */ - + /* Mobile zoom controls */ .zoom-controls { margin-left: 8px; gap: 2px; } .zoom-btn { width: 24px; height: 24px; font-size: 14px; } @@ -4824,7 +4824,7 @@ def get_local_ip(): .card { padding: 12px 14px; } .card-label { font-size: 10px; } .card-value { font-size: 20px; } - + /* Overview grid already 1-col, just tighten gap */ .grid { gap: 8px; } @@ -5515,7 +5515,7 @@ def get_local_ip(): - + @@ -6131,7 +6131,7 @@ def get_local_ip():
- +
@@ -7452,7 +7452,7 @@ def get_local_ip(): const body = document.body; const toggle = document.getElementById('theme-toggle-btn'); const isLight = !body.hasAttribute('data-theme') || body.getAttribute('data-theme') !== 'dark'; - + if (isLight) { body.setAttribute('data-theme', 'dark'); toggle.innerHTML = _sunSVG; @@ -7470,7 +7470,7 @@ def get_local_ip(): const savedTheme = 'dark'; localStorage.setItem('openclaw-theme', 'dark'); const body = document.body; const toggle = document.getElementById('theme-toggle-btn'); - + if (savedTheme === 'dark') { body.setAttribute('data-theme', 'dark'); if (toggle) { toggle.innerHTML = _sunSVG; toggle.title = 'Switch to light theme'; } @@ -7497,14 +7497,14 @@ def get_local_ip(): function applyZoom() { const wrapper = document.getElementById('zoom-wrapper'); const levelDisplay = document.getElementById('zoom-level'); - + if (wrapper) { wrapper.style.transform = `scale(${currentZoom})`; } if (levelDisplay) { levelDisplay.textContent = Math.round(currentZoom * 100) + '%'; } - + // Save to localStorage localStorage.setItem('openclaw-zoom', currentZoom.toString()); } @@ -8077,12 +8077,12 @@ def get_local_ip(): } async function loadMiniWidgets(overview, usage) { - // 💰 Cost Ticker + // 💰 Cost Ticker function fmtCost(c) { return c >= 0.01 ? '$' + c.toFixed(2) : c > 0 ? '<$0.01' : '$0.00'; } document.getElementById('cost-today').textContent = fmtCost(usage.todayCost || 0); document.getElementById('cost-week').textContent = fmtCost(usage.weekCost || 0); document.getElementById('cost-month').textContent = fmtCost(usage.monthCost || 0); - + var trend = ''; if (usage.trend && usage.trend.trend) { var trendIcon = usage.trend.trend === 'increasing' ? '📈' : usage.trend.trend === 'decreasing' ? '📉' : '➡️'; @@ -8155,15 +8155,15 @@ def get_local_ip(): if (burnFallback) burnFallback.textContent = '--'; if (projFallback) projFallback.textContent = '--'; } - + // ⚡ Tool Activity (load from logs) loadToolActivity(); - + // 📊 Token Burn Rate function fmtTokens(n) { return n >= 1000000 ? (n/1000000).toFixed(1) + 'M' : n >= 1000 ? (n/1000).toFixed(0) + 'K' : String(n); } document.getElementById('token-rate').textContent = fmtTokens(usage.month || 0); document.getElementById('tokens-today').textContent = fmtTokens(usage.today || 0); - + // 🔥 Hot Sessions -- use /api/sessions for consistency with modal fetch('/api/sessions').then(function(r){return r.json()}).then(function(sd) { var sl = sd.sessions || sd || []; @@ -8181,7 +8181,7 @@ def get_local_ip(): }).catch(function() { document.getElementById('hot-sessions-count').textContent = overview.sessionCount || 0; }); - + // 📈 Model Mix document.getElementById('model-primary').textContent = overview.model || 'unknown'; var modelLabel = document.getElementById('main-activity-model'); @@ -8203,10 +8203,10 @@ def get_local_ip(): modelBreakdown = 'Primary model'; } document.getElementById('model-breakdown').textContent = modelBreakdown; - + // 🐝 Worker Bees (Sub-Agents) loadSubAgents(); - + } async function loadSubAgents() { @@ -8214,10 +8214,10 @@ def get_local_ip(): var data = await fetch('/api/subagents').then(r => r.json()); var counts = data.counts; var subagents = data.subagents; - + // Update main counter document.getElementById('subagents-count').textContent = counts.total; - + // Update status text var statusText = ''; if (counts.active > 0) { @@ -8230,7 +8230,7 @@ def get_local_ip(): statusText = 'All idle/stale'; } document.getElementById('subagents-status').textContent = statusText; - + // Update preview with top sub-agents (human-readable) var previewHtml = ''; if (subagents.length === 0) { @@ -8249,14 +8249,14 @@ def get_local_ip(): previewHtml += '' + agent.runtime + ''; previewHtml += '
'; }); - + if (subagents.length > 3) { previewHtml += '
+' + (subagents.length - 3) + ' more
'; } } - + document.getElementById('subagents-preview').innerHTML = previewHtml; - + } catch(e) { document.getElementById('subagents-count').textContent = '?'; document.getElementById('subagents-status').textContent = 'Error loading sub-agents'; @@ -8379,28 +8379,28 @@ def get_local_ip(): var logs = await fetch('/api/logs?lines=100').then(r => r.json()); var toolCounts = { exec: 0, browser: 0, search: 0, other: 0 }; var recentTools = []; - + logs.lines.forEach(function(line) { var msg = line.toLowerCase(); if (msg.includes('tool') || msg.includes('invoke')) { - if (msg.includes('exec') || msg.includes('shell')) { - toolCounts.exec++; recentTools.push('exec'); - } else if (msg.includes('browser') || msg.includes('screenshot')) { - toolCounts.browser++; recentTools.push('browser'); - } else if (msg.includes('web_search') || msg.includes('web_fetch')) { - toolCounts.search++; recentTools.push('search'); + if (msg.includes('exec') || msg.includes('shell')) { + toolCounts.exec++; recentTools.push('exec'); + } else if (msg.includes('browser') || msg.includes('screenshot')) { + toolCounts.browser++; recentTools.push('browser'); + } else if (msg.includes('web_search') || msg.includes('web_fetch')) { + toolCounts.search++; recentTools.push('search'); } else { toolCounts.other++; } } }); - + document.getElementById('tools-active').textContent = recentTools.slice(0, 3).join(', ') || 'Idle'; document.getElementById('tools-recent').textContent = 'Last ' + Math.min(logs.lines.length, 100) + ' log entries'; - + var sparks = document.querySelectorAll('.tool-spark span'); sparks[0].textContent = toolCounts.exec; - sparks[1].textContent = toolCounts.browser; + sparks[1].textContent = toolCounts.browser; sparks[2].textContent = toolCounts.search; } catch(e) { document.getElementById('tools-active').textContent = '--'; @@ -8411,26 +8411,26 @@ def get_local_ip(): try { var transcripts = await fetchJsonWithTimeout('/api/transcripts', 4000); var activities = []; - + // Get the most recent transcript to parse for activity if (transcripts.transcripts && transcripts.transcripts.length > 0) { var recent = transcripts.transcripts[0]; try { var transcript = await fetchJsonWithTimeout('/api/transcript/' + recent.id, 4000); var recentMessages = transcript.messages.slice(-10); // Last 10 messages - + recentMessages.forEach(function(msg) { if (msg.role === 'assistant' && msg.content) { var content = msg.content.toLowerCase(); var activity = ''; var time = new Date(msg.timestamp || Date.now()).toLocaleTimeString(); - + if (content.includes('searching') || content.includes('search')) { activity = time + ' [check] Searching web for information'; } else if (content.includes('reading') || content.includes('file')) { activity = time + ' 📖 Reading files'; } else if (content.includes('writing') || content.includes('edit')) { - activity = time + ' ✏️ Editing files'; + activity = time + ' ✏️ Editing files'; } else if (content.includes('exec') || content.includes('command')) { activity = time + ' ⚡ Running commands'; } else if (content.includes('browser') || content.includes('screenshot')) { @@ -8439,24 +8439,24 @@ def get_local_ip(): var preview = msg.content.substring(0, 80).replace(/[^\w\s]/g, ' ').trim(); activity = time + ' 💭 ' + preview + '...'; } - + if (activity) activities.push(activity); } }); } catch(e) {} } - + if (activities.length === 0) { activities = [ new Date().toLocaleTimeString() + ' 🤖 AI agent initialized', new Date().toLocaleTimeString() + ' 📡 Monitoring for activity...' ]; } - + var html = activities.slice(-8).map(function(a) { return '
' + escHtml(a) + '
'; }).join(''); - + document.getElementById('activity-stream').innerHTML = html; } catch(e) { document.getElementById('activity-stream').innerHTML = '
Error loading activity stream
'; @@ -12579,11 +12579,11 @@ def validate_configuration(): path = os.path.join(WORKSPACE, f) if os.path.exists(path): found_files.append(f) - + if not found_files: warnings.append(f"[warn] No OpenClaw workspace files found in {WORKSPACE}") tips.append("[tip] Create SOUL.md, AGENTS.md, or MEMORY.md to set up your agent workspace") - + # Check if log directory exists and has recent logs if not os.path.exists(LOG_DIR): warnings.append(f"[warn] Log directory doesn't exist: {LOG_DIR}") @@ -12591,17 +12591,17 @@ def validate_configuration(): else: # Check for recent log files log_pattern = os.path.join(LOG_DIR, "*claw*.log") - recent_logs = [f for f in glob.glob(log_pattern) + recent_logs = [f for f in glob.glob(log_pattern) if os.path.getmtime(f) > time.time() - 86400] # Last 24h if not recent_logs: warnings.append(f"[warn] No recent log files found in {LOG_DIR}") tips.append("[tip] Start your OpenClaw agent to see real-time data") - + # Check if sessions directory exists if not SESSIONS_DIR or not os.path.exists(SESSIONS_DIR): warnings.append(f"[warn] Sessions directory not found: {SESSIONS_DIR}") tips.append("[tip] Sessions will appear when your agent starts conversations") - + return warnings, tips @@ -12679,13 +12679,14 @@ def detect_config(args=None): else: # Auto-detect: check common locations including Docker volumes data_dir = _auto_detect_data_dir() - + if data_dir and os.path.isdir(data_dir): # Auto-set workspace, sessions, crons from data dir ws = os.path.join(data_dir, 'workspace') if os.path.isdir(ws) and not (args and args.workspace): if not args: - import argparse; args = argparse.Namespace() + import argparse + args = argparse.Namespace() args.workspace = ws sess = os.path.join(data_dir, 'agents', 'main', 'sessions') if os.path.isdir(sess) and not (args and getattr(args, 'sessions_dir', None)): diff --git a/docs/blueprints/quality-cloud-parity.md b/docs/blueprints/quality-cloud-parity.md index 57af35951d..91c66ce252 100644 --- a/docs/blueprints/quality-cloud-parity.md +++ b/docs/blueprints/quality-cloud-parity.md @@ -185,7 +185,7 @@ touching function signatures. ### ADR-003: Calibration thresholds hoisted to slice root, not per-card Context: the first version of the snapshot slice embedded the full calibration -thresholds in every per-runtime card. At 14 runtimes the thresholds were +thresholds in every per-runtime card. At 26 runtimes the thresholds were repeated 14 times; each copy was byte-identical. The thresholds made up roughly a quarter of the slice by size. From ee686f4553a858be630e366fc349bad47ee73bc0 Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Sun, 23 Aug 2026 23:13:05 +0200 Subject: [PATCH 04/12] fix(lint): sync runtime count in blueprint (26 runtimes) to unblock CI The Syntax & Lint job was failing solely because `scripts/sync_runtime_count.py --check` detected a stale "14 runtimes" mention in docs/blueprints/quality-cloud-parity.md (catalogue now says 26). Running `scripts/sync_runtime_count.py` updated the file. Co-Authored-By: Claude From 2dcfe5edbe50ab1ea2bbda283d8b767d8e5a0222 Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Mon, 24 Aug 2026 17:43:03 +0200 Subject: [PATCH 05/12] chore(ci): trigger E2E Gate on quality-cloud-parity branch Co-Authored-By: Claude Code From 83f4720ff94ccc7d2263b938938e163c41395d5c Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Wed, 26 Aug 2026 05:20:22 +0200 Subject: [PATCH 06/12] docs: sync runtime count to 27 in quality-cloud-parity blueprint scripts/sync_runtime_count.py --check flagged one stale mention: docs/blueprints/quality-cloud-parity.md:188 still said '26 runtimes' (ADR-003 context paragraph). Catalogue now has 27 runtimes. --- docs/blueprints/quality-cloud-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blueprints/quality-cloud-parity.md b/docs/blueprints/quality-cloud-parity.md index 91c66ce252..fe05fb76ce 100644 --- a/docs/blueprints/quality-cloud-parity.md +++ b/docs/blueprints/quality-cloud-parity.md @@ -185,7 +185,7 @@ touching function signatures. ### ADR-003: Calibration thresholds hoisted to slice root, not per-card Context: the first version of the snapshot slice embedded the full calibration -thresholds in every per-runtime card. At 26 runtimes the thresholds were +thresholds in every per-runtime card. At 27 runtimes the thresholds were repeated 14 times; each copy was byte-identical. The thresholds made up roughly a quarter of the slice by size. From 3c7185a0b06c729c700e8e1cb27c07d74546d36e Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Thu, 27 Aug 2026 12:30:40 +0000 Subject: [PATCH 07/12] ci: re-trigger E2E Gate (stale required check) From ee9a5ff328973683aa4c51cd59a0af2b7e3e0e6f Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Thu, 27 Aug 2026 17:25:23 +0200 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20update=20ADR-003=20runtime=20count?= =?UTF-8?q?=20to=20match=20catalogue=20(27=20=E2=86=92=2028)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/blueprints/quality-cloud-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blueprints/quality-cloud-parity.md b/docs/blueprints/quality-cloud-parity.md index fe05fb76ce..2202209f39 100644 --- a/docs/blueprints/quality-cloud-parity.md +++ b/docs/blueprints/quality-cloud-parity.md @@ -185,7 +185,7 @@ touching function signatures. ### ADR-003: Calibration thresholds hoisted to slice root, not per-card Context: the first version of the snapshot slice embedded the full calibration -thresholds in every per-runtime card. At 27 runtimes the thresholds were +thresholds in every per-runtime card. At 28 runtimes the thresholds were repeated 14 times; each copy was byte-identical. The thresholds made up roughly a quarter of the slice by size. From 4d849ee813be06c795b7333454360f975fcfab4f Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Sat, 29 Aug 2026 21:14:59 +0000 Subject: [PATCH 09/12] chore: sync runtime count to 30 in quality-cloud-parity blueprint scripts/sync_runtime_count.py --check fails because ADR-003 mentions '28 runtimes'; main now has 30 (Replit Agent added in #5340). --- docs/blueprints/quality-cloud-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blueprints/quality-cloud-parity.md b/docs/blueprints/quality-cloud-parity.md index 2202209f39..ac1dd19e42 100644 --- a/docs/blueprints/quality-cloud-parity.md +++ b/docs/blueprints/quality-cloud-parity.md @@ -185,7 +185,7 @@ touching function signatures. ### ADR-003: Calibration thresholds hoisted to slice root, not per-card Context: the first version of the snapshot slice embedded the full calibration -thresholds in every per-runtime card. At 28 runtimes the thresholds were +thresholds in every per-runtime card. At 30 runtimes the thresholds were repeated 14 times; each copy was byte-identical. The thresholds made up roughly a quarter of the slice by size. From 0609d51b47de135870d9f697b9dc46c0b33d0443 Mon Sep 17 00:00:00 2001 From: vivekchand Date: Wed, 2 Sep 2026 00:20:46 +0200 Subject: [PATCH 10/12] chore: retrigger drift-bot after blueprint update (Local Observability Service v26, Cloud Fleet Dashboard v15) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011JYHXKUSfu72qPh2zuq92N From cb3e39ca2ea5efa3da07097e8176c9cadaee94b0 Mon Sep 17 00:00:00 2001 From: vivekchand Date: Wed, 2 Sep 2026 00:25:22 +0200 Subject: [PATCH 11/12] chore: retrigger drift-bot (blueprints LOS v26 / CFD v15 persisted; index refresh) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011JYHXKUSfu72qPh2zuq92N From 114a482ff8676ca18759010808675bf6481e3020 Mon Sep 17 00:00:00 2001 From: vivekchand Date: Wed, 2 Sep 2026 00:28:54 +0200 Subject: [PATCH 12/12] chore: retrigger drift-bot (LOS block moved to the top, CFD contracts pinned) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011JYHXKUSfu72qPh2zuq92N