Skip to content

Signal fatigue tracking: suppress repeatedly ignored signals #11

Description

@tyejcoleman

Problem

When the same signal fires every tick for days (e.g., CheckPendingWork for a task that stays open), the user gets desensitized. The current dedup system prevents spam within a short window:

  • Fingerprint dedup: 2 × SignalCooldownNormal window (~30 min for act mode)
  • Entity overlap dedup: 1h window, 85% overlap threshold
  • Content hash dedup: 1h window

But none of these track fatigue across days. A stale task that fires CheckPendingWork every tick for a week will keep appearing once each cooldown expires.

Proposal

Add a signal_fatigue table:

CREATE TABLE signal_fatigue (
    entity_id TEXT NOT NULL,
    agent_id TEXT NOT NULL DEFAULT 'default',
    signal_type TEXT NOT NULL,
    memory_id TEXT NOT NULL,
    fire_count INTEGER DEFAULT 1,
    act_count INTEGER DEFAULT 0,      -- times user acted on it
    first_fired_at TEXT NOT NULL,
    last_fired_at TEXT NOT NULL,
    suppressed_until TEXT,
    PRIMARY KEY (entity_id, signal_type, memory_id)
);

Behavior changes

  1. Recording: In evaluateShouldAct(), after signals pass confluence, upsert fatigue rows for each signal's memory IDs
  2. Fatigue escalation:
    • fire_count 1-3, no user action: normal behavior
    • fire_count 4-6, no user action: double the cooldown for this signal+memory pair
    • fire_count 7+, no user action: suppress entirely (set suppressed_until to +48h)
  3. Reset on engagement: When checkResponseTracking() detects a user response, reset fire_count and clear suppressed_until for all signal+memory pairs in that tick's fingerprint
  4. Check in decision pipeline: In evaluateShouldAct(), filter out signals whose memory IDs are in the fatigue table with active suppressed_until
  5. Cleanup: Remove rows where last_fired_at is older than 30 days (in decay job)

Where it fits in the pipeline

Insert between step 6 (signal classification) and step 7 (confluence scoring) in evaluateShouldAct(). Fatigued signals get removed before confluence is calculated, so they don't contribute to the score.

Files to modify

  • storage/sqlite_heartbeat.go — new table, upsert/query/reset/cleanup
  • storage/sqlite_migrate.go — migration
  • heartbeat_decide.go — filter fatigued signals before confluence, reset on response
  • watcher.go — record fire events after tick

Constraints

  • Only applies to signals tied to specific memory IDs (CheckPendingWork, CheckDeadlines, CheckStaleMonitors, CheckDecaying, CheckConflicts)
  • Does NOT apply to aggregate signals (CheckSentiment, CheckPatterns, CheckMemoryVelocity) since they don't have stable memory IDs
  • Suppression cap: 48h max, then re-evaluate
  • User action on ANY signal in the same tick resets fatigue for all signals in that tick (assumes engagement = attention)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions