CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 58s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / Build & push image (push) Successful in 27s
Step 2 of #4251. A System's `description` is a charter — several hundred words saying what belongs in that area and what does not — and it is the answer to "which part of this codebase does X live in". There was no semantic path to one: `list_systems` enumerates, and `search(system_id=…)` uses a System as a FILTER over notes. So a System could narrow a search and could never be the answer to one, and an agent asking where a record belonged had to read every charter or guess. ITS OWN SEARCH, not a `content_type` over notes, for the reason note 3163 gives about milestones: the row could be shared, the search cannot. A charter competing with the whole note corpus for one top-k is outranked by the records filed under it — the right answer crowded out by its own contents — and "where does this belong?" is a different question from "what prior art is there?", which a caller asking one should not have to read past answers to. So `system_embeddings` (0107) joins note_, rule_ and milestone_embeddings as the fourth sibling, with `system_document`, `upsert_system_embedding`, `semantic_search_systems`, a startup backfill and `search(content_type= "system")`. Scoped like milestones: with a project_id, that project's Systems if the caller can read the project (rule 78); without one, the caller's own. Archived Systems are excluded — an archived area is one the operator has said is no longer where things go, which is exactly the question being asked. `system_document` is the plainest of the four shapes on purpose. A charter is already written as the thing this search has to match, in the words someone asking would use — so there is no trigger to synthesise as `rule_document` must, and no second record to gather as `task_document` must. The stored charter IS the sharp document, the way a snippet's is. `color`, `status` and `order_index` stay out: presentation and bookkeeping, and a vector carrying them would be answering a question nobody asks of a charter. The search publishes `report["best_chunk"]` from the start rather than being retrofitted, which is what #4251 asked of any fourth search. It matters more here than anywhere: a charter runs long and a result shows its NAME, so a match on the paragraph that actually decides where a record belongs would otherwise be previewed by two words that cannot say. The id that comes back is the one `system_id`, `system_ids` and `list_system_records` already take, so the answer to "where does this belong?" is directly usable as "show me what is there" and as "file it here". `embed_system` sits beside `notes.embed_note` at the service for #2056's reason — every door gets it by construction. Not called on delete: that is a soft delete and the search joins through `System`, so the vectors are already unreachable, and leaving them means a restore is findable again immediately. `system_embeddings` is declared in backup's `_NOT_INCLUDED` as derived, beside its three siblings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
666 lines
35 KiB
Python
666 lines
35 KiB
Python
"""search — semantic search across the user's notes and tasks.
|
|
|
|
Mirrors the existing fable-mcp contract so Claude's prior usage pattern keeps
|
|
working. Differences from fable-mcp:
|
|
- calls services.embeddings.semantic_search_notes directly instead of HTTP
|
|
- user_id comes from mcp.current_user_id() rather than a global API key
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from scribe.mcp._context import current_user_id
|
|
from scribe.services.access import owner_names_for
|
|
from scribe.services.knowledge import content_type_filters
|
|
from scribe.services.text import MATCHED_PASSAGE, excerpt_fields
|
|
from scribe.services.embeddings import (
|
|
DEFAULT_SIMILARITY_THRESHOLD, semantic_search_milestones, semantic_search_notes,
|
|
semantic_search_rules, semantic_search_systems,
|
|
)
|
|
from scribe.services import rulebooks as rulebooks_svc
|
|
from scribe.services.retrieval_telemetry import record_retrieval, retrieval_summary
|
|
|
|
|
|
# A matched chunk is at most _CHUNK_CHAR_BUDGET (1400) characters, and it is
|
|
# the evidence the ranking was built on — so it is worth more room than the 240
|
|
# characters of document opening this used to send. Elision inside a chunk is
|
|
# far less lossy than a head cut of a whole record: the region is already the
|
|
# right one.
|
|
_EXCERPT_CHARS = 1000
|
|
|
|
|
|
# The kinds `content_type` accepts are DERIVED from the facet table, not listed
|
|
# again here — that table is where a kind is declared (#3161), and a second
|
|
# hand-kept copy in this module is precisely how the agent's door came to offer
|
|
# two kinds while browse offered nine (#4250). `content_type_filters` carries
|
|
# the mapping, the 'all'/'note' special cases and the refusal.
|
|
#
|
|
# These two do not reach `semantic_search_notes` at all: they have their own
|
|
# search and their own result shape, so they are dispatched before the mapping
|
|
# and passed in only so the refusal message lists everything THIS door takes.
|
|
_OWN_SEARCH = ("rule", "milestone", "system")
|
|
|
|
|
|
async def _search_rules(uid: int, q: str, limit: int, project_id: int) -> dict:
|
|
"""Rules by meaning — a separate result shape because a rule IS different.
|
|
|
|
A rule hit carries `why` and `how_to_apply`: they are the operational half
|
|
of a rule and the session-start payload never includes them, so a caller
|
|
who went looking should get the whole thing rather than a summary they then
|
|
have to re-fetch. It also carries the rule's check (`verify_with`,
|
|
`expires_when`, `last_verified`) when it has one — a search hit is exactly
|
|
the moment someone is about to act on a rule, and "this asserts a fact
|
|
nobody has confirmed" is part of what the rule says.
|
|
|
|
`project_id` scopes the way it does for notes, with one difference: a
|
|
GLOBAL rule (one in a rulebook) belongs to no project and applies in every
|
|
one, so a scoped search returns global rules plus that project's own.
|
|
Without a project it asks the whole rulebook — every rule, whatever its
|
|
home — because that is the question an unscoped "is there a rule about
|
|
this" is asking. `system_id` does not apply to rules.
|
|
"""
|
|
if project_id:
|
|
raw = await semantic_search_rules(uid, q, limit=limit, project_id=project_id)
|
|
else:
|
|
raw = await semantic_search_rules(uid, q, limit=limit, everywhere=True)
|
|
return {
|
|
"results": [
|
|
{
|
|
"id": rule.id,
|
|
"title": rule.title,
|
|
"statement": rule.statement,
|
|
"when_to_apply": rule.when_to_apply or "",
|
|
"why": rule.why or "",
|
|
"how_to_apply": rule.how_to_apply or "",
|
|
"verify_with": rule.verify_with or "",
|
|
"expires_when": rule.expires_when or "",
|
|
# Only on a rule that carries a check; its absence means the
|
|
# rule is a decision, not that nobody has looked.
|
|
**(
|
|
{"last_verified": rulebooks_svc.last_verified_label(rule)}
|
|
if rule.verify_with else {}
|
|
),
|
|
"topic_id": rule.topic_id,
|
|
"project_id": rule.project_id,
|
|
"similarity": float(score),
|
|
}
|
|
for score, rule in raw
|
|
],
|
|
"total": len(raw),
|
|
}
|
|
|
|
|
|
async def _search_milestones(uid: int, q: str, limit: int, project_id: int) -> dict:
|
|
"""Milestones by meaning — "is there already a plan for this?" (milestone 415).
|
|
|
|
Its own result shape, like rules: a milestone is a plan with progress, not
|
|
a note with a body. The plan itself is left out — get_milestone reads it —
|
|
because a search hit is for recognising a plan, and bodies run long. What
|
|
does come along is `matched`: the one passage of the body the query
|
|
actually hit, with `matched_is` saying whether it is that passage or
|
|
merely the body's opening. Recognising a plan means recognising the part
|
|
of it you were asking about, and a description written at the start need
|
|
not mention that part (#4243).
|
|
Not part of content_type="all", whose results are note-shaped.
|
|
"""
|
|
report: dict = {}
|
|
raw = await semantic_search_milestones(
|
|
uid, q, project_id=project_id or None, limit=limit, report=report,
|
|
)
|
|
chunks = report.get("best_chunk") or {}
|
|
progress: dict[int, dict] = {}
|
|
if raw:
|
|
from scribe.services import milestones as milestones_svc
|
|
|
|
for pid in {m.project_id for _s, m in raw}:
|
|
for row in await milestones_svc.get_project_milestone_summary(uid, pid):
|
|
progress[row["id"]] = row
|
|
return {
|
|
"results": [
|
|
{
|
|
"id": m.id,
|
|
"title": m.title,
|
|
"description": m.description or "",
|
|
# The plan body stays out — get_milestone reads it — but the
|
|
# passage that MATCHED comes along, because a plan is
|
|
# recognised by the part of it the query was about and a
|
|
# description need not mention that part at all (#4243).
|
|
**excerpt_fields(
|
|
m.body or "", chunks.get(int(m.id)), _EXCERPT_CHARS,
|
|
key="matched",
|
|
),
|
|
"status": m.status,
|
|
"project_id": m.project_id,
|
|
"total": progress.get(m.id, {}).get("total", 0),
|
|
"completed": progress.get(m.id, {}).get("completed", 0),
|
|
"similarity": float(score),
|
|
}
|
|
for score, m in raw
|
|
],
|
|
"total": len(raw),
|
|
}
|
|
|
|
|
|
async def _search_systems(uid: int, q: str, limit: int, project_id: int) -> dict:
|
|
"""Systems by meaning — "where does this belong?" (#4251).
|
|
|
|
A System's `description` is a charter: several hundred words saying what
|
|
belongs in that area and what does not. `list_systems` enumerates them and
|
|
`system_id` filters by one, so before this a System could NARROW a search
|
|
and could never be the answer to one — an agent asking where a record
|
|
belonged had to read every charter or guess.
|
|
|
|
Its own result shape and its own search, not a `content_type` over notes,
|
|
because the question is different: "where does this belong?" is not "what
|
|
prior art is there?". A charter competing with the whole note corpus for
|
|
one top-k would also be outranked by the records filed under it, and the
|
|
right answer would be crowded out by its own contents.
|
|
|
|
The charter's `matched` passage comes along rather than the whole thing.
|
|
A charter is long and the paragraph that decides where a record belongs is
|
|
the one worth reading; the rest is get_system (#4243).
|
|
|
|
The id that comes back is the one `search(system_id=…)`,
|
|
`list_system_records` and every `system_ids` argument take — so the answer
|
|
to "where does this belong?" is directly usable as "show me what is there"
|
|
and as "file it here".
|
|
"""
|
|
report: dict = {}
|
|
raw = await semantic_search_systems(
|
|
uid, q, project_id=project_id or None, limit=limit, report=report,
|
|
)
|
|
chunks = report.get("best_chunk") or {}
|
|
return {
|
|
"results": [
|
|
{
|
|
"id": sys_.id,
|
|
"name": sys_.name,
|
|
**excerpt_fields(
|
|
sys_.description or "", chunks.get(int(sys_.id)),
|
|
_EXCERPT_CHARS, key="matched",
|
|
),
|
|
"project_id": sys_.project_id,
|
|
"similarity": float(score),
|
|
}
|
|
for score, sys_ in raw
|
|
],
|
|
"total": len(raw),
|
|
}
|
|
|
|
|
|
def result_excerpt(note, chunk: dict | None) -> dict:
|
|
"""The part of a record a caller judges "should I open this?" on.
|
|
|
|
This used to be `(note.body or "")[:240]` — the document's opening, with
|
|
no marker that anything had been cut, so a 240-character preview of a
|
|
4000-character record was indistinguishable from a complete short one.
|
|
|
|
The opening is the wrong span. The match was semantic and per-chunk, and
|
|
`semantic_search_notes` collapses to best-chunk-per-note — so the system
|
|
already knows which passage earned the hit and used to discard it. A
|
|
record could rank first on its sixth paragraph and be previewed by its
|
|
first, which the search had already judged less relevant, and the caller
|
|
would decide from that and never know (#4243).
|
|
|
|
The choice of span lives in services/text.py, shared with the web's
|
|
knowledge search, so the two doors cannot drift on which text a reader is
|
|
shown or on whether they are told what it is.
|
|
"""
|
|
out = excerpt_fields(note.body or "", chunk, _EXCERPT_CHARS)
|
|
if out.get("excerpt_is") == MATCHED_PASSAGE and (chunk or {}).get("index") is not None:
|
|
out["chunk_index"] = int(chunk["index"])
|
|
return out
|
|
|
|
|
|
async def search(
|
|
q: str,
|
|
content_type: str = "all",
|
|
limit: int = 10,
|
|
project_id: int = 0,
|
|
system_id: int = 0,
|
|
) -> dict:
|
|
"""Semantic search over the user's existing notes and tasks — Scribe's recall.
|
|
|
|
Reach for this BEFORE answering a question about the user's work or starting
|
|
a task: the user's second-brain almost always already holds related prior
|
|
art. Check for an existing ticket before opening a new one (search with
|
|
content_type='task'), and for prior notes/decisions before re-deriving them.
|
|
Treating Scribe as the first place to look — not a place to only write — is
|
|
the difference between it being a trustworthy record and a write-only log.
|
|
|
|
Args:
|
|
q: search query string.
|
|
content_type: which kind of record to search. 'all' (default) spans
|
|
every note and task.
|
|
|
|
THE BROAD TWO: 'note' is any non-task record — it still includes
|
|
snippets, lessons and processes, so it means "knowledge, not work
|
|
items". 'task' is any task whatever its kind.
|
|
|
|
THE SPECIFIC KINDS, each narrowing to one: 'snippet' (recorded
|
|
prior art — reach for this BEFORE writing a helper, rather than
|
|
searching 'all' and reading past the issues), 'lesson' (a
|
|
transferable insight, the kind that exists to be recalled by
|
|
situation), 'process' (a stored procedure the operator saved),
|
|
'issue' (corrective work — "has this already been reported?"),
|
|
'spike' (a time-boxed investigation, whose output is an answer),
|
|
'work', and 'plan' (retired; the ~90 legacy plan-tasks).
|
|
|
|
An unrecognised value is REFUSED with the list of valid ones
|
|
rather than quietly returning nothing: an empty result set is a
|
|
claim that the corpus holds nothing, and a typo must not be able
|
|
to make that claim.
|
|
|
|
THREE KINDS WITH THEIR OWN SEARCH AND THEIR OWN RESULT SHAPE,
|
|
because each answers a question no note search can:
|
|
'rule' (the operator's standing instructions — searchable by
|
|
meaning since milestone 307), 'milestone' ("is there already a
|
|
plan for this?"), and 'system' ("where does this belong?" — a
|
|
System's charter says what belongs in an area and what does not,
|
|
and the id it returns is the one `system_id` and `system_ids`
|
|
take).
|
|
Reach for 'rule' when you want to know whether a standing
|
|
instruction covers something: "is there a rule about release
|
|
tagging?". A hit carries the rule's `why` and `how_to_apply`,
|
|
which the session-start payload does not. With a project_id,
|
|
rules come back as the global rules plus that project's own;
|
|
with 0, every rule in the rulebook. Or 'milestone' (PLANS):
|
|
reach for it before start_planning to ask whether a plan for
|
|
this work already exists — a match is where new steps go
|
|
(create_records(milestone_id=…)), not a reason to open a second
|
|
milestone. Hits carry title, description, status and progress;
|
|
get_milestone reads the plan. Not included in 'all'.
|
|
limit: maximum number of results (1-50).
|
|
project_id: Scope results to one project. PASS THE ACTIVE PROJECT'S ID
|
|
whenever a project is in scope (the one you entered with
|
|
enter_project) — otherwise this searches across ALL projects and
|
|
bleeds unrelated work into the result set. 0 = search everything
|
|
(use only when you genuinely want a cross-project sweep).
|
|
A LESSON is the exception and arrives whatever the scope: the kind
|
|
records an insight that transfers, so it is reachable from a
|
|
project it was not written on.
|
|
system_id: Narrow to records tagged to one System (a named
|
|
subsystem/area — enter_project lists them). Use when investigating
|
|
a specific subsystem: it cuts the candidates to records someone
|
|
deliberately filed under that area. 0 = no system filter.
|
|
list_system_records gives the same slice unranked.
|
|
|
|
Returns:
|
|
{"results": [{"id", "title", "excerpt", "excerpt_is", "body_length",
|
|
"is_task", "tags", "similarity"}],
|
|
"total": int}
|
|
|
|
`excerpt` is a SPAN of the record, not the record. `excerpt_is` says
|
|
which span: "matched_passage" is the chunk that actually earned the
|
|
hit — the evidence the ranking was built on, and the right thing to
|
|
judge relevance from. "body_opening" is a fallback for a record with
|
|
no stored chunk, and is only the beginning of the text, which may say
|
|
nothing about why it matched. `body_length` is the whole record's
|
|
size, so a long record previewed by a short span is visible as one;
|
|
`read_full` appears when there is more, and opening the id by
|
|
get_note / get_task is how you get it. Judge from the passage, not
|
|
from the fact that a preview looked thin.
|
|
|
|
A result marked `shared: true` with an `owner` belongs to another user —
|
|
that person's suggestion, not the operator's own record or settled practice.
|
|
Weigh it on its merits and say whose it is when you use it.
|
|
"""
|
|
uid = current_user_id()
|
|
limit = max(1, min(limit, 50))
|
|
if content_type == "rule":
|
|
return await _search_rules(uid, q, limit, project_id)
|
|
if content_type == "milestone":
|
|
return await _search_milestones(uid, q, limit, project_id)
|
|
if content_type == "system":
|
|
return await _search_systems(uid, q, limit, project_id)
|
|
filters = content_type_filters(content_type, extra=_OWN_SEARCH)
|
|
is_task = filters.get("is_task")
|
|
t0 = time.perf_counter()
|
|
report: dict = {}
|
|
raw = await semantic_search_notes(
|
|
uid, q, limit=limit, **filters,
|
|
project_id=project_id or None,
|
|
system_id=system_id or None,
|
|
# A LESSON is reachable from any project (milestone 385). The kind
|
|
# exists to carry an insight to the next project, so a project filter
|
|
# that hid it would hide it precisely where it is worth having. Only
|
|
# the project filter widens — everything else about the scoping holds,
|
|
# and a caller narrowing by `content_type` still gets what it asked
|
|
# for. This is the explicit search, where the operator asked; the
|
|
# unasked-for arms decide their own budget separately.
|
|
include_global_kinds=True,
|
|
# An explicit search reaches everything the operator may read, including
|
|
# records shared with them one-to-one.
|
|
scope="read",
|
|
report=report,
|
|
)
|
|
record_retrieval(
|
|
user_id=uid, source="mcp_search", query=q,
|
|
threshold=DEFAULT_SIMILARITY_THRESHOLD, limit=limit,
|
|
project_id=project_id or None, is_task=is_task, results=raw,
|
|
duration_ms=(time.perf_counter() - t0) * 1000.0,
|
|
best_available=report.get("best_available_score"),
|
|
best_available_id=report.get("best_available_id"),
|
|
searched=bool(report.get("searched", True)),
|
|
)
|
|
owners = await owner_names_for(
|
|
{int(note.user_id) for _s, note in raw if note.user_id != uid}
|
|
)
|
|
chunks = report.get("best_chunk") or {}
|
|
return {
|
|
"results": [
|
|
{
|
|
"id": note.id,
|
|
"title": note.title,
|
|
**result_excerpt(note, chunks.get(int(note.id))),
|
|
"is_task": bool(note.is_task),
|
|
"tags": list(note.tags or []),
|
|
"similarity": float(score),
|
|
**(
|
|
{"shared": True, "owner": owners.get(int(note.user_id))}
|
|
if note.user_id != uid else {}
|
|
),
|
|
}
|
|
for score, note in raw
|
|
],
|
|
"total": len(raw),
|
|
}
|
|
|
|
|
|
async def retrieval_telemetry(
|
|
days: int = 30, near_miss_samples: int = 0,
|
|
) -> dict:
|
|
"""What the retrieval telemetry says about YOUR surfaces, over a window.
|
|
|
|
The read half of the tuning loop, whose write half is `tune_retrieval`
|
|
(#2975, #4102). Reach for it before moving any floor or budget, and read
|
|
the records it names rather than its percentiles alone: this readout has
|
|
been measured pointing the WRONG WAY — 69 consecutive declines where every
|
|
percentile said "lower the bar" and the refused record was a false positive
|
|
— so `near_miss_samples=5` and opening the ids it returns is the step that
|
|
separates a real miss from a bar doing its job.
|
|
|
|
Three readouts, from the three tables built for them:
|
|
|
|
`sources` — per retrieval surface (`auto_inject`, `write_path`,
|
|
`mcp_search`, …), from `retrieval_logs`: `calls`, `zero_result_calls`,
|
|
`near_misses`, the `top_score` spread (p10/p50/p90/min/max),
|
|
`avg_result_count` and `p90_duration_ms`.
|
|
|
|
THE NUMBER TO READ FIRST IS `near_misses.p90`, AGAINST THE THRESHOLD IN
|
|
FORCE FOR THAT SURFACE. It is measured on the calls the BAR turned away —
|
|
zero-result calls, minus the ones whose zero was a repeat the reader had
|
|
already been shown — using the best score the ranker reached before the bar
|
|
rejected it. So it is the one figure here that says something the bar
|
|
cannot make true by construction, and `max` is always below the threshold:
|
|
an above-bar candidate nobody excluded would have been returned. A bar at 0.72 turning away a stream of 0.71s is set too
|
|
high by a hair and the surface is losing hits it should have had. The same
|
|
bar turning away 0.30s is working, and the corpus simply had nothing. Both
|
|
render as a zero-result call, and nothing else in this readout tells them
|
|
apart.
|
|
|
|
`near_miss_samples` (0-20, default 0) TURNS THE PERCENTILES INTO RECORDS
|
|
YOU CAN READ. Each source then carries `near_miss_records`: its highest
|
|
scoring declines, each with the `record_id` the bar refused and the `query`
|
|
that asked. Reach for it whenever you are about to move a threshold.
|
|
|
|
THE PERCENTILE CANNOT SETTLE A BAR ON ITS OWN, and this is the whole reason
|
|
the parameter exists. `near_misses.p90` says mass is sitting just under the
|
|
line; it says nothing about whether that mass is RELEVANT, and those are
|
|
different questions. Lowering a bar to where the mass is, without reading
|
|
what is there, is choosing a firing rate rather than a quality. Pull-through
|
|
cannot referee it either — the injected rule line already carries title and
|
|
trigger, so a session can comply without ever calling `get_rule`, which
|
|
makes rule pull-through understate usefulness by construction. Reading the
|
|
rejected records is the method that actually answers it.
|
|
|
|
Off by default because it is a LISTING, not a statistic: it is for the
|
|
moment you are making a decision, not for every readout.
|
|
|
|
`near_misses` is `null` when no declining call in the window measured it —
|
|
rows written before #3670 shipped cannot know. That is "not measured", not
|
|
"nothing came close"; a 0.0 there would be a claim about the corpus
|
|
invented out of a caller's silence.
|
|
|
|
A NULL HERE NOW MEANS ONE THING, which it did not at first. A semantic
|
|
search returns nothing three ways WITHOUT having run — an empty query, an
|
|
unavailable embedder, and a failed database query — and each used to write
|
|
a row indistinguishable from a ranker that declined (#3765). Those calls no
|
|
longer write a row at all, on the same reasoning that already keeps a blank
|
|
command out of the log: a row there reports a call that never happened and
|
|
drags the clear rate down with phantom declines. So a null is "searched,
|
|
and nothing came close", and a broken search shows up as a WARNING in the
|
|
application log rather than as a quiet zero in here.
|
|
|
|
THERE IS NO `cleared_threshold` ANY MORE, and if you remember one, that
|
|
memory is of a tautology (#3670). The search applies the bar before
|
|
returning, so every returned result cleared it by construction and a call
|
|
with no results has no score to compare: the field was true exactly when
|
|
`result_count > 0`, i.e. it was `calls - zero_result_calls` under a name
|
|
that promised a second opinion. `zero_result_calls + cleared_threshold ==
|
|
calls` held on all nineteen readings ever taken. The reading procedure
|
|
built on it — "clears its bar on nearly every call" — asked you to compare
|
|
a number with itself.
|
|
|
|
CHECK `suppression` BEFORE CONCLUDING ANYTHING FROM `zero_result_calls`. A
|
|
zero-result call is two different events wearing one number: the ranker
|
|
found nothing above the bar, or it found only what this session had already
|
|
been shown. Just the first is evidence about the bar. `suppression` splits
|
|
them where the surface can tell — `zero_because_already_shown` comes off
|
|
`zero_result_calls` to leave the true ranker declines.
|
|
|
|
`suppression` is `null` when NO row in the window reported it, and that is
|
|
"not measured here", NOT "none suppressed". Surfaces that pass their
|
|
exclusions into the search never see what was dropped, so they cannot say.
|
|
Do not read a null as a zero: reading an artifact as a measurement is how
|
|
this surface got mis-scoped once already (#3311, #3497).
|
|
|
|
`usage` — NOTES ONLY, from `note_usage_events`, at the per-note grain
|
|
`retrieval_logs` cannot be indexed at: `surfaced` (ranked surfacings — a
|
|
scored surface CHOSE the record), `ambient` (the rest), `pulled` split into
|
|
`pulled_by_agent` / `pulled_by_human`, the distinct-note counts, and
|
|
`pull_through`. That ratio is the corpus-side precision signal: records
|
|
surfaced often and opened never are dead weight competing for the injection
|
|
budget every turn.
|
|
|
|
`pull_through` is AGENT pulls over RANKED surfacings, and both halves of
|
|
that matter. "Is this record dead weight?" is answered by any pull; "was
|
|
that injected line useful?" — the question a threshold or a reranker is
|
|
tuned against — only by a pull the agent made. Aggregating across the
|
|
mcp_/rest_ prefix would silently answer the wrong one.
|
|
|
|
`usage["by_source"]` — THE number to tune a threshold against, because the
|
|
top-level `pull_through` is a corpus average and averages the surfaces
|
|
together. Per surface: `notes_surfaced`, `notes_pulled`, `pull_through`,
|
|
and `ambient: true` on surfaces whose surfacings were not scored choices
|
|
(their ratio is null — "surfaced often, opened never" is not a judgment
|
|
about a record nothing chose). Read it as: of the distinct notes THIS
|
|
surface put in front of the agent, how many did the agent then open?
|
|
|
|
It is an UPPER BOUND per surface: a pull records the door it came
|
|
through, not the surface that led there, so a note surfaced by two surfaces
|
|
and opened once counts for both — attribution would need the session
|
|
identity #2085 declined to invent. `by_source_failed: true` means that one
|
|
query failed while the rest of the readout stood.
|
|
|
|
`rule_usage` — the same question for RULES, from `rule_usage_events`:
|
|
`surfaced` and `ambient`, `pulled` split into `pulled_by_agent` /
|
|
`pulled_by_human`, the distinct-rule counts, and `pull_through` on the same
|
|
definition (agent pulls over RANKED surfacings).
|
|
|
|
`applied`, `departed` AND `distinct_rules_acted` ARE WHAT HAPPENED AFTER
|
|
THE RULE WAS OPENED (#4213). A pull says the rule was read; these say it
|
|
changed something. `applied` counts rules followed, `departed` rules
|
|
deliberately not followed — kept apart rather than summed, because a
|
|
departure carries the reason the agent gave and is evidence about the
|
|
RULE, while an application is evidence about the agent. There is
|
|
deliberately no count of rules read and quietly ignored: that state is what
|
|
is left over when a rule was pulled and neither outcome arrived, and
|
|
`read_and_unacted` below is where it is reported. Asking an agent to
|
|
declare it would be asking it to notice an omission it is defined by not
|
|
noticing.
|
|
|
|
A SEPARATE BLOCK, not folded into `usage`, and reading it as one number
|
|
with that is the mistake to avoid. The corpora differ by orders of
|
|
magnitude — a few dozen eligible rules against thousands of notes — so a
|
|
blended ratio would be the note ratio with noise on it and would hide the
|
|
rule arm entirely.
|
|
|
|
`surfaced` VS `ambient` IS THE READING THAT MATTERS HERE. `surfaced` counts
|
|
rules a ranker chose — today only the write-path arm — and those are claims
|
|
a pull can settle. `ambient` counts BULK DELIVERIES: the `rules_payload`
|
|
surfaces (`enter_project`, `get_project`, `get_milestone`,
|
|
`start_planning`, `get_task`), which hand over the whole applicable set at
|
|
once with nobody choosing anything — plus, in rows older than milestone
|
|
394, the SessionStart preload it removed. A large `ambient` says a bulk set
|
|
is big and arrives often — never that it is useful, and never that it is
|
|
read.
|
|
|
|
`pull_through` therefore divides by `surfaced` alone. Fold the preload in
|
|
and growing a bulk set would depress the arm's measured precision while
|
|
trimming it would flatter it, for reasons having nothing to do with the
|
|
arm. To judge a BULK surface instead, compare `ambient` against pulls of
|
|
those same rules over time: a set surfaced thousands of times and opened
|
|
never is the dead-weight signal, one level up.
|
|
|
|
Read it against `sources["write_path_rule"]`. That arm was once believed
|
|
never to decline — the reading that scoped #3311 — but it was the arm's
|
|
`retrieval_logs` row being written only on calls that FOUND something, so
|
|
the zeros were missing rather than absent (#3497). Measured since, it
|
|
declines the large majority of its calls like any other surface.
|
|
|
|
EVERY COUNTER BLOCK CARRIES ITS OWN COVERAGE — `complete_from` and
|
|
`covers_window`. `complete_from` is when the number became trustworthy:
|
|
for one source, its first recorded row; for a section that sums several,
|
|
the LATEST of theirs, because a total is complete only once every
|
|
contributor was being written. `covers_window: false` means the window
|
|
reaches back further than the recording does, so the count is a fraction
|
|
of the period it appears to describe.
|
|
|
|
READ IT BEFORE COMPARING TWO NUMBERS, and especially before comparing
|
|
across a deploy. A counter added last week, read over a 30-day window,
|
|
reports a real count against an imagined denominator — and the result is
|
|
a plausible fraction rather than an obvious zero, which is what makes it
|
|
dangerous. That reading cost milestone #379 five steps aimed at a defect
|
|
that did not exist.
|
|
|
|
`covers_window` is null, never false, when nothing was ever recorded:
|
|
"no measurement" is not "partial measurement", the same distinction
|
|
`suppression`'s null carries a few paragraphs up.
|
|
|
|
A SOURCE SHOWING `calls: 0` WAS RECORDING AND MADE NO CALLS. `sources`
|
|
lists every source the table has ever held, not only those active in the
|
|
window, so a surface that stopped firing stays visible rather than
|
|
disappearing — being absent is reserved for a source that has never
|
|
recorded at all. Its score fields are null, not zero: the calls are a
|
|
real observation, the distribution is not one.
|
|
|
|
`rule_usage_failed: true` means that read failed while the rest of the
|
|
readout stood. The counts are still present so a caller can render, but
|
|
they are zeros meaning "could not find out", not "nothing happened" — do
|
|
not report a pull-through from a block carrying that flag.
|
|
|
|
`warnings` IS THE PART TO READ FIRST (#3431). Everything above is a
|
|
distribution; this is a verdict, and it exists because the same four
|
|
checks were being redone by hand on every reading and were easy to
|
|
forget. An EMPTY LIST means checked and clean — it is always present, so
|
|
its emptiness is an answer rather than a gap. Each entry carries the
|
|
numbers that triggered it, so you can disagree with the rule instead of
|
|
having to redo the arithmetic:
|
|
|
|
- `cannot_decline` — an arm that fires unasked answered every one of its
|
|
calls. It cannot say nothing, which means it is not applying a floor.
|
|
Only ever raised for unbidden arms known to log unconditionally: a
|
|
search returning a list every time is doing its job, and an arm whose
|
|
zeros were never written would flag a LOGGING bug while pointing you at
|
|
a threshold, which is #3497 exactly. Nor for an arm whose query never
|
|
changes — see the next entry.
|
|
- `fixed_query_never_clears` — an arm that always searches the SAME query
|
|
returned nothing on every call. Its score is one constant, so this is
|
|
not a quiet window: the bar sits above that constant and no amount of
|
|
further traffic will produce a different result. The arm is off rather
|
|
than silent, and nothing else here would say so. The same property is
|
|
why `cannot_decline` is not raised for these arms: with a constant
|
|
score the decline rate is 0% or 100% by construction, so "never
|
|
declined" is arithmetic and not evidence about the floor. Read the
|
|
refused record (`near_miss_samples`) BEFORE moving the dial — the last
|
|
time an arm sat here, every percentile said lower it and the refused
|
|
record showed the refusal was right.
|
|
- `band_hugs_floor` — the weakest tenth of what an arm returns sits on
|
|
its floor. The bar is doing the selecting and the score is not, so
|
|
moving that floor changes how MUCH you get, not how good it is.
|
|
- `floor_moved_mid_window` — that arm's floor CHANGED inside the window,
|
|
by a release or by a dial turn, so its calls were made under two bars
|
|
and the band check above is suspended for it rather than answered
|
|
wrongly. Ask again with a `days` starting after the named date. The
|
|
warning replaces `band_hugs_floor` for that arm; it never accompanies
|
|
it.
|
|
- `no_duration` — rows written without timings. A logging gap, not a slow
|
|
arm, and it devalues every other number from that source.
|
|
- `surfaced_never_pulled` — distinct records shown and never opened, per
|
|
corpus. Read their titles before touching a threshold: a record nobody
|
|
opens is usually one whose title does not say when it matters.
|
|
- `read_and_unacted` — distinct rules OPENED in the window that recorded
|
|
no outcome, against the ones that did. The failure milestone 419 was
|
|
opened on, and the worse sibling of `surfaced_never_pulled` above: a
|
|
rule nobody opens is cheap, while a rule read and silently unchanged is
|
|
indistinguishable from one that worked. It does not say which of the two
|
|
causes it is — a rule mis-triggering, arriving where it does not apply,
|
|
or a rule being ignored — and those want opposite fixes, so read the
|
|
rules before moving anything.
|
|
- `outcomes_never_recorded` — rules were opened and NOT ONE outcome exists
|
|
anywhere in the window. Deliberately a separate code, and not a
|
|
`read_and_unacted` with a zero in it: a window with no outcomes at all
|
|
cannot tell "every rule was ignored" from "nothing on this install calls
|
|
`rule_outcome` yet", and reporting the first would manufacture a finding
|
|
out of an unwired feature. Wire the outcome call before reading this as
|
|
a fact about the corpus.
|
|
- `unregistered_source` — rows under a source missing from
|
|
`retrieval_registry`. Its numbers are real; no verdict could be
|
|
computed, because nothing says whether it was asked or fired unbidden.
|
|
|
|
`silent_surfaces` IS THE HALF THE ROWS CANNOT SHOW YOU. Every check above
|
|
reads rows, so an arm that produced none is invisible to all of them and
|
|
looks exactly like an arm that does not exist. This list is driven by the
|
|
declared registry instead: points expected to emit that emitted nothing.
|
|
Points that are legitimately quiet — the web-UI-only sources on an install
|
|
driven through MCP — are excluded by declaration rather than by silence,
|
|
so a justified quiet never reads as a gap. The list stays EMPTY on a
|
|
window with little traffic: on a fresh install every point is silent, and
|
|
reporting all of them would be describing the emptiness.
|
|
|
|
WARNINGS ARE COMPUTED OVER THE BLOCKS ABOVE, not over a second query, so
|
|
one can never disagree with the numbers printed beside it. A window whose
|
|
read failed produces none at all — a verdict over rows that did not load
|
|
would describe the outage while appearing to describe the system.
|
|
|
|
Two thresholds govern them, both settings so an install driven harder can
|
|
say so: `retrieval_warn_min_calls` (default 30) is how much traffic a
|
|
source needs before its silence means anything, and
|
|
`retrieval_warn_floor_epsilon` (default 0.02) is how close to the bar
|
|
counts as piled on it.
|
|
|
|
Scoped to your own telemetry — a retrieval log records what your agent
|
|
asked for, query text included, and is not a shared record kind.
|
|
|
|
`read_failed: true` means the query itself failed — deliberately distinct
|
|
from an empty window, because those two looked identical for weeks once
|
|
(#2663) and every counter silently read zero.
|
|
|
|
Args:
|
|
days: window size, default 30. Clamped to at least 1.
|
|
near_miss_samples: 0-20, default 0. How many of each source's highest
|
|
scoring DECLINES to list by record, with the query that asked.
|
|
Pass it when you are about to move a threshold; leave it off
|
|
otherwise. See the near-miss section above for why a percentile
|
|
alone cannot settle a bar.
|
|
"""
|
|
return await retrieval_summary(
|
|
current_user_id(), days=days, near_miss_samples=near_miss_samples,
|
|
)
|
|
|
|
|
|
def register(mcp) -> None:
|
|
mcp.tool(name="search")(search)
|
|
mcp.tool(name="retrieval_telemetry")(retrieval_telemetry)
|