Skip to content

dev → main: broad-match lesson guard (belt+suspenders)#52

Merged
metazen11 merged 1 commit into
mainfrom
dev
May 24, 2026
Merged

dev → main: broad-match lesson guard (belt+suspenders)#52
metazen11 merged 1 commit into
mainfrom
dev

Conversation

@metazen11
Copy link
Copy Markdown
Owner

Summary

Forbids broad-match input-triggered lessons (no trigger_tool AND no trigger_pattern) at three layers — API validator, DB CHECK constraint, 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:

  1. API validator (app/routes/lessons.py::_validate_trigger_on) — POST /api/lessons returns 400 with a clear error message when a new lesson would broad-match.
  2. DB CHECK constraint — migration 016 adds chk_input_trigger_has_filter with NOT 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.
  3. Runtime filter (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 a trigger_tool or trigger_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.sql is idempotent (IF NOT EXISTS guard).
  • Constraint added NOT VALID intentionally — no companion .concurrent.sql to VALIDATE. The 3 grandfathered rows would fail validation.
  • Down migration available: 016-lesson-trigger-broad-match-guard.down.sql.

Test plan

  • pytest -q --ignore=tests/fine_tune — 190 passed, 1 skipped
  • New test test_create_broad_match_input_lesson_returns_400 — passes
  • New test test_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)
  • Manual constraint check: direct SQL INSERT INTO mem_lessons (trigger_on='input', no tool, no pattern) rejected with chk_input_trigger_has_filter violation
  • Live hook verification: pre-tool-use.js with cwd inside agentMemory now returns the empty-reminder (or {}), not the 3-CRITICAL spam

🤖 Generated with Claude Code

…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
@metazen11 metazen11 merged commit bf31233 into main May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants