feat(rulings): system_usage_events is read back — per-System counts and a telemetry block (#4769)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 36s
#4769 "Rulings are counted where someone will read them": milestone 444 step 4 wrote system_usage_events and nothing read it. - retrieval_telemetry gains a `system_usage` block: surfacings and opens by source, distinct counts, and `by_system` naming the areas most shown. There is deliberately no pull-through ratio, because rulings travel in full in the line and opens are the exception. - usage_for_systems (one GROUP BY) adds `usage` to the REST Systems list and detail, and to MCP get_system. MCP list_systems is unchanged. - The Systems UI shows a "rulings shown N×" chip. - rulings_pre_tool, rulings_write_path and mcp_get_system are now declared registry points; the registry guard covers their recorders. - The Systems store merges a PATCH reply instead of replacing the row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,10 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.base import iso
|
||||
from scribe.models.system_usage import PULLED, SURFACED, SystemUsageEvent
|
||||
from scribe.services.background import report_telemetry_failure, spawn
|
||||
|
||||
@@ -65,3 +68,67 @@ def record_system_pulled(
|
||||
logger.debug("system usage payload build failed", exc_info=True)
|
||||
return
|
||||
spawn(_insert_events(rows), site="system_usage_write")
|
||||
|
||||
|
||||
def empty_system_usage() -> dict:
|
||||
"""The zero readout — a System no event has touched.
|
||||
|
||||
Rendered unconditionally, like `empty_rule_usage`, so a System predating
|
||||
the table reads as "never shown, never opened" rather than as a missing
|
||||
key. Every System in an install predates it, so for a while this is the
|
||||
normal state and must not look like a broken readout.
|
||||
|
||||
The same four keys as the client's `RecordUsage`, but read differently: a
|
||||
System's rulings are delivered IN FULL in the injected line, so a
|
||||
surfacing with no pull is the arm working, not dead weight. See
|
||||
`retrieval_telemetry`'s `system_usage` block for why no ratio is offered.
|
||||
"""
|
||||
return {
|
||||
"surfaced_count": 0,
|
||||
"pull_count": 0,
|
||||
"last_surfaced_at": None,
|
||||
"last_pulled_at": None,
|
||||
}
|
||||
|
||||
|
||||
async def usage_for_systems(system_ids: list[int]) -> dict[int, dict]:
|
||||
"""Aggregate usage for a set of Systems: {system_id: {counts + timestamps}}.
|
||||
|
||||
One GROUP BY for the whole list, never a query per row — this decorates a
|
||||
list view. Systems with no events come back as `empty_system_usage()`.
|
||||
Fails open: a readout that could break the Systems list is worse than a
|
||||
zero, but the failure is reported so the zero is not mistaken for silence
|
||||
(#2663).
|
||||
"""
|
||||
ids = [int(s) for s in system_ids]
|
||||
out: dict[int, dict] = {sid: empty_system_usage() for sid in ids}
|
||||
if not ids:
|
||||
return out
|
||||
try:
|
||||
async with async_session() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(
|
||||
SystemUsageEvent.system_id,
|
||||
SystemUsageEvent.event,
|
||||
func.count().label("n"),
|
||||
func.max(SystemUsageEvent.created_at).label("last_at"),
|
||||
)
|
||||
.where(SystemUsageEvent.system_id.in_(ids))
|
||||
.group_by(SystemUsageEvent.system_id, SystemUsageEvent.event)
|
||||
)
|
||||
).all()
|
||||
except Exception:
|
||||
await report_telemetry_failure("system_usage", "readout")
|
||||
return out
|
||||
for system_id, event, n, last_at in rows:
|
||||
slot = out.get(int(system_id))
|
||||
if slot is None:
|
||||
continue
|
||||
if event == SURFACED:
|
||||
slot["surfaced_count"] = int(n)
|
||||
slot["last_surfaced_at"] = iso(last_at)
|
||||
elif event == PULLED:
|
||||
slot["pull_count"] = int(n)
|
||||
slot["last_pulled_at"] = iso(last_at)
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user