8ebe227ae4
Slice 2 of issue #17 — the OBSERVE half. New ratatoskr.provider.opfeed: - OpEvent{ts, plane, op, session_id, status, req_summary, resp_summary, turn_id=None} — scope-only summaries, never record bodies / PAD content - OpSink Protocol + JsonlOpSink (continuous append-only JSONL, INV-007) - instrument_provider_app(app, *, plane, sink): an ASGI middleware over the built bifrost provider app. Buffers+replays the request, captures the response, reads session_id off the dispatch JWT's "sub" claim (INV-005 — present for ALL verbs incl. search/get/delete, which bifrost withholds from the store method), emits exactly one OpEvent per inbound bifrost-call incl. handshake + errors. Read-only over dispatch; store scope semantics untouched (INV-004). A sink/summary failure is swallowed + logged, never breaks serve (POST-003). - Per-verb summaries: search {scope_all,scope_any,top_k}->{hit_count,hits}; upsert_many {record_count,scopes}->{upserted,replayed}; get/get_many/ delete_many {ids}->{found_count|deleted}; emit (affect, opaque)->{stored}; handshake {caps_requested}->{caps_granted,ok}; error->{error: code} - serve_memory/serve wired: opt-in via RATATOSKR_OPFEED_PATH (maybe_instrument) Resolves the contract's open question: the dispatch JWT DOES carry session_id (= the "sub" claim). Tests drive the REAL bifrost dispatch end-to-end with minted JWTs. 11 new tests; full suite 453 green; ruff + mypy clean (opfeed.py).