# Fast Handoff And Stale Lock Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make the tracker abandon stale lock anchors quickly during sharp turns, switch to valid new YOLO/ByteTrack candidates in `1-2` frames when safe, and reset guidance smoothing so the blue point and yellow line immediately follow the new target region. **Architecture:** Extract the stale-lock and fast-handoff decision logic into a small pure-Python helper module so it can be unit-tested without running the full video pipeline. Then integrate that logic into `main.py`, add event-driven guidance reset hooks in `guidance.py`, and extend `decision_logger.py` so before/after behavior can be measured from logs. The plan assumes the current workspace is not a git repository; commit commands are included for use once the code is executed inside a repo-backed workspace. **Tech Stack:** Python 3.12, pytest, NumPy, OpenCV-based tracking pipeline, existing local CSV/JSON diagnostics. --- ## File Structure - Create: `target_handoff.py` - Responsibility: pure decision helpers for stale-lock detection, fast-handoff threshold selection, and lightweight switch event representation. - Create: `tests/test_target_handoff.py` - Responsibility: unit tests for stale-lock break and fast-handoff policy. - Modify: `guidance.py` - Responsibility: add explicit reset hook for smoothing state on stale-break and target switch. - Create: `tests/test_guidance_reset.py` - Responsibility: unit tests that prove aim/command smoothing is reset correctly. - Modify: `main.py` - Responsibility: compute stale-lock inputs, invoke handoff helpers, suppress stale prediction hold, propagate reset events to guidance, and emit diagnostics. - Modify: `decision_logger.py` - Responsibility: add CSV fields and summary accounting for stale-lock/handoff/guidance-reset events. - Create: `tests/test_decision_logger_events.py` - Responsibility: verify new CSV fields and event serialization. - Modify: `config.py` - Responsibility: add narrowly scoped tuning knobs for stale-lock break and fast handoff. ## Task 1: Add Pure Handoff Decision Module **Files:** - Create: `target_handoff.py` - Test: `tests/test_target_handoff.py` - [ ] **Step 1: Write the failing tests** ```python from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from target_handoff import ( HandoffDecision, compute_fast_handoff_hits, evaluate_stale_lock, ) def test_evaluate_stale_lock_requires_fresh_candidate_and_weak_anchor(): decision = evaluate_stale_lock( confirmed=True, have_fresh_candidate=True, target_has_fresh_update=False, candidate_dist_px=420.0, pred_diag_px=70.0, miss_streak=8, klt_valid=False, klt_quality=0.18, klt_iou=0.12, candidate_motion_ok=True, target_motion_ok=False, stale_break_dist_diag=3.5, stale_break_min_miss=2, stale_break_klt_quality=0.35, stale_break_klt_iou=0.25, ) assert decision.stale_lock_active is True assert "target_not_fresh" in decision.reasons assert "weak_klt_quality" in decision.reasons def test_evaluate_stale_lock_rejects_close_candidate_when_anchor_is_healthy(): decision = evaluate_stale_lock( confirmed=True, have_fresh_candidate=True, target_has_fresh_update=True, candidate_dist_px=90.0, pred_diag_px=70.0, miss_streak=0, klt_valid=True, klt_quality=0.92, klt_iou=0.71, candidate_motion_ok=True, target_motion_ok=True, stale_break_dist_diag=3.5, stale_break_min_miss=2, stale_break_klt_quality=0.35, stale_break_klt_iou=0.25, ) assert decision.stale_lock_active is False assert decision.reasons == [] def test_compute_fast_handoff_hits_reduces_confirmation_under_stale_lock(): hits = compute_fast_handoff_hits( stale_lock_active=True, motion_switch_mode=False, approach_extra_hits=2, fast_maneuver_extra_hits=2, default_switch_hits=4, fast_handoff_hits=2, motion_switch_hits=6, ) assert hits == 2 def test_handoff_decision_repr_is_stable_for_logging(): decision = HandoffDecision( stale_lock_active=True, allow_fast_handoff=True, required_switch_hits=2, disable_prediction_hold=True, reasons=["target_not_fresh", "weak_klt_quality"], ) assert decision.reason_text() == "target_not_fresh|weak_klt_quality" ``` - [ ] **Step 2: Run test to verify it fails** Run: `pytest tests/test_target_handoff.py -v` Expected: FAIL with `ModuleNotFoundError: No module named 'target_handoff'` - [ ] **Step 3: Write minimal implementation** ```python from dataclasses import dataclass @dataclass(frozen=True) class HandoffDecision: stale_lock_active: bool allow_fast_handoff: bool required_switch_hits: int disable_prediction_hold: bool reasons: list[str] def reason_text(self) -> str: return "|".join(self.reasons) def evaluate_stale_lock( *, confirmed, have_fresh_candidate, target_has_fresh_update, candidate_dist_px, pred_diag_px, miss_streak, klt_valid, klt_quality, klt_iou, candidate_motion_ok, target_motion_ok, stale_break_dist_diag, stale_break_min_miss, stale_break_klt_quality, stale_break_klt_iou, ): reasons = [] if not confirmed or not have_fresh_candidate: return HandoffDecision(False, False, 0, False, []) far_enough = candidate_dist_px >= max(40.0, stale_break_dist_diag * max(pred_diag_px, 1.0)) weak_anchor = ( (not klt_valid) or (klt_quality < stale_break_klt_quality) or (klt_iou < stale_break_klt_iou) or (miss_streak >= stale_break_min_miss) or (candidate_motion_ok and not target_motion_ok) or (not target_has_fresh_update) ) if not target_has_fresh_update: reasons.append("target_not_fresh") if not klt_valid: reasons.append("klt_invalid") if klt_quality < stale_break_klt_quality: reasons.append("weak_klt_quality") if klt_iou < stale_break_klt_iou: reasons.append("weak_klt_iou") if miss_streak >= stale_break_min_miss: reasons.append("miss_streak") if candidate_motion_ok and not target_motion_ok: reasons.append("motion_conflict") stale = bool(far_enough and weak_anchor) return HandoffDecision( stale_lock_active=stale, allow_fast_handoff=stale, required_switch_hits=0, disable_prediction_hold=stale, reasons=reasons if stale else [], ) def compute_fast_handoff_hits( *, stale_lock_active, motion_switch_mode, approach_extra_hits, fast_maneuver_extra_hits, default_switch_hits, fast_handoff_hits, motion_switch_hits, ): if stale_lock_active: return int(fast_handoff_hits) hits = int(default_switch_hits) + int(approach_extra_hits) + int(fast_maneuver_extra_hits) if motion_switch_mode: hits = max(hits, int(motion_switch_hits)) return int(hits) ``` - [ ] **Step 4: Run test to verify it passes** Run: `pytest tests/test_target_handoff.py -v` Expected: `4 passed` - [ ] **Step 5: Commit** ```bash git add target_handoff.py tests/test_target_handoff.py git commit -m "feat: add stale-lock and fast-handoff decision helpers" ``` ## Task 2: Add Guidance Reset Hook **Files:** - Modify: `guidance.py` - Create: `tests/test_guidance_reset.py` - [ ] **Step 1: Write the failing tests** ```python from pathlib import Path import sys import numpy as np PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from guidance import ScreenGuidanceController def test_guidance_reset_reinitializes_smoothed_point(): ctrl = ScreenGuidanceController() ctrl.smoothed_point = np.array([100.0, 200.0], dtype=np.float32) ctrl.smoothed_cmd = np.array([0.7, -0.4], dtype=np.float32) ctrl.reset_for_target_switch(np.array([500.0, 300.0], dtype=np.float32)) assert np.allclose(ctrl.smoothed_point, np.array([500.0, 300.0], dtype=np.float32)) assert np.allclose(ctrl.smoothed_cmd, np.zeros(2, dtype=np.float32)) def test_guidance_reset_can_damp_instead_of_zero_when_requested(): ctrl = ScreenGuidanceController() ctrl.smoothed_cmd = np.array([0.8, -0.6], dtype=np.float32) ctrl.reset_for_target_switch(np.array([10.0, 20.0], dtype=np.float32), cmd_damp=0.25) assert np.allclose(ctrl.smoothed_cmd, np.array([0.2, -0.15], dtype=np.float32)) ``` - [ ] **Step 2: Run test to verify it fails** Run: `pytest tests/test_guidance_reset.py -v` Expected: FAIL with `AttributeError: 'ScreenGuidanceController' object has no attribute 'reset_for_target_switch'` - [ ] **Step 3: Write minimal implementation** ```python def reset_for_target_switch(self, aim_point, cmd_damp=0.0): aim = np.array(aim_point, dtype=np.float32) self.smoothed_point = aim.copy() damp = float(clamp(cmd_damp, 0.0, 1.0)) self.smoothed_cmd = (self.smoothed_cmd * damp).astype(np.float32) ``` Add it inside `ScreenGuidanceController` near `status_line()` / `idle_state()` in `guidance.py`. - [ ] **Step 4: Run test to verify it passes** Run: `pytest tests/test_guidance_reset.py -v` Expected: `2 passed` - [ ] **Step 5: Commit** ```bash git add guidance.py tests/test_guidance_reset.py git commit -m "feat: add guidance reset hook for target handoff" ``` ## Task 3: Integrate Stale Lock And Fast Handoff Into Main Loop **Files:** - Modify: `config.py` - Modify: `main.py` - Modify: `decision_logger.py` - Create: `tests/test_decision_logger_events.py` - [ ] **Step 1: Write the failing logger test** ```python from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from decision_logger import FIELDNAMES def test_decision_logger_contains_handoff_fields(): assert "stale_lock_active" in FIELDNAMES assert "stale_lock_reason" in FIELDNAMES assert "fast_handoff_active" in FIELDNAMES assert "guidance_reset_event" in FIELDNAMES ``` - [ ] **Step 2: Run test to verify it fails** Run: `pytest tests/test_decision_logger_events.py -v` Expected: FAIL because the new field names are missing from `FIELDNAMES` - [ ] **Step 3: Add config defaults** Add these exact settings near the existing single-target lock policy section in `config.py`: ```python STALE_LOCK_BREAK_ENABLE = True STALE_LOCK_BREAK_DIST_DIAG = 3.5 STALE_LOCK_BREAK_MIN_MISS = 2 STALE_LOCK_BREAK_KLT_QUALITY = 0.35 STALE_LOCK_BREAK_KLT_IOU = 0.25 FAST_HANDOFF_ENABLE = True FAST_HANDOFF_CONFIRM_HITS = 2 FAST_HANDOFF_GUIDANCE_CMD_DAMP = 0.0 ``` - [ ] **Step 4: Wire stale-lock and fast-handoff in `main.py`** Add imports near the top of `main.py`: ```python from target_handoff import compute_fast_handoff_hits, evaluate_stale_lock ``` Add per-frame state defaults near the other per-frame flags: ```python stale_lock_active = False stale_lock_reason = "" fast_handoff_active = False guidance_reset_event = "" ``` After `best` candidate selection is known and before switch confirmation is finalized, add stale-lock evaluation using current values: ```python handoff_decision = evaluate_stale_lock( confirmed=confirmed, have_fresh_candidate=(best is not None and best.time_since_update == 0), target_has_fresh_update=(target_track is not None and target_track.time_since_update == 0), candidate_dist_px=float(dist) if pred_ref is not None else 0.0, pred_diag_px=float(pred_diag), miss_streak=int(miss_streak), klt_valid=bool(klt_valid), klt_quality=float(klt.quality), klt_iou=float(iou(klt.box, locked_box_eff)) if (klt.box is not None and locked_box_eff is not None) else 0.0, candidate_motion_ok=bool(residual_ok), target_motion_ok=bool(target_track is not None and target_track.time_since_update == 0), stale_break_dist_diag=float(STALE_LOCK_BREAK_DIST_DIAG), stale_break_min_miss=int(STALE_LOCK_BREAK_MIN_MISS), stale_break_klt_quality=float(STALE_LOCK_BREAK_KLT_QUALITY), stale_break_klt_iou=float(STALE_LOCK_BREAK_KLT_IOU), ) stale_lock_active = bool(STALE_LOCK_BREAK_ENABLE and handoff_decision.stale_lock_active) stale_lock_reason = handoff_decision.reason_text() ``` When computing switch confirmation hits, replace the current manual calculation with: ```python req_switch_hits = compute_fast_handoff_hits( stale_lock_active=bool(FAST_HANDOFF_ENABLE and stale_lock_active), motion_switch_mode=bool(motion_switch_mode), approach_extra_hits=int(approach_switch_extra_hits), fast_maneuver_extra_hits=int(max(0, SWITCH_FAST_MANEUVER_EXTRA_HITS)) if fast_maneuver_guard else 0, default_switch_hits=int(TARGET_SWITCH_CONFIRM_HITS), fast_handoff_hits=int(FAST_HANDOFF_CONFIRM_HITS), motion_switch_hits=int(MOTION_CONF_SWITCH_CONFIRM_HITS), ) fast_handoff_active = bool(FAST_HANDOFF_ENABLE and stale_lock_active and req_switch_hits == int(FAST_HANDOFF_CONFIRM_HITS)) ``` Disable prediction hold while stale-lock is active by modifying the existing hold branch: ```python if confirmed and pred_box_eff is not None and (not stale_lock_active) and miss_streak < KALMAN_HOLD_MAX: ``` When a fast handoff or target switch is accepted, reset guidance: ```python old_target_id = target_id ``` before replacing `target_id`, then after `locked_box_eff = chosen`: ```python if confirmed and target_track is not None and old_target_id is not None and target_track.track_id != old_target_id: guidance_ctrl.reset_for_target_switch( box_center(locked_box_eff), cmd_damp=float(FAST_HANDOFF_GUIDANCE_CMD_DAMP), ) guidance_reset_event = "target_switch" elif stale_lock_active and locked_box_eff is not None: guidance_ctrl.reset_for_target_switch( box_center(locked_box_eff), cmd_damp=float(FAST_HANDOFF_GUIDANCE_CMD_DAMP), ) guidance_reset_event = "stale_lock_break" ``` - [ ] **Step 5: Add logger fields** Extend `decision_logger.py` field names and row payload with: ```python "stale_lock_active", "stale_lock_reason", "fast_handoff_active", "guidance_reset_event", ``` and map them in `log_frame()`: ```python "stale_lock_active": int(bool(kwargs.get("stale_lock_active", False))), "stale_lock_reason": kwargs.get("stale_lock_reason", ""), "fast_handoff_active": int(bool(kwargs.get("fast_handoff_active", False))), "guidance_reset_event": kwargs.get("guidance_reset_event", ""), ``` Also append event names if present: ```python if kwargs.get("stale_lock_active", False): events.append("stale_lock_break") if kwargs.get("fast_handoff_active", False): events.append("fast_handoff_switch") if kwargs.get("guidance_reset_event", ""): events.append("guidance_reset") ``` - [ ] **Step 6: Run focused tests** Run: ```bash pytest tests/test_target_handoff.py tests/test_guidance_reset.py tests/test_decision_logger_events.py tests/test_track_score_gates.py -v ``` Expected: all tests PASS - [ ] **Step 7: Run a smoke video pass** Run: ```bash py -3 main.py ``` Expected: - no import/runtime errors - new CSV fields appear in `track_log_*.csv` - problem segment near `231.5s - 232.3s` shows earlier stale-lock break and faster visual realignment - [ ] **Step 8: Commit** ```bash git add config.py main.py guidance.py decision_logger.py tests/test_decision_logger_events.py git commit -m "feat: add stale-lock break and fast handoff integration" ``` ## Self-Review ### Spec coverage - Stale-lock detection: Task 1 + Task 3 - Fast handoff switch policy: Task 1 + Task 3 - Prediction hold restrictions: Task 3 - Guidance reset on switch: Task 2 + Task 3 - Logging and diagnostics: Task 3 - Verification on known failure segment: Task 3 smoke pass No spec gaps remain for the first-pass implementation. ### Placeholder scan - No `TBD`, `TODO`, or deferred placeholders included. - Every code-changing step contains explicit code snippets. - Every verification step contains an exact command and expected result. ### Type consistency - `HandoffDecision` property names are consistent across tests and integration steps. - `reset_for_target_switch()` is referenced consistently in tests and integration. - New logger field names match the values referenced in `main.py`. ## Execution Handoff Plan complete and saved to `docs/superpowers/plans/2026-06-24-fast-handoff-stale-lock-implementation.md`. Two execution options: **1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration **2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints **Which approach?**