@@ -30,7 +30,7 @@ use serde_json::{Value, json};
3030use sha2:: { Digest as _, Sha256 } ;
3131use tungstenite:: { Message as WebSocketMessage , WebSocket } ;
3232
33- use crate :: { ding, message, run, status} ;
33+ use crate :: { ding, harness_state , message, run, status} ;
3434
3535/// Every admitted version has a delivery-critical schema comparison and live remote-TUI evidence.
3636/// A later version stays rejected until both checks are repeated; semantic-version proximity is
@@ -210,6 +210,55 @@ pub enum CodexTerminalError {
210210 SystemError ,
211211}
212212
213+ impl CodexObservedState {
214+ /// Driver-side projection into the generic observed-harness-state vocabulary (#162). `Held` is
215+ /// a delivery predicate — the complement of steerable — and never leaks into the published
216+ /// record: holds Codex positively reported as work project to `active` (with the human-blocking
217+ /// ones setting the blocked axis), while holds that only mean "st2 cannot currently prove
218+ /// anything" project to `None`, the indeterminate observation that writes nothing.
219+ pub fn harness_observation ( & self ) -> Option < harness_state:: Observation > {
220+ use crate :: harness_state:: { Activity , BlockedOn , InputBuffer , Observation } ;
221+ let observation = |state, blocked_on| {
222+ // This producer reads the app-server control stream and cannot see the composer.
223+ Observation :: new ( state, blocked_on, InputBuffer :: Unknown )
224+ } ;
225+ match self {
226+ CodexObservedState :: AwaitingStatus => None ,
227+ CodexObservedState :: Idle => Some ( observation ( Activity :: Idle , BlockedOn :: None ) ) ,
228+ CodexObservedState :: TerminalError { .. } => {
229+ Some ( observation ( Activity :: Ended , BlockedOn :: None ) . with_reason ( "systemError" ) )
230+ }
231+ CodexObservedState :: Active { .. } => {
232+ Some ( observation ( Activity :: Active , BlockedOn :: None ) )
233+ }
234+ CodexObservedState :: Held { reason, .. } => match reason {
235+ CodexHoldReason :: Review => {
236+ Some ( observation ( Activity :: Active , BlockedOn :: Human ) . with_reason ( "review" ) )
237+ }
238+ CodexHoldReason :: WaitingOnApproval => Some (
239+ observation ( Activity :: Active , BlockedOn :: Human )
240+ . with_reason ( "waitingOnApproval" ) ,
241+ ) ,
242+ CodexHoldReason :: WaitingOnUserInput => Some (
243+ observation ( Activity :: Active , BlockedOn :: Human )
244+ . with_reason ( "waitingOnUserInput" ) ,
245+ ) ,
246+ CodexHoldReason :: Compaction => {
247+ Some ( observation ( Activity :: Active , BlockedOn :: None ) . with_reason ( "compaction" ) )
248+ }
249+ // Codex positively reported active; st2 merely cannot name a steerable turn.
250+ CodexHoldReason :: ActiveWithoutTurn => Some (
251+ observation ( Activity :: Active , BlockedOn :: None ) . with_reason ( "activeWithoutTurn" ) ,
252+ ) ,
253+ CodexHoldReason :: ConflictingTurn => Some (
254+ observation ( Activity :: Active , BlockedOn :: None ) . with_reason ( "conflictingTurn" ) ,
255+ ) ,
256+ CodexHoldReason :: NotLoaded | CodexHoldReason :: SystemError => None ,
257+ } ,
258+ }
259+ }
260+ }
261+
213262#[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
214263#[ serde( rename_all = "camelCase" , deny_unknown_fields) ]
215264pub struct CodexControlState {
@@ -330,6 +379,11 @@ struct CodexInboxDelivery {
330379 pending : Option < PendingCodexDelivery > ,
331380 rejected : Option < RejectedCodexDelivery > ,
332381 next_request_id : u64 ,
382+ harness_writer : harness_state:: Writer ,
383+ /// Whether the latest projection carried evidence. Indeterminate observations write nothing
384+ /// and stop the heartbeat, so a state the pump can no longer see ages out instead of staying
385+ /// artificially fresh.
386+ harness_evidence : bool ,
333387}
334388
335389impl CodexInboxDelivery {
@@ -345,8 +399,17 @@ impl CodexInboxDelivery {
345399 )
346400 } ) ?;
347401 let ( wake_tx, wake) = mpsc:: channel ( ) ;
348- let watcher = crate :: watch:: watch_recursive_mutations ( & config. agent_dir , wake_tx) ;
402+ // Scoped to inbox + status: this pump's own process group writes runtime records (presence
403+ // refreshes, harness-state transitions) into the same agent dir, and those must not wake it.
404+ let watcher = crate :: watch:: watch_delivery_inputs ( & config. agent_dir , wake_tx) ;
349405 let state = load_delivery_state ( & state_path, & config. identity , runtime. runtime_id ( ) ) ?;
406+ // An agent IS its pty: the session whose liveness vouches for the record is the identity.
407+ let harness_writer = harness_state:: Writer :: new (
408+ & config. agent_dir ,
409+ config. identity . clone ( ) ,
410+ "codex" ,
411+ Some ( config. identity . clone ( ) ) ,
412+ ) ;
350413 Ok ( Self {
351414 config,
352415 state_path,
@@ -361,9 +424,23 @@ impl CodexInboxDelivery {
361424 pending : None ,
362425 rejected : None ,
363426 next_request_id : FIRST_DELIVERY_REQUEST_ID ,
427+ harness_writer,
428+ harness_evidence : false ,
364429 } )
365430 }
366431
432+ /// Publish the generic observed-harness-state projection of a control-state change. Best-effort
433+ /// like the presence refresh: a failed record write must not disturb delivery.
434+ fn observe_harness ( & mut self , observed : & CodexObservedState ) {
435+ match observed. harness_observation ( ) {
436+ Some ( observation) => {
437+ let _ = self . harness_writer . observe ( observation) ;
438+ self . harness_evidence = true ;
439+ }
440+ None => self . harness_evidence = false ,
441+ }
442+ }
443+
367444 fn write_state ( & mut self , state : CodexDeliveryState ) -> Result < ( ) > {
368445 atomic_json ( & self . state_path , & state) ?;
369446 self . state = Some ( state) ;
@@ -382,6 +459,9 @@ impl CodexInboxDelivery {
382459 // This wrapper owns the live provider session. It therefore owns the presence lease.
383460 // Preserve busy or available, and let dnd age out.
384461 let _ = status:: refresh ( & status:: status_path ( & self . config . agent_dir ) ) ;
462+ if self . harness_evidence {
463+ let _ = self . harness_writer . heartbeat ( ) ;
464+ }
385465 self . next_presence_refresh = now + status:: STATUS_REFRESH ;
386466 }
387467 let mut due = now >= self . next_inbox_refresh ;
@@ -1226,6 +1306,8 @@ fn run_connected(
12261306 } else {
12271307 ( None , None )
12281308 } ;
1309+ let harness_agent_dir = delivery. agent_dir . clone ( ) ;
1310+ let harness_identity = delivery. identity . clone ( ) ;
12291311 let event_thread = thread:: spawn ( move || {
12301312 let resume = expected_resume
12311313 . as_deref ( )
@@ -1287,6 +1369,25 @@ fn run_connected(
12871369 drop ( resume_ready_tx) ;
12881370 let _ = shutdown. shutdown ( Shutdown :: Both ) ;
12891371 let _ = event_thread. join ( ) ;
1372+ // The pump is gone, so nothing can observe this session again: publish the terminal
1373+ // observation with the outcome the wrapper actually saw, before any staleness horizon.
1374+ let mut harness_writer = harness_state:: Writer :: new (
1375+ & harness_agent_dir,
1376+ harness_identity. clone ( ) ,
1377+ "codex" ,
1378+ Some ( harness_identity) ,
1379+ ) ;
1380+ let _ = match & result {
1381+ Ok ( ( ) ) => harness_writer. ended ( "exit 0" ) ,
1382+ Err ( error) => harness_writer. observe (
1383+ harness_state:: Observation :: new (
1384+ harness_state:: Activity :: Ended ,
1385+ harness_state:: BlockedOn :: None ,
1386+ harness_state:: InputBuffer :: Unknown ,
1387+ )
1388+ . with_reason ( format ! ( "{error}" ) ) ,
1389+ ) ,
1390+ } ;
12901391 result
12911392}
12921393
@@ -2034,6 +2135,9 @@ fn pump_control(
20342135 . context ( "persisting Codex resume binding" ) ?;
20352136 atomic_json ( control_state_path, & bound)
20362137 . context ( "persisting Codex control state" ) ?;
2138+ if let Some ( delivery) = delivery. as_mut ( ) {
2139+ delivery. observe_harness ( & bound. observed ) ;
2140+ }
20372141 control_state = Some ( bound) ;
20382142 let _ = events. send ( ControlEvent :: Bound ) ;
20392143 continue ;
@@ -2056,6 +2160,9 @@ fn pump_control(
20562160 bound. subscribed = true ;
20572161 atomic_json ( control_state_path, & bound)
20582162 . context ( "persisting Codex fresh control state" ) ?;
2163+ if let Some ( delivery) = delivery. as_mut ( ) {
2164+ delivery. observe_harness ( & bound. observed ) ;
2165+ }
20592166 control_state = Some ( bound) ;
20602167 let _ = events. send ( ControlEvent :: Bound ) ;
20612168 }
@@ -2106,6 +2213,9 @@ fn pump_control(
21062213 if changed {
21072214 atomic_json ( control_state_path, state)
21082215 . context ( "persisting Codex observed control state" ) ?;
2216+ if let Some ( delivery) = delivery. as_mut ( ) {
2217+ delivery. observe_harness ( & state. observed ) ;
2218+ }
21092219 let _ = events. send ( ControlEvent :: Observed ) ;
21102220 }
21112221 if !state. subscribed
@@ -2691,6 +2801,120 @@ mod tests {
26912801 assert ! ( steer[ "params" ] . get( "approvalPolicy" ) . is_none( ) ) ;
26922802 }
26932803
2804+ /// Behavioral oracle for the #268 §B projection: a projection that withheld every row — or
2805+ /// that reported the two misclassified rows as indeterminate — fails here, because each
2806+ /// emitting row is asserted positively.
2807+ #[ test]
2808+ fn harness_projection_is_faithful_and_withholds_only_unprovable_rows ( ) {
2809+ use crate :: harness_state:: { Activity , BlockedOn , InputBuffer } ;
2810+ let held = |reason| CodexObservedState :: Held {
2811+ reason,
2812+ turn_id : None ,
2813+ } ;
2814+
2815+ // Rows with no provable observation are withheld — and no absence may derive idle.
2816+ for state in [
2817+ CodexObservedState :: AwaitingStatus ,
2818+ held ( CodexHoldReason :: NotLoaded ) ,
2819+ held ( CodexHoldReason :: SystemError ) ,
2820+ ] {
2821+ assert_eq ! ( state. harness_observation( ) , None , "{state:?}" ) ;
2822+ }
2823+
2824+ // Codex positively reported work: active, even where st2 cannot name a steerable turn
2825+ // (the two rows a naive steerability decomposition reported as unknown) or where the
2826+ // delivery gate holds.
2827+ for state in [
2828+ CodexObservedState :: Active {
2829+ turn_id : "turn-current" . into ( ) ,
2830+ } ,
2831+ held ( CodexHoldReason :: ActiveWithoutTurn ) ,
2832+ held ( CodexHoldReason :: ConflictingTurn ) ,
2833+ held ( CodexHoldReason :: Compaction ) ,
2834+ ] {
2835+ let observation = state
2836+ . harness_observation ( )
2837+ . unwrap_or_else ( || panic ! ( "{state:?} must emit" ) ) ;
2838+ assert_eq ! ( observation. state, Activity :: Active , "{state:?}" ) ;
2839+ assert_eq ! ( observation. blocked_on, BlockedOn :: None , "{state:?}" ) ;
2840+ assert_eq ! ( observation. input_buffer, InputBuffer :: Unknown , "{state:?}" ) ;
2841+ }
2842+
2843+ // The holds a human resolves set the blocked axis instead of disappearing into active.
2844+ for reason in [
2845+ CodexHoldReason :: Review ,
2846+ CodexHoldReason :: WaitingOnApproval ,
2847+ CodexHoldReason :: WaitingOnUserInput ,
2848+ ] {
2849+ let observation = held ( reason)
2850+ . harness_observation ( )
2851+ . unwrap_or_else ( || panic ! ( "{reason:?} must emit" ) ) ;
2852+ assert_eq ! ( observation. state, Activity :: Active , "{reason:?}" ) ;
2853+ assert_eq ! ( observation. blocked_on, BlockedOn :: Human , "{reason:?}" ) ;
2854+ }
2855+
2856+ let idle = CodexObservedState :: Idle . harness_observation ( ) . unwrap ( ) ;
2857+ assert_eq ! ( idle. state, Activity :: Idle ) ;
2858+ assert_eq ! ( idle. blocked_on, BlockedOn :: None ) ;
2859+
2860+ let ended = CodexObservedState :: TerminalError {
2861+ reason : CodexTerminalError :: SystemError ,
2862+ }
2863+ . harness_observation ( )
2864+ . unwrap ( ) ;
2865+ assert_eq ! ( ended. state, Activity :: Ended ) ;
2866+ assert_eq ! ( ended. reason. as_deref( ) , Some ( "systemError" ) ) ;
2867+ }
2868+
2869+ #[ test]
2870+ fn pump_publishes_observations_and_stops_heartbeating_on_evidence_loss ( ) {
2871+ use crate :: harness_state:: { self , Activity } ;
2872+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
2873+ let config = delivery_config ( tmp. path ( ) ) ;
2874+ let agent_dir = config. agent_dir . clone ( ) ;
2875+ let record_path = harness_state:: harness_state_path ( & agent_dir) ;
2876+ let mut delivery = inbox_delivery ( tmp. path ( ) , config) ;
2877+
2878+ delivery. observe_harness ( & CodexObservedState :: Active {
2879+ turn_id : "turn-current" . into ( ) ,
2880+ } ) ;
2881+ let observed = harness_state:: read ( & record_path, None ) . expect ( "record written" ) ;
2882+ assert_eq ! ( observed. state, Activity :: Active ) ;
2883+ assert_eq ! ( observed. harness. as_deref( ) , Some ( "codex" ) ) ;
2884+
2885+ // An indeterminate projection writes nothing and stops the heartbeat: the presence
2886+ // refresh still runs, but the record's bytes stay untouched and age toward unknown.
2887+ delivery. observe_harness ( & CodexObservedState :: Held {
2888+ reason : CodexHoldReason :: NotLoaded ,
2889+ turn_id : None ,
2890+ } ) ;
2891+ let before = fs:: read ( & record_path) . unwrap ( ) ;
2892+ delivery. refresh_if_due ( ) . unwrap ( ) ;
2893+ assert ! (
2894+ status:: read_state( & status:: status_path( & agent_dir) ) != status:: State :: Offline ,
2895+ "presence refresh must still run"
2896+ ) ;
2897+ assert_eq ! (
2898+ fs:: read( & record_path) . unwrap( ) ,
2899+ before,
2900+ "no heartbeat without evidence"
2901+ ) ;
2902+
2903+ // Evidence returning resumes both observation and heartbeat.
2904+ delivery. observe_harness ( & CodexObservedState :: Idle ) ;
2905+ assert_eq ! (
2906+ harness_state:: read( & record_path, None ) . unwrap( ) . state,
2907+ Activity :: Idle
2908+ ) ;
2909+ delivery. next_presence_refresh = Instant :: now ( ) ;
2910+ delivery. refresh_if_due ( ) . unwrap ( ) ;
2911+ assert_ne ! (
2912+ fs:: read( & record_path) . unwrap( ) ,
2913+ before,
2914+ "heartbeat resumes with evidence"
2915+ ) ;
2916+ }
2917+
26942918 #[ test]
26952919 fn delivery_client_id_is_stable_and_binds_every_identity_component ( ) {
26962920 let id =
0 commit comments