`retrieval_logs` was write-only. `record_retrieval` inserted rows and nothing
in the tree ever selected from them: the only `select()` over RetrievalLog
lived in a test. So #1038's gate — "build the reranker once telemetry shows
precision is the bottleneck" — was unsatisfiable by construction, and the one
real tuning decision on record (the 0.68 write-path threshold, #2223) had to
be reached by hand-probing the live instance with eight payloads. This adds
the half that was missing.
`retrieval_summary(user_id, days=30)` returns two aggregates side by side,
each read from the table built for it — NOT a join. NoteUsageEvent's docstring
is explicit that the two are complements ("RetrievalLog tunes the threshold,
this tunes the corpus") and that RetrievalLog's JSONB `result_ids` cannot be
indexed at the per-note grain, so correlating through it would be both slower
and less honest than reading each source directly. That corrects the approach
sketched on the task.
- `sources`, per surface: calls, zero_result_calls, cleared_threshold (how
often the best hit beat the threshold in force for THAT call), the
top_score spread as p10/p50/p90/min/max, avg_result_count, p90 duration.
Zero-result calls are counted apart from low-scoring ones — they are a
different failure and averaging them together would hide both.
- `usage`, from note_usage_events: ranked surfacings, ambient surfacings,
and pulls split into `pulled_by_agent` / `pulled_by_human`.
That split is not decoration. NoteUsageEvent's own comment says the mcp_/rest_
prefix is load-bearing and names #1038 while saying so: "is this dead weight?"
is answered by any pull, "was that injected line useful?" only by an agent
pull. `pull_through` exists to answer the second, so it counts agent pulls
over ranked surfacings; both halves ship so the first stays answerable.
Two things the code made me get right rather than guess:
- Distinct-note counts get their own queries. `count(distinct note_id)` per
(event, source) group cannot be summed across groups — a note surfaced by
two sources is one distinct note and would be counted twice. A wrong
number labelled "distinct" is worse than no number.
- No CASE in the GROUP BY. #2663 is the bug where a second case() rendered
its own expanding bind names, Postgres rejected the query, a broad except
swallowed it, and every counter read zero in production while mocked tests
passed. Grouping on raw `source` and classifying in Python cannot fail
that way. For the same reason the readout distinguishes `read_failed` from
an empty window, and its tests are integration against real Postgres —
percentile_cont ... WITHIN GROUP only proves it parses against a database.
Exposed as the `retrieval_telemetry` MCP tool, added to `_READ_ONLY_TOOLS`:
it mutates nothing, but its name carries no read prefix, so the completeness
test cannot derive it and it would otherwise have failed closed for read-only
keys in silence — the same reason `enter_project` is spelled out there. Docs
updated to name both exceptions rather than leave the rule looking derivable.
Scoped to the caller's own telemetry: a retrieval log records what one user's
agent asked for, query text included, and is not a shared record kind — the
owner filter is the whole access rule, not a shortcut past access.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone #194 plugin/docs slice.
- plugin.json: "second brain" framing -> "system-of-record"; bump
0.1.11 -> 0.1.12 (any plugin/ change must bump the manifest, issue #1040)
- plugin/README.md + using-scribe SKILL + scribe_static_context: drop
events/typed-entities from the surface lists; "second brain" -> "system
of record" in the SessionStart context Claude reads each session
- docs/api-keys-and-mcp.md: drop the Typed-entities + Events MCP tool rows,
add the Systems row
- README.md: rewrite the stale front-matter (chat/RAG/calendar/weather/push
described a pre-pivot product) to the current Claude-driven work store
Deeper pre-pivot doc-rot in docs/features.md + docs/api-reference.md
(chat/journal/weather/web-research) is tracked separately as an issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPtbSzA4JLMAKgFZ8VTg7Q
The old standalone fable-mcp wheel/download flow is gone from code (no route,
no Dockerfile build, no FABLE_MCP_DIST_DIR). Update api-keys-and-mcp,
api-reference, architecture, configuration, development to describe the
in-app HTTP MCP at /mcp (Bearer auth). Untrack the 18 committed
docs/superpowers/ files so the existing .gitignore takes effect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>