feat(classifier): record why the classifier declined, and bound the degraded-warn knobs #112
Reference in New Issue
Block a user
Delete Branch "feat/classifier-observability"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
local_decision(qwen3.5:4b) was measured degrading about 30% of live turns on 2026-10-04, and the database could not say how close each declined answer was to the threshold:route_decisions.confidenceis exactly 1.0 on 43,804 of 43,856 rows because the chat path re-routes through the override branch, which hard-codes it. This records the classifier's own attempt soconfidence_mincan be tuned from data instead of a guess.ClassifierAttempt {confidence, coverage, reject_reason}onClassification, persisted asroute_decisions.classifier_confidence,classifier_coverage,classifier_reject. Old rows stay NULL.below_confidence_min,below_coverage_min,no_logprobs,timeout,transport_error,parse_error,primary_failed,skipped_<why>. They live insrc/classifier_rejection.py(stdlib only) and are raised fromlocal_decision.parse_logprobsasClassifierRejected, aRuntimeErrorsubclass, so the existing cascade handlers are unchanged./metricsdegraded-share warning now says why the classifier declined.session_history/session_staleso replayed labels are visible.classifier.degraded_warn_thresholdmust be in (0, 1] anddegraded_warn_minat least 1, refused at config load. The bounds are named constants inconfig.pyso a runtime admin write, which skips Pydantic, can import them instead of restating them.No cascade behaviour changes. This does not touch
confidence_min,session_historyage, or the static fallback; those wait on the data this collects.Operational notes
ALTERat startup, so the live DB gains them on the first restart after merge. The first rows to carry values are the next real traffic.config/config.yamlis unchanged. The live overlay hasdegraded_warn_threshold: 0.2(0.5 never fired at a 30% degraded day); that is machine-local.Test plan
scripts/verify_commit.py --repo <wt> --base origin/main --full HEADexits 0: 2644 tests, no new lint.tests/test_classifier_attempt.py: reason mapping for each raise site, chat-path capture, persistence on a migrated and an unmigrated DB, config validators, and the bound constants pinned to the validators' edges.route_decision_no_header.jsonupdated for the three new keys.classifier_confidencesplit byclassifier_rejecton real Atlas/Prometheus traffic before touchingconfidence_min.🤖 Generated with Claude Code
https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa