Streaming governance in Aurora-Lens
This document covers the v1 full-buffer streaming governance model as implemented in aurora_lens/lens.py and aurora_lens/govern/stream_gate.py. State machine and constitutional invariants only.
Model
Full-buffer streaming governance is the v1 streaming model. Provider output streams internally into a private buffer; raw model tokens never become user-visible consequence before the final admissibility decision. The user receives either the admitted buffer or a governed continuation — never both, never a fragment of the suppressed candidate.
Streaming state machine
UPSTREAM_STREAMING
│
│ all provider chunks received (or fallback generate() used)
▼
BUFFER_COMPLETE
│
│ checker.check(full_text) + governor.evaluate(full_text, pef)
▼
VERIFYING
│
├─── ADMIT (action ∈ {PASS, SOFT_CORRECT}) ────────────────────┐
│ │
▼ ▼
SUPPRESSING_BUFFER RELEASING_BUFFER
bridge.intervene() produces governed continuation. Buffered chunks emitted
Buffer permanently suppressed. as "chunk" events,
Governed continuation emitted as single preserving original cadence.
"governed_chunk" event.
│ │
└────────────────────────┬───────────────────────────────────────┘
│
▼
METADATA_EMITTED ("metadata" event)
│
▼
COMPLETE
ABORTED (provider stream raised before buffer was complete)
Event kinds emitted to the caller
| Kind | When | Content |
|---|---|---|
| chunk | ADMIT path only, one per buffered chunk | (chunk_dict, content_delta) from private buffer |
| governed_chunk | NON-ADMIT path, one event total | Fresh chunk_dict wrapping the governed continuation |
| progress | Optional (stream_emit_progress=True) | ProgressSignal — lifecycle label only |
| metadata | Final event on every non-aborted path | Aurora dict: governance, turn, stream fields |
Governed chunk_dicts are constructed fresh from the governed text, not derived from raw provider chunks. Reusing provider chunks would leak structural information about the suppressed candidate via delta boundaries.
Provider abort vs. client disconnect
Provider abort during private buffering
The upstream generate_stream() raises (e.g. asyncio.CancelledError) before the buffer is complete. No content has reached the user. The finally block logs a forensic abort entry (stream_completed=False, stream_abort_reason="provider_abort"). The exception propagates to the caller.
Client disconnect after governed release
The upstream stream completed normally during buffering. Governance ran. Chunks were released. The caller broke out of the generator mid-delivery. From the perspective of the upstream stream, this is not an early close — the provider stream completed before any chunk was released. No abort entry is written; the governance decision and its forensic record are already committed.
Constitutional invariants
The generator yields no chunk, governed_chunk, or progress event containing substantive content before VERIFYING completes. Accumulation is entirely private.
Once SUPPRESSING_BUFFER is entered, no fragment of the buffer appears in any subsequent user-visible output — not in the governed continuation, not in a progress event, not in metadata.
The same log_decision() call runs on both paths. Non-ADMIT entries include blocked_response_hash (sha256 of the suppressed buffer) and governed_response_hash (sha256 of the governed continuation). ADMIT entries carry no hash fields.
On a non-ADMIT path, self._history is appended with the governed continuation. The suppressed candidate is stored only in decision.original_response — forensic only, not returned to the caller.
ProgressSignal.status is constrained to {"streaming", "verifying", "releasing"} and validated at construction. These labels identify lifecycle phase only. They carry no flag names, no pathway identifiers, no content, and no field derived from user input or LLM output.
process() and process_stream() reach the same admissibility decision by the same code path (checker → governor → bridge). The only difference is delivery mechanism.