# Plan: Phase 2 — Sidecar Overlap Self-Heal

**Requirements:** HEAL-01, HEAL-02, HEAL-03, HEAL-04, HEAL-05, HEAL-06

## Tasks

### Task 1: Add \_check_and_heal_overlaps() function

**File:** `python/opendeviationbar/sidecar.py`

Add a new function near the top of the module (after the existing helper functions, before `run_sidecar()`):

```python
def _check_and_heal_overlaps(
    streaming_pairs: list[tuple[str, int]],
) -> int:
    """Detect and repair today's overlaps for streaming pairs.

    Closes the today-guard gap: kintsugi defers today's repairs (writer
    ownership), but the sidecar CAN repair today because it IS the today writer.

    Returns number of repairs performed.
    """
    import datetime

    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache
    from opendeviationbar.clickhouse.stathera_query import (
        FULL_TABLE,
        STATHERA_CORE_COLUMNS,
        VALID_TID_FILTER,
        WINDOW_ORDER,
    )

    today = datetime.datetime.now(datetime.timezone.utc).date()
    today_start_ms = int(datetime.datetime.combine(
        today, datetime.time.min, tzinfo=datetime.timezone.utc
    ).timestamp() * 1000)
    today_end_ms = today_start_ms + 86_400_000

    repairs = 0
    with OpenDeviationBarCache() as cache:
        for symbol, threshold in streaming_pairs:
            try:
                row = cache.client.query(
                    f"""
                    SELECT countIf(tid_delta < 0) AS overlap_count
                    FROM (
                        SELECT {STATHERA_CORE_COLUMNS}
                        FROM {FULL_TABLE} FINAL
                        WHERE symbol = {{symbol:String}}
                          AND threshold_decimal_bps = {{threshold:UInt32}}
                          AND {VALID_TID_FILTER}
                          AND close_time_ms >= {{start:UInt64}}
                          AND close_time_ms < {{end:UInt64}}
                        {WINDOW_ORDER}
                    )
                    WHERE tid_delta < 0 AND rn > 1
                    """,
                    parameters={
                        "symbol": symbol,
                        "threshold": threshold,
                        "start": today_start_ms,
                        "end": today_end_ms,
                    },
                )
                overlap_count = row.result_rows[0][0] if row.result_rows else 0
                if overlap_count > 0:
                    logger.warning(
                        "overlap_heal: detected overlaps, triggering repair",
                        symbol=symbol,
                        threshold=threshold,
                        overlap_count=overlap_count,
                    )
                    from opendeviationbar.kintsugi import _repair_under_lock, Shard

                    shard = Shard(
                        symbol=symbol,
                        threshold_decimal_bps=threshold,
                        anchor_last_tid=0,  # full-day recompute
                        resume_first_tid=0,
                        gap_start_ms=today_start_ms,
                        gap_end_ms=today_end_ms,
                    )
                    t0 = time.monotonic()
                    from opendeviationbar.clickhouse.write_lock import cache_write_lock

                    with cache_write_lock(symbol, threshold, timeout=300):
                        written = _repair_under_lock(
                            shard=shard,
                            day_start_ms=today_start_ms,
                            day_end_ms=today_end_ms,
                            repair_date=today,
                            ouroboros_mode="day",
                            include_microstructure=False,
                            verbose=False,
                        )
                    elapsed_ms = int((time.monotonic() - t0) * 1000)
                    logger.info(
                        "overlap_heal: repair complete",
                        symbol=symbol,
                        threshold=threshold,
                        overlap_count=overlap_count,
                        bars_written=written,
                        repaired_in_ms=elapsed_ms,
                    )
                    repairs += 1
            except Exception:
                logger.exception(
                    "overlap_heal: check/repair failed (non-fatal)",
                    symbol=symbol,
                    threshold=threshold,
                )
    return repairs
```

**Note:** The exact import of `Shard` needs verification — check if it's a named tuple or dataclass in kintsugi.py. The function signature must match `_repair_under_lock`'s expectations.

### Task 2: Wire into sidecar periodic cycle

**File:** `python/opendeviationbar/sidecar.py`

After the Charon block (line 2447), before `continue` (line 2449), add:

```python
                # Overlap self-heal: detect and repair today's overlaps
                now_heal = time.monotonic()
                if now_heal - heal_last_check >= _OVERLAP_HEAL_INTERVAL_S:
                    heal_last_check = now_heal
                    try:
                        healed = _check_and_heal_overlaps(streaming_pairs)
                        if healed > 0:
                            logger.info(
                                "overlap_heal: periodic cycle repaired %d pairs",
                                healed,
                            )
                    except Exception:
                        logger.exception(
                            "overlap_heal: periodic cycle failed (non-fatal)"
                        )
```

### Task 3: Add timer initialization

**File:** `python/opendeviationbar/sidecar.py`

At module level (near other constants):

```python
_OVERLAP_HEAL_INTERVAL_S = int(
    os.environ.get("OPENDEVIATIONBAR_OVERLAP_HEAL_INTERVAL_S", "300")
)
```

In `run_sidecar()`, before the main loop, add:

```python
heal_last_check = time.monotonic()  # overlap self-heal timer
```

### Task 4: Build streaming_pairs list

The `streaming_pairs` argument needs to be constructed from the config. Check how `SidecarConfig` exposes symbols and thresholds — likely `config.symbols` × `config.thresholds` or similar. Wire this into the `_check_and_heal_overlaps()` call.

## Verification

1. Function exists and follows Charon's non-fatal exception pattern
2. Timer-guarded at 300s intervals
3. Holds Redis write lock during repair
4. Logs at INFO level with symbol, threshold, overlap_count, repaired_in_ms
5. Does not crash the sidecar on any exception

## Commit

```
feat: sidecar overlap self-heal — detect and repair today's overlaps

Closes the today-guard gap: kintsugi defers today's repairs (writer
ownership), but the sidecar CAN repair today because it IS the today
writer. Queries Stathera for overlaps every 300s, triggers targeted
day-recompute via _repair_under_lock() with Redis write lock.

Non-fatal: exceptions are logged and suppressed, same pattern as Charon.
```
