# Detection gRPC Bi-Directional Stream + Frame Budgeting

**Task**: AZ-660_detection_client_grpc_stream
**Name**: Bi-directional gRPC stream to ../detections + drop-oldest frame budgeting
**Description**: Single bi-directional gRPC stream to the external `../detections` service. Reconnect on stream loss with bounded exponential backoff. Frame budgeting: drop older in-flight frames if a new frame arrives before the previous response, respecting the Tier-1 ≤100 ms/frame target.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-659_frame_ingest_publisher
**Component**: detection_client
**Tracker**: AZ-660
**Epic**: AZ-628

## Problem

`detection_client` is the only autopilot component talking to `../detections`. The contract is a bi-directional gRPC stream; the client must maintain it (reconnect with bounded backoff), respect the Tier-1 latency target by NOT queueing frames indefinitely (drop-oldest in-flight when a newer frame arrives), and never block the upstream `frame_ingest` publisher.

## Outcome

- `DetectionClient::run(frame_rx)` maintains one bi-directional gRPC stream to `../detections`; reconnect on stream loss with exponential backoff capped at 30 s.
- Outbound: send each `Frame` (skipping `ai_locked` ones) up to `max_concurrent_in_flight` (default 2); drop older in-flight frames when the budget is full and a new frame arrives (logged as `budget_drop`).
- Inbound: receive `DetectionBatch` and publish on the output channel; tag with the source frame's `monotonic_ts`.
- Health surface: `gRPC_connection_state`, `requests_in_flight`, `latency_p50/p99`, `errors_by_kind`, `budget_drops_total`.

## Scope

### Included
- `tonic` (or equivalent) gRPC client + bi-directional streaming.
- Reconnect state machine.
- In-flight tracker (sliding window of `frame_seq`).
- Drop-oldest budgeting.

### Excluded
- Schema validation + model_version handling (task 22).
- The `../detections` service itself (separate repo).

## Acceptance Criteria

**AC-1: Happy path against fixture**
Given a fixture gRPC server that returns a `DetectionBatch` per request within 50 ms
When `DetectionClient::run` is started against a 30 fps frame source for 10 s
Then ≥285 `DetectionBatch` are observed on the output channel; latency_p99 ≤100 ms; budget_drops_total = 0.

**AC-2: Reconnect after server restart**
Given a healthy stream
When the gRPC server is killed and restarted
Then the client reconnects within ≤2 s; subsequent frames flow through.

**AC-3: Budget drop on slow server**
Given the server takes 200 ms per response and the source is 30 fps
When the client runs for 5 s
Then `budget_drops_total > 0`, frames continue to flow, and the publisher is never blocked.

**AC-4: ai_locked frames are skipped**
Given a frame stream where every 5th frame has `ai_locked = true`
When the client runs
Then no requests are sent for `ai_locked` frames (observable via outgoing count).

## Non-Functional Requirements

**Performance**
- Per-frame round-trip ≤100 ms p99 (Tier-1 NFR; mostly owned by `../detections`).
- Reconnect latency: ≤2 s after `../detections` returns.

**Reliability**
- Drop-oldest never queues indefinitely.
- Reconnect is bounded.

## Contract

- gRPC service contract owner: `../_docs/03_detections.md`.
- Canonical typed model: `data_model.md §Detection`, `§DetectionBatch`.

## Runtime Completeness

- **Named capability**: bi-directional gRPC stream against `../detections`.
- **Production code that must exist**: real `tonic` (or equivalent) bi-directional stream; real budgeting.
- **Allowed external stubs**: a fixture gRPC server in tests; the real `../detections` for integration.
- **Unacceptable substitutes**: a unary call-per-frame instead of streaming is unacceptable (multiplies per-request overhead).
