Skip to content

feat: implement REPEATED_APPROACH action classification with cooldown#79

Merged
Devnil434 merged 2 commits into
Devnil434:mainfrom
tirth1356:feature/repeated-approach
May 19, 2026
Merged

feat: implement REPEATED_APPROACH action classification with cooldown#79
Devnil434 merged 2 commits into
Devnil434:mainfrom
tirth1356:feature/repeated-approach

Conversation

@tirth1356
Copy link
Copy Markdown
Contributor

@tirth1356 tirth1356 commented May 16, 2026

#63 fixed

This PR implements the ActionHint.REPEATED_APPROACH classification as requested in the issue. It successfully identifies suspicious behavior when a tracked person repeatedly enters and leaves a restricted area.

🚀 Key Changes

  • Pipeline Counts Tracking: Added state dictionaries (_zone_entry_counts and _last_repeated_approach) in pipeline.py to seamlessly track zone entry counts across frames without injecting massive dependencies.
  • Action Classifier Updates: Modified classify_action() in action_classifier.py to correctly evaluate fresh zone entries compared to the previous frame's bounding box.
  • Cooldown Implementation: Introduced a 10-second (10,000ms) cooldown behavior so the system doesn't spam alerts if a bounding box flickers around the boundary of a zone.
  • Test Environment Fix: Briefly patched missing MemoryStore definitions in services/memory/memory.py allowing tests to execute properly in the current environment structure.
  • Unit Tests: Added 3 new unit tests to test_memory.py:
    • test_repeated_approach_second_entry
    • test_repeated_approach_no_spam
    • test_repeated_approach_cooldown

✅ Acceptance Criteria Met

  • Fires only on second+ entry.
  • Does not spam every frame (differentiates dwelling vs fresh entries).
  • Supports cooldown behavior (10-second limit).
  • Existing tests remain green.
  • Added at least 3 unit tests.

📷 Screenshots / Logs

image

Summary by CodeRabbit

  • New Features

    • Detects when objects repeatedly approach the same zone, triggering repeat alerts after a second entry
    • Implements a per-track cooldown to prevent notification spam from repeated-approach events
    • Maintains per-object event history in memory with automatic capping to limit stored history
  • Tests

    • Added tests validating repeated-approach detection, spam suppression, and cooldown behavior

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@Devnil434 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 59 minutes and 10 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76af90d1-5bf2-42d6-82d1-3a4bf1e5fecc

📥 Commits

Reviewing files that changed from the base of the PR and between a03f8d9 and a69a8b2.

📒 Files selected for processing (2)
  • services/memory/memory.py
  • services/memory/pipeline.py
📝 Walkthrough

Walkthrough

This PR implements repeated-zone-approach detection with a 10s per-track cooldown, moves the memory store to an in-memory implementation (cap 100 events per track), wires pipeline registries for per-track state, and adds tests covering repeated-approach scenarios.

Changes

Repeated-Approach Detection Feature

Layer / File(s) Summary
Zone entry repeat detection logic
services/memory/action_classifier.py
classify_action now accepts zone_entry_counts, last_repeated_approach, and current_time_ms; computes just_entered by comparing to prev_obj, increments per-track/per-zone counts, returns REPEATED_APPROACH when count ≥ 2 and cooldown elapsed, and records first-time ZONE_ENTRY on first entry.
In-memory event storage
services/memory/memory.py
Replaces Redis-backed store with in-memory MemoryStore: MAX_EVENTS_PER_TRACK = 100, per-track _store lists truncated to the cap, _zones sets for visited zones, updated store_event, get_sequence, get_zone_entry_count, get_active_track_ids, and expire_track.
Pipeline state tracking and wiring
services/memory/pipeline.py
Adds _zone_entry_counts and _last_repeated_approach registries; computes current_time_ms per tracked object and passes registries and timestamp into classify_action.
Repeated-approach behavior validation
tests/test_memory.py
Three tests added to validate repeated-approach emission on second entry, suppression while remaining in-zone, and cooldown behavior for repeated-approach events.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related issues

Poem

A rabbit counts each careful hop,
Two entries and then it stops,
Ten seconds hush to catch a breath—
No spamming hints, just prudent steps. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing REPEATED_APPROACH action classification with a cooldown mechanism, which is the core feature across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/test_memory.py (1)

262-264: ⚡ Quick win

Add an exact-boundary cooldown assertion (10,000 ms elapsed).

The current cooldown test skips the equality edge. Add a check at exactly last_trigger + 10000 (e.g., 12000.0 after 2000.0) so cooldown semantics are explicit and regression-proof.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_memory.py` around lines 262 - 264, Add an exact-boundary cooldown
assertion: call classify_action with the same args used for the other checks
(classify_action(obj, None, registry, counts, cooldown, 12000.0)) and assert it
equals ActionHint.REPEATED_APPROACH so the behavior at last_trigger + 10000 ms
is explicitly tested; place this assertion just before the existing 15000.0
check and reference classify_action and ActionHint in the update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/memory/action_classifier.py`:
- Line 62: Update the cooldown condition in the check that uses last_time and
current_time_ms (the if with "last_time == 0.0 or (current_time_ms - last_time)
> 10000.0") to use an inclusive boundary so events at exactly 10,000 ms can
re-trigger; replace the strict greater-than with a greater-than-or-equal-to
comparison on (current_time_ms - last_time) to make the 10s cooldown inclusive.

In `@services/memory/memory.py`:
- Around line 251-252: The store currently keys events only by event.track_id
which causes cross-camera collisions; change the structure to partition by
camera_id first (e.g., self._store.setdefault(camera_id, {}) then per-camera
dict of track_id -> list) and update all places that read/write the store: in
the append path (where seq = self._store.setdefault(event.track_id, []);
seq.append(event)) use event.camera_id when creating/looking up the per-camera
bucket and then the event.track_id inside it, and modify read APIs like
get_active_track_ids(camera_id) and any other lookups (the code around the
blocks referencing self._store at the locations for lines ~259-266 and ~275-276)
to use the per-camera bucket so lookups become self._store.get(camera_id,
{})[track_id] (or iterate that inner dict) instead of global track_id keys;
ensure any function signatures that accept camera_id use it when accessing the
store.
- Around line 278-279: The expire_track method currently only removes entries
from self._store, leaving per-track state in self._zones (and related structures
like zones_visited) which causes stale state and memory leaks; update
expire_track to also remove any per-track entries from self._zones and any other
per-track collections (e.g., zones_visited) by popping/deleting the track_id
keys from those dicts/sets (refer to the expire_track function, self._store,
self._zones, and zones_visited symbols) so all per-track state is cleared when a
track expires.
- Around line 262-263: The code currently uses a truthy check for last_n so
last_n=0 is treated like falsy and returns the whole events list; update the
check to handle zero explicitly: replace the truthy branch with an explicit None
check and a zero-case, e.g. if last_n is None: leave events as-is; elif last_n >
0: events = events[-last_n:]; else: events = [] — apply this change where the
variable events is sliced based on the last_n parameter in
services/memory/memory.py.

---

Nitpick comments:
In `@tests/test_memory.py`:
- Around line 262-264: Add an exact-boundary cooldown assertion: call
classify_action with the same args used for the other checks
(classify_action(obj, None, registry, counts, cooldown, 12000.0)) and assert it
equals ActionHint.REPEATED_APPROACH so the behavior at last_trigger + 10000 ms
is explicitly tested; place this assertion just before the existing 15000.0
check and reference classify_action and ActionHint in the update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6f35941-3d2e-4e06-8183-c70e59e6b394

📥 Commits

Reviewing files that changed from the base of the PR and between 29a47a7 and f8c31d3.

📒 Files selected for processing (5)
  • .gitignore
  • services/memory/action_classifier.py
  • services/memory/memory.py
  • services/memory/pipeline.py
  • tests/test_memory.py

if counts[zone] >= 2:
if last_repeated_approach is not None:
last_time = last_repeated_approach.get(obj.track_id, 0.0)
if last_time == 0.0 or (current_time_ms - last_time) > 10000.0: # 10 second cooldown
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an inclusive cooldown boundary (>=) for exact 10s re-trigger.

Line 62 currently uses > 10000.0, so an event at exactly 10,000 ms is still suppressed. That makes the effective cooldown slightly longer than configured.

Proposed fix
-                        if last_time == 0.0 or (current_time_ms - last_time) > 10000.0:  # 10 second cooldown
+                        if last_time == 0.0 or (current_time_ms - last_time) >= 10000.0:  # 10 second cooldown
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if last_time == 0.0 or (current_time_ms - last_time) > 10000.0: # 10 second cooldown
if last_time == 0.0 or (current_time_ms - last_time) >= 10000.0: # 10 second cooldown
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/memory/action_classifier.py` at line 62, Update the cooldown
condition in the check that uses last_time and current_time_ms (the if with
"last_time == 0.0 or (current_time_ms - last_time) > 10000.0") to use an
inclusive boundary so events at exactly 10,000 ms can re-trigger; replace the
strict greater-than with a greater-than-or-equal-to comparison on
(current_time_ms - last_time) to make the 10s cooldown inclusive.

Comment thread services/memory/memory.py Outdated
Comment thread services/memory/memory.py Outdated
Comment on lines +262 to +263
if last_n:
events = events[-last_n:]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle last_n=0 explicitly in sequence slicing.

Line 262 uses a truthy check, so last_n=0 returns the full sequence instead of an empty one.

Small fix
- if last_n:
+ if last_n is not None:
    events = events[-last_n:]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if last_n:
events = events[-last_n:]
if last_n is not None and last_n > 0:
events = events[-last_n:]
elif last_n == 0:
events = []
Suggested change
if last_n:
events = events[-last_n:]
if last_n is not None:
events = events[-last_n:] if last_n > 0 else []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/memory/memory.py` around lines 262 - 263, The code currently uses a
truthy check for last_n so last_n=0 is treated like falsy and returns the whole
events list; update the check to handle zero explicitly: replace the truthy
branch with an explicit None check and a zero-case, e.g. if last_n is None:
leave events as-is; elif last_n > 0: events = events[-last_n:]; else: events =
[] — apply this change where the variable events is sliced based on the last_n
parameter in services/memory/memory.py.

Comment thread services/memory/memory.py Outdated
@tirth1356 tirth1356 force-pushed the feature/repeated-approach branch from f8c31d3 to a03f8d9 Compare May 19, 2026 10:42
@tirth1356
Copy link
Copy Markdown
Contributor Author

merge conflicts resolved and updates done

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/memory/memory.py`:
- Around line 250-255: store_event is truncating the per-track ring buffer
(MAX_EVENTS_PER_TRACK) which causes get_zone_entry_count and similar methods to
undercount once older zone_entry events are dropped; fix by maintaining a
separate persistent counter(s) for cumulative event types (e.g., zone_entry) per
track and update them in store_event: when appending an event increment the
appropriate counter for event.track_id (and for other methods mentioned around
lines 267-273), and when trimming the seq compute how many removed events of
each counted type were dropped and add those to the persistent counters before
replacing self._store[event.track_id] with the truncated list; keep references
to MAX_EVENTS_PER_TRACK, self._store, store_event and get_zone_entry_count to
locate and apply the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05c31ec9-3e6a-4d70-9130-bb28b7735e21

📥 Commits

Reviewing files that changed from the base of the PR and between f8c31d3 and a03f8d9.

📒 Files selected for processing (5)
  • .gitignore
  • services/memory/action_classifier.py
  • services/memory/memory.py
  • services/memory/pipeline.py
  • tests/test_memory.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/memory/pipeline.py
  • tests/test_memory.py
  • services/memory/action_classifier.py

Comment thread services/memory/memory.py Outdated
Comment on lines +250 to +255
def store_event(self, event):
seq = self._store.setdefault(event.track_id, [])
seq.append(event)
# Handle ring buffer
if len(seq) > MAX_EVENTS_PER_TRACK:
self._store[event.track_id] = seq[-MAX_EVENTS_PER_TRACK:]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

get_zone_entry_count() starts undercounting after the 100-event cap.

The ring buffer drops older events at Line 254, but this method recomputes counts from the truncated buffer. Once a long-lived track rolls over, earlier zone_entry events disappear and the returned count shrinks, which can suppress repeated-entry logic or skew analytics.

Suggested direction
 class MemoryStore:
     def __init__(self, redis_client=None):
         self._r = redis_client
         self._store = {}
         self._zones = {}
+        self._zone_entry_counts = {}

     def store_event(self, event):
         seq = self._store.setdefault(event.track_id, [])
         seq.append(event)
         # Handle ring buffer
         if len(seq) > MAX_EVENTS_PER_TRACK:
             self._store[event.track_id] = seq[-MAX_EVENTS_PER_TRACK:]
         if event.zone:
             self._zones.setdefault(event.track_id, set()).add(event.zone)
+        if event.zone and getattr(event.action_hint, "value", None) == "zone_entry":
+            counts = self._zone_entry_counts.setdefault(event.track_id, {})
+            counts[event.zone] = counts.get(event.zone, 0) + 1

     def get_zone_entry_count(self, track_id, zone):
-        events = self._store.get(track_id, [])
-        count = 0
-        for e in events:
-            if e.zone == zone and e.action_hint.value == "zone_entry":
-                count += 1
-        return count
+        return self._zone_entry_counts.get(track_id, {}).get(zone, 0)

Also applies to: 267-273

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/memory/memory.py` around lines 250 - 255, store_event is truncating
the per-track ring buffer (MAX_EVENTS_PER_TRACK) which causes
get_zone_entry_count and similar methods to undercount once older zone_entry
events are dropped; fix by maintaining a separate persistent counter(s) for
cumulative event types (e.g., zone_entry) per track and update them in
store_event: when appending an event increment the appropriate counter for
event.track_id (and for other methods mentioned around lines 267-273), and when
trimming the seq compute how many removed events of each counted type were
dropped and add those to the persistent counters before replacing
self._store[event.track_id] with the truncated list; keep references to
MAX_EVENTS_PER_TRACK, self._store, store_event and get_zone_entry_count to
locate and apply the change.

@Devnil434 Devnil434 merged commit f765fe6 into Devnil434:main May 19, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants