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 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 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, ¶ms.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 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 _ => 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 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) = ¶ms.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 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 && ®istration.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 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
2037pub(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
2049pub(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
2068pub(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 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}