2626
2727use std:: any:: Any ;
2828use std:: fmt;
29+ use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
2930use std:: sync:: Arc ;
3031use std:: time:: Duration ;
3132
@@ -309,6 +310,17 @@ pub struct Executor {
309310 /// `begin_effect` is not crash-safe or fail-closed. Installed from
310311 /// `plugin_settings.effect_log_path` or programmatically. Opt-in.
311312 effect_log : Option < Arc < dyn DurableEffectLog > > ,
313+
314+ /// Audit stream identity + counters, fresh per executor lifetime (a new
315+ /// identity on config reload). Each emitted record carries its per-stream
316+ /// counter (`decision_seq` / `effect_seq`, gap-free → completeness) and the
317+ /// shared `emission_seq` (global across both → interleaved order). `Arc`
318+ /// so copy-on-write snapshot mutations stay on the same stream.
319+ decision_stream_id : Arc < str > ,
320+ decision_seq : Arc < AtomicU64 > ,
321+ effect_stream_id : Arc < str > ,
322+ effect_seq : Arc < AtomicU64 > ,
323+ emission_seq : Arc < AtomicU64 > ,
312324}
313325
314326impl Executor {
@@ -318,6 +330,11 @@ impl Executor {
318330 config,
319331 audit_handlers : Vec :: new ( ) ,
320332 effect_log : None ,
333+ decision_stream_id : Arc :: from ( format ! ( "dec-{}" , uuid:: Uuid :: new_v4( ) . simple( ) ) ) ,
334+ decision_seq : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
335+ effect_stream_id : Arc :: from ( format ! ( "eff-{}" , uuid:: Uuid :: new_v4( ) . simple( ) ) ) ,
336+ effect_seq : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
337+ emission_seq : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
321338 }
322339 }
323340
@@ -356,6 +373,22 @@ impl Executor {
356373
357374 /// Invoke every audit sink with the finalized decision, once per pipeline
358375 /// run. Observation-only — the executor ignores whatever they return.
376+ /// Assign this decision's stream identity + sequence numbers. The executor
377+ /// writes its **own** record here — a step distinct from the read-only
378+ /// handoff in [`Self::emit_audit`] (which takes `&DecisionLog`), so a sink
379+ /// never receives anything mutable. `decision_seq` is gap-free within the
380+ /// decision stream (completeness); `emission_seq` is the shared global
381+ /// counter across decisions and effects (interleaved order). Stamped even
382+ /// with no sinks — it's a property of the stream and rides on
383+ /// `PipelineResult.decision_log`.
384+ fn stamp_decision_stream ( & self , decisions : & mut DecisionLog ) {
385+ decisions. set_stream (
386+ self . decision_stream_id . to_string ( ) ,
387+ self . decision_seq . fetch_add ( 1 , Ordering :: Relaxed ) ,
388+ self . emission_seq . fetch_add ( 1 , Ordering :: Relaxed ) ,
389+ ) ;
390+ }
391+
359392 async fn emit_audit (
360393 & self ,
361394 payload : & dyn PluginPayload ,
@@ -494,6 +527,7 @@ impl Executor {
494527 . await
495528 {
496529 decisions. finalize ( Verdict :: Deny ( v. clone ( ) ) ) ;
530+ self . stamp_decision_stream ( & mut decisions) ;
497531 self . emit_audit ( & * current_payload, & current_extensions, & decisions)
498532 . await ;
499533 return (
@@ -542,6 +576,7 @@ impl Executor {
542576 . await
543577 {
544578 decisions. finalize ( Verdict :: Deny ( violation. clone ( ) ) ) ;
579+ self . stamp_decision_stream ( & mut decisions) ;
545580 self . emit_audit ( & * current_payload, & current_extensions, & decisions)
546581 . await ;
547582 return (
@@ -564,6 +599,7 @@ impl Executor {
564599 ) ;
565600
566601 decisions. finalize ( Verdict :: Allow ) ;
602+ self . stamp_decision_stream ( & mut decisions) ;
567603 self . emit_audit ( & * current_payload, & current_extensions, & decisions)
568604 . await ;
569605 (
@@ -656,6 +692,9 @@ impl Executor {
656692 // The configured WAL (opt-in). `None` → ordering-only, not
657693 // fail-closed; `Some` → durable-before-fanout, fail-closed.
658694 durable : self . effect_log . clone ( ) ,
695+ stream_id : self . effect_stream_id . clone ( ) ,
696+ stream_seq : self . effect_seq . clone ( ) ,
697+ emission_seq : self . emission_seq . clone ( ) ,
659698 } ) ) ;
660699 }
661700
@@ -1305,6 +1344,12 @@ struct AuditEffectEmitter {
13051344 /// before fanning out and fails closed if that write fails. `None` until
13061345 /// slice 3b wires a real WAL — then emit is ordering-only.
13071346 durable : Option < Arc < dyn DurableEffectLog > > ,
1347+ /// Effect stream identity + counters (shared with the executor). Each
1348+ /// emitted record is stamped with `stream_seq` (gap-free within the effect
1349+ /// stream) and the global `emission_seq` (interleaved order vs decisions).
1350+ stream_id : Arc < str > ,
1351+ stream_seq : Arc < AtomicU64 > ,
1352+ emission_seq : Arc < AtomicU64 > ,
13081353}
13091354
13101355impl std:: fmt:: Debug for AuditEffectEmitter {
@@ -1322,9 +1367,15 @@ impl EffectEmitter for AuditEffectEmitter {
13221367 use futures:: FutureExt ;
13231368 use std:: panic:: AssertUnwindSafe ;
13241369
1325- // Stamp the causing plugin — set by the framework, not self-reported.
1370+ // Stamp the causing plugin + stream identity/sequences — all set by the
1371+ // framework, not self-reported. `stream_seq` is gap-free within the
1372+ // effect stream (completeness); `emission_seq` is the shared global
1373+ // counter across decisions and effects (interleaved order).
13261374 let mut stamped = effect. clone ( ) ;
13271375 stamped. plugin_name = Some ( self . plugin_name . clone ( ) ) ;
1376+ stamped. stream_id = Some ( self . stream_id . to_string ( ) ) ;
1377+ stamped. stream_seq = Some ( self . stream_seq . fetch_add ( 1 , Ordering :: Relaxed ) ) ;
1378+ stamped. emission_seq = Some ( self . emission_seq . fetch_add ( 1 , Ordering :: Relaxed ) ) ;
13281379
13291380 // Write-ahead: durably record BEFORE any observer sees it. Fail
13301381 // closed — if the durable write fails, return Err and do NOT fan out;
@@ -1546,6 +1597,9 @@ mod tests {
15461597 plugin_name : "delegator" . into ( ) ,
15471598 timeout : Duration :: from_secs ( 5 ) ,
15481599 durable : Some ( Arc :: new ( FailingLog ) ) ,
1600+ stream_id : Arc :: from ( "eff-test" ) ,
1601+ stream_seq : Arc :: new ( std:: sync:: atomic:: AtomicU64 :: new ( 0 ) ) ,
1602+ emission_seq : Arc :: new ( std:: sync:: atomic:: AtomicU64 :: new ( 0 ) ) ,
15491603 } ;
15501604 let res = emitter. emit ( & effect, & Extensions :: default ( ) ) . await ;
15511605 assert ! ( res. is_err( ) , "durable write failed → emit fails closed" ) ;
@@ -1562,6 +1616,9 @@ mod tests {
15621616 plugin_name : "delegator" . into ( ) ,
15631617 timeout : Duration :: from_secs ( 5 ) ,
15641618 durable : Some ( Arc :: new ( OkLog ) ) ,
1619+ stream_id : Arc :: from ( "eff-test" ) ,
1620+ stream_seq : Arc :: new ( std:: sync:: atomic:: AtomicU64 :: new ( 0 ) ) ,
1621+ emission_seq : Arc :: new ( std:: sync:: atomic:: AtomicU64 :: new ( 0 ) ) ,
15651622 } ;
15661623 let res2 = emitter2. emit ( & effect, & Extensions :: default ( ) ) . await ;
15671624 assert ! ( res2. is_ok( ) ) ;
0 commit comments