hotfix: stop trigger + shift_report notification spam#38
Merged
Conversation
Two correlated bugs spotted in production at 21:31-21:45 on 2026-05-03:
1. clipboard_url_fetch was the only skill with SKILL_OBSERVATION_TRIGGER
and was firing every ~7 min while the user was actively copying URLs
(each new URL beat the 600s per-trigger cooldown). Each fire produced
a `codec-triggers is asking a question` notification with
`require_confirmation: True`. Spam was burying real notifications
from the agent runner.
Fix: comment out the SKILL_OBSERVATION_TRIGGER block. The skill
itself stays callable via voice / chat / MCP — only the auto-fire
path is muted. The Phase 2 Step 6 trigger system stays operational
for future skills. Added a comment explaining how to re-enable.
2. shift_report.run_with_trigger_kind only called mark_fired_today()
for non-manual fires (skills/shift_report.py:529). Result: anything
calling shift_report.run() manually bypassed dedup AND left no
timestamp, so consecutive manual calls always succeeded. Audit log
showed 8 fires in 12 min (3 within 13ms of each other at 21:40 +
2 within 4ms at 21:45) — clear button-mash or polling-loop pattern.
Fix: introduce a 5-min cooldown for manual fires, gated on
last_fired_at + last_trigger_kind == "manual". Per-day dedup for
idle/time fires unchanged. Always call mark_fired_today (manual
path now writes timestamp so cooldown can read it).
Tests:
- test_manual_5min_cooldown_suppresses_repeats — verifies second
immediate manual fire is suppressed
- test_manual_cooldown_does_not_block_after_5min — seeds a 6-min-old
fire, verifies new one goes through
- All 22 prior shift_report tests still pass (1 pre-existing
ModuleNotFoundError on pynput unchanged, unrelated to this PR)
Open follow-ups (separate tasks):
- Find what's calling shift_report.run() repeatedly (likely a chat
handler or auto-polling tab; the cooldown is the safety net)
- Add ~/.codec/triggers.json runtime mute config so users can
toggle individual triggers without editing skill files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
AVADSA25
added a commit
that referenced
this pull request
May 3, 2026
Adds `~/.codec/triggers.json` so users can soft-disable a trigger by skill name (permanently or via `muted_until` ISO-8601 timestamp) without editing the skill file. Default contents preserve PR #38's behavior (`clipboard_url_fetch` muted) when the file is absent. PR #38 muted the noisy clipboard URL trigger by commenting out its `SKILL_OBSERVATION_TRIGGER` block. That works but loses the declaration and can't be re-enabled per user. The existing `triggers_killed.json` kill switch is per-(skill, pattern_hash) and silent; the global `TRIGGERS_ENABLED` env var is a sledgehammer. Mute fills the gap with a per-skill, audit-visible (`trigger_muted`, warning) suppression layer. Wired in `evaluate()` between `_emit_evaluated` and the cooldown check, so muted matches don't burn cooldown budget and stay visible in audit. Restored `clipboard_url_fetch.SKILL_OBSERVATION_TRIGGER` (uncommented); old behavior preserved by config defaults, not commented-out code. Tests: 3 new in tests/test_triggers.py §7.6 (38/38 trigger tests pass). Docs: docs/PHASE2-STEP6-TRIGGER-MUTE.md (config shape, examples, re-enable, kill-vs-mute comparison). AGENTS.md §3 + §6 + §10 updated. Co-authored-by: Mickael Farina <farina.mickael@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Symptom
User dropped a forex briefing project at ~21:00 local. By 21:45 the Reports tab was full of:
codec-triggers is asking a question× every ~7 min (clipboard URL trigger)CODEC Shift Report — 2026-05-03× 8 fires in 12 minutesThe forex agent itself died on a corrupt empty-plan manifest (separate issue, manually aborted) but the noise was burying real signals. Two bugs working together.
Bug 1 — clipboard_url_fetch trigger spam
The only skill in the tree with
SKILL_OBSERVATION_TRIGGER. Configured withrequire_confirmation: Trueandcooldown_seconds: 600. While the user was actively pasting URLs into a Claude chat for a CODEC walkthrough, every NEW URL on the clipboard beat the per-trigger cooldown (cooldown is per-pattern, not per-URL — but each new URL re-fired). Result: a fresh ask_user notification every ~7 min.Fix: comment out the
SKILL_OBSERVATION_TRIGGERblock inskills/clipboard_url_fetch.py. Skill remains callable via voice / chat / MCP; only the auto-fire path is muted. Phase 2 Step 6 trigger system stays alive for future skills. Comment explains how to re-enable.Bug 2 — shift_report manual fires bypass dedup completely
Manual fires never wrote
last_fired_at. So consecutive manual calls always succeeded. The audit caught a particularly clear smoking gun:3 manual fires within 13ms = button-mash or polling loop. With every fire writing the audit + a notification, the user got drowned.
Fix: introduce
_MANUAL_COOLDOWN_SECONDS = 300(5 min). New helper_manual_cooldown_active()checkslast_fired_at + last_trigger_kind == "manual"against now.mark_fired_todaynow runs for ALL fire types (manual writes timestamp so cooldown can read it; idle/time still get per-day dedup as before).Tests
test_manual_5min_cooldown_suppresses_repeats— two immediate manual fires, second is suppressedtest_manual_cooldown_does_not_block_after_5min— seed 6-min-old fire, verify new one passestests/test_shift_report.pycases still passModuleNotFoundError: pynput) unchanged, unrelated to this PROpen follow-ups (not in this PR)
shift_report.run()calls (chat handler? auto-polling tab in PWA? scheduler entry?). The cooldown is the safety net; root cause hunt is separate.~/.codec/triggers.jsonruntime mute config so users can toggle individual triggers without editing skill files.🤖 Generated with Claude Code