Skip to content

Stop-hook: schärferer once-per-session /model-Nudge bei Tier-3-Arbeit #305

@achimdehnert

Description

@achimdehnert

Motivation

C4-Spend-Audit (2026-05-28, 14d, dedupliziert nach request_id): 99,7% der interaktiven Claude-Code-Kosten laufen auf claude-opus-4-7, nur 0,3% auf Sonnetsession-routing.md wird in der Praxis ignoriert. 26% der Calls (≥500k Input) = 50% der Kosten; eine einzelne Session war 2.509 Calls / $2.294.

Der bestehende Nudge im Stop-hook ist zu schwach: er feuert pro Turn, nur wenn ein einzelner Opus-Turn < $0.05 kostet, und sagt vage ⚠ tier-3 reicht für routine work. Das geht im Output unter und gibt keine konkrete Handlung.

Datei

~/.claude/hooks/log_llm_call.py (Claude-Code Stop-hook). Aktuell unversioniert — siehe optionalen Sub-Task unten.

Relevante Stellen:

  • Nudge-Block: main(), der if rows:-Block (~Z. 290–315), Bedingung if "opus" in m.lower() and 0 < turn_cost < 0.05:.
  • Session-scoped DB-Query-Muster: _query_session_total() (~Z. 320) — WHERE source='claude_code' AND task_id = cc-<session>.
  • State-File: _load_state()/_save_state() (~Z. 98–118), aktuell nur {"logged_request_ids": [...]} pro Session.

Ziel

Statt des per-Turn-Geflackers: genau einmal pro Session eine konkrete /model sonnet-Empfehlung, wenn die Session erkennbar überwiegend Tier-3-Routine auf Opus ist. Policy session-routing.md: "mention once, do not nag".

Vorgeschlagene Heuristik (Schwellen tunebar)

Nudge feuern, wenn alle zutreffen:

  • Modell-Familie = opus
  • Session hat ≥ 8 geloggte Turns (genug Signal)
  • ≥ 70% der bisherigen Session-Turns kosten < $0.10 (Routine-Proxy)
  • Session wurde noch nicht genudged

Aktion — eine Zeile auf stderr, z.B.:

💡 Session auf Opus 4.7, aber {pct}% der {n} Turns waren Routine (Median ${med}/Turn).
   Tier-3 → /model sonnet ≈ 5× günstiger (session-routing.md). [einmalige Empfehlung]

Datenquelle: ein kleiner Query über task_id = cc-<session_id> (analog _query_session_total) liefert count, Median-Turn-Cost und Cheap-Ratio in einem Rutsch (deleted_at IS NULL filtern).

Once-per-session

State-File um ein Feld tier3_nudged: true erweitern. Dafür _load_state/_save_state von "Set von request_ids" auf ein Dict {logged_request_ids: [...], tier3_nudged: bool} umstellen (Abwärtskompatibel laden: altes Format = nur Liste).

Constraints (nicht brechen)

  • Hook-Kontrakt: immer exit 0, fail-silent — ein Logging/Nudge-Fehler darf Stop nie blockieren.
  • Bestehender per-Turn-Burn-Nudge (🔥 burn rate hoch bei turn_cost > 0.20) bleibt.
  • Kein zusätzlicher spürbarer Latenz-Hit (eine kleine DB-Query ist ok; _query_session_total macht ohnehin schon eine).

Acceptance Criteria

  • Reine-Routine-Opus-Session (≥8 Turns, Cheap-Ratio hoch) → genau eine /model sonnet-Zeile, danach nie wieder in derselben Session
  • Architektur-/Tier-4-Session (teure Turns) → kein Tier-3-Nudge
  • Sonnet-Session → kein Opus-Nudge
  • Hook bleibt exit 0 auch bei DB-/State-Fehler
  • Empfehlung nennt konkret /model sonnet (nicht nur "tier-3 reicht")

Test

tests/ für den Hook gibt es noch nicht → kleiner pytest mit gemocktem _query_session_total/State, der die Trigger-Matrix oben abdeckt (Routine→1×, Tier-4→0×, Sonnet→0×, Idempotenz→1×). Feedback-Memory: "Tests für neue Funktionen gleich mitliefern".

Optionaler Sub-Task (separat, nicht blockierend)

Hook + backfill_llm_calls.py + inject_policies.py sind unversioniert in ~/.claude/hooks/. Wie claude-policy nach platform/tools/claude-hooks/ versionieren (Symlink zurück nach ~/.claude/hooks/), damit billing-relevante Hooks reviewbar/diffbar sind.

Kontext-Referenzen

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions