pub struct EventSink { /* private fields */ }Expand description
Per-session event fanout + audit. Emits to the registered emitter (WS subscribers) and journals the audit-relevant subset to a JSONL event log.
Implementations§
Source§impl EventSink
impl EventSink
pub fn new( session_id: impl Into<String>, emitter: Option<EventEmitter>, journal_path: Option<PathBuf>, ) -> Self
Sourcepub fn test_sink() -> Self
pub fn test_sink() -> Self
A sink that drops everything — unit tests that don’t assert on events.
Sourcepub fn collecting(session_id: &str) -> (Self, Arc<Mutex<Vec<CoderEvent>>>)
pub fn collecting(session_id: &str) -> (Self, Arc<Mutex<Vec<CoderEvent>>>)
Collect events into a shared Vec — tests that DO assert on events.
pub fn emit(&self, kind: CoderEventKind) -> CoderEvent
Sourcepub fn record_turn_completed(
&self,
decision: &str,
stop_reason: Option<&str>,
was_truncated: bool,
turns: u32,
model: &str,
models_served: &[String],
)
pub fn record_turn_completed( &self, decision: &str, stop_reason: Option<&str>, was_truncated: bool, turns: u32, model: &str, models_served: &[String], )
Append a durable TurnCompleted audit record for a coder-loop terminal.
The coder loop has no Runtime in scope (only this sink), so this mirrors
car_engine::Runtime::record_turn_completed directly onto the coder
session journal — the same EventKind::TurnCompleted + data shape the
assistant path emits (via the shared car_engine::goal::turn_completed_data),
so the coder-path false-success / truncation / turn-budget-burn signal is
captured in the exact form the harness miners already understand.
Consumption is a separate follow-up, NOT done here: these events land in
the coder session journal (<state_dir>/<session_id>.events.jsonl), a
durable record read offline / via the FFI diagnose_from_jsonl. The
in-process daemon miners (harness_adapt::diagnose,
evolution::failed_trace_events) run over session.runtime.log (the
assistant path), so they do NOT yet consume this coder journal — wiring it
into the daemon evolution path is tracked separately. Journal-only: not a
WS-streamed CoderEvent, matching how P0b kept TurnCompleted off the
live AssistantEvent stream (no WS/FFI surface change).
Sourcepub fn record_model_fallback(&self, from: &str, to: &str, reason: &str)
pub fn record_model_fallback(&self, from: &str, to: &str, reason: &str)
Journal one mid-run backbone change.
Written directly rather than recognized in EventSink::audit, once
per distinct hop rather than once per session. The live
ModelFallback event is behind a once-per-phase latch so the stream
does not narrate every routing decision — right for a stream, wrong for
a record. A run that degraded twice has two facts, and journaling off
the latched emit would keep only the first.
The caller supplies the deduplication, because the fact worth recording is a distinct transition and a persistent condition re-reports the same one on every turn. This method itself always appends: it is the record, not the policy.
car#1333 recorded WHO wrote each turn (models_served on the turn
terminal). This records WHY the backbone moved, which is a genuinely
different fact rather than the same one twice: without it, a run that
silently degraded to another model looks — to harness_adapt-style
mining and to a human reading the journal — like the code under test
behaving badly (car#1351).
Journal-only, no WS/FFI surface, same as the two above.
Sourcepub fn record_gate_verdict(&self, kind: EventKind, data: HashMap<String, Value>)
pub fn record_gate_verdict(&self, kind: EventKind, data: HashMap<String, Value>)
Journal one merge-gate verdict.
Written here rather than derived from the event stream, deliberately.
The obvious shape is to bridge the verdict out as a foreman: "gate"
CoderEvent and have EventSink::audit recognize it — one path, live
and durable together. That is forgeable. process_stream fires the
emitter on every line the supervised CLI prints, and StreamEvent’s
#[serde(flatten)] extra absorbs arbitrary top-level keys and re-emits
them at top level — so a single line of stdout from the model being
supervised satisfies any predicate audit could key on, and writes “the
gate accepted this patch” into the audit record with the gate never
having run. It would not even be scoped to foreman sessions: audit runs
for every coder session with a journal.
car#1243 is what makes that fatal rather than untidy. The patches this
gate rules on are authored on peers this host does not control, which is
an argument for a record the audited party CANNOT write to. So the live
foreman: "gate" event stays narration — spoofable, and only ever read
as narration — and the durable record is written straight to the journal
from the foreman loop, on the same shape as
EventSink::record_turn_completed above and for the same reason.
kind is the gate’s own EventKind::GateAccepted / EventKind::GateRejected,
and data is its payload verbatim. Note the gate treats Inconclusive
(verify timed out, or not configured) as not-accepted, so “we don’t know”
journals as GateRejected carrying outcome: "inconclusive" — the kind
is binary, the payload is not.
Journal-only, no WS/FFI surface: same as record_turn_completed.
Every model that completed a turn in this session, from the journal, distinct and in first-seen order.
record_turn_completed already writes model_id on every terminal
native path, so this reads what is there rather than adding a second
record that could disagree with it.
A set, not the last one. TurnCompleted is a PER-ITERATION
terminal, and an unpinned session is free to route each iteration
differently — strict_model is cfg.model.is_some(), so an unpinned
chain may also degrade to another model on an outage. A reviewer judges
the accumulated worktree diff, not the last iteration, so taking the
last name would let the model that wrote most of the change review it
as long as something else finished the run.
Mid-iteration models count too (car#1333). A terminal names only the
model that reached it, so a model that wrote turns 1-3 of an iteration
another model finished used to leave no record at all — and could then
sit on the panel reviewing what it had written. models_served carries
the rest, and both are folded here.
A record written before that field existed simply has no models_served,
and folds to its model_id alone — the same answer it gave before.
Empty for a run with no native turns — a foreman or external session farms to a coding CLI whose backbone CAR never resolved, so there is no honest answer. Empty means “nobody asked CAR’s own loop to write this”, NOT “nobody wrote it”.
pub fn events(&self) -> Vec<Event>
pub fn record_proactive_memory( &self, maintenance: &ProactiveMaintenanceReport, decision: &ProactiveMemoryDecision, )
Auto Trait Implementations§
impl !Freeze for EventSink
impl !RefUnwindSafe for EventSink
impl !UnwindSafe for EventSink
impl Send for EventSink
impl Sync for EventSink
impl Unpin for EventSink
impl UnsafeUnpin for EventSink
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more