fix(librarian): quiet healthy-search coverage notes, tag clearing, strict result pairing

Phase A review minors:

- Coverage note: source_status (when present) is now used exclusively;
  the source_counts-absence fallback only considers the optional legs
  the request explicitly enabled (web/documents/volatile). library-desk
  computes source_counts from the final top-N fused results only, so
  absence of the always-on vector/graph legs is normal ranking behavior
  - the old heuristic warned on virtually every healthy search
- update_wiki_page: the empty-list tags sentinel (leave unchanged) made
  clearing all tags impossible; pass exactly ["__CLEAR__"] to send an
  empty tag list, documented in the docstring for the local model
- Text-delegation parallel fallback: zip(..., strict=True) with an
  explicit count-mismatch guard so results can never be silently
  attributed to the wrong agent

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
2026-07-14 11:59:15 +02:00
co-authored by Claude Fable 5
parent 31b948a748
commit 8cf3609948
5 changed files with 119 additions and 12 deletions
+27 -8
View File
@@ -55,8 +55,13 @@ def _coverage_note(
enabled source leg contributed nothing, so outages stay visible to
the model and the user instead of silently narrowing results.
Prefers the additive source_status/degraded contract when present;
falls back to inferring silent legs from source_counts.
When the additive source_status/degraded contract is present it is
authoritative and used EXCLUSIVELY - no count heuristics. Without
it, absence from source_counts is only inferred for the optional
legs this request explicitly enabled (web/documents/volatile);
the always-on wiki legs (vector/graph) are never inferred, because
source_counts only tallies the sources of the final top-N fused
results, so their absence is normal ranking behavior, not an outage.
"""
if response.source_status:
failed = sorted(
@@ -80,7 +85,9 @@ def _coverage_note(
# Older library-desk without per-source reporting - nothing to infer
return ""
expected = {"vector", "graph"}
# Only legs the request explicitly enabled; never vector/graph (their
# absence from the top-N counts is healthy, see docstring)
expected = set()
if include_web:
expected.add("web")
if include_documents:
@@ -88,8 +95,8 @@ def _coverage_note(
if include_volatile:
expected.add("volatile")
# Normalize count keys to leg names (document/documents, wiki/vector)
aliases = {"document": "documents", "wiki": "vector"}
# Normalize count keys to leg names (document/documents)
aliases = {"document": "documents"}
reported = {aliases.get(key, key) for key in response.source_counts}
missing = sorted(expected - reported)
if missing:
@@ -791,6 +798,9 @@ async def read_urls_batch(
# Wiki Write Operations
# ============================================================================
CLEAR_TAGS_SENTINEL = "__CLEAR__"
async def update_wiki_page(
page_id: int,
content: str = "",
@@ -817,7 +827,9 @@ async def update_wiki_page(
page_id: ID of the page to update (get from search_wiki results)
content: New markdown content (empty = leave unchanged)
title: New title (empty = leave unchanged)
tags: New tag list, replaces existing tags (empty = leave unchanged)
tags: New tag list, replaces existing tags (empty = leave unchanged).
To remove ALL tags from a page, pass exactly ["__CLEAR__"]
(an empty list means "leave unchanged", not "clear")
description: New description (empty = leave unchanged)
Returns:
@@ -826,15 +838,20 @@ async def update_wiki_page(
Examples:
update_wiki_page(42, content="# Updated Content\\n\\nNew information here")
update_wiki_page(42, tags=["projects", "devops"]) # Add to dossiers
update_wiki_page(42, tags=["__CLEAR__"]) # Remove all tags
update_wiki_page(42, description="Updated description")
"""
# Empty list = leave unchanged; the explicit clear sentinel sends an
# empty tag list to the service, which replaces (clears) all tags.
clear_tags = tags == [CLEAR_TAGS_SENTINEL]
try:
async with LibraryDeskClient() as client:
page = await client.update_wiki_page(
page_id=page_id,
content=content if content else None,
title=title if title else None,
tags=tags if tags else None,
tags=[] if clear_tags else (tags if tags else None),
description=description if description else None,
)
@@ -844,7 +861,9 @@ async def update_wiki_page(
updated_fields.append("content")
if title:
updated_fields.append("title")
if tags:
if clear_tags:
updated_fields.append("tags (cleared)")
elif tags:
updated_fields.append("tags")
if description:
updated_fields.append("description")
+16 -1
View File
@@ -178,9 +178,24 @@ async def _handle_text_delegation(
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# asyncio.gather returns exactly one item per task; a length
# mismatch would mean results are attributed to the wrong
# agent, so fail loudly instead of mispairing silently.
if len(results) != len(matches):
logger.error(
"delegation_result_count_mismatch",
expected=len(matches),
got=len(results),
conversation_id=conversation_id,
)
return (
"I apologize, sir. I was unable to complete the "
"requested delegations."
)
# Combine results (failures carry curated user-safe sentences)
summaries = []
for (agent, task), item in zip(matches, results, strict=False):
for (agent, task), item in zip(matches, results, strict=True):
agent_name = agent.lower()
if isinstance(item, BaseException):
logger.error(