---
phase: 02-sidecar-fill-pipeline
plan: 02
type: execute
wave: 1
depends_on: ["02-01"]
files_modified:
  - python/opendeviationbar/sidecar/__init__.py
  - python/opendeviationbar/sidecar/http_server.py
  - tests/test_sync_flush_atomic_replace.py
autonomous: true
requirements: [STREAM-02]
gap_closure: true

must_haves:
  truths:
    - "ws_first_tid is recorded per symbol when the first WS bar arrives after startup"
    - "rest_first_tid is exposed in /status endpoint response per symbol"
    - "ws_first_tid is exposed in /status endpoint response per symbol"
    - "Heartbeat can verify REST->WS junction gap is zero by checking ws_first_tid == rest_last_tid + 1"
  artifacts:
    - path: "python/opendeviationbar/sidecar/__init__.py"
      provides: "ws_first_tid tracking in _main_loop"
      contains: "ws_first_tid"
    - path: "python/opendeviationbar/sidecar/http_server.py"
      provides: "rest_first_tid and ws_first_tid in /status response"
      contains: "ws_first_tid"
    - path: "tests/test_sync_flush_atomic_replace.py"
      provides: "Test for junction telemetry completeness"
      contains: "ws_first_tid"
  key_links:
    - from: "python/opendeviationbar/sidecar/__init__.py"
      to: "python/opendeviationbar/sidecar/http_server.py"
      via: "_junction_state dict write"
      pattern: "_junction_state.*ws_first_tid"
    - from: "python/opendeviationbar/sidecar/http_server.py"
      to: "/status endpoint JSON response"
      via: "_handle_status builds symbols dict"
      pattern: "rest_first_tid|ws_first_tid"
---

<objective>
Close the verification gap for Phase 02 Truth #3: junction telemetry must record both rest_last_tid and ws_first_tid per symbol so the REST-to-WS handoff can be verified post-startup.

Purpose: Without ws_first_tid, a consumer checking /status after startup cannot verify Stathera continuity at the junction (ws_first_tid == rest_last_tid + 1). This completes STREAM-02 observability.
Output: Complete junction telemetry in \_junction_state exposed via /status endpoint.
</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/02-sidecar-fill-pipeline/02-01-SUMMARY.md

<interfaces>
<!-- Key structures the executor needs -->

From python/opendeviationbar/sidecar/http_server.py:

```python
# Line 40 — module-level mutable dict shared across sidecar modules
_junction_state: dict[str, dict] = {}

# Line 340-376 — _handle_status builds per-symbol dict from _junction_state
# Currently exposes only: rest_last_tid, forming_bar (None), committed_floors (None)
```

From python/opendeviationbar/sidecar/**init**.py:

```python
# Line 123 — imports _junction_state
from .http_server import _junction_state as _junction_state

# Line 1121-1168 — _main_loop bar processing
# Each bar dict has: _symbol, _threshold, first_agg_trade_id, last_agg_trade_id, etc.
# bar_first_tid = burst_bar.get("first_agg_trade_id", 0)
```

From python/opendeviationbar/sidecar/midnight.py:

```python
# Lines 797-813 — writes to _junction_state after sync_flush
_junction_state[symbol]["rest_last_tid"] = last_tid
_junction_state[symbol]["rest_first_tid"] = min(...)
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Track ws_first_tid in _main_loop and expose full junction telemetry in /status</name>
  <files>python/opendeviationbar/sidecar/__init__.py, python/opendeviationbar/sidecar/http_server.py, tests/test_sync_flush_atomic_replace.py</files>
  <read_first>
    - python/opendeviationbar/sidecar/__init__.py (lines 1104-1170 — burst bar processing in _main_loop)
    - python/opendeviationbar/sidecar/http_server.py (lines 340-376 — _handle_status)
    - python/opendeviationbar/sidecar/midnight.py (lines 795-815 — _junction_state writes)
    - tests/test_sync_flush_atomic_replace.py (existing test structure)
  </read_first>
  <behavior>
    - Test 1: When _junction_state has rest_first_tid, rest_last_tid, and ws_first_tid for a symbol, /status includes all three in the symbols dict
    - Test 2: ws_first_tid is only recorded once per symbol (first bar arrival), subsequent bars do not overwrite it
  </behavior>
  <action>
    **ws_first_tid tracking** (in __init__.py _main_loop, inside the `for burst_bar in burst_bars:` loop at ~line 1121):

    After extracting `symbol` from `burst_bar` (line 1122) and before the valid_pairs check (line 1129), add a one-shot ws_first_tid recorder:

    ```python
    # Record first WS bar TID per symbol for junction telemetry (STREAM-02 gap closure)
    if symbol in _junction_state and "ws_first_tid" not in _junction_state[symbol]:
        ws_tid = burst_bar.get("first_agg_trade_id", 0)
        if ws_tid > 0:
            _junction_state[symbol]["ws_first_tid"] = ws_tid
    ```

    The check `symbol in _junction_state` ensures we only track symbols that went through fill_from_rest (which populates _junction_state with rest_last_tid). The `"ws_first_tid" not in` guard makes it one-shot — only the FIRST WS bar per symbol is recorded.

    **Expose in /status** (in http_server.py _handle_status, ~line 354):

    Update the symbols dict builder to include rest_first_tid and ws_first_tid:

    ```python
    symbols[sym] = {
        "rest_first_tid": sym_data.get("rest_first_tid"),
        "rest_last_tid": sym_data.get("rest_last_tid"),
        "ws_first_tid": sym_data.get("ws_first_tid"),
        "forming_bar": None,
        "committed_floors": None,
    }
    ```

    **Tests**: Add test class in tests/test_sync_flush_atomic_replace.py that verifies:
    1. _junction_state populated with all three TID fields produces correct /status output
    2. ws_first_tid one-shot behavior (set once, not overwritten by subsequent bars)

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_sync_flush_atomic_replace.py -k "junction" -v</automated>
  </verify>
  <acceptance_criteria>
    - grep -rn "ws_first_tid" python/opendeviationbar/sidecar/ returns matches in both __init__.py and http_server.py
    - grep -n "rest_first_tid" python/opendeviationbar/sidecar/http_server.py returns match in _handle_status
    - New tests pass covering junction telemetry in /status response
    - ws_first_tid is one-shot (only first WS bar per symbol, not overwritten)
  </acceptance_criteria>
  <done>
    - ws_first_tid recorded in _junction_state when first WS bar arrives per symbol
    - /status endpoint exposes rest_first_tid, rest_last_tid, and ws_first_tid per symbol
    - Post-startup consumers can verify junction gap: ws_first_tid == rest_last_tid + 1
    - Tests confirm behavior
  </done>
</task>

</tasks>

<verification>
1. `grep -rn "ws_first_tid" python/opendeviationbar/sidecar/` — must match in __init__.py AND http_server.py
2. `grep -n "rest_first_tid" python/opendeviationbar/sidecar/http_server.py` — must match in _handle_status
3. `uv run --python 3.13 pytest tests/test_sync_flush_atomic_replace.py -k "junction or stathera" -v` — all tests pass
4. `uv run --python 3.13 pytest tests/test_sync_flush_atomic_replace.py -v` — full file passes (no regressions)
</verification>

<success_criteria>

- VERIFICATION.md Truth #3 gap is closed: ws_first_tid tracked and exposed
- STREAM-02 observability complete: REST->WS junction verifiable via /status
- All existing tests continue to pass
  </success_criteria>

<output>
After completion, create `.planning/phases/02-sidecar-fill-pipeline/02-02-SUMMARY.md`
</output>
