scripts/xkb_failures.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""One way for a swallowed failure to leave a trace.
Forty-one places in this codebase catch a broad exception and continue. Most
of them are right to: a file that will not read while scanning nine hundred, an
optional enrichment, a telemetry write. XKB must never break a conversation
because a hint could not be produced.
The danger is not that they are silent. It is that they are *identical* from
the outside. "The semantic backend is down" and "there is nothing on this
subject" both arrive as an empty list, so a recall system that had been broken
for twelve weeks kept answering, politely, that it knew nothing. A corrupt
index and an empty index both arrive as `[]`, so a worker re-processes
everything or skips deduplication with no complaint.
The fix is not to raise — that would break the conversation the handler exists
to protect. It is to make the two cases *distinguishable*: keep the fallback,
and say on stderr that it happened. A log line costs nothing when everything
works and is the whole difference when it does not.
Deliberately tiny, and deliberately incapable of raising: a failure reporter
that can itself fail is worse than none.
"""
from __future__ import annotations
import os
import sys
# 同一個地方壞一百次,只需要說一次;掃九百個檔案時,每個都印會把有用的訊息淹掉。
_seen: set[str] = set()
# 說過的和發生過的是兩件事。_seen 管「還要不要再印」,這一份管「這次跑的時候
# 有哪些地方退回了 fallback」——後者要能被附進紀錄,即使一個字都沒印出來
# (XKB_QUIET_FAILURES=1 或同一個地方第二次)。
_degraded: set[str] = set()
QUIET = os.getenv("XKB_QUIET_FAILURES") == "1"
def note(where: str, err: BaseException, *, detail: str = "", repeat: bool = False) -> None:
"""Record that a fallback was taken, and why.
Args:
where: 哪個功能退回了 fallback,例如 "semantic recall"。
err: 被吞掉的例外。
detail: 補充脈絡,例如出問題的檔名。
repeat: 預設同一個地方只說一次;批次外層想每次都說時設 True。
"""
try:
key = f"{where}:{type(err).__name__}"
# 先記下來再決定要不要印。印不印是給人看的事,記不記是給紀錄看的事,
# 而這兩件事原本綁在一起:安靜模式或第二次發生時,連「發生過」都留不下。
_degraded.add(where)
if not repeat:
if key in _seen:
return
_seen.add(key)
if QUIET:
return
suffix = f" ({detail})" if detail else ""
print(f"[xkb] {where} 退回 fallback:{type(err).__name__}: {err}{suffix}",
file=sys.stderr, flush=True)
except Exception: # noqa: BLE001 — 報告失敗的東西自己不能失敗
pass
def taken() -> list[str]:
"""這個行程到目前為止,哪些地方退回了 fallback。
stderr 上說一次,對「有人正在看終端機」的情境夠了。召回不是那種情境:
它跑在 MCP 伺服器或 hook 裡,stderr 沒有人讀,而呼叫端拿到的是一個空
清單——跟「這個主題確實沒有東西」一模一樣。
所以 note() 除了印出來,還要留下可以被附進紀錄的痕跡。召回的 telemetry
帶上這個清單之後,「查無資料」和「語意後端逾時」才在事後分得出來。
回傳的是 where 而不是例外內容:紀錄要能拿來統計,不是拿來讀堆疊。
"""
return sorted(_degraded)
def reset() -> None:
"""測試用:清掉「已經說過」與「發生過」的記錄。"""
_seen.clear()
_degraded.clear()