feat(heads): incremental retraining — refit only changed tags (#1317 phase 2, m138)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m35s

train_all_heads is now incremental by default: a per-tag training-data
fingerprint (positive + rejection count/latest-timestamp, stored on
tag_head.train_fingerprint) means a manual Retrain refits ONLY the tags whose
data changed — O(what you touched), not O(all heads). The nightly
scheduled_train_heads passes full=True to reconcile sampled-negative + hygiene
drift across every head. First incremental run after deploy still refits
everyone (NULL fingerprints), stamping them, then it's incremental.

The refit decision + fingerprint are split into sklearn-free helpers
(_head_fingerprints, _heads_needing_retrain) so the incremental logic is
unit-tested directly (train_head itself needs scikit-learn). Migration 0080.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-06 16:36:30 -04:00
parent a94f6a2789
commit 2cfbb284d5
5 changed files with 273 additions and 7 deletions
+4 -1
View File
@@ -356,7 +356,10 @@ def scheduled_train_heads() -> str:
if running is not None:
return "already running"
run = HeadTrainingRun(
params={"source": "scheduled"}, status="running",
# Nightly = FULL reconcile (refit every head) so sampled-negative +
# hygiene drift is folded in; the manual Retrain button stays
# incremental (#1317 p2).
params={"source": "scheduled", "full": True}, status="running",
last_progress_at=datetime.now(UTC),
)
session.add(run)