Conversation
…ders)
A lesson with trigger_on='input' AND no trigger_tool AND no trigger_pattern
matches every Edit/Write/Bash/NotebookEdit call and dominates the
per-tool-call systemMessage budget. The synth-lesson #86 ('never X') was
the canonical bad case; three legit cross-cutting CRITICAL safeguards
(#35 read-before-edit, #36 docker-restart-zombie-cron, #44 infra-dev-first)
also fall in this set today.
Three layers of defense:
1. API validator (app/routes/lessons.py::_validate_trigger_on) — returns
400 for any new POST /api/lessons with trigger_on='input' and neither
trigger_tool nor trigger_pattern. Clean error message points to the
actual constraint.
2. DB CHECK constraint (migration 016, chk_input_trigger_has_filter,
added NOT VALID) — refuses any INSERT or UPDATE that would create a
broad-match input-triggered row. NOT VALID intentionally — three
existing legacy rows (#35/#36/#44) are grandfathered per the agreed
policy. Operator can run VALIDATE CONSTRAINT later once they're
narrowed.
3. Runtime filter (app/routes/lessons.py::match_lessons) — the SQL
condition '(l.trigger_tool IS NOT NULL OR l.trigger_pattern IS NOT NULL)'
for trigger_on='input' skips legacy broad-match rows at match time,
so the 3 existing CRITICAL safeguards no longer fire on every Bash
call (which was the actual user-visible spam problem). When they're
relevant they can fire again by being narrowed.
Tests pin:
- POST /api/lessons rejects broad-match input lessons with 400
- /api/lessons/match skips broad-match rows even if inserted via direct
SQL (self-skips when the CHECK constraint blocks the test setup, which
itself confirms the constraint is enforcing)
Updated test_create_global_lesson to include a trigger_pattern (was
implicitly broad-match before; the test only verified the create path,
not match semantics).
Working-branch: fix/trigger-lessons
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Forbids broad-match input-triggered lessons (no
trigger_toolAND notrigger_pattern) at three layers — API validator, DBCHECKconstraint, runtime SQL filter. Eliminates the per-Bash-call CRITICAL-lesson spam that was dominating every tool call.What's in this PR
Single commit on dev:
ff580b3 fix(lessons): forbid broad-match input-triggered lessons (belt+suspenders)Three layers of defense:
app/routes/lessons.py::_validate_trigger_on) —POST /api/lessonsreturns 400 with a clear error message when a new lesson would broad-match.chk_input_trigger_has_filterwithNOT VALID. New INSERTs and UPDATEs that would create a broad-match row are rejected by Postgres. Three existing legacy rows (feat(migrations): 012 — schema for prompt↔tool_call linkage (closes #27) #35 read-before-edit, M-FT-2-9: Project consolidation — git root + remote + branch tracking #36 docker-restart-zombie-cron, feat(fine-tune): v2 retrain — kills empty-args loop bug (#33) #44 infra-dev-first) are grandfathered per the agreed policy.app/routes/lessons.py::match_lessons) — the SQL match condition skips legacy broad-match rows so the 3 CRITICAL safeguards no longer fire on every tool call. When operators narrow them with atrigger_toolortrigger_pattern, they'll fire again.User-visible result: The pre-tool-use hook no longer injects 3 CRITICAL lessons on every Bash/Edit/Write call. Empty-result reminder fires once per session instead.
Migration notes
016-lesson-trigger-broad-match-guard.sqlis idempotent (IF NOT EXISTSguard).NOT VALIDintentionally — no companion.concurrent.sqltoVALIDATE. The 3 grandfathered rows would fail validation.016-lesson-trigger-broad-match-guard.down.sql.Test plan
pytest -q --ignore=tests/fine_tune— 190 passed, 1 skippedtest_create_broad_match_input_lesson_returns_400— passestest_match_endpoint_skips_broad_match_input_lessons— self-skips because the CHECK constraint correctly blocks the test setup (proving the constraint enforces on new inserts)INSERT INTO mem_lessons (trigger_on='input', no tool, no pattern)rejected withchk_input_trigger_has_filterviolationpre-tool-use.jswith cwd inside agentMemory now returns the empty-reminder (or{}), not the 3-CRITICAL spam🤖 Generated with Claude Code