Skip to main content

cookie_agent_engine/runtime/
producers.rs

1use std::{collections::HashSet, sync::Arc, time::Instant};
2
3use cookie_agent_protocol::*;
4use tokio::sync::oneshot;
5
6use super::{Engine, EngineError, Event, SessionCommand, event_origin};
7use crate::goal_projection::{GoalProducerProjection, ProducerMessageRecord};
8
9pub(super) fn producer_description(prefix: &str, detail: &str) -> SafeDisplayText {
10    let mut text = String::from(prefix);
11    for character in detail.trim().chars() {
12        let character = if character.is_control() {
13            ' '
14        } else {
15            character
16        };
17        if text.len() + character.len_utf8() > SafeDisplayText::MAX_BYTES {
18            break;
19        }
20        text.push(character);
21    }
22    SafeDisplayText::new(text).expect("bounded control-free producer description")
23}
24
25#[test]
26fn producer_descriptions_are_control_free_and_utf8_bounded() {
27    assert_eq!(
28        producer_description("Goal started: ", "  first\nsecond\tstep  ").as_str(),
29        "Goal started: first second step"
30    );
31    for prefix in [
32        "Goal reminder: ",
33        "Goal paused: ",
34        "Goal cancelled: ",
35        "Delegation completed: ",
36    ] {
37        let text = producer_description(prefix, &"\u{e9}".repeat(1024));
38        assert!(text.as_str().starts_with(prefix));
39        assert!(text.as_str().len() <= SafeDisplayText::MAX_BYTES);
40        assert!(text.as_str().len() >= SafeDisplayText::MAX_BYTES - 1);
41    }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub(crate) struct ProducerAuthority {
46    pub owner: ProducerOwner,
47    pub connection_epoch: Option<u64>,
48}
49
50struct Registration {
51    id: ProducerId,
52    authority: ProducerAuthority,
53    registered: Instant,
54}
55
56#[derive(Default)]
57pub(super) struct SessionProducers {
58    registrations: Vec<Registration>,
59    wake_scheduled: bool,
60    pub(super) starting: bool,
61    pub(super) preempted: bool,
62    diagnostics: HashSet<String>,
63}
64
65pub(super) enum ProducerCommand {
66    GetGoal {
67        reply: oneshot::Sender<Result<SessionGoalGetResult, EngineError>>,
68    },
69    SetGoal {
70        objective: String,
71        selection: Option<RunSelection>,
72        origin: EventOrigin,
73        reply: oneshot::Sender<Result<SessionGoalSetResult, EngineError>>,
74    },
75    Lifecycle {
76        params: SessionGoalLifecycleParams,
77        origin: EventOrigin,
78        reply: oneshot::Sender<Result<SessionGoalLifecycleResult, EngineError>>,
79    },
80    UpdateGoal {
81        params: GoalUpdateParams,
82        reply: oneshot::Sender<Result<GoalUpdateResult, EngineError>>,
83    },
84    Inspect {
85        reply: oneshot::Sender<Result<SessionProducersResult, EngineError>>,
86    },
87    Register {
88        authority: ProducerAuthority,
89        reply: oneshot::Sender<Result<ProducerId, EngineError>>,
90    },
91    Send {
92        authority: ProducerAuthority,
93        producer_id: ProducerId,
94        mode: ProducerDeliveryMode,
95        key: ProducerIdempotencyKey,
96        description: SafeDisplayText,
97        body: String,
98        reply: oneshot::Sender<Result<ProducerMessageId, EngineError>>,
99    },
100    /// Atomic agent-mail accept: the hop guard, pending-agent-mail cap check,
101    /// pair-window guard, and durable `ProducerMessageAccepted` append happen
102    /// inside one actor handler, so concurrent senders cannot interleave
103    /// count-then-accept and the readable envelope renders once. Guards run
104    /// strictly after the idempotency replay
105    /// check, so a retried acceptance never re-judges a stored message.
106    SendAgentMessage {
107        authority: ProducerAuthority,
108        producer_id: ProducerId,
109        mode: ProducerDeliveryMode,
110        key: ProducerIdempotencyKey,
111        description: SafeDisplayText,
112        sender: SessionId,
113        sender_agent_type: String,
114        body: String,
115        include_reply_hint: bool,
116        /// Chain depth computed from the sender's own log before this command
117        /// is issued; stamped onto the accepted event as guard metadata.
118        hop: u32,
119        reply: oneshot::Sender<Result<ProducerMessageId, EngineError>>,
120    },
121    CommitDelegationCompletion {
122        reservation: DelegationReservation,
123        producer_id: Option<ProducerId>,
124        teaser: super::delegation::DelegateTeaser,
125        reply: oneshot::Sender<Result<bool, EngineError>>,
126    },
127    Unregister {
128        authority: ProducerAuthority,
129        producer_id: ProducerId,
130        reply: oneshot::Sender<Result<(), EngineError>>,
131    },
132    Discard {
133        authority: ProducerAuthority,
134        message_id: ProducerMessageId,
135        reply: oneshot::Sender<Result<(), EngineError>>,
136    },
137    ClaimInputs {
138        run: RunId,
139        reply: oneshot::Sender<Result<super::producer_claims::ClaimedPrompt, EngineError>>,
140    },
141    ReleaseClaim {
142        run: RunId,
143        claim_seq: u64,
144        reply: oneshot::Sender<Result<(), EngineError>>,
145    },
146    Reconcile {
147        projection: Option<GoalProducerProjection>,
148        reply: oneshot::Sender<Result<(), EngineError>>,
149    },
150    Wake {
151        reply: oneshot::Sender<Result<(), EngineError>>,
152    },
153    CommitStart {
154        run: RunId,
155        event: Box<Event>,
156        reply: oneshot::Sender<Result<(), EngineError>>,
157    },
158    WakeFinished {
159        successful: bool,
160        reply: oneshot::Sender<Result<(), EngineError>>,
161    },
162}
163
164impl Engine {
165    pub(super) fn install_producer_runtime(&self) {
166        let weak = Arc::downgrade(&self.inner);
167        self.inner
168            .plugins
169            .set_producer_handler(Arc::new(move |authority, request| {
170                let weak = weak.clone();
171                Box::pin(async move {
172                    let inner = weak
173                        .upgrade()
174                        .ok_or_else(|| producer_fault(EngineError::ActorStopped))?;
175                    Engine { inner }
176                        .dispatch_plugin_producer(authority, request)
177                        .await
178                })
179            }));
180        let Some(runtime) = self.inner.runtime.clone() else {
181            return;
182        };
183        let weak = Arc::downgrade(&self.inner);
184        let mut changes = self.inner.plugins.subscribe_producer_changes();
185        self.spawn_admission_task(&runtime, async move {
186            loop {
187                if let Some(inner) = weak.upgrade() {
188                    Engine { inner }.reconcile_plugin_producer_sessions().await;
189                } else {
190                    return;
191                }
192                if changes.changed().await.is_err() {
193                    return;
194                }
195            }
196        });
197    }
198
199    async fn dispatch_plugin_producer(
200        &self,
201        authenticated: crate::plugin::PluginConnectionAuthority,
202        request: crate::plugin::PluginProducerRequest,
203    ) -> Result<crate::plugin::PluginProducerResponse, JsonRpcError> {
204        use crate::plugin::{PluginProducerRequest as Request, PluginProducerResponse as Response};
205        let authority = ProducerAuthority {
206            owner: ProducerOwner::Plugin {
207                plugin: authenticated.plugin.clone(),
208            },
209            connection_epoch: Some(authenticated.connection_epoch),
210        };
211        self.validate_producer_authority(&authority)
212            .map_err(producer_fault)?;
213        match request {
214            Request::Register(params) => self
215                .register_producer(params.session_id, authority)
216                .await
217                .map(|producer_id| {
218                    Response::Register(ExtensionProducerRegisterResult { producer_id })
219                })
220                .map_err(producer_fault),
221            Request::Send(params) => self
222                .send_producer_message(
223                    params.session_id,
224                    authority,
225                    params.producer_id,
226                    params.mode,
227                    params.idempotency_key,
228                    params.description,
229                    params.body,
230                )
231                .await
232                .map(|message_id| Response::Send(ExtensionProducerSendResult { message_id }))
233                .map_err(producer_fault),
234            Request::Unregister(params) => self
235                .unregister_producer(params.session_id, authority, params.producer_id)
236                .await
237                .map(|()| Response::Unregister(ExtensionProducerUnregisterResult {}))
238                .map_err(producer_fault),
239            Request::Discard(params) => self
240                .discard_producer_message(params.session_id, authority, params.message_id)
241                .await
242                .map(|()| Response::Discard(ExtensionProducerDiscardResult {}))
243                .map_err(producer_fault),
244            Request::RecoveryComplete(params) => {
245                self.inner
246                    .plugins
247                    .complete_producer_recovery(&authenticated, &params.outcome)?;
248                self.reconcile_plugin_producer_sessions().await;
249                Ok(Response::RecoveryComplete(
250                    ExtensionRecoveryCompleteResult {},
251                ))
252            }
253        }
254    }
255
256    async fn reconcile_plugin_producer_sessions(&self) {
257        let mut sessions: HashSet<_> = self
258            .inner
259            .sessions
260            .producers
261            .lock()
262            .unwrap_or_else(|e| e.into_inner())
263            .keys()
264            .copied()
265            .collect();
266        // Roots only, plus children whose tree was already loaded: scanning every
267        // child log here is the startup cost this pass exists to avoid (ยง4.4).
268        for session in self.inner.store.producer_scan_sessions() {
269            if self
270                .inner
271                .store
272                .recovery_event_snapshot(session)
273                .is_ok_and(|events| producer_state_pending(&events))
274            {
275                sessions.insert(session);
276            }
277        }
278        for session in sessions {
279            let _ = self.reconcile_producers(session).await;
280        }
281    }
282    pub(super) async fn handle_producer_command(
283        &self,
284        session: SessionId,
285        command: ProducerCommand,
286    ) {
287        match command {
288            ProducerCommand::GetGoal { reply } => {
289                let _ = reply.send(self.require_root_goal(session).and_then(|()| {
290                    self.goal_producer_projection(session)
291                        .map(|projection| SessionGoalGetResult {
292                            goal: projection.goal,
293                        })
294                }));
295            }
296            ProducerCommand::SetGoal {
297                objective,
298                selection,
299                origin,
300                reply,
301            } => {
302                let _ = reply.send(self.set_goal_direct(session, objective, selection, origin));
303            }
304            ProducerCommand::Lifecycle {
305                params,
306                origin,
307                reply,
308            } => {
309                let _ = reply.send(self.lifecycle_direct(session, params, origin));
310            }
311            ProducerCommand::UpdateGoal { params, reply } => {
312                let _ = reply.send(self.update_goal_direct(session, params));
313            }
314            ProducerCommand::Inspect { reply } => {
315                let producers = self
316                    .inner
317                    .sessions
318                    .producers
319                    .lock()
320                    .unwrap_or_else(|e| e.into_inner())
321                    .entry(session)
322                    .or_default()
323                    .registrations
324                    .iter()
325                    .map(|record| ProducerRegistration {
326                        producer_id: record.id,
327                        producer_owner: record.authority.owner.clone(),
328                        session_id: session,
329                        age_ms: record
330                            .registered
331                            .elapsed()
332                            .as_millis()
333                            .min(u128::from(u64::MAX)) as u64,
334                    })
335                    .collect();
336                let _ = reply.send(Ok(SessionProducersResult {
337                    producers,
338                    plugin_recovery: self.inner.plugins.producer_recovery_states(),
339                }));
340            }
341            ProducerCommand::Register { authority, reply } => {
342                let _ = reply.send(self.register_producer_direct(session, authority));
343            }
344            ProducerCommand::Send {
345                authority,
346                producer_id,
347                mode,
348                key,
349                description,
350                body,
351                reply,
352            } => {
353                let _ = reply.send(self.accept_producer_direct(
354                    &authority,
355                    ExtensionProducerSendParams {
356                        session_id: session,
357                        producer_id,
358                        mode,
359                        idempotency_key: key,
360                        description,
361                        body,
362                    },
363                    None,
364                ));
365            }
366            ProducerCommand::SendAgentMessage {
367                authority,
368                producer_id,
369                mode,
370                key,
371                description,
372                sender,
373                sender_agent_type,
374                body,
375                include_reply_hint,
376                hop,
377                reply,
378            } => {
379                let _ = reply.send(self.accept_agent_message_direct(
380                    session,
381                    &authority,
382                    producer_id,
383                    mode,
384                    key,
385                    description,
386                    sender,
387                    &sender_agent_type,
388                    body,
389                    include_reply_hint,
390                    hop,
391                ));
392            }
393            ProducerCommand::CommitDelegationCompletion {
394                reservation,
395                producer_id,
396                teaser,
397                reply,
398            } => {
399                let _ = reply.send(self.commit_delegation_completion_direct(
400                    session,
401                    &reservation,
402                    producer_id,
403                    teaser,
404                ));
405            }
406            ProducerCommand::Unregister {
407                authority,
408                producer_id,
409                reply,
410            } => {
411                let result = self
412                    .require_registration(session, producer_id, &authority)
413                    .map(|()| {
414                        self.inner
415                            .sessions
416                            .producers
417                            .lock()
418                            .unwrap_or_else(|e| e.into_inner())
419                            .entry(session)
420                            .or_default()
421                            .registrations
422                            .retain(|record| record.id != producer_id);
423                    });
424                let _ = reply.send(result);
425            }
426            ProducerCommand::Discard {
427                authority,
428                message_id,
429                reply,
430            } => {
431                let _ = reply
432                    .send(self.discard_producer_message_direct(session, &authority, message_id));
433            }
434            ProducerCommand::ClaimInputs { run, reply } => {
435                let _ = reply.send(self.claim_producer_snapshot_direct(session, run));
436            }
437            ProducerCommand::ReleaseClaim {
438                run,
439                claim_seq,
440                reply,
441            } => {
442                let _ =
443                    reply.send(self.release_producer_claim_direct(session, run, claim_seq, false));
444            }
445            ProducerCommand::Reconcile { projection, reply } => {
446                let _ = reply.send(self.reconcile_producers_direct(session, projection));
447            }
448            ProducerCommand::CommitStart { run, event, reply } => {
449                let _ = reply.send(self.commit_producer_start(session, run, *event));
450            }
451            ProducerCommand::Wake { reply } => {
452                let result = self.begin_producer_wake(session);
453                let _ = reply.send(result);
454            }
455            ProducerCommand::WakeFinished { successful, reply } => {
456                let preempted = {
457                    let mut registry = self
458                        .inner
459                        .sessions
460                        .producers
461                        .lock()
462                        .unwrap_or_else(|e| e.into_inner());
463                    let state = registry.entry(session).or_default();
464                    let preempted = state.preempted;
465                    state.starting = false;
466                    state.wake_scheduled = false;
467                    state.preempted = false;
468                    preempted
469                };
470                let _ = reply.send(if successful || preempted {
471                    self.reconcile_producers_direct(session, None)
472                } else {
473                    Ok(())
474                });
475            }
476        }
477    }
478
479    pub(super) fn reconcile_producers_direct(
480        &self,
481        session: SessionId,
482        harvested: Option<GoalProducerProjection>,
483    ) -> Result<(), EngineError> {
484        if self.ensure_not_shutting_down().is_err() || !self.inner.store.is_owned(session) {
485            return Ok(());
486        }
487        self.reconcile_consumed_producers(session, false, harvested.as_ref())?;
488        self.repair_goal_completion(session, false, harvested.as_ref())?;
489        {
490            let mut registry = self
491                .inner
492                .sessions
493                .producers
494                .lock()
495                .unwrap_or_else(|e| e.into_inner());
496            registry
497                .entry(session)
498                .or_default()
499                .registrations
500                .retain(|record| match &record.authority.owner {
501                    ProducerOwner::Plugin { plugin } => {
502                        record.authority.connection_epoch.is_some_and(|epoch| {
503                            self.inner
504                                .plugins
505                                .producer_connection_is_current(plugin, &epoch)
506                        })
507                    }
508                    // Agent registrations are runtime-only sender slots for tree
509                    // peer mail; they must survive reconcile sweeps so pending
510                    // idempotency retried sends keep resolving. Retaining them
511                    // is required for the messaging retry contract.
512                    _ => true,
513                });
514        }
515        self.reconcile_goal_registration(session, harvested.as_ref())?;
516        self.record_recovery_diagnostics(session)?;
517        if self
518            .inner
519            .sessions
520            .active
521            .lock()
522            .unwrap_or_else(|e| e.into_inner())
523            .values()
524            .any(|run| run.session == session)
525            || self.inner.store.get(session)?.status == SessionStatus::Running
526            || self
527                .inner
528                .compaction
529                .in_progress
530                .lock()
531                .unwrap_or_else(|e| e.into_inner())
532                .contains(&session)
533        {
534            return Ok(());
535        }
536        let projection = self.goal_producer_projection(session)?;
537        let real = projection
538            .messages
539            .iter()
540            .any(|message| pending(message) && message.reminder.is_none());
541        let user_pending = super::residency::has_runless_pending_inputs(
542            &self.inner.store.get(session)?.log.event_snapshot(),
543        );
544        if !real
545            && !user_pending
546            && let Some(goal) = projection.goal.as_ref().filter(|goal| {
547                goal.status == GoalStatus::Active
548                    && (goal.items.is_empty() || goal.items.iter().any(|item| !item.finished))
549            })
550            && let Some(producer_id) = self.ready_goal_producer(session, goal.goal_id)
551            && !projection
552                .messages
553                .iter()
554                .any(|message| pending(message) && message.reminder.is_some())
555        {
556            let kind = projection.next_reminder_kind(goal.goal_id);
557            let body = format!(
558                "{} Verify evidence directly or through subagents before marking items finished.\n{}\n{}",
559                match kind {
560                    GoalReminderKind::Started =>
561                        "Goal started. Pursue the new root objective below.",
562                    GoalReminderKind::Continuation => "Continue the root goal.",
563                },
564                if goal.items.is_empty() {
565                    "Establish the checklist with goal_update, then pursue its unfinished work."
566                } else {
567                    "Continue unfinished checklist work; unchanged revision is not completion."
568                },
569                serde_json::to_string(goal).expect("goal state serializes")
570            );
571            let authority = ProducerAuthority {
572                owner: ProducerOwner::Goal {
573                    goal_id: goal.goal_id,
574                },
575                connection_epoch: None,
576            };
577            self.accept_producer_direct(
578                &authority,
579                ExtensionProducerSendParams {
580                    session_id: session,
581                    producer_id,
582                    mode: ProducerDeliveryMode::Queue,
583                    idempotency_key: ProducerIdempotencyKey::new(
584                        ProducerMessageId::new_v7().to_string(),
585                    )
586                    .expect("UUID idempotency key"),
587                    description: producer_description(
588                        match kind {
589                            GoalReminderKind::Started => "Goal started: ",
590                            GoalReminderKind::Continuation => "Goal reminder: ",
591                        },
592                        &goal.objective,
593                    ),
594                    body,
595                },
596                Some(GoalReminderIdentity {
597                    goal_id: goal.goal_id,
598                    revision: goal.revision,
599                    kind,
600                }),
601            )?;
602        }
603        let projection = self.goal_producer_projection(session)?;
604        if !projection.messages.iter().any(pending) {
605            return Ok(());
606        }
607        if !real && user_pending {
608            return Ok(());
609        }
610        if !real
611            && projection
612                .goal
613                .as_ref()
614                .is_none_or(|goal| self.ready_goal_producer(session, goal.goal_id).is_none())
615        {
616            return Ok(());
617        }
618        let runtime = tokio::runtime::Handle::try_current()
619            .ok()
620            .or_else(|| self.inner.runtime.clone());
621        let Some(runtime) = runtime else {
622            return Ok(());
623        };
624        {
625            let mut registry = self
626                .inner
627                .sessions
628                .producers
629                .lock()
630                .unwrap_or_else(|e| e.into_inner());
631            let state = registry.entry(session).or_default();
632            if state.wake_scheduled {
633                return Ok(());
634            }
635            state.wake_scheduled = true;
636        }
637        let engine = self.clone();
638        if !self.spawn_admission_task(&runtime, async move {
639            let _ = engine
640                .request(session, |reply| {
641                    SessionCommand::Producer(ProducerCommand::Wake { reply })
642                })
643                .await;
644        }) {
645            self.inner
646                .sessions
647                .producers
648                .lock()
649                .unwrap_or_else(|e| e.into_inner())
650                .entry(session)
651                .or_default()
652                .wake_scheduled = false;
653        }
654        Ok(())
655    }
656
657    fn ready_goal_producer(&self, session: SessionId, goal_id: GoalId) -> Option<ProducerId> {
658        if !self.plugin_goals_ready() {
659            return None;
660        }
661        if self
662            .inner
663            .delegation
664            .by_session
665            .lock()
666            .unwrap_or_else(|e| e.into_inner())
667            .values()
668            .any(|record| {
669                record.parent_session_id == session
670                    && record.background
671                    && !record.notification_sent
672            })
673        {
674            return None;
675        }
676        let registry = self
677            .inner
678            .sessions
679            .producers
680            .lock()
681            .unwrap_or_else(|e| e.into_inner());
682        let state = registry.get(&session)?;
683        let own = state
684            .registrations
685            .iter()
686            .find(|record| record.authority.owner == ProducerOwner::Goal { goal_id })?;
687        state
688            .registrations
689            .iter()
690            .all(|record| record.id == own.id)
691            .then_some(own.id)
692    }
693
694    fn begin_producer_wake(&self, session: SessionId) -> Result<(), EngineError> {
695        let projection = self.inner.store.get(session)?;
696        if projection.status == SessionStatus::Running
697            || self
698                .inner
699                .sessions
700                .active
701                .lock()
702                .unwrap_or_else(|e| e.into_inner())
703                .values()
704                .any(|active| active.session == session)
705            || !self.reserve_compaction(session)
706        {
707            self.inner
708                .sessions
709                .producers
710                .lock()
711                .unwrap_or_else(|e| e.into_inner())
712                .entry(session)
713                .or_default()
714                .wake_scheduled = false;
715            return Ok(());
716        }
717        let events = projection.log.event_snapshot();
718        let goal = GoalProducerProjection::from_events(&events);
719        let goal_selection = goal
720            .goal
721            .as_ref()
722            .filter(|goal| matches!(goal.status, GoalStatus::Active | GoalStatus::Paused))
723            .and(goal.selection);
724        let selection = goal_selection.unwrap_or_else(|| {
725            cookie_agent_protocol::SessionModelState::from_events(&events)
726                .selection
727                .unwrap_or_else(|| projection.meta.creation_selection.clone())
728        });
729        {
730            let mut registry = self
731                .inner
732                .sessions
733                .producers
734                .lock()
735                .unwrap_or_else(|e| e.into_inner());
736            let state = registry.entry(session).or_default();
737            state.starting = true;
738            state.preempted = false;
739        }
740        let engine = self.clone();
741        tokio::spawn(async move {
742            #[cfg(test)]
743            engine.wait_for_producer_wake_hook().await;
744            let params = RunStartParams {
745                reset_fallback: false,
746                session_id: session,
747                client_run_id: ClientRunId::new(format!(
748                    "producer:{}",
749                    ProducerMessageId::new_v7()
750                ))
751                .expect("producer run ID"),
752                selection,
753                input: String::new(),
754            };
755            let result = engine
756                .start_run_direct(params, event_origin("engine:producer"), None, true)
757                .await;
758            let _ = engine.finish_compaction(session).await;
759            let successful = result.is_ok();
760            if let Err(error) = result
761                && !matches!(error, EngineError::Producer(_))
762            {
763                eprintln!("session {session} producer wake failed: {error}");
764            }
765            let _ = engine
766                .request(session, |reply| {
767                    SessionCommand::Producer(ProducerCommand::WakeFinished { successful, reply })
768                })
769                .await;
770        });
771        Ok(())
772    }
773
774    /// A producer wake this session has scheduled or started but not finished.
775    /// Complements the durable pending-mail signal while a wake is in flight.
776    pub(super) fn producer_wake_in_flight(&self, session: SessionId) -> bool {
777        self.inner
778            .sessions
779            .producers
780            .lock()
781            .unwrap_or_else(|e| e.into_inner())
782            .get(&session)
783            .is_some_and(|state| state.starting || state.wake_scheduled)
784    }
785
786    #[cfg(test)]
787    pub(crate) fn install_producer_wake_hook(
788        &self,
789    ) -> (oneshot::Receiver<()>, Arc<tokio::sync::Notify>) {
790        let (reached, receiver) = oneshot::channel();
791        let release = Arc::new(tokio::sync::Notify::new());
792        *self
793            .inner
794            .test_hooks
795            .producer_wake_hook
796            .lock()
797            .expect("producer wake hook lock poisoned") = Some(Arc::new(super::PagingRaceHook {
798            reached: std::sync::Mutex::new(Some(reached)),
799            release: Arc::clone(&release),
800        }));
801        (receiver, release)
802    }
803
804    #[cfg(test)]
805    async fn wait_for_producer_wake_hook(&self) {
806        let hook = self
807            .inner
808            .test_hooks
809            .producer_wake_hook
810            .lock()
811            .expect("producer wake hook lock poisoned")
812            .clone();
813        if let Some(hook) = hook {
814            if let Some(reached) = hook
815                .reached
816                .lock()
817                .expect("producer wake hook reached lock poisoned")
818                .take()
819            {
820                let _ = reached.send(());
821            }
822            hook.release.notified().await;
823        }
824    }
825
826    fn commit_producer_start(
827        &self,
828        session: SessionId,
829        run: RunId,
830        event: Event,
831    ) -> Result<(), EngineError> {
832        self.reconcile_goal_registration(session, None)?;
833        if self
834            .inner
835            .sessions
836            .producers
837            .lock()
838            .unwrap_or_else(|e| e.into_inner())
839            .get(&session)
840            .is_some_and(|state| state.preempted)
841        {
842            return Err(EngineError::Producer(
843                "session input or goal changed before automatic admission".into(),
844            ));
845        }
846        let projection = self.goal_producer_projection(session)?;
847        let real = projection
848            .messages
849            .iter()
850            .any(|message| pending(message) && message.reminder.is_none());
851        let user_pending = super::residency::has_runless_pending_inputs(
852            &self.inner.store.get(session)?.log.event_snapshot(),
853        );
854        if !real {
855            let valid = projection.goal.as_ref().is_some_and(|goal| {
856                goal.status == GoalStatus::Active
857                    && self.ready_goal_producer(session, goal.goal_id).is_some()
858                    && !user_pending
859            });
860            if !valid {
861                return Err(EngineError::Producer(
862                    "goal reminder is no longer ready".into(),
863                ));
864            }
865        }
866        let messages: Vec<_> = projection
867            .messages
868            .iter()
869            .filter(|message| pending(message) && (!real || message.reminder.is_none()))
870            .collect();
871        if messages.is_empty() {
872            return Err(EngineError::Producer("no pending producer input".into()));
873        }
874        self.inner.store.persist_buffered_session(session)?;
875        self.append_direct(session, Some(run), event_origin("engine:producer"), event)?;
876        for message in messages {
877            self.append_direct(
878                session,
879                Some(run),
880                event_origin("engine:producer"),
881                Event::ProducerMessageAdmitted {
882                    message_id: message.message_id,
883                },
884            )?;
885        }
886        Ok(())
887    }
888
889    fn record_recovery_diagnostics(&self, session: SessionId) -> Result<(), EngineError> {
890        for state in self.inner.plugins.producer_recovery_states() {
891            let kind = match state.status {
892                PluginRecoveryStatus::Failed => PluginDiagnosticKind::RecoveryFailed,
893                PluginRecoveryStatus::Disabled => PluginDiagnosticKind::RecoveryDisabled,
894                _ => continue,
895            };
896            let key = format!("{}:{:?}", state.plugin, state.status);
897            if self
898                .inner
899                .sessions
900                .producers
901                .lock()
902                .unwrap_or_else(|e| e.into_inner())
903                .entry(session)
904                .or_default()
905                .diagnostics
906                .contains(&key)
907            {
908                continue;
909            }
910            self.append_direct(session, None, event_origin("engine:plugin-host"), Event::PluginDiagnostic { plugin: state.plugin, kind, message: "Producer recovery is unavailable; external work is unknown and goal continuation is held.".into(), count: 1 })?;
911            self.inner
912                .sessions
913                .producers
914                .lock()
915                .unwrap_or_else(|e| e.into_inner())
916                .entry(session)
917                .or_default()
918                .diagnostics
919                .insert(key);
920        }
921        Ok(())
922    }
923    pub async fn get_session_goal(
924        &self,
925        params: SessionGoalGetParams,
926    ) -> Result<SessionGoalGetResult, EngineError> {
927        self.request(params.session_id, |reply| {
928            SessionCommand::Producer(ProducerCommand::GetGoal { reply })
929        })
930        .await
931    }
932
933    pub async fn set_session_goal(
934        &self,
935        params: SessionGoalSetParams,
936        origin: EventOrigin,
937    ) -> Result<SessionGoalSetResult, EngineError> {
938        self.request(params.session_id, |reply| {
939            SessionCommand::Producer(ProducerCommand::SetGoal {
940                objective: params.objective,
941                selection: params.selection,
942                origin,
943                reply,
944            })
945        })
946        .await
947    }
948
949    pub async fn change_session_goal_lifecycle(
950        &self,
951        params: SessionGoalLifecycleParams,
952        origin: EventOrigin,
953    ) -> Result<SessionGoalLifecycleResult, EngineError> {
954        self.request(params.session_id, |reply| {
955            SessionCommand::Producer(ProducerCommand::Lifecycle {
956                params,
957                origin,
958                reply,
959            })
960        })
961        .await
962    }
963
964    pub async fn session_producers(
965        &self,
966        params: SessionProducersParams,
967    ) -> Result<SessionProducersResult, EngineError> {
968        self.request(params.session_id, |reply| {
969            SessionCommand::Producer(ProducerCommand::Inspect { reply })
970        })
971        .await
972    }
973
974    pub async fn goal_get(&self, session: SessionId) -> Result<GoalGetResult, EngineError> {
975        let result = self
976            .get_session_goal(SessionGoalGetParams {
977                session_id: session,
978            })
979            .await?;
980        Ok(GoalGetResult { goal: result.goal })
981    }
982
983    pub async fn goal_update(
984        &self,
985        session: SessionId,
986        params: GoalUpdateParams,
987    ) -> Result<GoalUpdateResult, EngineError> {
988        self.request(session, |reply| {
989            SessionCommand::Producer(ProducerCommand::UpdateGoal { params, reply })
990        })
991        .await
992    }
993
994    pub(crate) async fn register_producer(
995        &self,
996        session: SessionId,
997        authority: ProducerAuthority,
998    ) -> Result<ProducerId, EngineError> {
999        self.request(session, |reply| {
1000            SessionCommand::Producer(ProducerCommand::Register { authority, reply })
1001        })
1002        .await
1003    }
1004
1005    #[allow(clippy::too_many_arguments)]
1006    pub(crate) async fn send_producer_message(
1007        &self,
1008        session: SessionId,
1009        authority: ProducerAuthority,
1010        producer_id: ProducerId,
1011        mode: ProducerDeliveryMode,
1012        key: ProducerIdempotencyKey,
1013        description: SafeDisplayText,
1014        body: String,
1015    ) -> Result<ProducerMessageId, EngineError> {
1016        self.request(session, |reply| {
1017            SessionCommand::Producer(ProducerCommand::Send {
1018                authority,
1019                producer_id,
1020                mode,
1021                key,
1022                description,
1023                body,
1024                reply,
1025            })
1026        })
1027        .await
1028    }
1029
1030    pub(crate) async fn unregister_producer(
1031        &self,
1032        session: SessionId,
1033        authority: ProducerAuthority,
1034        producer_id: ProducerId,
1035    ) -> Result<(), EngineError> {
1036        self.request(session, |reply| {
1037            SessionCommand::Producer(ProducerCommand::Unregister {
1038                authority,
1039                producer_id,
1040                reply,
1041            })
1042        })
1043        .await
1044    }
1045
1046    pub(crate) fn goal_producer_projection(
1047        &self,
1048        session: SessionId,
1049    ) -> Result<GoalProducerProjection, EngineError> {
1050        Ok(GoalProducerProjection::from_events(
1051            &self.inner.store.get(session)?.log.event_snapshot(),
1052        ))
1053    }
1054
1055    fn require_root_goal(&self, session: SessionId) -> Result<(), EngineError> {
1056        if !matches!(
1057            self.inner.store.get(session)?.meta.origin,
1058            SessionOrigin::Root
1059        ) {
1060            return Err(EngineError::Goal(
1061                "goals are available only in root sessions".into(),
1062            ));
1063        }
1064        Ok(())
1065    }
1066
1067    fn set_goal_direct(
1068        &self,
1069        session: SessionId,
1070        objective: String,
1071        selection: Option<RunSelection>,
1072        origin: EventOrigin,
1073    ) -> Result<SessionGoalSetResult, EngineError> {
1074        self.require_root_goal(session)?;
1075        if objective.trim().is_empty() {
1076            return Err(EngineError::Goal("objective must not be blank".into()));
1077        }
1078        if self
1079            .goal_producer_projection(session)?
1080            .goal
1081            .is_some_and(|goal| matches!(goal.status, GoalStatus::Active | GoalStatus::Paused))
1082        {
1083            return Err(EngineError::Goal(
1084                "cancel or complete the current goal before setting another".into(),
1085            ));
1086        }
1087        if let Some(selection) = &selection {
1088            self.freeze_root_selection(selection, false)?;
1089        }
1090        let goal_id = GoalId::new_v7();
1091        self.append_direct(
1092            session,
1093            None,
1094            origin,
1095            Event::GoalActivated {
1096                goal_id,
1097                objective,
1098                revision: 0,
1099                selection,
1100            },
1101        )?;
1102        self.invalidate_pending_goal_admission(session);
1103        Ok(SessionGoalSetResult {
1104            goal: self
1105                .goal_producer_projection(session)?
1106                .goal
1107                .expect("validated activation"),
1108        })
1109    }
1110
1111    fn checked_goal(
1112        &self,
1113        session: SessionId,
1114        goal_id: GoalId,
1115        expected_revision: u64,
1116    ) -> Result<GoalState, EngineError> {
1117        self.require_root_goal(session)?;
1118        let goal = self
1119            .goal_producer_projection(session)?
1120            .goal
1121            .ok_or_else(|| EngineError::Goal("no goal is set".into()))?;
1122        if goal.goal_id != goal_id || goal.revision != expected_revision {
1123            return Err(EngineError::Goal(format!(
1124                "stale goal: current goal_id={}, revision={}",
1125                goal.goal_id, goal.revision
1126            )));
1127        }
1128        if !matches!(goal.status, GoalStatus::Active | GoalStatus::Paused) {
1129            return Err(EngineError::Goal("terminal goals cannot be changed".into()));
1130        }
1131        Ok(goal)
1132    }
1133
1134    fn lifecycle_direct(
1135        &self,
1136        session: SessionId,
1137        params: SessionGoalLifecycleParams,
1138        origin: EventOrigin,
1139    ) -> Result<SessionGoalLifecycleResult, EngineError> {
1140        let goal = self.checked_goal(session, params.goal_id, params.expected_revision)?;
1141        if params.selection.is_some() && params.action != GoalLifecycleAction::Resume {
1142            return Err(EngineError::Goal(
1143                "selection is only allowed when resuming a goal".into(),
1144            ));
1145        }
1146        if let Some(selection) = &params.selection {
1147            self.freeze_root_selection(selection, false)?;
1148        }
1149        let status = match (goal.status, params.action) {
1150            (GoalStatus::Active, GoalLifecycleAction::Pause) => GoalStatus::Paused,
1151            (GoalStatus::Paused, GoalLifecycleAction::Resume) => GoalStatus::Active,
1152            (_, GoalLifecycleAction::Cancel) => GoalStatus::Cancelled,
1153            _ => return Err(EngineError::Goal("invalid lifecycle transition".into())),
1154        };
1155        let revision = next_revision(goal.revision)?;
1156        let notify_root = matches!(status, GoalStatus::Paused | GoalStatus::Cancelled)
1157            && self.has_goal_control_audience(session, goal.goal_id)?;
1158        self.append_direct(
1159            session,
1160            None,
1161            origin,
1162            Event::GoalLifecycleChanged {
1163                goal_id: goal.goal_id,
1164                status,
1165                revision,
1166                selection: params.selection,
1167            },
1168        )?;
1169        self.invalidate_pending_goal_admission(session);
1170        if notify_root {
1171            self.accept_goal_control_direct(session, &goal, status, revision)?;
1172        }
1173        self.reconcile_goal_registration(session, None)?;
1174        Ok(SessionGoalLifecycleResult {
1175            goal: self
1176                .goal_producer_projection(session)?
1177                .goal
1178                .expect("validated lifecycle"),
1179        })
1180    }
1181
1182    fn invalidate_pending_goal_admission(&self, session: SessionId) {
1183        if let Some(state) = self
1184            .inner
1185            .sessions
1186            .producers
1187            .lock()
1188            .unwrap_or_else(|error| error.into_inner())
1189            .get_mut(&session)
1190            && state.starting
1191        {
1192            state.preempted = true;
1193        }
1194    }
1195
1196    fn has_goal_control_audience(
1197        &self,
1198        session: SessionId,
1199        goal_id: GoalId,
1200    ) -> Result<bool, EngineError> {
1201        let projection = self.inner.store.get(session)?;
1202        if projection.status == SessionStatus::Running {
1203            return Ok(true);
1204        }
1205        {
1206            let registry = self
1207                .inner
1208                .sessions
1209                .producers
1210                .lock()
1211                .unwrap_or_else(|error| error.into_inner());
1212            if let Some(state) = registry.get(&session) {
1213                let own = state
1214                    .registrations
1215                    .iter()
1216                    .find(|record| record.authority.owner == ProducerOwner::Goal { goal_id })
1217                    .map(|record| record.id);
1218                if state.registrations.iter().any(|record| {
1219                    Some(record.id) != own
1220                        && self.validate_producer_authority(&record.authority).is_ok()
1221                }) {
1222                    return Ok(true);
1223                }
1224            }
1225        }
1226        let events = projection.log.event_snapshot();
1227        Ok(super::residency::has_runless_pending_inputs(&events)
1228            || GoalProducerProjection::from_events(&events)
1229                .messages
1230                .iter()
1231                .any(|message| pending(message) && message.reminder.is_none()))
1232    }
1233
1234    fn accept_goal_control_direct(
1235        &self,
1236        session: SessionId,
1237        goal: &GoalState,
1238        status: GoalStatus,
1239        revision: u64,
1240    ) -> Result<(), EngineError> {
1241        let objective = serde_json::to_string(&goal.objective).expect("goal objective serializes");
1242        let body = match status {
1243            GoalStatus::Paused => format!(
1244                "[engine goal control]\nThe goal {objective} was paused by the user. Stop pursuing it autonomously. Wrap up current work and report status, or follow new user directions."
1245            ),
1246            GoalStatus::Cancelled => format!(
1247                "[engine goal control]\nThe goal {objective} was cancelled by the user. Stop pursuing this cancelled goal and do not call goal_update to maintain it. Follow new user directions."
1248            ),
1249            _ => return Ok(()),
1250        };
1251        let authority = ProducerAuthority {
1252            owner: ProducerOwner::GoalControl {
1253                goal_id: goal.goal_id,
1254            },
1255            connection_epoch: None,
1256        };
1257        let producer_id = self.register_producer_direct(session, authority.clone())?;
1258        let accepted = self.accept_producer_direct(
1259            &authority,
1260            ExtensionProducerSendParams {
1261                session_id: session,
1262                producer_id,
1263                mode: ProducerDeliveryMode::Steer,
1264                idempotency_key: ProducerIdempotencyKey::new(format!("goal-control:{revision}"))
1265                    .expect("goal control idempotency key"),
1266                description: producer_description(
1267                    match status {
1268                        GoalStatus::Paused => "Goal paused: ",
1269                        GoalStatus::Active => "Goal resumed: ",
1270                        GoalStatus::Cancelled => "Goal cancelled: ",
1271                        GoalStatus::Completed => unreachable!("no completion control message"),
1272                    },
1273                    &goal.objective,
1274                ),
1275                body,
1276            },
1277            None,
1278        );
1279        // This registration exists only during the serialized control operation.
1280        // Its durable message remains pending even if the run ends before promotion.
1281        self.inner
1282            .sessions
1283            .producers
1284            .lock()
1285            .unwrap_or_else(|error| error.into_inner())
1286            .entry(session)
1287            .or_default()
1288            .registrations
1289            .retain(|record| record.id != producer_id);
1290        accepted.map(|_| ())
1291    }
1292
1293    fn update_goal_direct(
1294        &self,
1295        session: SessionId,
1296        params: GoalUpdateParams,
1297    ) -> Result<GoalUpdateResult, EngineError> {
1298        self.require_root_goal(session)?;
1299        let goal = self
1300            .goal_producer_projection(session)?
1301            .goal
1302            .ok_or_else(|| EngineError::Goal("no goal is set".into()))?;
1303        if !matches!(goal.status, GoalStatus::Active | GoalStatus::Paused) {
1304            return Err(EngineError::Goal("terminal goals cannot be changed".into()));
1305        }
1306        if params
1307            .items
1308            .iter()
1309            .any(|item| item.description.trim().is_empty())
1310        {
1311            return Err(EngineError::Goal(
1312                "items require nonblank descriptions".into(),
1313            ));
1314        }
1315        let revision = next_revision(goal.revision)?;
1316        let completed = !params.items.is_empty() && params.items.iter().all(|item| item.finished);
1317        let completion_revision = completed.then(|| next_revision(revision)).transpose()?;
1318        self.append_direct(
1319            session,
1320            None,
1321            event_origin("engine:goal"),
1322            Event::GoalChecklistRevised {
1323                goal_id: goal.goal_id,
1324                items: params.items,
1325                revision,
1326            },
1327        )?;
1328        if let Some(revision) = completion_revision {
1329            self.append_direct(
1330                session,
1331                None,
1332                event_origin("engine:goal"),
1333                Event::GoalLifecycleChanged {
1334                    goal_id: goal.goal_id,
1335                    status: GoalStatus::Completed,
1336                    revision,
1337                    selection: None,
1338                },
1339            )?;
1340        }
1341        self.invalidate_pending_goal_admission(session);
1342        self.reconcile_goal_registration(session, None)?;
1343        Ok(GoalUpdateResult {
1344            goal: self
1345                .goal_producer_projection(session)?
1346                .goal
1347                .expect("validated checklist"),
1348        })
1349    }
1350
1351    fn register_producer_direct(
1352        &self,
1353        session: SessionId,
1354        authority: ProducerAuthority,
1355    ) -> Result<ProducerId, EngineError> {
1356        self.ensure_not_shutting_down()?;
1357        self.validate_producer_authority(&authority)?;
1358        let id = ProducerId::new_v7();
1359        self.inner
1360            .sessions
1361            .producers
1362            .lock()
1363            .unwrap_or_else(|e| e.into_inner())
1364            .entry(session)
1365            .or_default()
1366            .registrations
1367            .push(Registration {
1368                id,
1369                authority,
1370                registered: Instant::now(),
1371            });
1372        Ok(id)
1373    }
1374
1375    fn validate_producer_authority(
1376        &self,
1377        authority: &ProducerAuthority,
1378    ) -> Result<(), EngineError> {
1379        if let ProducerOwner::Plugin { plugin } = &authority.owner {
1380            let Some(connection_epoch) = authority.connection_epoch else {
1381                return Err(EngineError::Producer(
1382                    "missing plugin connection epoch".into(),
1383                ));
1384            };
1385            if !self
1386                .inner
1387                .plugins
1388                .producer_connection_is_current(plugin, &connection_epoch)
1389            {
1390                return Err(EngineError::Producer(
1391                    "plugin connection is no longer live".into(),
1392                ));
1393            }
1394        }
1395        Ok(())
1396    }
1397
1398    fn require_registration(
1399        &self,
1400        session: SessionId,
1401        id: ProducerId,
1402        authority: &ProducerAuthority,
1403    ) -> Result<(), EngineError> {
1404        self.ensure_not_shutting_down()?;
1405        self.validate_producer_authority(authority)?;
1406        if !self
1407            .inner
1408            .sessions
1409            .producers
1410            .lock()
1411            .unwrap_or_else(|e| e.into_inner())
1412            .get(&session)
1413            .is_some_and(|state| {
1414                state.registrations.iter().any(|registration| {
1415                    registration.id == id && &registration.authority == authority
1416                })
1417            })
1418        {
1419            return Err(EngineError::Producer(
1420                "closed, foreign, or wrong-session registration".into(),
1421            ));
1422        }
1423        Ok(())
1424    }
1425
1426    fn accept_producer_direct(
1427        &self,
1428        authority: &ProducerAuthority,
1429        params: ExtensionProducerSendParams,
1430        reminder: Option<GoalReminderIdentity>,
1431    ) -> Result<ProducerMessageId, EngineError> {
1432        let ExtensionProducerSendParams {
1433            session_id: session,
1434            producer_id,
1435            mode,
1436            idempotency_key: key,
1437            description,
1438            body,
1439        } = params;
1440        self.require_registration(session, producer_id, authority)?;
1441        if description.as_str().trim().is_empty() {
1442            return Err(EngineError::Producer(
1443                "description must not be blank".into(),
1444            ));
1445        }
1446        if let Some(existing) = self
1447            .goal_producer_projection(session)?
1448            .messages
1449            .iter()
1450            .find(|message| {
1451                message.producer_owner == authority.owner && message.idempotency_key == key
1452            })
1453        {
1454            if existing.mode != mode
1455                || existing.description != description
1456                || existing.body != body
1457                || existing.reminder != reminder
1458            {
1459                return Err(EngineError::Producer(
1460                    "idempotency key already accepted with different payload".into(),
1461                ));
1462            }
1463            self.inner.store.persist_buffered_session(session)?;
1464            return Ok(existing.message_id);
1465        }
1466        let message_id = ProducerMessageId::new_v7();
1467        self.append_direct(
1468            session,
1469            None,
1470            event_origin("engine:producer"),
1471            Event::ProducerMessageAccepted {
1472                message_id,
1473                producer_owner: authority.owner.clone(),
1474                mode,
1475                idempotency_key: key,
1476                description,
1477                body,
1478                reminder,
1479                agent_hop: None,
1480            },
1481        )?;
1482        self.inner.store.persist_buffered_session(session)?;
1483        Ok(message_id)
1484    }
1485
1486    #[cfg(test)]
1487    #[allow(clippy::too_many_arguments)]
1488    pub(crate) fn accept_agent_message_direct_for_test(
1489        &self,
1490        session: SessionId,
1491        authority: &ProducerAuthority,
1492        producer_id: ProducerId,
1493        mode: ProducerDeliveryMode,
1494        key: ProducerIdempotencyKey,
1495        description: SafeDisplayText,
1496        sender: SessionId,
1497        sender_agent_type: &str,
1498        body: String,
1499        hop: u32,
1500    ) -> Result<ProducerMessageId, EngineError> {
1501        self.accept_agent_message_direct(
1502            session,
1503            authority,
1504            producer_id,
1505            mode,
1506            key,
1507            description,
1508            sender,
1509            sender_agent_type,
1510            body,
1511            true,
1512            hop,
1513        )
1514    }
1515
1516    #[allow(clippy::too_many_arguments)]
1517    fn accept_agent_message_direct(
1518        &self,
1519        session: SessionId,
1520        authority: &ProducerAuthority,
1521        producer_id: ProducerId,
1522        mode: ProducerDeliveryMode,
1523        key: ProducerIdempotencyKey,
1524        description: SafeDisplayText,
1525        sender: SessionId,
1526        _sender_agent_type: &str,
1527        body: String,
1528        include_reply_hint: bool,
1529        hop: u32,
1530    ) -> Result<ProducerMessageId, EngineError> {
1531        self.require_registration(session, producer_id, authority)?;
1532        let sender_short_id = self.session_short_id(sender);
1533        let projection = self.goal_producer_projection(session)?;
1534        if let Some(existing) = projection.messages.iter().find(|message| {
1535            message.producer_owner == authority.owner && message.idempotency_key == key
1536        }) {
1537            let envelope = super::messaging_api::render_agent_message_envelope(
1538                sender,
1539                sender_short_id.as_deref(),
1540                &body,
1541                include_reply_hint,
1542            );
1543            if existing.mode != mode
1544                || existing.description != description
1545                || existing.body != envelope
1546            {
1547                return Err(EngineError::Producer(
1548                    "idempotency key already accepted with different payload".into(),
1549                ));
1550            }
1551            self.inner.store.persist_buffered_session(session)?;
1552            return Ok(existing.message_id);
1553        }
1554        // Guards follow the replay check: a retried send keeps its original
1555        // receipt no matter how much chain depth or inbox pressure appeared
1556        // since the first acceptance.
1557        agent_message_guard(
1558            &projection,
1559            &authority.owner,
1560            hop,
1561            &self.inner.config.runtime.messaging,
1562        )
1563        .map_err(|code| EngineError::Messaging(code.to_owned()))?;
1564        let message_id = ProducerMessageId::new_v7();
1565        let envelope = super::messaging_api::render_agent_message_envelope(
1566            sender,
1567            sender_short_id.as_deref(),
1568            &body,
1569            include_reply_hint,
1570        );
1571        self.append_direct(
1572            session,
1573            None,
1574            event_origin("engine:producer"),
1575            Event::ProducerMessageAccepted {
1576                message_id,
1577                producer_owner: authority.owner.clone(),
1578                mode,
1579                idempotency_key: key,
1580                description,
1581                body: envelope,
1582                reminder: None,
1583                agent_hop: Some(hop),
1584            },
1585        )?;
1586        self.inner.store.persist_buffered_session(session)?;
1587        Ok(message_id)
1588    }
1589
1590    fn commit_delegation_completion_direct(
1591        &self,
1592        session: SessionId,
1593        reservation: &DelegationReservation,
1594        producer_id: Option<ProducerId>,
1595        teaser: super::delegation::DelegateTeaser,
1596    ) -> Result<bool, EngineError> {
1597        if teaser.session_id != reservation.child_session_id {
1598            return Err(EngineError::Producer(
1599                "completion child does not match reservation".into(),
1600            ));
1601        }
1602        let body = super::delegation::render_background_completion(&teaser);
1603        let description = producer_description("Delegation completed: ", &teaser.preview);
1604        let super::delegation::DelegateTeaser {
1605            short_id,
1606            status,
1607            preview,
1608            total_lines,
1609            ..
1610        } = teaser;
1611        let parent = self.inner.store.get(session)?;
1612        let visible = parent.log.event_snapshot().iter().any(|event| {
1613            !parent.log.delegation_event_tainted(event)
1614                && matches!(
1615                    &event.payload,
1616                    Event::DelegationReserved { reservation: candidate, .. }
1617                        if event.run_id == Some(reservation.parent_run_id)
1618                            && candidate == reservation
1619                            && reservation.parent_session_id == session
1620                )
1621        });
1622        if !visible {
1623            let authority = ProducerOwner::Delegation {
1624                invocation_id: reservation.invocation_id,
1625            };
1626            if let (Some(producer_id), Some(state)) = (
1627                producer_id,
1628                self.inner
1629                    .sessions
1630                    .producers
1631                    .lock()
1632                    .unwrap_or_else(|error| error.into_inner())
1633                    .get_mut(&session),
1634            ) {
1635                state.registrations.retain(|record| {
1636                    record.id != producer_id || record.authority.owner != authority
1637                });
1638            }
1639            return Ok(false);
1640        }
1641
1642        let authority = ProducerAuthority {
1643            owner: ProducerOwner::Delegation {
1644                invocation_id: reservation.invocation_id,
1645            },
1646            connection_epoch: None,
1647        };
1648        let events = self.inner.store.get(session)?.log.event_snapshot();
1649        let already_logged = events.iter().any(|event| {
1650            matches!(
1651                event.payload,
1652                Event::DelegateFinishedV2 { invocation_id, session_id, .. }
1653                    if invocation_id == reservation.invocation_id
1654                        && session_id == reservation.child_session_id
1655            )
1656        });
1657        let already_accepted = events.iter().any(|event| {
1658            matches!(
1659                &event.payload,
1660                Event::ProducerMessageAccepted {
1661                    producer_owner: ProducerOwner::Delegation { invocation_id },
1662                    ..
1663                } if *invocation_id == reservation.invocation_id
1664            )
1665        });
1666        if !already_accepted && !already_logged {
1667            let producer_id = producer_id.ok_or_else(|| {
1668                EngineError::Producer("delegation completion registration is missing".into())
1669            })?;
1670            self.accept_producer_direct(
1671                &authority,
1672                ExtensionProducerSendParams {
1673                    session_id: session,
1674                    producer_id,
1675                    mode: ProducerDeliveryMode::Steer,
1676                    idempotency_key: ProducerIdempotencyKey::new("delegation-completion")
1677                        .expect("static delegation idempotency key is valid"),
1678                    description,
1679                    body,
1680                },
1681                None,
1682            )?;
1683        }
1684        if !already_logged {
1685            self.append_direct(
1686                session,
1687                Some(reservation.parent_run_id),
1688                event_origin("engine:delegation"),
1689                Event::DelegateFinishedV2 {
1690                    invocation_id: reservation.invocation_id,
1691                    session_id: reservation.child_session_id,
1692                    short_id: Some(short_id),
1693                    status,
1694                    preview,
1695                    total_lines,
1696                },
1697            )?;
1698        }
1699        Ok(true)
1700    }
1701
1702    pub(super) fn reconcile_consumed_producers(
1703        &self,
1704        session: SessionId,
1705        recovery: bool,
1706        harvested: Option<&GoalProducerProjection>,
1707    ) -> Result<(), EngineError> {
1708        let projection = harvested
1709            .cloned()
1710            .unwrap_or(self.goal_producer_projection(session)?);
1711        for message in projection.messages {
1712            if let Some(run_id) = message
1713                .consumed_run
1714                .filter(|_| !message.consumption_recorded)
1715            {
1716                let event = Event::ProducerMessageConsumed {
1717                    message_id: message.message_id,
1718                    run_id,
1719                };
1720                if recovery {
1721                    self.append_recovery_direct(
1722                        session,
1723                        Some(run_id),
1724                        event_origin("engine:producer"),
1725                        event,
1726                    )?;
1727                } else {
1728                    self.append_direct(
1729                        session,
1730                        Some(run_id),
1731                        event_origin("engine:producer"),
1732                        event,
1733                    )?;
1734                }
1735            }
1736        }
1737        Ok(())
1738    }
1739
1740    pub(super) fn repair_goal_completion(
1741        &self,
1742        session: SessionId,
1743        recovery: bool,
1744        harvested: Option<&GoalProducerProjection>,
1745    ) -> Result<(), EngineError> {
1746        if !matches!(
1747            self.inner.store.get(session)?.meta.origin,
1748            SessionOrigin::Root
1749        ) {
1750            return Ok(());
1751        }
1752        let projection = harvested
1753            .cloned()
1754            .unwrap_or(self.goal_producer_projection(session)?);
1755        if let Some(goal) = projection.goal.filter(|goal| {
1756            matches!(goal.status, GoalStatus::Active | GoalStatus::Paused)
1757                && !goal.items.is_empty()
1758                && goal.items.iter().all(|item| item.finished)
1759        }) {
1760            let event = Event::GoalLifecycleChanged {
1761                goal_id: goal.goal_id,
1762                status: GoalStatus::Completed,
1763                revision: next_revision(goal.revision)?,
1764                selection: None,
1765            };
1766            if recovery {
1767                self.append_recovery_direct(session, None, event_origin("engine:goal"), event)?;
1768            } else {
1769                self.append_direct(session, None, event_origin("engine:goal"), event)?;
1770            }
1771        }
1772        Ok(())
1773    }
1774
1775    fn plugin_goals_ready(&self) -> bool {
1776        self.inner
1777            .plugins
1778            .producer_recovery_states()
1779            .iter()
1780            .all(|state| state.status == PluginRecoveryStatus::Ready)
1781    }
1782
1783    pub(super) fn reconcile_goal_registration(
1784        &self,
1785        session: SessionId,
1786        harvested: Option<&GoalProducerProjection>,
1787    ) -> Result<(), EngineError> {
1788        let is_root = matches!(
1789            self.inner.store.get(session)?.meta.origin,
1790            SessionOrigin::Root
1791        );
1792        let projection = harvested
1793            .cloned()
1794            .unwrap_or(self.goal_producer_projection(session)?);
1795        let active = projection.goal.as_ref().filter(|goal| {
1796            is_root && goal.status == GoalStatus::Active && self.plugin_goals_ready()
1797        });
1798        let mut registry = self
1799            .inner
1800            .sessions
1801            .producers
1802            .lock()
1803            .unwrap_or_else(|e| e.into_inner());
1804        let state = registry.entry(session).or_default();
1805        state.registrations.retain(|record| !matches!(&record.authority.owner, ProducerOwner::Goal { goal_id } if active.is_none_or(|goal| goal.goal_id != *goal_id)));
1806        if let Some(goal) = active
1807            && !state.registrations.iter().any(|record| {
1808                record.authority.owner
1809                    == (ProducerOwner::Goal {
1810                        goal_id: goal.goal_id,
1811                    })
1812            })
1813        {
1814            state.registrations.push(Registration {
1815                id: ProducerId::new_v7(),
1816                authority: ProducerAuthority {
1817                    owner: ProducerOwner::Goal {
1818                        goal_id: goal.goal_id,
1819                    },
1820                    connection_epoch: None,
1821                },
1822                registered: Instant::now(),
1823            });
1824        }
1825        drop(registry);
1826        for message in projection
1827            .messages
1828            .iter()
1829            .filter(|message| pending(message) && message.reminder.is_some())
1830        {
1831            if !message.claims.is_empty() {
1832                continue;
1833            }
1834            let identity = message.reminder.expect("filtered reminder");
1835            if !active.is_some_and(|goal| {
1836                goal.goal_id == identity.goal_id && goal.revision == identity.revision
1837            }) {
1838                self.append_direct(
1839                    session,
1840                    None,
1841                    event_origin("engine:goal"),
1842                    Event::ProducerMessageDiscarded {
1843                        message_id: message.message_id,
1844                        reminder: Some(identity),
1845                        producer_owner: Some(message.producer_owner.clone()),
1846                    },
1847                )?;
1848            }
1849        }
1850        Ok(())
1851    }
1852
1853    pub(super) fn promote_producer_inputs_direct(
1854        &self,
1855        session: SessionId,
1856        run: RunId,
1857        include_queue: bool,
1858    ) -> Result<bool, EngineError> {
1859        if !self
1860            .inner
1861            .store
1862            .get(session)?
1863            .runs
1864            .get(&run)
1865            .is_some_and(|run| run.status == SessionStatus::Running)
1866        {
1867            return Ok(false);
1868        }
1869        let projection = self.goal_producer_projection(session)?;
1870        let mut promoted = false;
1871        for message in projection.messages.iter().filter(|message| {
1872            pending(message)
1873                && (include_queue
1874                    || (message.mode == ProducerDeliveryMode::Steer && message.reminder.is_none()))
1875        }) {
1876            if message
1877                .admission
1878                .is_some_and(|(admitted_run, _)| admitted_run == run)
1879            {
1880                continue;
1881            }
1882            self.append_direct(
1883                session,
1884                Some(run),
1885                event_origin("engine:producer"),
1886                Event::ProducerMessageAdmitted {
1887                    message_id: message.message_id,
1888                },
1889            )?;
1890            promoted = true;
1891        }
1892        Ok(promoted)
1893    }
1894
1895    pub(super) async fn reconcile_producers(&self, session: SessionId) -> Result<(), EngineError> {
1896        self.request(session, |reply| {
1897            SessionCommand::Producer(ProducerCommand::Reconcile {
1898                projection: None,
1899                reply,
1900            })
1901        })
1902        .await
1903    }
1904
1905    pub(crate) async fn discard_producer_message(
1906        &self,
1907        session: SessionId,
1908        authority: ProducerAuthority,
1909        message_id: ProducerMessageId,
1910    ) -> Result<(), EngineError> {
1911        self.request(session, |reply| {
1912            SessionCommand::Producer(ProducerCommand::Discard {
1913                authority,
1914                message_id,
1915                reply,
1916            })
1917        })
1918        .await
1919    }
1920
1921    fn discard_producer_message_direct(
1922        &self,
1923        session: SessionId,
1924        authority: &ProducerAuthority,
1925        message_id: ProducerMessageId,
1926    ) -> Result<(), EngineError> {
1927        self.ensure_not_shutting_down()?;
1928        self.validate_producer_authority(authority)?;
1929        let projection = self.goal_producer_projection(session)?;
1930        let message = projection
1931            .messages
1932            .iter()
1933            .find(|message| message.message_id == message_id)
1934            .ok_or_else(|| EngineError::Producer("message not found".into()))?;
1935        if message.producer_owner != authority.owner {
1936            return Err(EngineError::Producer(
1937                "message belongs to another producer owner".into(),
1938            ));
1939        }
1940        if message.consumed || !message.claims.is_empty() {
1941            return Err(EngineError::Producer(
1942                "too late: message is consumed or reserved by a model request".into(),
1943            ));
1944        }
1945        if !message.discarded {
1946            self.append_direct(
1947                session,
1948                None,
1949                event_origin("engine:producer"),
1950                Event::ProducerMessageDiscarded {
1951                    message_id,
1952                    reminder: message.reminder,
1953                    producer_owner: Some(authority.owner.clone()),
1954                },
1955            )?;
1956        }
1957        self.inner.store.persist_buffered_session(session)?;
1958        Ok(())
1959    }
1960
1961    pub(super) fn producers_pin_session(&self, session: SessionId) -> bool {
1962        self.inner
1963            .sessions
1964            .producers
1965            .lock()
1966            .unwrap_or_else(|e| e.into_inner())
1967            .get(&session)
1968            .is_some_and(|state| !state.registrations.is_empty() || state.wake_scheduled)
1969            || self
1970                .goal_producer_projection(session)
1971                .is_ok_and(|projection| projection.messages.iter().any(pending))
1972    }
1973
1974    pub(super) fn reconcile_reverted_producers_direct(
1975        &self,
1976        session: SessionId,
1977    ) -> Result<(), EngineError> {
1978        let events = self.inner.store.get(session)?.log.event_snapshot();
1979        let surviving: HashSet<_> = events
1980            .iter()
1981            .filter_map(|event| match &event.payload {
1982                Event::DelegationReserved { reservation, .. } => Some(reservation.invocation_id),
1983                _ => None,
1984            })
1985            .collect();
1986        let removed_children = {
1987            let mut records = self
1988                .inner
1989                .delegation
1990                .by_session
1991                .lock()
1992                .unwrap_or_else(|e| e.into_inner());
1993            let removed: HashSet<_> = records
1994                .iter()
1995                .filter(|(_, record)| {
1996                    record.parent_session_id == session
1997                        && !surviving.contains(&record.invocation_id)
1998                })
1999                .map(|(child, _)| *child)
2000                .collect();
2001            records.retain(|child, _| !removed.contains(child));
2002            removed
2003        };
2004        self.inner
2005            .delegation
2006            .queue
2007            .lock()
2008            .unwrap_or_else(|e| e.into_inner())
2009            .retain(|child| !removed_children.contains(child));
2010        if let Some(state) = self
2011            .inner
2012            .sessions
2013            .producers
2014            .lock()
2015            .unwrap_or_else(|e| e.into_inner())
2016            .get_mut(&session)
2017        {
2018            state
2019                .registrations
2020                .retain(|record| match &record.authority.owner {
2021                    ProducerOwner::Delegation { invocation_id } => {
2022                        surviving.contains(invocation_id)
2023                    }
2024                    _ => true,
2025                });
2026        }
2027        self.reconcile_goal_registration(session, None)
2028    }
2029}
2030
2031fn next_revision(revision: u64) -> Result<u64, EngineError> {
2032    revision
2033        .checked_add(1)
2034        .ok_or_else(|| EngineError::Goal("revision exhausted".into()))
2035}
2036
2037/// Whether a log carries goal-producer state that needs reconciliation after a
2038/// restart: an active goal, an unread message, or a message consumed without its
2039/// consumption record.
2040pub(crate) fn producer_state_pending(events: &[cookie_agent_protocol::StoredEvent]) -> bool {
2041    let projection = GoalProducerProjection::from_events(events);
2042    projection.goal.is_some()
2043        || projection
2044            .messages
2045            .iter()
2046            .any(|message| pending(message) || (message.consumed && !message.consumption_recorded))
2047}
2048
2049/// Whether accepted-but-unconsumed producer mail is addressed to this session.
2050///
2051/// This is the race-free "a wake is owed" signal: `ProducerMessageAccepted` is
2052/// durable before a send reports success, and a message stays pending through
2053/// the whole window before its consumer's `RunStarted` flips session status to
2054/// `Running` (it clears only when a committed model turn has actually consumed
2055/// it). Liveness checks therefore cannot miss a steered finished child in the
2056/// gap between acceptance and run admission.
2057pub(super) fn has_pending_producer_message(events: &[cookie_agent_protocol::StoredEvent]) -> bool {
2058    GoalProducerProjection::from_events(events)
2059        .messages
2060        .iter()
2061        .any(pending)
2062}
2063
2064fn pending(message: &ProducerMessageRecord) -> bool {
2065    !message.consumed && !message.discarded
2066}
2067
2068/// Acceptance guards for one agent-mail send, evaluated against the recipient's
2069/// projected inbox *after* the idempotency replay check.
2070///
2071/// Pure (projection plus configuration only) so the chain-depth and window
2072/// arithmetic is testable without an actor, a log, or a model server. Order and
2073/// the `0` disable rules are the production contract: depth first, then the
2074/// recipient-wide inbox cap, then the directed sender window.
2075pub(crate) fn agent_message_guard(
2076    projection: &GoalProducerProjection,
2077    owner: &ProducerOwner,
2078    hop: u32,
2079    messaging: &cookie_agent_config::MessagingConfig,
2080) -> Result<(), &'static str> {
2081    let max_hops = messaging.max_hops;
2082    if max_hops > 0 && i64::from(hop) > i64::from(max_hops) {
2083        return Err(super::messaging_api::MESSAGE_MAX_HOPS_EXCEEDED);
2084    }
2085    let limit = messaging.max_pending_per_session;
2086    let pending_agent = projection
2087        .messages
2088        .iter()
2089        .filter(|message| {
2090            pending(message) && matches!(message.producer_owner, ProducerOwner::Agent { .. })
2091        })
2092        .count();
2093    if pending_agent >= limit {
2094        return Err(super::messaging_api::MESSAGE_INBOX_FULL);
2095    }
2096    // The pair window counts this sender's unacknowledged mail only: a
2097    // busy third party must not be able to starve one conversation.
2098    let pair_limit = messaging.max_inflight_per_pair;
2099    if pair_limit > 0 {
2100        let inflight = projection
2101            .messages
2102            .iter()
2103            .filter(|message| pending(message) && message.producer_owner == *owner)
2104            .count();
2105        if inflight >= usize::try_from(pair_limit).unwrap_or(usize::MAX) {
2106            return Err(super::messaging_api::MESSAGE_INFLIGHT_FULL);
2107        }
2108    }
2109    Ok(())
2110}
2111
2112fn producer_fault(error: EngineError) -> JsonRpcError {
2113    JsonRpcError {
2114        code: -32000,
2115        message: error.to_string(),
2116        data: None,
2117    }
2118}