---
phase: 20-aion-aware-sidecar-startup
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - python/opendeviationbar/sidecar/__init__.py
  - python/opendeviationbar/sidecar/config_helpers.py
autonomous: true
requirements: [START-01, START-02, START-03]

must_haves:
  truths:
    - "Aion fill_from_rest seeds at max(ch_last_tid, crossover_tid - 1), not just ch_last_tid"
    - "Sidecar startup DELETEs today's Aion bars from CH before fill_from_rest (preserving pre-midnight bars)"
    - "After sidecar restart spanning midnight, Stathera audit shows zero gaps for Aion (symbol, threshold) pairs"
  artifacts:
    - path: "python/opendeviationbar/sidecar/__init__.py"
      provides: "Aion startup path with max-seed + targeted today DELETE"
      contains: "max(.*crossover_tid"
    - path: "python/opendeviationbar/sidecar/config_helpers.py"
      provides: "Aion-aware seed helper that incorporates crossover TID"
      contains: "crossover_tid"
  key_links:
    - from: "python/opendeviationbar/sidecar/__init__.py"
      to: "python/opendeviationbar/crossover.py"
      via: "get_crossover_tid() call for Aion max-seed"
      pattern: "get_crossover_tid"
    - from: "python/opendeviationbar/sidecar/__init__.py"
      to: "opendeviationbar_cache.open_deviation_bars"
      via: "DELETE IN PARTITION for today's Aion bars"
      pattern: "DELETE IN PARTITION"
---

<objective>
Fix Aion sidecar startup to (1) seed fill_from_rest at max(last_committed_bar_tid, crossover_tid - 1) and (2) DELETE only today's Aion bars before fill_from_rest, preserving pre-midnight bars.

Purpose: Eliminate 8 midnight-boundary Stathera gaps found by A3 audit (BTC/ETH/SOL at 100dbps on Mar 5, 22, 26) caused by fill_from_rest always anchoring at CH seed without considering crossover TID.
Output: Modified sidecar startup that produces zero Stathera gaps on midnight-spanning restarts.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-sidecar-aion-startup/16-02-SUMMARY.md

@python/opendeviationbar/sidecar/**init**.py
@python/opendeviationbar/sidecar/config_helpers.py
@python/opendeviationbar/crossover.py

<interfaces>
<!-- Key types and contracts the executor needs. -->

From python/opendeviationbar/crossover.py:

```python
def get_crossover_tid(symbol: str, date: datetime.date | None = None) -> int | None:
    """Get the crossover TID for a symbol on a given date (None = today)."""
```

From python/opendeviationbar/sidecar/config_helpers.py:

```python
def _seed_trade_ids_from_clickhouse(engine: object, symbols: list[str], thresholds: list[int]) -> None:
    """Seed StreamManager trade-ID tracker from ClickHouse last_agg_trade_id."""
```

From python/opendeviationbar/clickhouse/query_operations.py:

```python
def get_latest_bar_with_trade_id(self, symbol, threshold_decimal_bps, ouroboros_mode=None) -> tuple[int | None, int | None]:
    """Returns (latest_close_time_us, latest_last_agg_trade_id)."""
```

From StreamManager (Rust):

```python
engine.seed_trade_id(symbol: str, threshold: int, last_agg_trade_id: int) -> None
# Seeds are max()-accumulated: calling seed(sym, thr, 100) then seed(sym, thr, 200) results in seed=200
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add crossover-TID-aware seeding for Aion symbols</name>
  <files>python/opendeviationbar/sidecar/__init__.py, python/opendeviationbar/sidecar/config_helpers.py</files>
  <action>
**Root cause:** The Aion startup path in `_create_engine()` (line ~258-274) calls `_seed_trade_ids_from_clickhouse()` which seeds from CH's `last_agg_trade_id`. After a midnight-spanning restart, if no bars were committed today yet, the seed equals the pre-midnight last bar TID. But `fill_from_rest` needs to start from `crossover_tid - 1` to cover the midnight boundary — otherwise the gap between the pre-midnight seed and the first WS trade goes unfilled.

**Fix in `_seed_trade_ids_from_clickhouse` (config_helpers.py):**
After the CH seeding loop (line ~46-58), add a crossover-TID max-seed step for Aion symbols. This keeps the function self-contained:

1. Accept an optional `symbol_modes: dict[str, str] | None = None` parameter.
2. After the existing CH seeding loop, for symbols where `symbol_modes.get(sym) == "aion"`:
   - Call `get_crossover_tid(symbol)` to get today's crossover TID.
   - If `crossover_tid` is not None, call `engine.seed_trade_id(symbol, threshold, crossover_tid - 1)` for each threshold.
   - Since `seed_trade_id` accumulates via `max()` in Rust, this correctly results in `max(ch_last_tid, crossover_tid - 1)`.
3. Log the crossover-TID seeds: `"aion crossover max-seed: %d symbols seeded with crossover_tid"`.

**Fix in `_create_engine` (**init**.py):**

1. Pass `symbol_modes=symbol_modes` to the `_seed_trade_ids_from_clickhouse()` call on line ~269 (the Aion path).
2. The existing Day path call (line ~459) does NOT pass symbol_modes (or passes None) — crossover seeding only applies to Aion.

**Why this works:** `seed_trade_id` uses `max()` accumulation in Rust (see stream_manager.rs line 188: `if last_agg_trade_id > *entry { *entry = last_agg_trade_id; }`). So calling it twice — once with CH last_tid, once with crossover_tid - 1 — results in `max(ch_last_tid, crossover_tid - 1)` which is exactly START-01.
</action>
<verify>
<automated>cd /Users/terryli/eon/opendeviationbar-py && grep -n "crossover_tid" python/opendeviationbar/sidecar/config_helpers.py | head -10 && grep -n "symbol_modes" python/opendeviationbar/sidecar/config_helpers.py | head -5</automated>
</verify>
<done>

- `_seed_trade_ids_from_clickhouse` accepts `symbol_modes` parameter and seeds Aion symbols with `crossover_tid - 1` after CH seeding
- `_create_engine` passes `symbol_modes` to the Aion seeding call
- Day symbols unaffected (no crossover seed applied to them in this function)
  </done>
  </task>

<task type="auto">
  <name>Task 2: Add targeted today-DELETE for Aion symbols before fill_from_rest</name>
  <files>python/opendeviationbar/sidecar/__init__.py</files>
  <action>
**Root cause:** The Aion path in `_create_engine()` skips DELETE entirely (Phase 16-02 design: "no midnight preflight, no DELETE"). This means today's Aion bars from a previous sidecar run remain in CH. When `fill_from_rest` recomputes today's bars, they duplicate the existing ones — different processor instances produce bars with different TID boundaries, so dedup doesn't catch them.

**Fix in `_create_engine` (**init**.py), in the Aion path (after line ~274, before the shared fill_from_rest on line ~417):**

Add a targeted DELETE for today's Aion bars only. This preserves pre-midnight bars (START-02) while ensuring clean-slate for today:

```python
# --- Aion path: DELETE today's bars only (preserve pre-midnight) ---
if aion_symbols:
    try:
        from datetime import datetime
        from opendeviationbar.clickhouse.cache import OpenDeviationBarCache
        from opendeviationbar.symbol_registry import get_thresholds_for_symbol

        today_str = datetime.now(UTC).strftime("%Y-%m-%d")
        aion_pairs = []
        for sym in aion_symbols:
            for thr in get_thresholds_for_symbol(sym):
                aion_pairs.append((sym, thr))

        t_aion_del_start = time.monotonic()
        with OpenDeviationBarCache() as cache:
            for sym, thr in aion_pairs:
                cache.client.command(
                    "ALTER TABLE opendeviationbar_cache.open_deviation_bars "
                    f"DELETE IN PARTITION ('{sym}', {thr}) "
                    "WHERE ouroboros_mode = 'aion' "
                    f"AND toDate(open_time_us / 1000000) = '{today_str}' "
                    "SETTINGS mutations_sync = 1"
                )
        t_aion_del_elapsed = time.monotonic() - t_aion_del_start
        logger.info(
            "aion pre-fill: DELETE today (%s) — %d pairs in %.1fs (pre-midnight bars preserved)",
            today_str,
            len(aion_pairs),
            t_aion_del_elapsed,
        )
    except Exception:
        logger.warning("aion pre-fill: today DELETE failed (non-fatal)", exc_info=True)
```

**Placement:** Insert this block between the existing Aion seeding (line ~258-274) and the Day path (line ~276). It must run BEFORE `fill_from_rest` (line ~417) but AFTER seeding (line ~269) since seeding reads from CH.

**Key details:**

- Uses `DELETE IN PARTITION` (not `DELETE FROM` — banned) with `mutations_sync=1` (mandatory per CLAUDE.md rule 7)
- `toDate(open_time_us / 1000000)` filters to today only — pre-midnight bars on previous dates are untouched (START-02)
- `ouroboros_mode = 'aion'` filter is belt-and-suspenders (partition already scoped to symbol)
- Non-fatal try/except matches Day path pattern (line ~385-386)
  </action>
  <verify>
  <automated>cd /Users/terryli/eon/opendeviationbar-py && grep -n "aion pre-fill" python/opendeviationbar/sidecar/**init**.py | head -5 && grep -c "DELETE IN PARTITION" python/opendeviationbar/sidecar/**init**.py</automated>
  </verify>
  <done>
- Aion startup path DELETEs today's bars before fill_from_rest (START-02: pre-midnight bars preserved)
- Uses DELETE IN PARTITION with mutations_sync=1 (per CH rule 7)
- Non-fatal: falls back gracefully on CH errors
- Combined with Task 1's max-seed, sidecar restart produces zero midnight Stathera gaps (START-03)
  </done>
  </task>

</tasks>

<verification>
After both tasks:
1. `grep -n "crossover_tid" python/opendeviationbar/sidecar/config_helpers.py` shows crossover seed logic
2. `grep -n "aion pre-fill" python/opendeviationbar/sidecar/__init__.py` shows targeted DELETE
3. `grep -c "DELETE IN PARTITION" python/opendeviationbar/sidecar/__init__.py` returns 2 (Day path + Aion path)
4. No `DELETE FROM` anywhere in sidecar code (banned pattern)
</verification>

<success_criteria>

1. `_seed_trade_ids_from_clickhouse` seeds Aion symbols at max(ch_last_tid, crossover_tid - 1)
2. Aion startup DELETEs today's bars only (pre-midnight bars preserved)
3. Day-mode startup path unchanged (backward compatible)
4. After deploy + restart, Stathera audit shows zero midnight gaps for all Aion symbols
   </success_criteria>

<output>
After completion, create `.planning/phases/20-aion-aware-sidecar-startup/20-01-SUMMARY.md`
</output>
