Skip to content

Fix/66 safety monitor multi turn metrics - #1022

Open
aliabbaka wants to merge 12 commits into
ascherj:mainfrom
aliabbaka:fix/66-safety-monitor-multi-turn-metrics
Open

Fix/66 safety monitor multi turn metrics#1022
aliabbaka wants to merge 12 commits into
ascherj:mainfrom
aliabbaka:fix/66-safety-monitor-multi-turn-metrics

Conversation

@aliabbaka

Copy link
Copy Markdown

Summary

SafetyMonitor.get_event_count accepted a window_hours argument but ignored it,
because events were stored as a single cumulative INCR counter with no per-event
timestamps. A 1-hour window and a 1000-hour window returned the same number, so a
multi-turn conversation that trips the content filter across several turns was never
reflected in a time-windowed metric. This switches storage to a Redis sorted set of
timestamped events and computes windowed counts with zremrangebyscore + zcard,
reusing the exact pattern already in RateLimiter.

Issue

Closes #66

Changes

  • log_event: store each event as a timestamped member in a sorted set
    (zadd {str(now): now}) with a 24h expiry, instead of incr.
  • get_event_count: compute window_start = now - window_hours*3600, trim old
    entries with zremrangebyscore, return zcard.
  • Removed the now-unused datetime import.
  • Added tests for the timestamped write, the windowed count, and the error path;
    the existing reproduction test now passes.

Testing

  • Unit tests pass (.venv/bin/pytest tests/unit/test_monitoring.py -v — 5/5)
  • Integration tests pass (make test-integration) — N/A, no integration surface
  • Linter passes (make lint on changed files)
  • Type checker passes (mypy safety/)
  • New/updated tests cover the changes

Notes for Reviewers

  • Deliberately mirrors safety/rate_limiter.py's sorted-set approach for consistency.
  • Data-model change: counts under the old INCR key don't carry over. Acceptable —
    safety metrics are ephemeral (24h expiry, dev-only).
  • SafetyMonitor currently has no callers; wiring get_event_count into the content-
    filter/prompt-defense path (PLAN.md §3.3) is intentionally left as a follow-up to
    keep this PR focused on the bug.

aliabbaka and others added 11 commits July 21, 2026 10:01
…t_count

Add a failing unit test showing SafetyMonitor.get_event_count returns the
all-time event total regardless of window_hours, so a multi-turn bypass that
trips the content filter across turns is never reflected in a windowed metric.

Refs ascherj#66

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document root cause (untimestamped counter + ignored window_hours),
the reproduction, and the sorted-set fix mirroring rate_limiter.

Refs ascherj#66

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
log_event stored a single cumulative counter with no per-event timestamps,
so get_event_count ignored window_hours and always returned the all-time
total. Store each event as a timestamped member in a sorted set and count
with zremrangebyscore + zcard, mirroring RateLimiter.

Fixes ascherj#66
@aliabbaka
aliabbaka marked this pull request as ready for review August 10, 2026 03:29
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.

Safety monitoring doesn't emit metrics when the content filter is bypassed by a multi-turn conversation

1 participant