Skip to content

fix(tracker): exclude Lost tracks from bridge output (#420, ADR-082)#426

Merged
ruvnet merged 1 commit intomainfrom
feat/adr-082-pose-tracker-lifecycle
Apr 26, 2026
Merged

fix(tracker): exclude Lost tracks from bridge output (#420, ADR-082)#426
ruvnet merged 1 commit intomainfrom
feat/adr-082-pose-tracker-lifecycle

Conversation

@ruvnet
Copy link
Copy Markdown
Owner

@ruvnet ruvnet commented Apr 26, 2026

Fixes #420.

Symptom

3× ESP32-S3 nodes streaming CSI to the Rust sensing server. GET /api/v1/sensing/latest reports estimated_persons: 1. The live UI renders 22-24 phantom skeletons that flicker at high rate.

Root cause

crates/wifi-densepose-sensing-server/src/tracker_bridge.rs::tracker_to_person_detections documented itself as:

Only tracks whose lifecycle is_alive() are included.

…but called tracker.active_tracks(), which itself filters by is_alive(). is_alive() returns true for Tentative ∪ Active ∪ Lost — every non-Terminated state. The bridge therefore shipped all non-terminated tracks to the WebSocket stream.

Lost tracks are deliberately kept in the tracker for reid_window (~3 s by default) so re-identification can match them when a similar detection reappears. They are by definition not currently observed (they've missed loss_misses updates already). Rendering them as live skeletons is the bug.

With 3 nodes × 10 Hz CSI, every Mahalanobis-gate miss creates a new Tentative track and ages the previous one to Lost. Up to reid_window × n_nodes ≈ 90 phantom Lost tracks can co-exist at once. The actually-observed person is one of them; the rest are ghosts.

Fix

  • Add PoseTracker::confirmed_tracks() — returns only Tentative ∪ Active. (Tentative is included so first-detection visibility latency stays at one tick.)
  • Rewire tracker_to_person_detections to call confirmed_tracks().
  • active_tracks() is unchanged — AETHER re-ID consumers (ADR-024) need the full alive set.
  • Re-export TrackerConfig from signal::ruvsense so consumers can construct configured trackers without reaching into pose_tracker::.

Pure egress filter. No state-machine change. No schema change.

ADR

ADR-082 — Pose Tracker Confirmed-Track Output Filter included in this PR. Status: Accepted.

Validation

$ cargo test --workspace --no-default-features
… 1,539 passed, 0 failed, 8 ignored

New regression test test_lost_tracks_excluded_from_bridge_output:

  1. Drives a track to Active over 2 hits.
  2. Submits empty detections for loss_misses + 1 cycles → track transitions to Lost.
  3. Asserts tracker_update(empty) returns Vec::new().
  4. Asserts the Lost track is still in tracker.all_tracks() (re-ID still works).

ESP32-S3 on COM7 streamed unmodified firmware throughout — verified live CSI cb #32800, RSSI −46 dBm.

Test plan

  • cargo test --workspace --no-default-features → 1,539 / 0
  • New regression test passes
  • Existing test_tracker_update_stable_ids still passes (Tentative→Active path unaffected)
  • ESP32-S3 unaffected
  • CI: workspace tests on Linux + Windows runners

🤖 Generated with claude-flow

`tracker_bridge::tracker_to_person_detections` documented itself as filtering
to `is_alive()` but never actually filtered — it forwarded every non-Terminated
track to the WebSocket stream. With 3 ESP32-S3 nodes × ~10 Hz CSI, transient
detections that fell outside the Mahalanobis gate created a steady stream of
new Tentative tracks that aged through Active and into Lost. Lost tracks are
kept in the tracker for `reid_window` (~3 s) so re-identification can match
them when a similar detection reappears, but they are NOT currently observed
and must not render as live skeletons. Up to ~90 ghost skeletons could
accumulate at any moment, hence the 22-24 phantoms users saw while
`estimated_persons` correctly reported 1.

Add `PoseTracker::confirmed_tracks()` that returns only `Tentative ∪ Active`
and rewire the bridge to use it. `Lost` tracks remain in the tracker for
re-ID; they just no longer ship to the UI. `active_tracks()` is left
unchanged for the AETHER re-ID consumers (ADR-024).

Regression test `test_lost_tracks_excluded_from_bridge_output` drives a
track to Active, lapses for `loss_misses + 1` ticks to push it to Lost,
and asserts `tracker_update` returns an empty Vec while the Lost track
is still present in `all_tracks()` (re-ID still works).

Validated:
- cargo test --workspace --no-default-features → 1,539 passed, 0 failed
- ESP32-S3 on COM7 still streaming live CSI (cb #32800)

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet ruvnet merged commit 7f201bd into main Apr 26, 2026
@ruvnet ruvnet deleted the feat/adr-082-pose-tracker-lifecycle branch April 26, 2026 00:03
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.

is someone able to spin this up with rust?

1 participant