Skip to content

hotfix: stop trigger + shift_report notification spam#38

Merged
AVADSA25 merged 1 commit intomainfrom
fix/trigger-spam-and-shift-report-loop
May 3, 2026
Merged

hotfix: stop trigger + shift_report notification spam#38
AVADSA25 merged 1 commit intomainfrom
fix/trigger-spam-and-shift-report-loop

Conversation

@AVADSA25
Copy link
Copy Markdown
Owner

@AVADSA25 AVADSA25 commented May 3, 2026

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 minutes

The 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 with require_confirmation: True and cooldown_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_TRIGGER block in skills/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

# OLD - skills/shift_report.py:529
if trigger_kind != "manual":
    mark_fired_today(trigger_kind)

Manual fires never wrote last_fired_at. So consecutive manual calls always succeeded. The audit caught a particularly clear smoking gun:

19:40:24.188 manual (started)
19:40:24.192 manual (started)   ← 4ms later
19:40:24.201 idle (started)     ← 13ms later
19:45:51.062 manual (started)
19:45:51.066 manual (started)   ← 4ms later  
19:45:51.076 idle (started)

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() checks last_fired_at + last_trigger_kind == "manual" against now. mark_fired_today now 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 suppressed
  • test_manual_cooldown_does_not_block_after_5min — seed 6-min-old fire, verify new one passes
  • All 22 prior tests/test_shift_report.py cases still pass
  • 1 pre-existing failure (ModuleNotFoundError: pynput) unchanged, unrelated to this PR

Open follow-ups (not in this PR)

  • Find the source of the repeated manual shift_report.run() calls (chat handler? auto-polling tab in PWA? scheduler entry?). The cooldown is the safety net; root cause hunt is separate.
  • Build ~/.codec/triggers.json runtime mute config so users can toggle individual triggers without editing skill files.

🤖 Generated with Claude Code

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>
@AVADSA25 AVADSA25 merged commit 03d7660 into main May 3, 2026
1 check passed
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>
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