1use std::collections::{HashMap, VecDeque};
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use futures::StreamExt;
27use tokio::sync::{broadcast, mpsc, oneshot};
28
29use crate::engine::reactor::{
30 auto_turn_cap_reached, claim_auto_turn_with_cap, drain_event_queue, wake_action_with_cap,
31 EventDisposition, WakeAction,
32};
33use crate::engine::session::ConversationState;
34use crate::engine::setup::BackgroundTasks;
35use crate::extensions::invoke_output::{invoke_event_channel, InvokeOutputBudget};
36use crate::extensions::session_driver::{self as protocol, Grant, Reply};
37use crate::runtime::compaction::{
38 apply_compaction, compact_conversation, preview_compaction_disclosure, CompactionPolicy,
39 CompactionTransition,
40};
41use crate::tools::{SecretPromptHandle, SecretPromptRequest};
42use crate::{
43 AgentEvent, CancellationToken, EngineHost, Result, Runtime, SessionEvent,
44 StreamEvent,
45};
46
47use super::budgets;
48use super::handle::{SessionEndpoints, SessionHandle};
49use super::types::*;
50use super::view::RuntimeView;
51
52pub type ActiveStream = std::pin::Pin<Box<dyn futures::Stream<Item = StreamEvent> + Send>>;
54
55const TURN_REPLAY_CAP: usize = 4096;
57
58#[derive(Default)]
62pub(crate) struct TurnDraftState {
63 open: Option<String>,
65 base_len: usize,
67 text: String,
69 dirty: bool,
71}
72
73#[derive(Default)]
76pub(crate) struct CancelDrain {
77 pub(crate) history: Option<Vec<crate::SharedMessage>>,
79 pub(crate) closed: bool,
81}
82
83pub(crate) struct Live<T>(Option<T>);
88
89impl<T> Live<T> {
90 pub(crate) fn new(v: T) -> Self {
91 Self(Some(v))
92 }
93 pub(crate) fn is_live(&self) -> bool {
94 self.0.is_some()
95 }
96 pub(crate) fn park_take(&mut self) -> Option<T> {
97 self.0.take()
98 }
99 pub(crate) fn unpark_set(&mut self, v: T) {
100 self.0 = Some(v);
101 }
102}
103
104impl<T> std::ops::Deref for Live<T> {
105 type Target = T;
106 fn deref(&self) -> &T {
107 self.0.as_ref().expect("session is parked: runtime/conv unavailable")
108 }
109}
110
111impl<T> std::ops::DerefMut for Live<T> {
112 fn deref_mut(&mut self) -> &mut T {
113 self.0.as_mut().expect("session is parked: runtime/conv unavailable")
114 }
115}
116
117pub fn parked_evict_after() -> Option<std::time::Duration> {
123 let cfg_secs = crate::config::load_config().daemon.parked_evict_secs;
125 parked_evict_after_from(
126 std::env::var("SYNAPS_DAEMON_PARKED_EVICT_SECS").ok().as_deref(),
127 cfg_secs,
128 )
129}
130
131fn parked_evict_after_from(v: Option<&str>, cfg_secs: u64) -> Option<std::time::Duration> {
132 let from_cfg = || (cfg_secs != 0).then(|| std::time::Duration::from_secs(cfg_secs));
133 match v.map(str::trim) {
134 Some("never" | "0" | "off") => None,
135 Some(n) => n
136 .parse::<u64>()
137 .ok()
138 .map(std::time::Duration::from_secs)
139 .or_else(from_cfg),
140 None => from_cfg(),
141 }
142}
143
144pub fn idle_end_grace() -> std::time::Duration {
149 std::env::var("SYNAPS_DAEMON_IDLE_END_GRACE_SECS")
150 .ok()
151 .and_then(|v| v.trim().parse::<u64>().ok())
152 .map(std::time::Duration::from_secs)
153 .unwrap_or(std::time::Duration::from_secs(5))
154}
155
156pub fn park_grace() -> Option<std::time::Duration> {
159 match std::env::var("SYNAPS_DAEMON_PARK_GRACE_SECS") {
160 Ok(v) if v.trim().eq_ignore_ascii_case("never") => None,
161 Ok(v) => v
162 .trim()
163 .parse::<u64>()
164 .ok()
165 .map(std::time::Duration::from_secs)
166 .or(Some(DEFAULT_PARK_GRACE)),
167 Err(_) => Some(DEFAULT_PARK_GRACE),
168 }
169}
170
171pub const DEFAULT_PARK_GRACE: std::time::Duration = std::time::Duration::from_secs(60);
172
173pub fn prompt_abandon_timeout() -> Option<std::time::Duration> {
185 let cfg_secs = crate::config::load_config().daemon.prompt_abandon_secs;
187 prompt_abandon_timeout_from(
188 std::env::var("SYNAPS_DAEMON_PROMPT_ABANDON_SECS").ok().as_deref(),
189 cfg_secs,
190 )
191}
192
193fn prompt_abandon_timeout_from(v: Option<&str>, cfg_secs: u64) -> Option<std::time::Duration> {
194 let from_cfg = || (cfg_secs != 0).then(|| std::time::Duration::from_secs(cfg_secs));
195 match v.map(str::trim) {
196 Some("never" | "0" | "off") => None,
197 Some(n) => n
198 .parse::<u64>()
199 .ok()
200 .map(std::time::Duration::from_secs)
201 .or_else(from_cfg),
202 None => from_cfg(),
203 }
204}
205
206fn replayable(setting: &SessionSetting) -> bool {
210 matches!(
211 setting,
212 SessionSetting::SystemPrompt { .. }
213 | SessionSetting::ContextWindow { .. }
214 | SessionSetting::CompactionModel { .. }
215 | SessionSetting::ApiRetries { .. }
216 | SessionSetting::SubagentTimeout { .. }
217 | SessionSetting::MaxToolOutput { .. }
218 | SessionSetting::BashTimeout { .. }
219 | SessionSetting::BashMaxTimeout { .. }
220 | SessionSetting::GrantWorkerModel { .. }
221 )
222}
223
224fn replay_setting(rt: &mut Runtime, setting: &SessionSetting) {
225 match setting {
226 SessionSetting::SystemPrompt { text } => rt.set_system_prompt(text.clone()),
227 SessionSetting::ContextWindow { tokens } => rt.set_context_window(*tokens),
228 SessionSetting::CompactionModel { model } => rt.set_compaction_model(model.clone()),
229 SessionSetting::ApiRetries { n } => rt.set_api_retries(*n),
230 SessionSetting::SubagentTimeout { secs } => rt.set_subagent_timeout(*secs),
231 SessionSetting::MaxToolOutput { bytes } => rt.set_max_tool_output(*bytes),
232 SessionSetting::BashTimeout { secs } => rt.set_bash_timeout(*secs),
233 SessionSetting::BashMaxTimeout { secs } => rt.set_bash_max_timeout(*secs),
234 SessionSetting::GrantWorkerModel { model } => {
235 let _ = rt.grant_worker_model(model);
236 }
237 _ => {}
238 }
239}
240
241pub(crate) struct AttachedClient {
244 pub(crate) meta: ClientMeta,
245 pub(crate) mode: AttachMode,
246}
247
248pub(crate) struct CompactionJob {
250 pub(crate) task: tokio::task::JoinHandle<Result<crate::runtime::compaction::CompactionOutcome>>,
251 pub(crate) source: String,
252 #[allow(dead_code)]
253 pub(crate) started: tokio::time::Instant,
254}
255
256fn is_input_command(cmd: &SessionCommand) -> bool {
262 matches!(
263 cmd,
264 SessionCommand::Checkpoint { .. }
265 | SessionCommand::Submit { .. }
266 | SessionCommand::SubmitPrepared { .. }
267 | SessionCommand::Steer { .. }
268 | SessionCommand::Cancel
269 | SessionCommand::Set { .. }
270 | SessionCommand::Compact { .. }
271 | SessionCommand::NewSession
272 | SessionCommand::EngineCommand { .. }
273 | SessionCommand::PluginCommand { .. }
274 | SessionCommand::Resume { .. }
275 | SessionCommand::KeepWarm { .. }
276 | SessionCommand::DriverStart { .. }
277 | SessionCommand::End {
278 reason: EndReason::ClientQuit
279 }
280 )
281}
282
283fn command_name(cmd: &SessionCommand) -> &'static str {
284 match cmd {
285 SessionCommand::Submit { .. } => "submit",
286 SessionCommand::SubmitPrepared { .. } => "submit_prepared",
287 SessionCommand::Steer { .. } => "steer",
288 SessionCommand::Cancel => "cancel",
289 SessionCommand::Answer { .. } => "answer",
290 SessionCommand::Set { .. } => "set",
291 SessionCommand::Compact { .. } => "compact",
292 SessionCommand::NewSession => "new_session",
293 SessionCommand::Save => "save",
294 SessionCommand::Query { .. } => "query",
295 SessionCommand::Attach { .. } => "attach",
296 SessionCommand::Detach { .. } => "detach",
297 SessionCommand::End { .. } => "end",
298 SessionCommand::Resync { .. } => "resync",
299 SessionCommand::EngineCommand { .. } => "engine_command",
300 SessionCommand::PluginCommand { .. } => "plugin_command",
301 SessionCommand::Resume { .. } => "resume",
302 SessionCommand::Checkpoint { .. } => "checkpoint",
303 SessionCommand::KeepWarm { .. } => "keep_warm",
304 SessionCommand::Park => "park",
305 SessionCommand::HostEvent(_) => "host_event",
306 SessionCommand::DriverStart { .. } => "driver_start",
307 }
308}
309
310pub struct SessionActor {
311 pub(crate) id: SessionId,
312 pub(crate) meta: SessionMeta,
313 pub(crate) config: SessionConfig,
314 pub(crate) runtime: Live<Runtime>,
316 pub(crate) conv: Live<ConversationState>,
318 pub(crate) host: Arc<EngineHost>,
320 pub(crate) hook_bus: Arc<crate::extensions::hooks::HookBus>,
322 pub(crate) event_queue: Arc<crate::events::EventQueue>,
325 pub(crate) settings_replay: Vec<SessionSetting>,
327 pub(crate) keep_warm: bool,
328 pub(crate) cache_keepwarm: super::cache_keepwarm::KeepWarmState,
331 pub(crate) park_deadline: Option<tokio::time::Instant>,
333 pub(crate) prompt_abandon_deadline: Option<tokio::time::Instant>,
338 pub(crate) stream: Option<ActiveStream>,
340 pub(crate) cancel: Option<CancellationToken>,
341 pub(crate) steer_tx: Option<mpsc::UnboundedSender<String>>,
342 pub(crate) streaming: bool,
343 pub(crate) turn_baseline: usize,
344 pub(crate) consecutive_auto_turns: u32,
345 pub(crate) turn_steered_events: Vec<String>,
350 pub(crate) turn_completion: Option<crate::runtime::TurnCompletion>,
353 pub(crate) revoked_turn_deadline: Option<tokio::time::Instant>,
358 pub(crate) turn_draft: TurnDraftState,
360 pub(crate) persister: super::persister::Persister,
363 pub(crate) last_queued_save: Option<super::persister::SnapshotKey>,
366 pub(crate) secret_prompt_handle: SecretPromptHandle,
368 pub(crate) secret_prompt_rx: mpsc::UnboundedReceiver<SecretPromptRequest>,
369 pub(crate) pending_prompts: VecDeque<(PromptRequest, oneshot::Sender<Option<String>>)>,
371 pub(crate) next_prompt_id: u64,
372 pub(crate) cmd_rx: mpsc::Receiver<Addressed>,
374 pub(crate) events: broadcast::Sender<Envelope>,
375 pub(crate) view: Arc<arc_swap::ArcSwap<RuntimeView>>,
376 pub(crate) attached: HashMap<ClientId, AttachedClient>,
377 pub(crate) input_owner: Option<ClientId>,
380 pub(crate) next_client_id: u64,
381 pub(crate) seq: u64,
382 pub(crate) turn_replay: VecDeque<Envelope>,
383 pub(crate) state: AttachState,
384 pub(crate) background: BackgroundTasks,
385 pub(crate) compact: Option<CompactionJob>,
387 pub(crate) ext_ready: Option<oneshot::Receiver<()>>,
390 pub(crate) subagent_tick: Option<tokio::time::Interval>,
392 pub(crate) lifecycle: Arc<std::sync::atomic::AtomicU8>,
394 pub(crate) journal_id: Arc<arc_swap::ArcSwap<String>>,
396 pub(crate) presence: Arc<arc_swap::ArcSwap<super::handle::Presence>>,
398 pub(crate) name: Arc<arc_swap::ArcSwap<Option<String>>>,
400 pub(crate) session_lock: Option<agent_core::session_lock::SessionLock>,
402 pub(crate) driver: Option<super::driver::DriverState>,
404 pub(crate) driver_pending: Option<super::driver::DriverPending>,
405 pub(crate) driver_generation: u64,
407 pub(crate) driver_interrupted_owner: Option<String>,
409 pub(crate) driver_tick_interval: tokio::time::Interval,
411}
412
413impl SessionActor {
414 pub(crate) async fn create(
419 host: &Arc<EngineHost>,
420 mut cfg: SessionConfig,
421 ) -> Result<(SessionHandle, SessionTask)> {
422 let mut runtime = host.foreground_runtime_for(cfg.cwd.clone()).await?;
425 let config: crate::SynapsConfig = (**host.config()).clone();
426
427 let mut sb = crate::engine::setup::resolve_session_and_prompt(
428 &mut runtime,
429 &cfg.continue_session,
430 cfg.system.as_deref(),
431 cfg.prompt_manifest.as_deref(),
432 )?;
433 if let Some(name) = cfg.name.as_deref().map(str::trim).filter(|n| !n.is_empty()) {
437 sb.session
438 .set_name(name)
439 .map_err(|e| crate::RuntimeError::Session(format!("--name {name:?}: {e}")))?;
440 if cfg.persist {
441 if let Err(e) = sb.session.save().await {
444 tracing::warn!(session = %sb.session.id, "failed to save named session at create: {e}");
445 }
446 }
447 }
448 runtime.set_cwd(cfg.cwd.clone());
449 if cfg.env.is_none() {
453 if let Some(ref journal_env) = sb.session.env {
454 cfg.env = Some(journal_env.clone());
455 }
456 if cfg.env_stripped.is_empty() && !sb.session.env_stripped.is_empty() {
457 cfg.env_stripped = sb.session.env_stripped.clone();
458 }
459 }
460 if cfg.env.is_some() {
464 sb.session.env = cfg.env.clone();
465 sb.session.env_stripped = cfg.env_stripped.clone();
466 }
467 runtime.set_env(cfg.env.clone());
468 runtime.set_env_stripped(cfg.env_stripped.clone());
469 if let Some(ref m) = cfg.model_override {
471 runtime.set_model(m.clone());
472 }
473
474 let background =
475 crate::engine::setup::spawn_session_background(&runtime, &sb.session)?;
476 crate::engine::setup::finish_session_setup(
477 &mut runtime,
478 &config,
479 &sb.session,
480 cfg.cwd.clone(),
481 crate::engine::setup::IndexRecord::Start,
482 );
483
484 let ext_ready_timeout = {
491 let secs = config.startup.extensions_ready_timeout_secs;
492 if secs == 0 {
493 budgets::EXTENSIONS_READY_TIMEOUT
494 } else {
495 std::time::Duration::from_secs(secs)
496 }
497 };
498 let mut ext_ready = None;
499 if cfg.await_extensions {
500 if tokio::time::timeout(ext_ready_timeout, host.extensions_ready())
501 .await
502 .is_err()
503 {
504 tracing::warn!(
505 budget_secs = ext_ready_timeout.as_secs(),
506 "extensions_ready timed out — on_session_start may miss late extensions"
507 );
508 }
509 crate::extensions::loader::emit_session_start(runtime.hook_bus(), &sb.session.id)
510 .await;
511 } else {
512 let (tx, rx) = oneshot::channel();
513 let waiter_host = Arc::clone(host);
514 tokio::spawn(async move {
515 let _ = tokio::time::timeout(ext_ready_timeout, waiter_host.extensions_ready())
516 .await;
517 let _ = tx.send(());
518 });
519 ext_ready = Some(rx);
520 }
521
522 let id = SessionId::from(sb.session.id.clone());
523
524 let lock_kind = if cfg.await_extensions { "daemon" } else { "tui" };
526 let session_lock = if sb.continued {
527 let dir = agent_core::session_lock::sessions_dir();
530 let holder = agent_core::session_lock::LockHolder {
531 pid: std::process::id(),
532 kind: lock_kind.to_string(),
533 };
534 match agent_core::session_lock::SessionLock::try_acquire(&dir, &sb.session.id, holder) {
535 Ok(lock) => Some(lock),
536 Err(agent_core::session_lock::SessionLockError::Held { session_id, holder }) => {
537 let msg = agent_core::session_lock::SessionLockError::Held { session_id, holder };
538 return Err(crate::RuntimeError::Session(msg.to_string()));
539 }
540 Err(e) => {
541 tracing::warn!(session = %sb.session.id, "session lock: {e}");
542 None
543 }
544 }
545 } else {
546 let dir = agent_core::session_lock::sessions_dir();
548 let holder = agent_core::session_lock::LockHolder {
549 pid: std::process::id(),
550 kind: lock_kind.to_string(),
551 };
552 match agent_core::session_lock::SessionLock::try_acquire(&dir, &sb.session.id, holder) {
553 Ok(lock) => Some(lock),
554 Err(e) => {
555 tracing::warn!(session = %sb.session.id, "session lock: {e}");
556 None
557 }
558 }
559 };
560
561 let meta = SessionMeta {
562 id: id.clone(),
563 name: sb.session.name.clone(),
564 model: runtime.model().to_string(),
565 cwd: cfg.cwd.clone(),
566 created_at: sb.session.created_at,
567 continued: sb.continued,
568 continue_info: sb.continue_info.as_ref().map(ContinueInfoWire::from),
569 host_pid: std::process::id(),
570 lifecycle: SessionLifecycle::Live,
571 clients: 0,
572 input_owner: None,
573 awaiting_input: 0,
574 journal_id: sb.session.id.clone(),
575 locked_by: None,
576 };
577 let mut conv = if sb.continued {
578 ConversationState::from_resumed(sb.session)
579 } else {
580 ConversationState::new(sb.session)
581 };
582 conv.api_messages = sb.api_messages;
583 conv.total_input_tokens = sb.total_input_tokens;
584 conv.total_output_tokens = sb.total_output_tokens;
585 conv.session_cost = sb.session_cost;
586 if sb.continued && cfg.persist && session_lock.is_some() {
589 crate::engine::setup::recover_turn_draft(&mut conv).await;
590 }
591
592 let view = RuntimeView::from_runtime(&runtime).await;
593 let hook_bus = Arc::clone(runtime.hook_bus());
594 let event_queue = Arc::clone(runtime.event_queue());
595 let keep_warm = cfg.keep_warm;
596 let (
597 handle,
598 SessionEndpoints {
599 cmd_rx,
600 events,
601 view,
602 lifecycle,
603 journal_id,
604 presence,
605 name,
606 cache_keepwarm,
607 },
608 ) = SessionHandle::new(meta.clone(), view);
609 let (sp_tx, secret_prompt_rx) = mpsc::unbounded_channel();
610
611 let actor = SessionActor {
612 id,
613 meta,
614 config: cfg,
615 runtime: Live::new(runtime),
616 conv: Live::new(conv),
617 host: Arc::clone(host),
618 hook_bus,
619 event_queue,
620 settings_replay: Vec::new(),
621 keep_warm,
622 cache_keepwarm: super::cache_keepwarm::KeepWarmState::new(
623 config.cache_keepwarm,
624 config.cache_keepwarm_auto_max_hours,
625 cache_keepwarm,
626 ),
627 park_deadline: None,
628 prompt_abandon_deadline: None,
629 stream: None,
630 cancel: None,
631 steer_tx: None,
632 streaming: false,
633 turn_baseline: 0,
634 consecutive_auto_turns: 0,
635 turn_steered_events: Vec::new(),
636 turn_completion: None,
637 revoked_turn_deadline: None,
638 turn_draft: TurnDraftState::default(),
639 persister: super::persister::Persister::new(agent_core::session_lock::sessions_dir()),
640 last_queued_save: None,
641 secret_prompt_handle: SecretPromptHandle::new(sp_tx),
642 secret_prompt_rx,
643 pending_prompts: VecDeque::new(),
644 next_prompt_id: 1,
645 cmd_rx,
646 events,
647 view,
648 attached: HashMap::new(),
649 input_owner: None,
650 next_client_id: 1,
651 seq: 0,
652 turn_replay: VecDeque::new(),
653 state: AttachState::Detached { running: false },
654 background,
655 compact: None,
656 ext_ready,
657 subagent_tick: None,
658 lifecycle,
659 journal_id,
660 presence,
661 name,
662 session_lock,
663 driver: None,
664 driver_pending: None,
665 driver_generation: 0,
666 driver_interrupted_owner: None,
667 driver_tick_interval: {
668 let mut interval = tokio::time::interval(Duration::from_millis(200));
669 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
670 interval
671 },
672 };
673 Ok((handle, SessionTask(actor)))
674 }
675
676 pub(crate) fn sync_name(&mut self) {
680 let name = self.conv.session.name.clone();
681 self.meta.name = name.clone();
682 self.name.store(Arc::new(name.clone()));
683 if let Err(e) = self.background.update_session_name(name.as_deref()) {
684 tracing::warn!(session = %self.id, "registry name update failed: {e}");
685 }
686 }
687
688 pub(crate) fn emit(&mut self, event: SessionEventWire) {
701 let replay = self.streaming
702 && !matches!(
703 event,
704 SessionEventWire::Prompt(_)
705 | SessionEventWire::PromptResolved { .. }
706 | SessionEventWire::Attached { .. }
707 | SessionEventWire::QueryResult { .. }
708 | SessionEventWire::Conversation(_)
709 | SessionEventWire::Stream(StreamEvent::Session(
710 SessionEvent::MessageHistory(_)
711 ))
712 );
713 let env = Envelope {
714 session_id: self.id.clone(),
715 seq: self.seq,
716 ts: chrono::Utc::now(),
717 event,
718 };
719 self.seq += 1;
720 if replay {
721 if self.turn_replay.len() >= TURN_REPLAY_CAP {
722 self.turn_replay.pop_front();
723 }
724 let mut ring = env.clone();
725 if let SessionEventWire::TurnStarted { user_text, .. } = &mut ring.event {
726 *user_text = None;
727 }
728 self.turn_replay.push_back(ring);
729 }
730 let _ = self.events.send(env);
732 }
733
734 fn trim_replay_to_checkpoint(&mut self) {
742 self.turn_replay.retain(|env| {
743 !matches!(
744 env.event,
745 SessionEventWire::Stream(StreamEvent::Llm(_))
746 | SessionEventWire::Stream(StreamEvent::Agent(
747 AgentEvent::SteeringDelivered { .. }
748 ))
749 )
750 });
751 }
752
753 pub(crate) fn emit_conversation(&mut self) {
754 let snap = self.conv.snapshot(self.consecutive_auto_turns);
755 self.emit(SessionEventWire::Conversation(snap));
756 }
757
758 pub(crate) async fn publish_view(&mut self) {
759 let v = RuntimeView::from_runtime(&self.runtime).await;
760 self.view.store(Arc::new(v));
761 }
762
763 pub(crate) fn request_save(&mut self) {
767 if !self.config.persist || !self.conv.is_live() {
768 return;
769 }
770 let Some(session) = self.conv.prepare_save() else {
771 return;
772 };
773 let key = super::persister::SnapshotKey::of(&session);
774 if self.last_queued_save.as_ref() == Some(&key) && self.persister.saves_ok() {
777 return;
778 }
779 self.last_queued_save = Some(key);
780 self.persister.save(session);
781 }
782
783 pub(crate) async fn flush_saves(&mut self) -> bool {
787 let ok = match tokio::time::timeout(budgets::SAVE_TIMEOUT, self.persister.flush()).await {
788 Ok(ok) => ok,
789 Err(_) => {
790 tracing::warn!(session = %self.id, "session save still pending after the save budget");
791 false
792 }
793 };
794 if !ok {
795 self.last_queued_save = None;
798 }
799 ok
800 }
801
802 pub(crate) async fn save(&mut self) -> bool {
805 self.request_save();
806 self.flush_saves().await
807 }
808
809 pub(crate) async fn announce_idle(&mut self) {
815 self.flush_saves().await;
816 self.emit(SessionEventWire::Idle);
817 self.cache_keepwarm_on_idle().await;
818 }
819
820 async fn handle_context_head_checkpoint(
830 &mut self,
831 session_id: String,
832 messages: Vec<crate::SharedMessage>,
833 receipt: agent_core::core::context_head::ContextHeadReceipt,
834 ) {
835 if session_id != self.conv.session.id {
837 tracing::warn!(
838 event_id = %session_id,
839 actor_id = %self.conv.session.id,
840 "context head checkpoint: stale session id — ignoring"
841 );
842 receipt.complete(Err(std::io::Error::new(
843 std::io::ErrorKind::InvalidInput,
844 "stale session id after compaction",
845 )));
846 return;
847 }
848
849 if !self.config.persist {
851 receipt.complete(Ok(()));
852 return;
853 }
854
855 if !self.persister.flush().await {
858 tracing::warn!(session = %self.id, "context head checkpoint: an earlier save failed");
859 }
860 let result = self.conv.persist_context_head(&session_id, messages).await;
861 if let Err(ref e) = result {
862 tracing::error!(session = %session_id, "context head checkpoint save failed: {e}");
863 }
864 let succeeded = result.is_ok();
865 receipt.complete(result);
866
867 if let Some(driver) = &self.driver {
869 if !succeeded
870 || driver.grant.session_id != session_id
871 || session_id != self.conv.session.id
872 {
873 self.driver_revoke("context checkpoint failed or session replaced");
874 }
875 }
876 }
877
878
879 pub(crate) fn reload_record(&self) -> SessionReloadRecord {
881 let view = self.view.load();
882 SessionReloadRecord {
883 config: self.config.clone(),
884 keep_warm: self.keep_warm,
885 lifecycle: SessionLifecycle::from_u8(
886 self.lifecycle.load(std::sync::atomic::Ordering::Acquire),
887 ),
888 settings_replay: self.settings_replay.clone(),
889 cache_keepwarm: self.cache_keepwarm.override_mode.map(|m| m.to_string()),
890 model: view.model.clone(),
891 thinking_level: view.thinking_level.clone(),
892 }
893 }
894
895 pub(crate) fn publish_presence(&self) {
896 self.presence.store(Arc::new(super::handle::Presence {
897 clients: self.attached.len(),
898 input_owner: self.input_owner,
899 awaiting_input: self.pending_prompts.len(),
900 }));
901 }
902
903 pub(crate) fn update_attach_state(&mut self) {
906 self.publish_presence();
907 if matches!(self.state, AttachState::Parking | AttachState::Parked) {
908 return;
909 }
910 self.state = if self.attached.is_empty() {
911 AttachState::Detached {
912 running: self.streaming,
913 }
914 } else {
915 AttachState::Attached(self.attached.len())
916 };
917 self.rearm_park();
918 self.rearm_prompt_abandon();
919 }
920
921 pub(crate) fn is_parked(&self) -> bool {
924 matches!(self.state, AttachState::Parked)
925 }
926
927 pub(crate) fn can_park(&self) -> bool {
931 self.can_park_ignoring_keepwarm() && !self.cache_keepwarm.active()
935 }
936
937 pub(crate) fn can_park_ignoring_keepwarm(&self) -> bool {
939 self.attached.is_empty()
940 && !self.streaming
941 && self.compact.is_none()
942 && self.pending_prompts.is_empty()
943 && self.conv.is_live()
944 && !self.conv.api_messages.is_empty()
945 && self.conv.queued_message.is_none()
946 && !self.keep_warm
947 && self.config.persist
948 && !self.is_parked()
949 && self.driver.is_none()
950 }
951
952 fn cost_cap_breach(&self) -> Option<(&'static str, f64, f64)> {
958 let total = self.conv.session_cost;
959 if let Some(cap) = self.config.max_session_cost {
960 if cap.is_finite() && total >= cap {
961 return Some(("session", total, cap));
962 }
963 }
964 if let Some(driver) = &self.driver {
965 if let Some(cap) = driver.grant.effective_cost_cap(None) {
969 let run_cost = total - driver.cost_at_arm;
970 if cap.is_finite() && run_cost >= cap {
971 return Some(("run", run_cost, cap));
972 }
973 }
974 }
975 None
976 }
977
978 fn journal_exists(&self) -> bool { let id = (**self.journal_id.load()).clone();
980 crate::config::resolve_write_path("sessions")
981 .join(format!("{id}.json"))
982 .is_file()
983 }
984
985 pub(crate) fn driver_revoke(&mut self, reason: &str) {
990 let undelivered = self
991 .driver
992 .as_mut()
993 .map(|d| d.steering.drain(..).collect::<Vec<_>>())
994 .unwrap_or_default();
995 let was_active = self.driver.is_some() || self.driver_pending.is_some();
996 if self.driver.as_ref().is_some_and(|d| d.awaiting_terminal) {
998 self.note_cancel_cause(crate::engine::interrupt::InterruptReason::Driver);
999 }
1000 if let Some(driver) = &self.driver {
1001 self.driver_interrupted_owner = Some(driver.grant.plugin_id.clone());
1002 self.host
1003 .release_driver(&driver.grant.plugin_id, &self.id);
1004 }
1005 self.driver_generation = self.driver_generation.wrapping_add(1);
1006 self.driver_pending = None;
1007 self.driver = None;
1008 if was_active {
1009 self.emit(SessionEventWire::DriverRevoked {
1010 reason: reason.to_string(),
1011 undelivered_steering: undelivered,
1012 });
1013 }
1014 if self.turn_cancelled() && self.revoked_turn_deadline.is_none() {
1023 self.resolve_pending_prompts();
1024 let wait = if self.stream.is_some() {
1025 budgets::CANCEL_DRAIN_TIMEOUT
1026 } else {
1027 std::time::Duration::ZERO
1028 };
1029 self.revoked_turn_deadline = Some(tokio::time::Instant::now() + wait);
1030 }
1031 self.rearm_park();
1032 }
1033
1034 pub(crate) fn driver_start(&mut self, plugin: String, command: String, arg: String) {
1038 if self.driver.is_some() || self.driver_pending.is_some() {
1040 self.driver_revoke("explicit command");
1041 }
1042
1043 let manager = self.host.ext_manager().clone();
1044 let session_id = self.id.0.clone();
1045
1046 let (handler, timeout) = match manager.try_read() {
1048 Ok(mgr) => {
1049 let handler = match mgr.user_action_handler(&plugin) {
1050 Ok(h) => h,
1051 Err(error) => {
1052 self.emit(SessionEventWire::SystemNotice(error));
1053 return;
1054 }
1055 };
1056 let timeout = if mgr.session_driver_handler(&plugin).is_ok() {
1057 5
1058 } else {
1059 self.emit(SessionEventWire::SystemNotice(
1060 "session driver extension lacks validated session.drive permission".into(),
1061 ));
1062 return;
1063 };
1064 (handler, timeout)
1065 }
1066 Err(_) => {
1067 self.emit(SessionEventWire::SystemNotice(
1068 "extensions are loading or busy — try again shortly".into(),
1069 ));
1070 return;
1071 }
1072 };
1073
1074 let handler_generation = super::driver::live_generation(&handler).ok();
1075
1076 let owner = plugin.clone();
1077 let cmd = command.clone();
1078 let args: Vec<String> = arg.split_whitespace().map(str::to_owned).collect();
1079
1080 let generation = self.driver_generation;
1081 let task_handler = handler.clone();
1082 self.driver_pending = Some(super::driver::DriverPending {
1083 generation,
1084 session_id: session_id.clone(),
1085 task: super::driver::Task(tokio::spawn(async move {
1086 let (sink, collector) =
1087 invoke_event_channel(InvokeOutputBudget::default());
1088 let request_id = uuid::Uuid::new_v4().to_string();
1089 let (result, report) =
1090 tokio::time::timeout(Duration::from_secs(timeout), async {
1091 tokio::join!(
1092 task_handler.invoke_command(&cmd, args, &request_id, sink),
1093 collector.collect()
1094 )
1095 })
1096 .await
1097 .unwrap_or_else(|_| {
1098 (
1099 Err(format!("interactive command timed out ({timeout}s)")),
1100 crate::extensions::invoke_output::InvokeOutputReport {
1101 events: Vec::new(),
1102 counters: Default::default(),
1103 },
1104 )
1105 });
1106 super::driver::TaskResult::Command {
1107 owner,
1108 command,
1109 handler: task_handler,
1110 handler_generation,
1111 result,
1112 report,
1113 }
1114 })),
1115 });
1116 }
1117
1118 pub(crate) fn driver_arm(
1121 &mut self,
1122 owner: String,
1123 handler: Arc<dyn crate::extensions::runtime::ExtensionHandler>,
1124 handler_generation: u64,
1125 reply: Reply,
1126 ) -> std::result::Result<(), String> {
1127 let manager = self.host.ext_manager().clone();
1128 super::driver::same_handler(&manager, &owner, &handler, handler_generation)?;
1129
1130 if self.driver.is_some() {
1131 return Err("cannot replace an armed grant".into());
1132 }
1133
1134 let session_id = self.id.0.clone();
1135 let (grant, proposal) = Grant::from_start(&owner, &session_id, reply)?;
1136
1137 self.host
1139 .claim_driver(&owner, &self.id)
1140 .map_err(|e| e.to_string())?;
1141
1142 let cancel = CancellationToken::new();
1143 let workers = self.runtime.subagent_registry().clone();
1144 let worker_epoch = workers
1145 .lock()
1146 .unwrap_or_else(|p| p.into_inner())
1147 .set_spawn_cancellation(Some(cancel.clone()));
1148 let deadline_task = grant.deadline().map(|deadline| {
1149 let c = cancel.clone();
1150 let w = workers.clone();
1151 super::driver::Task(tokio::spawn(async move {
1152 tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
1153 c.cancel();
1154 super::driver::cancel_workers(&w, worker_epoch);
1155 }))
1156 });
1157
1158 let models = grant.models().to_vec();
1159 let proposal_notice = proposal.notice.clone();
1160 let selection = proposal.selection.clone();
1161 let deadline_ms = super::driver::deadline_ms(&grant);
1162 let run_id = grant.run_id.clone();
1163 let cost_at_arm = self.conv.session_cost;
1164
1165 let mut state = super::driver::DriverState {
1166 selection: proposal.selection.clone(),
1167 grant,
1168 handler,
1169 handler_generation,
1170 cancel,
1171 workers,
1172 worker_epoch,
1173 deadline_task,
1174 proposal: None,
1175 awaiting_terminal: false,
1176 outcome: None,
1177 feedback: crate::extensions::feedback::Tracker::default(),
1178 completed_feedback: "unknown",
1179 steering: VecDeque::new(),
1180 auto_wakes_blocked: false,
1181 cost_at_arm,
1182 };
1183
1184 super::driver::schedule(&mut state, proposal)?;
1185
1186 let context_notice = state.grant.apply_context_mode(&self.runtime)?;
1188 if let Some(text) = context_notice {
1189 self.emit(SessionEventWire::SystemNotice(text));
1190 }
1191
1192 self.driver_interrupted_owner = None;
1193 self.driver = Some(state);
1194
1195 self.emit(SessionEventWire::DriverArmed {
1196 plugin_id: owner,
1197 run_id,
1198 models,
1199 selection,
1200 deadline_ms,
1201 notice: proposal_notice,
1202 });
1203
1204 Ok(())
1205 }
1206
1207 fn driver_idle_conflict(&self) -> Option<&'static str> {
1212 if self.conv.queued_message.is_some() || !self.conv.pending_events.is_empty() {
1213 return Some("other queued work");
1214 }
1215 if self.compact.is_some() {
1216 return Some("compaction");
1217 }
1218 if self.ext_ready.is_some() {
1219 return Some("extensions loading or reloading");
1220 }
1221 if self.conv.context_head.is_blocked(&self.conv.session) {
1222 return Some("unverified context head");
1223 }
1224 if self.driver_completion_blocked() {
1225 return Some("outstanding workers require collection/reconciliation");
1226 }
1227 None
1228 }
1229
1230 fn driver_completion_blocked(&self) -> bool {
1232 use agent_core::orchestration::CompletionGate;
1233 self.runtime.orchestration().is_some_and(|o| {
1234 !matches!(o.completion_gate(), CompletionGate::Allowed)
1235 }) || self
1236 .runtime
1237 .subagent_registry()
1238 .lock()
1239 .map(|r| {
1240 r.list_active().iter().any(|(_, _, status)| {
1241 *status == crate::runtime::subagent::SubagentStatus::Running
1242 })
1243 })
1244 .unwrap_or(true)
1245 }
1246
1247 pub(crate) async fn driver_tick(&mut self) {
1251 use crate::extensions::session_driver::Outcome;
1252
1253 if self.stream.is_none() && self.turn_cancelled() {
1257 self.finish_revoked_turn(false).await;
1258 }
1259
1260 if let Some(driver) = &self.driver {
1262 let invalid = if driver.grant.session_id != self.id.0 {
1263 Some("session replaced".to_string())
1264 } else if driver.grant.expired() {
1265 Some("grant deadline reached".into())
1266 } else if driver.cancel.is_cancelled() {
1267 Some("canceled".into())
1268 } else {
1269 let manager = self.host.ext_manager().clone();
1270 super::driver::same_handler(
1271 &manager,
1272 &driver.grant.plugin_id,
1273 &driver.handler,
1274 driver.handler_generation,
1275 )
1276 .err()
1277 };
1278 if let Some(reason) = invalid {
1279 self.driver_revoke(&reason);
1280 return;
1281 }
1282 }
1283
1284 if self.driver.is_some() {
1286 if self.attached.is_empty() {
1290 self.driver_revoke("no clients attached");
1291 return;
1292 }
1293 if let Some(reason) = self.driver_idle_conflict() {
1296 self.driver_revoke(reason);
1297 return;
1298 }
1299
1300 if !self.runtime.event_queue().is_empty() {
1303 return;
1304 }
1305 }
1306
1307 if self
1309 .driver_pending
1310 .as_ref()
1311 .is_some_and(|p| p.task.0.is_finished())
1312 {
1313 let mut pending = self.driver_pending.take().expect("checked pending");
1314 if pending.generation != self.driver_generation
1315 || pending.session_id != self.id.0
1316 {
1317 return;
1318 }
1319 let result = match (&mut pending.task.0).await {
1320 Ok(result) => result,
1321 Err(error) => {
1322 self.driver_revoke("driver task failed");
1323 self.emit(SessionEventWire::SystemNotice(format!(
1324 "Session driver task failed: {error}"
1325 )));
1326 return;
1327 }
1328 };
1329 match result {
1330 super::driver::TaskResult::Command {
1331 owner,
1332 command: _,
1333 handler,
1334 handler_generation,
1335 result,
1336 report,
1337 } => {
1338 let current = self
1339 .host
1340 .ext_manager()
1341 .try_read()
1342 .ok()
1343 .and_then(|m| m.user_action_handler(&owner).ok());
1344 if !current
1345 .as_ref()
1346 .is_some_and(|current| Arc::ptr_eq(current, &handler))
1347 {
1348 self.driver_revoke(
1349 "command owner unloaded, replaced, or unavailable",
1350 );
1351 return;
1352 }
1353 let output_failed = report.is_limited()
1354 || report.events.iter().any(|event| {
1355 matches!(
1356 event,
1357 crate::extensions::runtime::InvokeCommandEvent::Output(
1358 crate::extensions::commands::CommandOutputEvent::Error { .. }
1359 )
1360 )
1361 });
1362 let mut lines: Vec<String> = Vec::new();
1367 for event in &report.events {
1368 if let crate::extensions::runtime::InvokeCommandEvent::Output(out) = event {
1369 let text = match out {
1370 crate::extensions::commands::CommandOutputEvent::Text { content }
1371 | crate::extensions::commands::CommandOutputEvent::System { content } => {
1372 Some(content.clone())
1373 }
1374 crate::extensions::commands::CommandOutputEvent::Error { content } => {
1375 Some(format!("Error: {content}"))
1376 }
1377 _ => None,
1378 };
1379 if let Some(text) = text {
1380 lines.push(text);
1381 }
1382 }
1383 }
1384 if !lines.is_empty() {
1385 const MAX_LINES: usize = 20;
1386 let truncated = lines.len() > MAX_LINES;
1387 if truncated {
1388 lines.truncate(MAX_LINES);
1389 lines.push("…output truncated".into());
1390 }
1391 self.emit(SessionEventWire::SystemNotice(lines.join("\n")));
1392 }
1393 if let Some(notice) = report.limit_notice() {
1394 self.emit(SessionEventWire::SystemNotice(format!(
1395 "command output truncated: {}",
1396 notice.message,
1397 )));
1398 }
1399 if let Ok(value) = result {
1400 let result = match protocol::parse_reply(&value) {
1401 Ok(Some(reply @ Reply::Start { .. })) => {
1402 if output_failed {
1403 Err("command reported an error or exceeded its output budget"
1404 .into())
1405 } else if self.streaming || self.stream.is_some() {
1406 Err("foreground turn still active".into())
1407 } else if let Some(reason) = self.driver_idle_conflict() {
1408 Err(reason.into())
1409 } else {
1410 match handler_generation {
1411 Some(generation) => {
1412 self.driver_arm(
1413 owner, handler, generation, reply,
1414 )
1415 }
1416 None => Err(
1417 "command owner did not expose a live lifecycle at dispatch"
1418 .into(),
1419 ),
1420 }
1421 }
1422 }
1423 Ok(Some(
1424 Reply::Stop { notice: text }
1425 | Reply::Status { notice: text },
1426 )) => {
1427 self.emit(SessionEventWire::SystemNotice(text));
1428 Ok(())
1429 }
1430 Ok(Some(Reply::Next { .. })) => Err(
1431 "next requires an armed poll, not an interactive command"
1432 .into(),
1433 ),
1434 Ok(None) => Ok(()),
1435 Err(error) => Err(error),
1436 };
1437 if let Err(error) = result {
1438 self.emit(SessionEventWire::SystemNotice(format!(
1439 "Session driver rejected: {error}"
1440 )));
1441 }
1442 }
1443 }
1444 super::driver::TaskResult::Poll(result) => {
1445 let Some(driver) = self.driver.as_mut() else {
1446 return;
1447 };
1448 let stop_notice = match &result {
1449 Ok(Reply::Stop { notice }) => Some(notice.clone()),
1450 _ => None,
1451 };
1452 match result.and_then(|reply| driver.grant.accept(reply)) {
1453 Ok(Some(proposal)) => {
1454 let text = proposal.notice.clone();
1455 if let Err(error) =
1456 super::driver::schedule(driver, proposal)
1457 {
1458 self.driver_revoke(&error);
1459 } else {
1460 self.emit(SessionEventWire::SystemNotice(text));
1461 }
1462 }
1463 Ok(None) => {
1464 self.driver_revoke("owner requested stop");
1465 if let Some(text) = stop_notice {
1466 self.emit(SessionEventWire::SystemNotice(text));
1467 }
1468 }
1469 Err(error) => {
1470 self.driver_revoke(&format!("invalid poll response: {error}"))
1471 }
1472 }
1473 }
1474 super::driver::TaskResult::Prepared { proposal, result } => {
1475 if self.streaming || self.stream.is_some() {
1476 self.driver_revoke("foreground work took priority");
1477 return;
1478 }
1479 if let Some(reason) = self.driver_idle_conflict() {
1480 self.driver_revoke(reason);
1481 return;
1482 }
1483 match *result {
1484 Err(protocol::PrepareError::Selection(error)) => {
1485 self.emit(SessionEventWire::SystemNotice(format!(
1486 "Session driver selection rejected (nothing sent): {error}"
1487 )));
1488 if let Some(driver) = self.driver.as_mut() {
1489 driver.outcome = Some((
1490 Outcome::SelectionRejected,
1491 "unknown".into(),
1492 ));
1493 }
1494 }
1495 Err(protocol::PrepareError::Blocked(error)) => {
1496 self.driver_revoke(&format!("preflight blocked: {error}"));
1497 }
1498 Ok(candidate) => {
1499 let validated = proposal.clone();
1500 let history = super::driver::history_with_steering(
1501 &self.conv.api_messages,
1502 &self
1503 .driver
1504 .as_ref()
1505 .map(|d| &d.steering)
1506 .cloned()
1507 .unwrap_or_default(),
1508 );
1509 if let Err(error) = protocol::validate_prepared(
1510 &candidate, &validated, &history,
1511 ) {
1512 self.driver_revoke(&format!(
1513 "prepared selection changed before commit: {error}"
1514 ));
1515 return;
1516 }
1517 {
1521 let driver = self.driver.as_mut().expect("driver present");
1522 let conv = &mut *self.conv;
1523 super::driver::commit_submission(
1524 &mut conv.api_messages,
1525 &mut driver.steering,
1526 &proposal.prompt,
1527 );
1528 }
1529 *self.runtime = candidate;
1531 self.conv.session.model = self.runtime.model().to_owned();
1532 self.conv.session.thinking_level =
1533 self.runtime.thinking_level().to_owned();
1534 self.consecutive_auto_turns = 0;
1535 self.turn_baseline = self.conv.api_messages.len();
1536 self.turn_steered_events.clear();
1537 self.turn_replay.clear();
1538 self.streaming = true;
1539 self.open_turn_draft();
1540 self.update_attach_state();
1541 self.emit(SessionEventWire::TurnStarted {
1542 turn_baseline: self.turn_baseline,
1543 trigger: TurnTrigger::DriverAuto,
1544 user_text: Some(proposal.prompt.clone()),
1545 });
1546
1547 if let Some(driver) = self.driver.as_mut() {
1549 driver.awaiting_terminal = true;
1550 driver.completed_feedback = "unknown";
1551 if driver.grant.feedback_enabled() {
1552 driver.feedback.begin_turn(
1553 &driver.selection.model,
1554 &driver.selection.effort,
1555 );
1556 }
1557 }
1558
1559 let handler = self
1561 .driver
1562 .as_ref()
1563 .expect("driver")
1564 .handler
1565 .clone();
1566 let handler_generation = self
1567 .driver
1568 .as_ref()
1569 .expect("driver")
1570 .handler_generation;
1571 let ct = self
1572 .driver
1573 .as_ref()
1574 .expect("driver")
1575 .cancel
1576 .child_token();
1577 self.cancel = Some(ct.clone());
1578 let completion = crate::runtime::TurnCompletion::with_default_cause(
1581 crate::runtime::CancelCause::Driver,
1582 );
1583 self.turn_completion = Some(completion.clone());
1584 let (tx, rx) = mpsc::unbounded_channel();
1585 self.steer_tx = Some(tx);
1586 let mut runtime = self.runtime.clone();
1587 runtime.share_ttl_latches(&self.runtime);
1591 let history = self.conv.api_messages.clone();
1592 let secret = self.secret_prompt_handle.clone();
1593 let session_id = self.id.0.clone();
1594 let generation = self.driver_generation;
1595 self.driver_pending = Some(super::driver::DriverPending {
1596 generation,
1597 session_id,
1598 task: super::driver::Task(tokio::spawn(async move {
1599 if ct.is_cancelled() {
1600 return super::driver::TaskResult::Started(
1601 Err("canceled before stream setup".into()),
1602 );
1603 }
1604 if let Err(error) =
1605 super::driver::same_lifecycle(
1606 &handler,
1607 handler_generation,
1608 )
1609 {
1610 return super::driver::TaskResult::Started(
1611 Err(error),
1612 );
1613 }
1614 tokio::select! {
1615 biased;
1616 _ = ct.cancelled() => super::driver::TaskResult::Started(Err("canceled during stream setup".into())),
1617 started = runtime.run_stream_tracked(history, ct.clone(), Some(rx), Some(secret), false, completion) => super::driver::TaskResult::Started(Ok(started)),
1618 }
1619 })),
1620 });
1621
1622 let mut tick = tokio::time::interval(
1624 std::time::Duration::from_secs(1),
1625 );
1626 tick.set_missed_tick_behavior(
1627 tokio::time::MissedTickBehavior::Delay,
1628 );
1629 tick.reset();
1630 self.subagent_tick = Some(tick);
1631 self.request_save();
1632 }
1633 }
1634 }
1635 super::driver::TaskResult::Started(started) => match started {
1636 Ok(started) if self.driver.is_some() => {
1637 self.stream = Some(started);
1638 }
1639 Err(error) => {
1640 self.driver_revoke(&error);
1641 }
1642 _ => {}
1643 },
1644 }
1645 }
1646
1647 if self.streaming || self.stream.is_some() || self.driver_pending.is_some() {
1649 return;
1650 }
1651 if let Some(reason) = self.driver_idle_conflict() {
1652 self.driver_revoke(reason);
1653 return;
1654 }
1655 let session_cost = self.conv.session_cost;
1656 let Some(driver) = self.driver.as_mut() else {
1657 return;
1658 };
1659 if let Some((outcome, error_kind)) = driver.outcome.take() {
1660 let request = super::driver::poll_request(driver, outcome, error_kind, session_cost);
1661 let handler = driver.handler.clone();
1662 let session_id = self.id.0.clone();
1663 let generation = self.driver_generation;
1664 self.driver_pending = Some(super::driver::DriverPending {
1665 generation,
1666 session_id,
1667 task: super::driver::Task(tokio::spawn(async move {
1668 let result = tokio::time::timeout(
1671 Duration::from_secs(30),
1672 protocol::poll(handler, request),
1673 )
1674 .await
1675 .unwrap_or_else(|_| Err("poll timed out (30s)".into()));
1676 super::driver::TaskResult::Poll(result)
1677 })),
1678 });
1679 } else if driver
1680 .proposal
1681 .as_ref()
1682 .is_some_and(|p| Instant::now() >= p.due)
1683 {
1684 let proposal =
1685 driver.proposal.take().expect("checked proposal").proposal;
1686 let runtime = self.runtime.clone();
1689 let history = super::driver::history_with_steering(
1690 &self.conv.api_messages,
1691 &driver.steering,
1692 );
1693 let session_id = self.id.0.clone();
1694 let generation = self.driver_generation;
1695 self.driver_pending = Some(super::driver::DriverPending {
1696 generation,
1697 session_id,
1698 task: super::driver::Task(tokio::spawn(async move {
1699 let result = tokio::time::timeout(
1700 Duration::from_secs(5),
1701 protocol::prepare(&runtime, &proposal, &history),
1702 )
1703 .await
1704 .unwrap_or_else(|_| {
1705 Err(protocol::PrepareError::Blocked(
1706 "preparation timed out (5s)".into(),
1707 ))
1708 });
1709 super::driver::TaskResult::Prepared {
1710 proposal,
1711 result: Box::new(result),
1712 }
1713 })),
1714 });
1715 }
1716 }
1717
1718 pub(crate) fn reacquire_session_lock(&mut self, new_id: &str) {
1723 match Self::try_lock_session(new_id) {
1724 Ok(lock) => self.session_lock = Some(lock),
1725 Err(e) => {
1726 tracing::warn!(session = %new_id, "reacquire session lock: {e}");
1727 self.session_lock = None;
1728 }
1729 }
1730 }
1731
1732 pub(crate) fn try_lock_session(
1734 id: &str,
1735 ) -> std::result::Result<agent_core::session_lock::SessionLock, agent_core::session_lock::SessionLockError>
1736 {
1737 let dir = agent_core::session_lock::sessions_dir();
1738 let holder = agent_core::session_lock::LockHolder {
1739 pid: std::process::id(),
1740 kind: "daemon".to_string(),
1741 };
1742 agent_core::session_lock::SessionLock::try_acquire(&dir, id, holder)
1743 }
1744
1745 pub(crate) fn can_end_idle(&self) -> bool {
1750 self.attached.is_empty()
1751 && !self.streaming
1752 && self.compact.is_none()
1753 && self.pending_prompts.is_empty()
1754 && self.conv.is_live()
1755 && self.conv.api_messages.is_empty()
1756 && self.conv.queued_message.is_none()
1757 && !self.keep_warm
1758 && !self.is_parked()
1759 && self.driver.is_none()
1764 && self.driver_pending.is_none()
1765 }
1766
1767 pub(crate) fn rearm_park(&mut self) {
1768 if self.is_parked() {
1769 return;
1772 }
1773 if self.can_end_idle() {
1774 if self.park_deadline.is_none() {
1779 self.park_deadline =
1780 Some(tokio::time::Instant::now() + idle_end_grace());
1781 }
1782 return;
1783 }
1784 if !self.can_park() {
1785 self.park_deadline = None;
1786 return;
1787 }
1788 match park_grace() {
1789 Some(grace) => {
1790 if self.park_deadline.is_none() {
1791 self.park_deadline = Some(tokio::time::Instant::now() + grace);
1792 }
1793 }
1794 None => self.park_deadline = None,
1795 }
1796 }
1797
1798 fn rearm_prompt_abandon(&mut self) {
1808 let armed_driver = self.driver.is_some() || self.driver_pending.is_some();
1809 let abandoned = self.attached.is_empty()
1810 && !self.pending_prompts.is_empty()
1811 && !armed_driver
1812 && !self.is_parked();
1813 if !abandoned {
1814 self.prompt_abandon_deadline = None;
1817 return;
1818 }
1819 if self.prompt_abandon_deadline.is_none() {
1820 self.prompt_abandon_deadline =
1822 prompt_abandon_timeout().map(|d| tokio::time::Instant::now() + d);
1823 }
1824 }
1825
1826 fn on_prompt_abandon_deadline(&mut self) {
1831 self.prompt_abandon_deadline = None;
1832 if !self.attached.is_empty() {
1833 return;
1834 }
1835 let had_prompts = !self.pending_prompts.is_empty();
1836 while let Some((pr, tx)) = self.pending_prompts.pop_front() {
1837 let _ = tx.send(None);
1838 self.emit(SessionEventWire::PromptResolved { prompt_id: pr.id });
1839 }
1840 if had_prompts {
1841 tracing::info!(session = %self.id, "pending prompt abandoned (no clients before deadline) — denied");
1842 self.publish_presence();
1843 self.rearm_park();
1845 }
1846 }
1847
1848 fn set_lifecycle(&mut self, l: SessionLifecycle) {
1849 self.lifecycle
1850 .store(l as u8, std::sync::atomic::Ordering::Release);
1851 self.emit(SessionEventWire::Lifecycle(l));
1852 }
1853
1854 pub(crate) async fn park(&mut self) -> std::ops::ControlFlow<EndReason> {
1858 self.park_deadline = None;
1859 if self.is_parked() {
1860 if self.attached.is_empty() {
1861 tracing::info!(session = %self.id, "parked past the eviction age — leaving the map (journal kept)");
1862 return std::ops::ControlFlow::Break(EndReason::Evicted);
1863 }
1864 return std::ops::ControlFlow::Continue(());
1865 }
1866 if self.can_end_idle() {
1867 tracing::info!(session = %self.id, "idle with no history — ending instead of parking (F18)");
1868 if let Some(lock) = self.session_lock.take() {
1871 lock.release_and_remove();
1872 }
1873 return std::ops::ControlFlow::Break(EndReason::Idle);
1874 }
1875 if !self.can_park() {
1876 return std::ops::ControlFlow::Continue(());
1877 }
1878 self.state = AttachState::Parking;
1879 self.set_lifecycle(SessionLifecycle::Parking);
1880 if !self.save().await {
1883 tracing::warn!(session = %self.id, "park: save failed or timed out — staying live");
1884 self.state = AttachState::Detached { running: false };
1885 self.set_lifecycle(SessionLifecycle::Live);
1886 return std::ops::ControlFlow::Continue(());
1887 }
1888 if !self.journal_exists() {
1889 tracing::warn!(session = %self.id, "park: no journal on disk — staying live");
1891 self.state = AttachState::Detached { running: false };
1892 self.set_lifecycle(SessionLifecycle::Live);
1893 return std::ops::ControlFlow::Continue(());
1894 }
1895 if self.runtime.session_manager().active_count() > 0 {
1896 self.emit(SessionEventWire::SystemNotice(
1897 "parked: background shells closed".into(),
1898 ));
1899 }
1900 self.runtime.session_manager().shutdown_all();
1901 self.turn_replay.clear();
1902 let conv = self.conv.park_take();
1905 drop(conv);
1906 let runtime = self.runtime.park_take();
1907 drop(runtime);
1908 crate::core::memstat::purge_arenas();
1912 self.session_lock = None;
1916 self.state = AttachState::Parked;
1917 self.set_lifecycle(SessionLifecycle::Parked);
1918 tracing::info!(session = %self.id, "session parked");
1919 self.park_deadline = parked_evict_after().map(|d| tokio::time::Instant::now() + d);
1923 std::ops::ControlFlow::Continue(())
1924 }
1925
1926 pub(crate) async fn unpark(&mut self) -> Result<()> {
1931 let started = std::time::Instant::now();
1932 self.park_deadline = None;
1934 let journal_id = (**self.journal_id.load()).clone();
1935
1936 {
1940 let dir = agent_core::session_lock::sessions_dir();
1941 let holder = agent_core::session_lock::LockHolder {
1942 pid: std::process::id(),
1943 kind: "daemon".to_string(),
1944 };
1945 match agent_core::session_lock::SessionLock::try_acquire(&dir, &journal_id, holder) {
1946 Ok(lock) => self.session_lock = Some(lock),
1947 Err(agent_core::session_lock::SessionLockError::Held { .. }) => {
1948 return Err(crate::RuntimeError::Session(format!(
1949 "cannot unpark session {}: journal locked by another process",
1950 journal_id
1951 )));
1952 }
1953 Err(e) => {
1954 tracing::warn!(session = %journal_id, "unpark: session lock: {e}");
1955 }
1957 }
1958 }
1959
1960 let host = Arc::clone(&self.host);
1961 let cfg = self.config.clone();
1962 let queue = Arc::clone(&self.event_queue);
1963 let replay = self.settings_replay.clone();
1964 let journal_present = self.journal_exists();
1965 let view = self.view.load_full();
1966 let build = async move {
1967 let config: crate::SynapsConfig = (**host.config()).clone();
1968 let mut runtime = host.foreground_runtime_for(cfg.cwd.clone()).await?;
1970 runtime.set_event_queue(queue);
1971 let mut sb = if journal_present {
1972 crate::engine::setup::resolve_session_and_prompt(
1973 &mut runtime,
1974 &Some(Some(journal_id)),
1975 cfg.system.as_deref(),
1976 cfg.prompt_manifest.as_deref(),
1977 )?
1978 } else {
1979 tracing::warn!(
1983 session = %journal_id,
1984 "unpark: journal missing — restoring as a fresh conversation"
1985 );
1986 let mut sb = crate::engine::setup::resolve_session_and_prompt(
1987 &mut runtime,
1988 &None,
1989 cfg.system.as_deref(),
1990 cfg.prompt_manifest.as_deref(),
1991 )?;
1992 sb.session.id = journal_id.clone();
1993 runtime.set_session_id(Some(journal_id));
1994 sb
1995 };
1996 runtime.set_cwd(cfg.cwd.clone());
1997 runtime.set_env(cfg.env.clone());
1998 runtime.set_env_stripped(cfg.env_stripped.clone());
1999 runtime.set_model(view.model.clone());
2002 let _ = runtime.restore_session_reasoning(&view.thinking_level);
2003 sb.session.model = view.model.clone();
2004 sb.session.thinking_level = runtime.thinking_level().to_string();
2005 crate::engine::setup::finish_session_setup(
2006 &mut runtime,
2007 &config,
2008 &sb.session,
2009 cfg.cwd.clone(),
2010 crate::engine::setup::IndexRecord::Skip,
2011 );
2012 for s in &replay {
2013 replay_setting(&mut runtime, s);
2014 }
2015 Ok::<_, crate::RuntimeError>((runtime, sb))
2016 };
2017 let (runtime, sb) = match tokio::time::timeout(budgets::UNPARK_TIMEOUT, build).await {
2018 Ok(Ok(v)) => v,
2019 Ok(Err(e)) => return Err(e),
2020 Err(_) => {
2021 return Err(crate::RuntimeError::Session(format!(
2022 "unpark timed out after {}s",
2023 budgets::UNPARK_TIMEOUT_SECS
2024 )))
2025 }
2026 };
2027 let mut conv = ConversationState::from_resumed(sb.session);
2028 conv.api_messages = sb.api_messages;
2029 conv.total_input_tokens = sb.total_input_tokens;
2030 conv.total_output_tokens = sb.total_output_tokens;
2031 conv.session_cost = sb.session_cost;
2032 if journal_present && self.config.persist && self.session_lock.is_some() {
2033 crate::engine::setup::recover_turn_draft(&mut conv).await;
2034 }
2035 self.runtime.unpark_set(runtime);
2036 self.conv.unpark_set(conv);
2037 self.publish_view().await;
2038 self.state = AttachState::Detached { running: false };
2039 self.set_lifecycle(SessionLifecycle::Live);
2040 self.update_attach_state();
2041 tracing::info!(session = %self.id, ms = started.elapsed().as_millis() as u64, "session unparked");
2042 Ok(())
2043 }
2044
2045 pub(crate) async fn ensure_live(&mut self) -> std::result::Result<(), String> {
2049 if !self.is_parked() {
2050 return Ok(());
2051 }
2052 self.unpark()
2053 .await
2054 .map_err(|e| format!("session parked and could not be restored: {e}"))
2055 }
2056
2057 pub(crate) fn busy(&self) -> bool {
2059 self.streaming || self.compact.is_some()
2060 }
2061
2062 pub(crate) fn publish_subagent_rows(&mut self) {
2073 let rows = self
2074 .runtime
2075 .subagent_registry()
2076 .lock()
2077 .unwrap_or_else(|p| p.into_inner())
2078 .display_rows();
2079 let running = rows
2080 .iter()
2081 .any(|r| matches!(r.status, crate::runtime::subagent::SubagentStatus::Running));
2082 if !rows.is_empty() {
2083 self.emit(SessionEventWire::SubagentRows(rows));
2084 }
2085 if !keep_subagent_tick(self.streaming, running) {
2086 self.subagent_tick = None;
2087 }
2088 }
2089
2090 fn subagents_running(&self) -> bool {
2092 self.runtime
2093 .subagent_registry()
2094 .lock()
2095 .unwrap_or_else(|p| p.into_inner())
2096 .display_rows()
2097 .iter()
2098 .any(|r| matches!(r.status, crate::runtime::subagent::SubagentStatus::Running))
2099 }
2100
2101 pub(crate) async fn start_turn(&mut self, trigger: TurnTrigger, user_text: Option<String>) {
2104 self.cache_keepwarm_turn_started();
2105 let ct = CancellationToken::new();
2106 let (s_tx, s_rx) = mpsc::unbounded_channel::<String>();
2107 self.streaming = true;
2108 self.turn_baseline = self.conv.api_messages.len();
2109 self.open_turn_draft();
2110 self.turn_steered_events.clear();
2111 self.turn_replay.clear();
2112 self.update_attach_state();
2113 self.emit(SessionEventWire::TurnStarted {
2114 turn_baseline: self.turn_baseline,
2115 trigger,
2116 user_text,
2117 });
2118 let auto_approve = self.config.auto_approve_confirms && self.driver.is_none();
2126 let completion = crate::runtime::TurnCompletion::new();
2127 self.turn_completion = Some(completion.clone());
2128 let stream = self
2129 .runtime
2130 .run_stream_tracked(
2131 self.conv.api_messages.clone(),
2132 ct.clone(),
2133 Some(s_rx),
2134 Some(self.secret_prompt_handle.clone()),
2135 auto_approve,
2136 completion,
2137 )
2138 .await;
2139 self.stream = Some(stream);
2140 self.cancel = Some(ct);
2141 self.steer_tx = Some(s_tx);
2142 let mut tick = tokio::time::interval(std::time::Duration::from_secs(1));
2143 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2144 tick.reset();
2145 self.subagent_tick = Some(tick);
2146 }
2147
2148 pub(crate) fn clear_stream(&mut self) {
2156 self.stream = None;
2157 self.cancel = None;
2158 self.steer_tx = None;
2159 self.streaming = false;
2160 self.turn_completion = None;
2161 self.revoked_turn_deadline = None;
2162 if !keep_subagent_tick(false, self.subagents_running()) {
2165 self.subagent_tick = None;
2166 }
2167 if !self.turn_steered_events.is_empty() && self.conv.is_live() {
2168 let undelivered = std::mem::take(&mut self.turn_steered_events);
2169 self.conv.pending_events.splice(0..0, undelivered);
2170 }
2171 self.request_save();
2174 self.close_turn_draft();
2175 self.update_attach_state();
2176 }
2177
2178 fn open_turn_draft(&mut self) {
2185 if !self.config.persist || !self.conv.is_live() || self.session_lock.is_none() {
2186 return;
2187 }
2188 self.turn_draft = TurnDraftState {
2189 open: Some(self.conv.session.id.clone()),
2190 base_len: self.conv.api_messages.len(),
2191 text: String::new(),
2192 dirty: true,
2193 };
2194 self.flush_turn_draft();
2195 }
2196
2197 fn turn_draft_text(&mut self, delta: &str) {
2198 let d = &mut self.turn_draft;
2199 if d.open.is_some()
2200 && d.text.len() < agent_core::core::session_draft::TURN_DRAFT_MAX_TEXT_BYTES
2201 {
2202 d.text.push_str(delta);
2203 d.dirty = true;
2204 }
2205 }
2206
2207 fn turn_draft_reset_text(&mut self) {
2208 let d = &mut self.turn_draft;
2209 if d.open.is_some() && !d.text.is_empty() {
2210 d.text.clear();
2211 d.dirty = true;
2212 }
2213 }
2214
2215 fn turn_draft_committed(&mut self) {
2217 if self.turn_draft.open.is_none() {
2218 return;
2219 }
2220 self.turn_draft.base_len = self.conv.api_messages.len();
2221 self.turn_draft.text.clear();
2222 self.turn_draft.dirty = true;
2223 self.flush_turn_draft();
2224 }
2225
2226 pub(crate) fn flush_turn_draft(&mut self) {
2229 let Some(open) = self.turn_draft.open.clone() else {
2230 return;
2231 };
2232 if !self.turn_draft.dirty {
2233 return;
2234 }
2235 let id = self.conv.session.id.clone();
2237 if open != id {
2238 self.persister.remove_draft(&open);
2239 self.turn_draft.open = Some(id.clone());
2240 }
2241 self.turn_draft.dirty = false;
2242 self.persister.write_draft(
2243 &id,
2244 agent_core::core::session_draft::TurnDraft {
2245 base_len: self.turn_draft.base_len,
2246 partial_text: self.turn_draft.text.clone(),
2247 },
2248 );
2249 }
2250
2251 fn close_turn_draft(&mut self) {
2257 if let Some(id) = self.turn_draft.open.take() {
2258 self.persister.remove_draft(&id);
2259 }
2260 self.turn_draft = TurnDraftState::default();
2261 }
2262
2263 pub(crate) async fn submit(
2265 &mut self,
2266 text: String,
2267 attachments: Vec<serde_json::Value>,
2268 from: Option<ClientId>,
2269 ) {
2270 if self.conv.context_head.is_blocked(&self.conv.session) {
2275 let reason = "context head is unverified after a failed checkpoint; reload the \
2276 session (`--continue`) or start a new one before continuing"
2277 .to_string();
2278 match from {
2281 Some(client) => self.emit(SessionEventWire::Refused {
2282 client,
2283 command: "submit".into(),
2284 reason,
2285 }),
2286 None => self.emit(SessionEventWire::SystemNotice(reason)),
2287 }
2288 return;
2289 }
2290 if self.streaming {
2291 if let Some(driver) = self.driver.as_mut() {
2297 let byte_total: usize = driver.steering.iter().map(|s| s.len()).sum();
2298 if driver.steering.len() >= super::driver::STEERING_MAX_MESSAGES {
2299 self.emit(SessionEventWire::SystemNotice(
2300 "steering queue full (16 messages) — message dropped".into(),
2301 ));
2302 return;
2303 }
2304 if byte_total + text.len() > super::driver::STEERING_MAX_BYTES {
2305 self.emit(SessionEventWire::SystemNotice(
2306 "steering queue full (256 KiB) — message dropped".into(),
2307 ));
2308 return;
2309 }
2310 driver.steering.push_back(text.clone());
2311 }
2312 self.steer(text);
2313 return;
2314 }
2315 if self.driver.is_some() {
2318 let driver = self.driver.as_mut().unwrap();
2319 let byte_total: usize = driver.steering.iter().map(|s| s.len()).sum();
2320 if driver.steering.len() >= super::driver::STEERING_MAX_MESSAGES {
2321 self.emit(SessionEventWire::SystemNotice(
2322 "steering queue full (16 messages) — message dropped".into(),
2323 ));
2324 return;
2325 }
2326 if byte_total + text.len() > super::driver::STEERING_MAX_BYTES {
2327 self.emit(SessionEventWire::SystemNotice(
2328 "steering queue full (256 KiB) — message dropped".into(),
2329 ));
2330 return;
2331 }
2332 driver.steering.push_back(text.clone());
2333 driver.auto_wakes_blocked = false;
2335 self.emit(SessionEventWire::Steered {
2336 text,
2337 delivered: false,
2338 });
2339 return;
2340 }
2341 if self.compact.is_some() {
2342 self.emit(SessionEventWire::Steered {
2344 text: text.clone(),
2345 delivered: false,
2346 });
2347 self.conv.queued_message = Some(text);
2348 return;
2349 }
2350 self.consecutive_auto_turns = 0;
2352 let user_text = text.clone();
2354 let api_content = text;
2355
2356 if attachments.is_empty() {
2357 self.conv.api_messages.push(std::sync::Arc::new(
2359 serde_json::json!({"role": "user", "content": api_content}),
2360 ));
2361 } else {
2362 let mut blocks = Vec::with_capacity(attachments.len() + 1);
2364 if !api_content.is_empty() {
2365 blocks.push(serde_json::json!({"type": "text", "text": api_content}));
2366 }
2367 blocks.extend(attachments);
2368 let candidate = std::sync::Arc::new(
2369 serde_json::json!({"role": "user", "content": blocks}),
2370 );
2371 let mut proposed = self.conv.api_messages.clone();
2374 proposed.push(candidate.clone());
2375 let model = self.runtime.model().to_string();
2376 if let Err(e) = crate::runtime::attachments::validate_history_for_request(&model, &proposed) {
2377 match from {
2381 Some(client) => self.emit(SessionEventWire::Refused {
2382 client,
2383 command: "submit".into(),
2384 reason: format!("attachments rejected: {e}"),
2385 }),
2386 None => self.emit(SessionEventWire::SystemNotice(format!(
2387 "attachments rejected: {e}"
2388 ))),
2389 }
2390 return;
2391 }
2392 self.conv.api_messages.push(candidate);
2393 }
2394 self.start_turn(TurnTrigger::User, Some(user_text)).await;
2395 }
2396
2397 pub(crate) fn steer(&mut self, text: String) {
2399 let delivered = self
2400 .steer_tx
2401 .as_ref()
2402 .map(|tx| tx.send(text.clone()).is_ok())
2403 .unwrap_or(false);
2404 self.emit(SessionEventWire::Steered {
2405 text: text.clone(),
2406 delivered,
2407 });
2408 self.conv.queued_message = Some(text);
2409 }
2410
2411 pub(crate) async fn cancel_turn(&mut self, reason: crate::engine::interrupt::InterruptReason) {
2433 if !self.streaming {
2434 if self.compact.is_some() {
2435 self.abort_compaction();
2436 if let Some(q) = self.conv.queued_message.take() {
2437 self.emit(SessionEventWire::Dequeued { text: q });
2438 }
2439 self.update_attach_state();
2440 }
2441 self.emit(SessionEventWire::Idle);
2442 return;
2443 }
2444 self.note_cancel_cause(reason);
2445 if let Some(ref ct) = self.cancel {
2446 ct.cancel();
2447 }
2448 self.resolve_pending_prompts();
2453 let drain = self.drain_cancelled_stream().await;
2454 match drain.history {
2455 Some(history) => self.conv.api_messages = history,
2456 None if !drain.closed => tracing::warn!(
2457 session = %self.id,
2458 budget_ms = budgets::CANCEL_DRAIN_TIMEOUT_MS,
2459 "cancelled turn did not publish its history within the drain budget; \
2460 keeping the last adopted history"
2461 ),
2462 None => {}
2463 }
2464 let completed = drain.closed && self.turn_completed();
2467 self.finish_interrupted_turn(reason, completed).await;
2468 }
2469
2470 fn turn_completed(&self) -> bool {
2472 self.turn_completion.as_ref().is_some_and(|c| c.completed())
2473 }
2474
2475 fn note_cancel_cause(&self, reason: crate::engine::interrupt::InterruptReason) {
2480 if !self.streaming {
2481 return;
2482 }
2483 if let (Some(turn), Some(cause)) = (&self.turn_completion, reason.cancel_cause()) {
2484 turn.note_cancel_cause(cause);
2485 }
2486 }
2487
2488 async fn finish_interrupted_turn(
2498 &mut self,
2499 reason: crate::engine::interrupt::InterruptReason,
2500 completed: bool,
2501 ) {
2502 crate::engine::stream::repair_history_after_failure(
2504 &mut self.conv.api_messages,
2505 self.turn_baseline,
2506 );
2507 let kept_partial = self.conv.api_messages.len() > self.turn_baseline;
2508 if !completed {
2509 crate::engine::interrupt::append_marker(&mut self.conv.api_messages, reason);
2510 }
2511 if let Some(q) = self.conv.queued_message.take() {
2513 self.emit(SessionEventWire::Dequeued { text: q });
2514 }
2515 {
2519 let undelivered = std::mem::take(&mut self.turn_steered_events);
2520 let conv: &mut ConversationState = &mut self.conv;
2521 for formatted in undelivered.into_iter().chain(conv.pending_events.drain(..)) {
2522 conv.api_messages.push(std::sync::Arc::new(serde_json::json!({
2523 "role": "user",
2524 "content": formatted
2525 })));
2526 }
2527 }
2528 self.clear_stream();
2530 {
2533 let mut registry = match self.runtime.subagent_registry().lock() {
2534 Ok(g) => g,
2535 Err(poisoned) => {
2536 tracing::warn!(
2537 "subagent registry mutex poisoned during abort; recovering to cancel running handles"
2538 );
2539 poisoned.into_inner()
2540 }
2541 };
2542 for handle in registry.iter_mut_handles() {
2543 if handle.status() == crate::runtime::subagent::SubagentStatus::Running {
2544 handle.cancel();
2545 }
2546 }
2547 }
2548 if completed {
2549 self.emit(SessionEventWire::Stream(StreamEvent::Session(SessionEvent::Done)));
2550 } else {
2551 self.emit(SessionEventWire::Aborted {
2554 context_saved: kept_partial,
2555 });
2556 }
2557 self.emit_conversation();
2558 self.announce_idle().await;
2559 }
2560
2561 async fn finish_revoked_turn(&mut self, stream_closed: bool) {
2569 if !self.streaming {
2570 return;
2571 }
2572 let completed = stream_closed && self.turn_completed();
2573 self.stream = None;
2574 self.finish_interrupted_turn(crate::engine::interrupt::InterruptReason::Driver, completed)
2575 .await;
2576 }
2577
2578 fn turn_cancelled(&self) -> bool {
2581 self.streaming && self.cancel.as_ref().is_some_and(|ct| ct.is_cancelled())
2582 }
2583
2584 fn resolve_pending_prompts(&mut self) {
2586 if self.pending_prompts.is_empty() {
2587 return;
2588 }
2589 while let Some((pr, tx)) = self.pending_prompts.pop_front() {
2590 let _ = tx.send(None);
2591 self.emit(SessionEventWire::PromptResolved { prompt_id: pr.id });
2592 }
2593 self.publish_presence();
2594 self.rearm_prompt_abandon();
2595 }
2596
2597 async fn drain_cancelled_stream(&mut self) -> CancelDrain {
2616 let mut out = CancelDrain::default();
2617 let Some(mut stream) = self.stream.take() else {
2618 out.closed = true;
2619 return out;
2620 };
2621 let deadline = tokio::time::Instant::now() + budgets::CANCEL_DRAIN_TIMEOUT;
2622 loop {
2623 let event = tokio::select! {
2624 biased;
2625 Some(req) = self.secret_prompt_rx.recv() => {
2626 let _ = req.response_tx.send(None);
2627 continue;
2628 }
2629 next = tokio::time::timeout_at(deadline, stream.next()) => match next {
2630 Err(_) => break,
2631 Ok(None) => {
2632 out.closed = true;
2633 break;
2634 }
2635 Ok(Some(event)) => event,
2636 },
2637 };
2638 match event {
2639 StreamEvent::Session(SessionEvent::Done) => {
2640 out.closed = true;
2641 break;
2642 }
2643 StreamEvent::Session(SessionEvent::MessageHistory(history)) => {
2644 out.history = Some(history);
2645 }
2646 StreamEvent::Session(SessionEvent::ContextHeadCheckpoint {
2647 session_id,
2648 messages,
2649 receipt,
2650 }) => {
2651 self.handle_context_head_checkpoint(session_id, messages, receipt)
2652 .await;
2653 out.history = None;
2657 }
2658 StreamEvent::Session(SessionEvent::Error(err)) => match err.outcome {
2659 crate::TurnOutcome::Canceled => {}
2660 crate::TurnOutcome::InterruptedAfterSideEffect { .. } => {
2661 self.emit(SessionEventWire::SystemNotice(err.message));
2662 }
2663 _ => {
2664 tracing::warn!(
2665 session = %self.id,
2666 category = err.category_label(),
2667 "cancelled turn reported an error while draining"
2668 );
2669 }
2670 },
2671 StreamEvent::Agent(AgentEvent::SteeringDelivered { ref message }) => {
2672 self.note_steering_delivered(message);
2673 self.emit(SessionEventWire::Stream(event));
2674 }
2675 StreamEvent::Session(SessionEvent::Usage { .. }) => {
2676 self.record_usage(&event);
2677 self.emit(SessionEventWire::Stream(event));
2678 }
2679 other => self.emit(SessionEventWire::Stream(other)),
2680 }
2681 }
2682 drop(stream);
2683 out
2684 }
2685
2686 fn note_steering_delivered(&mut self, message: &str) {
2688 if self.conv.queued_message.as_deref() == Some(message) {
2689 self.conv.queued_message = None;
2690 self.emit_conversation();
2691 }
2692 if let Some(driver) = self.driver.as_mut() {
2694 if driver.steering.front().map(String::as_str) == Some(message) {
2695 driver.steering.pop_front();
2696 }
2697 }
2698 if let Some(pos) = self.turn_steered_events.iter().position(|e| e == message) {
2699 self.turn_steered_events.remove(pos);
2700 }
2701 }
2702
2703 fn record_usage(&mut self, event: &StreamEvent) {
2705 if let StreamEvent::Session(SessionEvent::Usage {
2706 input_tokens,
2707 output_tokens,
2708 cache_read_input_tokens,
2709 cache_creation_input_tokens,
2710 cache_creation_5m,
2711 cache_creation_1h,
2712 model: usage_model,
2713 }) = event
2714 {
2715 let model_for_pricing = usage_model
2716 .as_deref()
2717 .unwrap_or(self.runtime.model())
2718 .to_string();
2719 self.cache_keepwarm_note_usage(crate::runtime::keepwarm::KeepWarmUsage {
2720 input_tokens: *input_tokens,
2721 output_tokens: *output_tokens,
2722 cache_read_tokens: *cache_read_input_tokens,
2723 cache_write_tokens: *cache_creation_input_tokens,
2724 cache_write_5m: *cache_creation_5m,
2725 cache_write_1h: *cache_creation_1h,
2726 model: usage_model.clone(),
2727 });
2728 self.conv.add_usage(
2729 *input_tokens,
2730 *output_tokens,
2731 *cache_read_input_tokens,
2732 *cache_creation_input_tokens,
2733 *cache_creation_5m,
2734 *cache_creation_1h,
2735 &model_for_pricing,
2736 );
2737 }
2738 }
2739
2740 async fn on_queue_wake(&mut self) {
2743 if self.is_parked() {
2744 if self.event_queue.is_empty() {
2745 return;
2746 }
2747 if let Err(e) = self.ensure_live().await {
2748 tracing::warn!(session = %self.id, error = %e, "event wake could not unpark; events stay queued");
2749 return;
2750 }
2751 }
2752 let busy = self.busy();
2753 let conv: &mut ConversationState = &mut self.conv;
2754 let drained = drain_event_queue(
2755 &self.event_queue,
2756 &mut conv.api_messages,
2757 &mut conv.pending_events,
2758 busy,
2759 self.steer_tx.as_ref(),
2760 );
2761 if drained.is_empty() {
2762 return;
2763 }
2764 for de in &drained {
2765 if de.disposition == EventDisposition::Steered {
2766 self.turn_steered_events.push(de.formatted.clone());
2767 }
2768 self.emit(SessionEventWire::External(de.event.clone()));
2769 }
2770 let injected = drained
2771 .iter()
2772 .any(|d| d.disposition == EventDisposition::Injected);
2773 if injected || busy {
2774 self.emit_conversation();
2775 }
2776
2777 if self.streaming
2780 && self
2781 .driver
2782 .as_ref()
2783 .is_some_and(|d| d.awaiting_terminal)
2784 {
2785 let competing = drained
2786 .iter()
2787 .any(|d| !matches!(d.disposition, EventDisposition::Steered | EventDisposition::DisplayOnly));
2788 if competing {
2789 self.driver_revoke("event-bus work took priority");
2790 }
2791 }
2792
2793 let auto_turn_enabled = self.host.config().events.auto_turn;
2798 let auto_turn_cap = self.host.config().events.auto_turn_cap;
2799 let action = wake_action_with_cap(
2800 &drained,
2801 &self.conv.api_messages,
2802 busy,
2803 auto_turn_enabled,
2804 self.consecutive_auto_turns,
2805 auto_turn_cap,
2806 );
2807 match action {
2808 WakeAction::RunTurn => {
2809 if self.driver.is_some() {
2812 tracing::debug!("on_queue_wake: RunTurn inhibited — driver armed");
2813 } else if self.stream.is_some() {
2814 tracing::warn!("handle_event_arm: RunTurn with active stream — skipping");
2815 } else {
2816 self.consecutive_auto_turns += 1;
2817 self.start_turn(TurnTrigger::EventAuto, None).await;
2818 }
2819 }
2820 WakeAction::Forward => {
2821 let hit_cap = injected
2822 && !busy
2823 && auto_turn_enabled
2824 && auto_turn_cap_reached(self.consecutive_auto_turns, auto_turn_cap);
2825 if hit_cap {
2826 self.emit(SessionEventWire::AutoTurnCapReached { cap: auto_turn_cap });
2827 }
2828 }
2829 WakeAction::Nothing => {}
2830 }
2831 }
2832
2833 async fn on_stream_event(&mut self, event: StreamEvent) {
2836 if self.turn_cancelled() {
2840 match event {
2841 StreamEvent::Session(SessionEvent::Done) => {
2842 self.finish_revoked_turn(true).await;
2843 return;
2844 }
2845 StreamEvent::Session(SessionEvent::Error(err)) => {
2847 match err.outcome {
2848 crate::TurnOutcome::Canceled => {}
2849 crate::TurnOutcome::InterruptedAfterSideEffect { .. } => {
2850 self.emit(SessionEventWire::SystemNotice(err.message));
2851 }
2852 _ => tracing::warn!(
2853 session = %self.id,
2854 category = err.category_label(),
2855 "revoked turn reported an error"
2856 ),
2857 }
2858 return;
2859 }
2860 _ => {}
2861 }
2862 }
2863 if !matches!(
2868 event,
2869 StreamEvent::Session(SessionEvent::ContextHeadCheckpoint { .. })
2870 ) {
2871 self.emit(SessionEventWire::Stream(event.clone()));
2872 }
2873
2874 if let Some(driver) = self.driver.as_mut() {
2876 if driver.awaiting_terminal && driver.grant.feedback_enabled() {
2877 driver.feedback.observe(&event);
2878 }
2879 }
2880
2881 let is_canceled = self
2883 .cancel
2884 .as_ref()
2885 .is_some_and(|ct| ct.is_cancelled());
2886 let terminal = if self
2887 .driver
2888 .as_ref()
2889 .is_some_and(|d| d.awaiting_terminal)
2890 {
2891 super::driver::capture_terminal(Some(&event), is_canceled)
2892 } else {
2893 None
2894 };
2895
2896 enum After {
2897 Continue,
2898 AutoSendQueued(String),
2899 AutoTriggerEvents,
2900 Failed,
2901 }
2902 let mut after = After::Continue;
2903
2904 match event {
2905 StreamEvent::Llm(crate::LlmEvent::Text(text)) => self.turn_draft_text(&text),
2906 StreamEvent::Llm(crate::LlmEvent::ResponseStart | crate::LlmEvent::ResponseReset) => {
2909 self.turn_draft_reset_text()
2910 }
2911 StreamEvent::Llm(_) => {}
2912 StreamEvent::Session(SessionEvent::MessageHistory(history)) => {
2913 self.conv.api_messages = history;
2919 self.request_save();
2920 self.turn_draft_committed();
2922 self.trim_replay_to_checkpoint();
2923 self.emit_conversation();
2924 }
2925 StreamEvent::Agent(AgentEvent::SteeringDelivered { ref message }) => {
2926 self.note_steering_delivered(message);
2927 }
2928 StreamEvent::Agent(_) => {}
2929 StreamEvent::Session(SessionEvent::Usage { .. }) => {
2930 self.record_usage(&event);
2931 if let Some((scope, cost, cap)) = self.cost_cap_breach() {
2936 self.emit(SessionEventWire::CostCapReached {
2937 scope: scope.to_string(),
2938 cost,
2939 cap,
2940 });
2941 self.note_cancel_cause(crate::engine::interrupt::InterruptReason::CostCap);
2942 if self.driver.is_some() || self.driver_pending.is_some() {
2943 self.driver_revoke(&format!("{scope} cost cap reached (${cost:.4} ≥ ${cap:.4})"));
2944 }
2945 self.cancel_turn(crate::engine::interrupt::InterruptReason::CostCap)
2946 .await;
2947 return;
2948 }
2949 }
2950 StreamEvent::Session(SessionEvent::Notice(_)) => {}
2951 StreamEvent::Session(SessionEvent::Done) => {
2952 self.clear_stream();
2953 self.publish_subagent_rows();
2954 let had_pending = !self.conv.pending_events.is_empty();
2956 {
2957 let conv: &mut ConversationState = &mut self.conv;
2958 for formatted in conv.pending_events.drain(..) {
2959 conv.api_messages
2960 .push(std::sync::Arc::new(serde_json::json!({
2961 "role": "user",
2962 "content": formatted
2963 })));
2964 }
2965 }
2966 if let Some(queued) = self.conv.queued_message.take() {
2967 after = After::AutoSendQueued(queued);
2968 } else if had_pending {
2969 self.request_save();
2970 after = After::AutoTriggerEvents;
2971 }
2972 self.emit_conversation();
2973 if matches!(after, After::Continue) {
2974 if self.config.auto_compact {
2975 self.post_turn_chat().await;
2976 }
2977 if self.compact.is_none() {
2979 self.announce_idle().await;
2980 }
2981 }
2982 }
2983 StreamEvent::Session(SessionEvent::Error(_)) => {
2984 crate::engine::stream::repair_history_after_failure(
2987 &mut self.conv.api_messages,
2988 self.turn_baseline,
2989 );
2990 self.clear_stream();
2991 self.publish_subagent_rows();
2992 self.emit_conversation();
2993 after = After::Failed;
2994 }
2995 StreamEvent::Session(SessionEvent::ContextHeadCheckpoint {
2996 session_id,
2997 messages,
2998 receipt,
2999 }) => {
3000 self.handle_context_head_checkpoint(session_id, messages, receipt)
3001 .await;
3002 }
3003 }
3004
3005 match after {
3006 After::Continue => {}
3007 After::Failed => {
3008 if self.config.auto_compact {
3009 self.post_turn_chat().await;
3010 }
3011 if self.compact.is_none() {
3012 self.announce_idle().await;
3013 }
3014 }
3015 After::AutoSendQueued(queued) => {
3016 if self.config.auto_compact {
3017 self.post_turn_chat().await;
3018 }
3019 if self.compact.is_some() {
3020 self.conv.queued_message = Some(queued);
3023 return;
3024 }
3025 self.consecutive_auto_turns = 0;
3027 let user_text = queued.clone();
3028 self.conv.api_messages.push(std::sync::Arc::new(
3029 serde_json::json!({"role": "user", "content": queued}),
3030 ));
3031 self.start_turn(TurnTrigger::QueuedAuto, Some(user_text)).await;
3032 }
3033 After::AutoTriggerEvents => {
3034 if self.config.auto_compact {
3035 self.post_turn_chat().await;
3036 }
3037 if self.compact.is_some() {
3038 return;
3039 }
3040 let auto_turn_cap = self.host.config().events.auto_turn_cap;
3043 if claim_auto_turn_with_cap(&mut self.consecutive_auto_turns, auto_turn_cap) {
3044 self.start_turn(TurnTrigger::EventAuto, None).await;
3045 } else {
3046 self.emit(SessionEventWire::AutoTurnCapReached { cap: auto_turn_cap });
3047 self.announce_idle().await;
3048 }
3049 }
3050 }
3051
3052 if let Some(terminal) = terminal {
3054 let revoke_reason = if let Some(driver) = self.driver.as_mut() {
3055 super::driver::observe_terminal(driver, &self.runtime, terminal)
3056 } else {
3057 None
3058 };
3059 if let Some(driver) = self.driver.as_ref() {
3061 if let Some((outcome, _)) = &driver.outcome {
3062 self.emit(SessionEventWire::DriverTurnOutcome {
3063 outcome: *outcome,
3064 selection: driver.selection.clone(),
3065 feedback: driver.grant.feedback_enabled().then(|| {
3066 driver.completed_feedback.to_string()
3067 }),
3068 });
3069 }
3070 }
3071 if let Some(reason) = revoke_reason {
3072 self.driver_revoke(&reason);
3073 }
3074 }
3075 }
3076
3077 async fn post_turn_chat(&mut self) {
3079 self.request_save();
3080 let assessment = self.runtime.assess_context(&self.conv.api_messages).await;
3081 if assessment.should_compact() {
3082 self.emit(SessionEventWire::SystemNotice(format!(
3083 "[auto-compacting ~{} tokens...]",
3084 assessment.used_tokens()
3085 )));
3086 self.compact(None, "auto").await;
3087 }
3088 }
3089
3090 fn compact_inline_env() -> bool {
3093 matches!(
3094 std::env::var("SYNAPS_SESSION_COMPACT_INLINE").as_deref(),
3095 Ok("1") | Ok("true")
3096 )
3097 }
3098
3099 pub(crate) async fn compact(&mut self, instructions: Option<String>, source: &str) {
3106 self.cache_keepwarm_compaction();
3108 if Self::compact_inline_env() {
3109 return self.compact_inline(instructions, source).await;
3110 }
3111 if self.compact.is_some() {
3112 self.emit(SessionEventWire::SystemNotice(
3113 "compaction already in progress".into(),
3114 ));
3115 return;
3116 }
3117 if self.streaming {
3118 self.emit(SessionEventWire::SystemNotice(
3119 "cannot compact while a turn is running".into(),
3120 ));
3121 return;
3122 }
3123 let disclosure =
3124 preview_compaction_disclosure(&self.runtime, &self.conv.api_messages).render_line();
3125 self.emit(SessionEventWire::CompactionStarted {
3126 source: source.to_string(),
3127 disclosure,
3128 });
3129 let msgs = self.conv.api_messages.clone();
3130 let rt: Runtime = (*self.runtime).clone();
3131 let task = tokio::spawn(async move {
3132 compact_conversation(&msgs, &rt, instructions.as_deref()).await
3133 });
3134 self.compact = Some(CompactionJob {
3135 task,
3136 source: source.to_string(),
3137 started: tokio::time::Instant::now(),
3138 });
3139 self.update_attach_state();
3140 }
3141
3142 async fn on_compaction_done(
3146 &mut self,
3147 res: std::result::Result<
3148 Result<crate::runtime::compaction::CompactionOutcome>,
3149 tokio::task::JoinError,
3150 >,
3151 ) {
3152 let Some(job) = self.compact.take() else {
3153 return;
3154 };
3155 let msg_count = self.conv.api_messages.len();
3156 match res {
3157 Err(join) => {
3158 if !join.is_cancelled() {
3159 self.emit(SessionEventWire::CompactionFailed {
3160 message: join.to_string(),
3161 panicked: true,
3162 });
3163 }
3164 }
3165 Ok(Err(e)) => self.emit(SessionEventWire::CompactionFailed {
3166 message: e.to_string(),
3167 panicked: false,
3168 }),
3169 Ok(Ok(outcome)) => {
3170 if !self.persister.flush().await {
3174 tracing::warn!(session = %self.id, "compaction: an earlier save failed");
3175 }
3176 let policy: CompactionPolicy = self.config.compaction_policy.into();
3177 let queued = self.conv.queued_message.clone();
3178 let applied = apply_compaction(
3179 &self.runtime,
3180 &self.conv.session,
3181 &self.conv.api_messages,
3182 &outcome,
3183 CompactionTransition {
3184 policy,
3185 pending_events: self.conv.pending_events.clone(),
3186 queued_message: queued.clone(),
3187 hook_source: job.source.clone(),
3188 },
3189 )
3190 .await;
3191 match applied {
3192 Ok(applied) => {
3193 let previous = applied.previous_session_id.clone();
3194 self.conv.session = applied.session;
3195 self.conv.api_messages = applied.api_messages;
3196 self.conv.pending_events.clear();
3197 self.conv.queued_message = None;
3198 if policy == CompactionPolicy::LinkedSuccessor {
3199 self.conv.total_input_tokens = 0;
3200 self.conv.total_output_tokens = 0;
3201 self.conv.session_cost = 0.0;
3202 self.conv.keepwarm_cost = 0.0;
3207 self.conv.keepwarm_tokens.input = 0;
3208 self.conv.keepwarm_tokens.output = 0;
3209 }
3210 let new_id = self.conv.session.id.clone();
3211 self.runtime.set_session_id(Some(new_id.clone()));
3212 self.journal_id.store(Arc::new(new_id.clone()));
3213 self.reacquire_session_lock(&new_id);
3215 self.conv.context_head = crate::engine::session::ContextHeadPersistence::default();
3219 self.runtime
3220 .reset_context_continuation(&new_id, &self.conv.api_messages);
3221 self.emit(SessionEventWire::CompactionApplied {
3222 previous_session_id: previous,
3223 session_id: new_id,
3224 chains_advanced: applied.chains_advanced,
3225 queued_restored: queued,
3226 msg_count,
3227 });
3228 }
3229 Err(e) => self.emit(SessionEventWire::CompactionFailed {
3230 message: e.to_string(),
3231 panicked: false,
3232 }),
3233 }
3234 }
3235 }
3236 self.emit_conversation();
3237 self.update_attach_state();
3238 self.announce_idle().await;
3239 }
3240
3241 async fn compact_inline(&mut self, instructions: Option<String>, source: &str) {
3243 self.emit(SessionEventWire::SystemNotice(format!(
3244 "[{}]",
3245 preview_compaction_disclosure(&self.runtime, &self.conv.api_messages).render_line()
3246 )));
3247 let outcome =
3248 compact_conversation(&self.conv.api_messages, &self.runtime, instructions.as_deref())
3249 .await;
3250 let applied = match outcome {
3251 Ok(outcome) => {
3252 if !self.persister.flush().await {
3254 tracing::warn!(session = %self.id, "compaction: an earlier save failed");
3255 }
3256 apply_compaction(
3257 &self.runtime,
3258 &self.conv.session,
3259 &self.conv.api_messages,
3260 &outcome,
3261 CompactionTransition {
3262 policy: CompactionPolicy::InPlace,
3263 pending_events: Vec::new(),
3264 queued_message: None,
3265 hook_source: source.to_string(),
3266 },
3267 )
3268 .await
3269 }
3270 Err(e) => Err(e),
3271 };
3272 match applied {
3273 Ok(applied) => {
3274 self.conv.session = applied.session;
3275 self.conv.api_messages = applied.api_messages;
3276 let after = self.runtime.assess_context(&self.conv.api_messages).await;
3277 self.emit(SessionEventWire::SystemNotice(format!(
3278 "[compacted → ~{} tokens]",
3279 after.used_tokens()
3280 )));
3281 }
3282 Err(e) => self.emit(SessionEventWire::SystemNotice(format!(
3283 "[compaction failed: {}]",
3284 e
3285 ))),
3286 }
3287 self.emit_conversation();
3288 }
3289
3290 fn on_prompt_request(&mut self, req: SecretPromptRequest) {
3293 let id = self.next_prompt_id;
3294 self.next_prompt_id += 1;
3295 let pr = PromptRequest {
3296 id,
3297 kind: PromptKind::from_title(&req.title),
3298 title: req.title,
3299 prompt: req.prompt,
3300 raised_at: chrono::Utc::now(),
3301 };
3302 self.pending_prompts.push_back((pr.clone(), req.response_tx));
3303 self.publish_presence();
3304 self.emit(SessionEventWire::Prompt(pr));
3305 self.rearm_prompt_abandon();
3308 }
3309
3310 fn answer(&mut self, prompt_id: u64, value: Option<String>) {
3311 let Some(pos) = self.pending_prompts.iter().position(|(p, _)| p.id == prompt_id) else {
3312 return; };
3314 let (_, tx) = self.pending_prompts.remove(pos).expect("position");
3315 let _ = tx.send(value);
3316 self.publish_presence();
3317 self.emit(SessionEventWire::PromptResolved { prompt_id });
3318 self.rearm_prompt_abandon();
3320 }
3321
3322 pub(crate) async fn apply_setting(&mut self, id: u64, setting: SessionSetting) {
3325 if replayable(&setting) {
3326 let disc = std::mem::discriminant(&setting);
3327 self.settings_replay
3328 .retain(|s| std::mem::discriminant(s) != disc);
3329 self.settings_replay.push(setting.clone());
3330 }
3331 let name = match &setting {
3332 SessionSetting::Model { .. } => "model",
3333 SessionSetting::ReasoningLevel { .. } => "reasoning_level",
3334 SessionSetting::ContextWindow { .. } => "context_window",
3335 SessionSetting::CompactionModel { .. } => "compaction_model",
3336 SessionSetting::ApiRetries { .. } => "api_retries",
3337 SessionSetting::SubagentTimeout { .. } => "subagent_timeout",
3338 SessionSetting::MaxToolOutput { .. } => "max_tool_output",
3339 SessionSetting::BashTimeout { .. } => "bash_timeout",
3340 SessionSetting::BashMaxTimeout { .. } => "bash_max_timeout",
3341 SessionSetting::SystemPrompt { .. } => "system_prompt",
3342 SessionSetting::ReloadPrompt => "reload_prompt",
3343 SessionSetting::GrantWorkerModel { .. } => "grant_worker_model",
3344 };
3345 let rt = &mut self.runtime;
3346 let mut clamp_wire = None;
3347 let result: std::result::Result<Option<String>, String> = match setting {
3348 SessionSetting::Model { model } => rt.try_set_model(model).map(|clamp| {
3349 self.conv.session.model = rt.model().to_string();
3350 clamp.map(|c| {
3351 self.conv.session.thinking_level = rt.thinking_level().to_string();
3352 clamp_wire = Some(ReasoningClampWire {
3353 from: c.from.as_str().to_string(),
3354 to: c.to.as_str().to_string(),
3355 });
3356 format!(
3357 "thinking → {} (clamped from {}: not supported by {})",
3358 c.to.as_str(),
3359 c.from.as_str(),
3360 rt.model()
3361 )
3362 })
3363 }),
3364 SessionSetting::ReasoningLevel { level } => {
3365 rt.set_reasoning_level_checked(level).map(|_| {
3366 self.conv.session.thinking_level = rt.thinking_level().to_string();
3367 None
3368 })
3369 }
3370 SessionSetting::ContextWindow { tokens } => {
3371 rt.set_context_window(tokens);
3372 Ok(None)
3373 }
3374 SessionSetting::CompactionModel { model } => {
3375 rt.set_compaction_model(model);
3376 Ok(None)
3377 }
3378 SessionSetting::ApiRetries { n } => {
3379 rt.set_api_retries(n);
3380 Ok(None)
3381 }
3382 SessionSetting::SubagentTimeout { secs } => {
3383 rt.set_subagent_timeout(secs);
3384 Ok(None)
3385 }
3386 SessionSetting::MaxToolOutput { bytes } => {
3387 rt.set_max_tool_output(bytes);
3388 Ok(None)
3389 }
3390 SessionSetting::BashTimeout { secs } => {
3391 rt.set_bash_timeout(secs);
3392 Ok(None)
3393 }
3394 SessionSetting::BashMaxTimeout { secs } => {
3395 rt.set_bash_max_timeout(secs);
3396 Ok(None)
3397 }
3398 SessionSetting::SystemPrompt { text } => {
3399 rt.set_system_prompt(text);
3400 Ok(None)
3401 }
3402 SessionSetting::ReloadPrompt => rt
3403 .reload_prompt()
3404 .map(|generation| Some(format!("prompt reloaded (generation {generation})")))
3405 .map_err(|e| e.to_string()),
3406 SessionSetting::GrantWorkerModel { model } => {
3407 rt.grant_worker_model(&model).map(|_| None)
3408 }
3409 };
3410 self.publish_view().await;
3411 let view = (**self.view.load()).clone();
3412 let (ok, message) = match result {
3413 Ok(m) => (true, m),
3414 Err(e) => (false, Some(e)),
3415 };
3416 self.emit(SessionEventWire::SettingChanged(SettingApplied {
3417 id,
3418 setting: name.to_string(),
3419 ok,
3420 message,
3421 view,
3422 clamp: clamp_wire,
3423 }));
3424 }
3425
3426 pub(crate) async fn query(&mut self, id: u64, query: SessionQuery) {
3427 if self.is_parked() {
3429 let value = match query {
3430 SessionQuery::Status => serde_json::json!({
3431 "session": (**self.journal_id.load()).clone(),
3432 "model": self.view.load().model,
3433 "lifecycle": SessionLifecycle::Parked,
3434 "streaming": false,
3435 "auto_turns": self.consecutive_auto_turns,
3436 "attached": 0,
3437 "pending_prompts": 0,
3438 }),
3439 SessionQuery::View => serde_json::to_value(&**self.view.load()).unwrap_or_default(),
3440 _ => match self.ensure_live().await {
3441 Ok(()) => return Box::pin(self.query(id, query)).await,
3442 Err(e) => serde_json::json!({ "error": e }),
3443 },
3444 };
3445 self.emit(SessionEventWire::QueryResult { id, value });
3446 return;
3447 }
3448 let value = match query {
3449 SessionQuery::Status => serde_json::json!({
3450 "session": self.conv.session.id,
3451 "model": self.runtime.model(),
3452 "lifecycle": SessionLifecycle::from_u8(self.lifecycle.load(std::sync::atomic::Ordering::Acquire)),
3453 "tokens": { "input": self.conv.total_input_tokens, "output": self.conv.total_output_tokens },
3454 "cost": self.conv.session_cost,
3455 "keepwarm_cost": self.conv.keepwarm_cost,
3457 "keepwarm": self.cache_keepwarm_status_json(None),
3458 "messages": self.conv.api_messages.len(),
3459 "streaming": self.streaming,
3460 "auto_turns": self.consecutive_auto_turns,
3461 "attached": self.attached.len(),
3462 "pending_prompts": self.pending_prompts.len(),
3463 }),
3464 SessionQuery::Messages => {
3465 serde_json::to_value(&self.conv.api_messages).unwrap_or_default()
3466 }
3467 SessionQuery::DisplayTail { items } => {
3468 serde_json::to_value(super::display::display_tail(&self.conv.api_messages, items))
3469 .unwrap_or_default()
3470 }
3471 SessionQuery::SubagentRows => {
3472 let rows = self
3473 .runtime
3474 .subagent_registry()
3475 .lock()
3476 .unwrap_or_else(|p| p.into_inner())
3477 .display_rows();
3478 serde_json::Value::Array(
3479 rows.iter()
3480 .map(|r| {
3481 serde_json::json!({
3482 "subagent_id": r.subagent_id,
3483 "agent_name": r.agent_name,
3484 "status": format!("{:?}", r.status),
3485 "cancel_requested": r.cancel_requested,
3486 "elapsed_secs": r.elapsed_secs,
3487 })
3488 })
3489 .collect(),
3490 )
3491 }
3492 SessionQuery::PromptInspection => {
3493 serde_json::to_value(self.runtime.prompt_inspection_json()).unwrap_or_default()
3494 }
3495 SessionQuery::ToolsSchema => serde_json::json!({ "unsupported": "tools_schema" }),
3496 SessionQuery::View => serde_json::to_value(&**self.view.load()).unwrap_or_default(),
3497 SessionQuery::ContextAssessment => {
3498 let a = self.runtime.assess_context(&self.conv.api_messages).await;
3499 serde_json::json!({
3500 "used_tokens": a.used_tokens(),
3501 "budget_tokens": a.budget_tokens(),
3502 "provider_window": a.provider_window,
3503 "should_compact": a.should_compact(),
3504 })
3505 }
3506 SessionQuery::ContextReport => {
3507 use crate::engine::commands::{context_command, CommandResult};
3508 match context_command(&self.runtime, Some(&self.conv.api_messages)) {
3509 CommandResult::Output(text) => serde_json::json!({ "text": text }),
3510 other => serde_json::json!({ "unsupported": format!("{other:?}") }),
3511 }
3512 }
3513 };
3514 self.emit(SessionEventWire::QueryResult { id, value });
3515 }
3516
3517 pub(crate) fn snapshot(&self) -> AttachSnapshot {
3520 AttachSnapshot {
3521 meta: self.meta.clone(),
3522 view: (**self.view.load()).clone(),
3523 conversation: self.conv.snapshot(self.consecutive_auto_turns),
3524 streaming: self.streaming,
3525 replay: self.turn_replay.iter().cloned().collect(),
3526 pending_prompts: self.pending_prompts.iter().map(|(p, _)| p.clone()).collect(),
3527 clients: self.attached.iter().map(|(c, a)| (*c, a.meta.kind)).collect(),
3528 input_owner: self.input_owner,
3529 display_tail: None,
3530 }
3531 }
3532
3533 pub(crate) fn snapshot_for(&self, client: &ClientMeta) -> AttachSnapshot {
3538 let mut snap = self.snapshot();
3539 if client.history == HistoryMode::Digest {
3540 snap.display_tail = Some(super::display::display_tail(
3541 &snap.conversation.api_messages,
3542 client.tail_items,
3543 ));
3544 snap.conversation.api_messages = Vec::new();
3545 snap.replay.retain(|e| {
3546 !matches!(
3547 e.event,
3548 SessionEventWire::Stream(StreamEvent::Session(SessionEvent::MessageHistory(_)))
3549 )
3550 });
3551 }
3552 snap
3553 }
3554
3555 async fn attach(&mut self, client: ClientMeta, mode: AttachMode) {
3560 if let Err(e) = self.ensure_live().await {
3561 self.emit(SessionEventWire::AttachRefused { message: e });
3562 return;
3563 }
3564 let cid = ClientId(self.next_client_id);
3565 self.next_client_id += 1;
3566 let kind = client.kind;
3567 self.cache_keepwarm_note_attach(kind);
3568 let snapshot_meta = client.clone();
3569 self.attached.insert(cid, AttachedClient { meta: client, mode });
3570 self.update_attach_state();
3571 let mut owner_change: Option<(Option<ClientId>, OwnerChangeReason)> = None;
3572 match mode {
3573 AttachMode::Observe => {}
3574 AttachMode::Mirror => {
3578 if self.input_owner.is_none() {
3579 owner_change = Some((None, OwnerChangeReason::Attach));
3580 }
3581 }
3582 AttachMode::Takeover => {
3583 let reason = if self.input_owner.is_some() {
3584 OwnerChangeReason::Takeover
3585 } else {
3586 OwnerChangeReason::Attach
3587 };
3588 owner_change = Some((self.input_owner, reason));
3589 }
3590 }
3591 if let Some((from, reason)) = owner_change {
3592 self.input_owner = Some(cid);
3593 self.emit(SessionEventWire::InputOwnerChanged {
3594 from,
3595 to: Some(cid),
3596 reason,
3597 });
3598 }
3599 self.publish_presence();
3600 let snapshot = self.snapshot_for(&snapshot_meta);
3601 self.emit(SessionEventWire::Attached {
3602 client: cid,
3603 snapshot,
3604 });
3605 self.emit(SessionEventWire::ClientJoined { client: cid, kind });
3606 }
3607
3608 fn detach(&mut self, client: ClientId) {
3612 if self.attached.remove(&client).is_none() {
3613 return;
3614 }
3615 self.update_attach_state();
3616 self.emit(SessionEventWire::ClientLeft { client });
3617 if self.input_owner == Some(client) {
3618 let next = self
3619 .attached
3620 .iter()
3621 .filter(|(_, a)| a.mode != AttachMode::Observe)
3622 .map(|(c, _)| *c)
3623 .min_by_key(|c| c.0);
3624 self.input_owner = next;
3625 self.emit(SessionEventWire::InputOwnerChanged {
3626 from: Some(client),
3627 to: next,
3628 reason: OwnerChangeReason::OwnerDetached,
3629 });
3630 self.publish_presence();
3631 }
3632 if self.attached.is_empty() && (self.driver.is_some() || self.driver_pending.is_some()) {
3649 self.driver_revoke("no clients attached");
3650 let had_prompts = !self.pending_prompts.is_empty();
3651 while let Some((pr, tx)) = self.pending_prompts.pop_front() {
3652 let _ = tx.send(None);
3653 self.emit(SessionEventWire::PromptResolved { prompt_id: pr.id });
3654 }
3655 if had_prompts {
3656 self.publish_presence();
3657 }
3658 }
3659 }
3660
3661 pub(crate) async fn checkpoint(&mut self, reason: CheckpointReason) {
3666 let interrupt = match reason {
3667 CheckpointReason::Reload => crate::engine::interrupt::InterruptReason::Restart,
3668 CheckpointReason::HostRequest => crate::engine::interrupt::InterruptReason::Host,
3669 };
3670 self.note_cancel_cause(interrupt);
3672 if self.driver.is_some() {
3674 self.driver_revoke("daemon reloaded");
3675 }
3676 if self.streaming {
3677 self.cancel_turn(interrupt).await;
3678 }
3679 self.abort_compaction();
3680 while let Some((pr, tx)) = self.pending_prompts.pop_front() {
3681 let _ = tx.send(None);
3682 self.emit(SessionEventWire::PromptResolved { prompt_id: pr.id });
3683 }
3684 if !self.save().await {
3687 tracing::warn!(session = %self.id, "checkpoint: save failed or timed out");
3688 }
3689 let notice = match reason {
3690 CheckpointReason::Reload => {
3691 "daemon reloading — background shells/PTYs will be closed; the turn was checkpointed"
3692 }
3693 CheckpointReason::HostRequest => {
3694 "checkpoint — background shells/PTYs closed; the turn was checkpointed"
3695 }
3696 };
3697 self.emit(SessionEventWire::SystemNotice(notice.to_string()));
3698 self.runtime.session_manager().shutdown_all();
3699 let record = serde_json::to_value(self.reload_record()).unwrap_or_default();
3700 self.emit(SessionEventWire::QueryResult {
3701 id: super::wire::CHECKPOINT_QUERY_ID,
3702 value: serde_json::json!({ "ok": true, "record": record }),
3703 });
3704 }
3705
3706 pub(crate) fn abort_compaction(&mut self) {
3708 if let Some(job) = self.compact.take() {
3709 job.task.abort();
3710 self.emit(SessionEventWire::CompactionCancelled);
3711 }
3712 }
3713
3714 async fn handle(&mut self, addressed: Addressed) -> std::ops::ControlFlow<EndReason> {
3719 use std::ops::ControlFlow;
3720 let Addressed { from, cmd } = addressed;
3721 if let Some(c) = from {
3722 if is_input_command(&cmd) && self.input_owner != Some(c) {
3723 let reason = match self.input_owner {
3724 Some(o) => format!("input owned by client #{}", o.0),
3725 None => "input has no owner; attach with --takeover".to_string(),
3726 };
3727 self.emit(SessionEventWire::Refused {
3728 client: c,
3729 command: command_name(&cmd).to_string(),
3730 reason,
3731 });
3732 return ControlFlow::Continue(());
3733 }
3734 }
3735 if self.is_parked() {
3737 let needs_runtime = !matches!(
3738 cmd,
3739 SessionCommand::Attach { .. }
3740 | SessionCommand::Detach { .. }
3741 | SessionCommand::End { .. }
3742 | SessionCommand::Query { .. }
3743 | SessionCommand::Answer { .. }
3744 | SessionCommand::Save
3745 | SessionCommand::Cancel
3746 | SessionCommand::Checkpoint { .. }
3747 | SessionCommand::Park
3748 | SessionCommand::Resync { .. }
3749 | SessionCommand::HostEvent(_)
3750 );
3751 if matches!(cmd, SessionCommand::HostEvent(_)) {
3752 return ControlFlow::Continue(()); }
3754 if matches!(
3755 cmd,
3756 SessionCommand::Save
3757 | SessionCommand::Cancel
3758 | SessionCommand::Park
3759 | SessionCommand::Checkpoint { .. }
3760 ) {
3761 if let SessionCommand::Checkpoint { .. } = cmd {
3762 let record = serde_json::to_value(self.reload_record()).unwrap_or_default();
3763 self.emit(SessionEventWire::QueryResult {
3764 id: super::wire::CHECKPOINT_QUERY_ID,
3765 value: serde_json::json!({ "ok": true, "parked": true, "record": record }),
3766 });
3767 }
3768 return ControlFlow::Continue(()); }
3770 if needs_runtime {
3771 if let Err(e) = self.ensure_live().await {
3772 match from {
3773 Some(c) => self.emit(SessionEventWire::Refused {
3774 client: c,
3775 command: command_name(&cmd).to_string(),
3776 reason: e,
3777 }),
3778 None => self.emit(SessionEventWire::SystemNotice(e)),
3779 }
3780 return ControlFlow::Continue(());
3781 }
3782 }
3783 }
3784 match cmd {
3785 SessionCommand::Submit { text, attachments } => self.submit(text, attachments, from).await,
3786 SessionCommand::Steer { text } => {
3787 if self.streaming {
3788 self.steer(text)
3789 } else {
3790 self.submit(text, vec![], from).await
3791 }
3792 }
3793 SessionCommand::Cancel => {
3794 self.note_cancel_cause(crate::engine::interrupt::InterruptReason::User);
3797 if self.driver.is_some() {
3798 self.driver_revoke("canceled");
3799 }
3800 self.cancel_turn(crate::engine::interrupt::InterruptReason::User)
3801 .await;
3802 }
3803 SessionCommand::Answer { prompt_id, value } => self.answer(prompt_id, value),
3804 SessionCommand::Set { id, setting } => self.apply_setting(id, setting).await,
3805 SessionCommand::Compact { instructions } => {
3807 self.compact(instructions, "manual").await;
3808 }
3809 SessionCommand::NewSession => {
3810 if self.driver.is_some() {
3811 self.driver_revoke("session replaced");
3812 }
3813 self.cache_keepwarm_conversation_replaced();
3816 self.persister.flush().await;
3819 self.conv.clear(&self.runtime).await;
3820 self.runtime
3821 .set_session_id(Some(self.conv.session.id.clone()));
3822 self.journal_id.store(Arc::new(self.conv.session.id.clone()));
3823 self.reacquire_session_lock(&self.conv.session.id.clone());
3825 self.emit(SessionEventWire::Cleared {
3826 session_id: self.conv.session.id.clone(),
3827 });
3828 self.emit_conversation();
3829 }
3830 SessionCommand::Save => {
3831 self.save().await;
3832 }
3833 SessionCommand::Query { id, query } => self.query(id, query).await,
3834 SessionCommand::EngineCommand { id, name, arg } => {
3835 self.engine_command(id, name, arg).await
3836 }
3837 SessionCommand::Attach { client, mode } => self.attach(client, mode).await,
3838 SessionCommand::Detach { client } => {
3839 self.detach(client);
3840 if self.attached.is_empty() && self.cache_keepwarm.active() {
3844 self.cache_keepwarm_recheck().await;
3845 }
3846 }
3847 SessionCommand::End { reason } => {
3848 self.note_cancel_cause(match reason {
3851 EndReason::ClientQuit => crate::engine::interrupt::InterruptReason::User,
3852 _ => crate::engine::interrupt::InterruptReason::Host,
3853 });
3854 if self.driver.is_some() {
3855 self.driver_revoke("session ending");
3856 }
3857 return ControlFlow::Break(reason);
3858 }
3859 SessionCommand::Resync { .. } => self.emit(SessionEventWire::SystemNotice(
3860 "resync not supported yet".into(),
3861 )),
3862 SessionCommand::SubmitPrepared {
3865 messages,
3866 user_text,
3867 } => self.submit_prepared(messages, user_text).await,
3868 SessionCommand::PluginCommand {
3869 id,
3870 plugin,
3871 name,
3872 arg,
3873 } => self.plugin_command(id, plugin, name, arg).await,
3874 SessionCommand::Resume { id, query } => self.resume(id, query).await,
3875 SessionCommand::Checkpoint { reason } => self.checkpoint(reason).await,
3876 SessionCommand::Park => {
3877 self.cache_keepwarm_park_requested();
3879 if let std::ops::ControlFlow::Break(reason) = self.park().await {
3880 return ControlFlow::Break(reason);
3881 }
3882 }
3883 SessionCommand::KeepWarm { on } => {
3884 self.keep_warm = on;
3885 self.rearm_park();
3886 self.emit(SessionEventWire::SystemNotice(format!(
3887 "keep-warm {}",
3888 if on { "on: this session will not be parked" } else { "off" }
3889 )));
3890 }
3891 SessionCommand::HostEvent(ev) => match ev {
3892 HostEvent::ExtensionNotification {
3893 extension_id,
3894 method,
3895 params,
3896 } => self.emit(SessionEventWire::ExtensionNotification {
3897 extension_id,
3898 method,
3899 params,
3900 }),
3901 HostEvent::LoaderProgress(ev) => self.emit(SessionEventWire::LoaderProgress(ev)),
3902 },
3903 SessionCommand::DriverStart { plugin, command, arg } => {
3905 self.driver_start(plugin, command, arg);
3906 }
3907 }
3908 ControlFlow::Continue(())
3909 }
3910
3911 async fn finish(&mut self, reason: EndReason) {
3914 self.cache_keepwarm_end(super::cache_keepwarm::PeriodEnd::SessionEnding);
3915 self.lifecycle.store(
3916 SessionLifecycle::Ending as u8,
3917 std::sync::atomic::Ordering::Release,
3918 );
3919 let interrupt = match reason {
3922 EndReason::ClientQuit => crate::engine::interrupt::InterruptReason::User,
3923 _ => crate::engine::interrupt::InterruptReason::Host,
3924 };
3925 self.note_cancel_cause(interrupt);
3927 if self.driver.is_some() {
3929 self.driver_revoke("session ending");
3930 }
3931 if self.streaming {
3932 self.cancel_turn(interrupt).await;
3933 }
3934 self.abort_compaction();
3935 while let Some((pr, tx)) = self.pending_prompts.pop_front() {
3937 let _ = tx.send(None);
3938 self.emit(SessionEventWire::PromptResolved { prompt_id: pr.id });
3939 }
3940
3941 let parked = !self.conv.is_live();
3942 let session_id = (**self.journal_id.load()).clone();
3943 let api_messages = if parked {
3944 None
3945 } else {
3946 Some(self.conv.api_messages.clone())
3947 };
3948
3949 let persist = self.config.persist;
3953 if !parked {
3954 self.request_save();
3955 }
3956 let persister = &self.persister;
3957 let save_fut = async {
3958 if persist {
3959 if !persister.flush().await {
3960 tracing::warn!(session = %session_id, "session end: save failed");
3961 }
3962 let mut index_record =
3963 crate::core::session_index::SessionIndexRecord::end(&session_id);
3964 index_record.turns = api_messages.as_ref().map(|m| m.len());
3965 if let Err(err) = crate::core::session_index::append_record(&index_record) {
3966 tracing::warn!("failed to append session end index record: {}", err);
3967 }
3968 }
3969 };
3970 if tokio::time::timeout(budgets::SAVE_TIMEOUT, save_fut)
3971 .await
3972 .is_err()
3973 {
3974 tracing::warn!(
3975 budget_secs = budgets::SAVE_TIMEOUT_SECS,
3976 "session save timed out — data may be incomplete"
3977 );
3978 }
3979
3980 let hook_bus = Arc::clone(&self.hook_bus);
3983 crate::extensions::loader::emit_session_end(
3984 &hook_bus,
3985 &session_id,
3986 api_messages,
3987 budgets::HOOKS_TIMEOUT,
3988 )
3989 .await;
3990
3991 if self.runtime.is_live() {
3993 if let Some(outcome) = self
3994 .runtime
3995 .shutdown_observability_async(
3996 crate::runtime::telemetry::DEFAULT_SHUTDOWN_FLUSH_TIMEOUT,
3997 )
3998 .await
3999 {
4000 if !outcome.is_flushed() {
4001 tracing::warn!(
4002 stats = ?outcome.stats(),
4003 "observability flush timed out — detached worker keeps draining"
4004 );
4005 }
4006 }
4007 }
4008
4009 self.background.shutdown();
4011 self.emit(SessionEventWire::Ended { reason });
4012 }
4013
4014 pub fn id(&self) -> &SessionId {
4015 &self.id
4016 }
4017}
4018
4019pub struct SessionTask(SessionActor);
4021
4022async fn next_stream_event(stream: &mut Option<ActiveStream>) -> Option<StreamEvent> {
4023 match stream {
4024 Some(s) => s.next().await,
4025 None => std::future::pending().await,
4026 }
4027}
4028
4029fn keep_subagent_tick(streaming: bool, any_running: bool) -> bool {
4033 streaming || any_running
4034}
4035
4036async fn next_tick(tick: &mut Option<tokio::time::Interval>) {
4037 match tick {
4038 Some(t) => {
4039 t.tick().await;
4040 }
4041 None => std::future::pending().await,
4042 }
4043}
4044
4045async fn poll_compaction(
4046 job: &mut Option<CompactionJob>,
4047) -> std::result::Result<Result<crate::runtime::compaction::CompactionOutcome>, tokio::task::JoinError> {
4048 match job {
4049 Some(j) => (&mut j.task).await,
4050 None => std::future::pending().await,
4051 }
4052}
4053
4054async fn park_timer(deadline: Option<tokio::time::Instant>) {
4055 match deadline {
4056 Some(t) => tokio::time::sleep_until(t).await,
4057 None => std::future::pending().await,
4058 }
4059}
4060
4061async fn revoked_turn_timer(deadline: Option<tokio::time::Instant>) {
4063 match deadline {
4064 Some(t) => tokio::time::sleep_until(t).await,
4065 None => std::future::pending().await,
4066 }
4067}
4068
4069async fn prompt_abandon_timer(deadline: Option<tokio::time::Instant>) {
4072 match deadline {
4073 Some(t) => tokio::time::sleep_until(t).await,
4074 None => std::future::pending().await,
4075 }
4076}
4077
4078async fn driver_tick_timer(
4080 has_driver: bool,
4081 has_pending: bool,
4082 interval: &mut tokio::time::Interval,
4083) {
4084 if has_driver || has_pending {
4085 interval.tick().await;
4086 } else {
4087 std::future::pending().await
4088 }
4089}
4090
4091async fn ext_ready(rx: &mut Option<oneshot::Receiver<()>>) {
4092 match rx {
4093 Some(r) => {
4094 let _ = r.await;
4095 }
4096 None => std::future::pending().await,
4097 }
4098}
4099
4100impl SessionTask {
4101 pub fn id(&self) -> &SessionId {
4102 self.0.id()
4103 }
4104
4105 pub async fn run(mut self) {
4107 let queue = Arc::clone(&self.0.event_queue);
4108 let reason = loop {
4109 let actor = &mut self.0;
4110 tokio::select! {
4111 cmd = actor.cmd_rx.recv() => match cmd {
4112 Some(cmd) => {
4113 if let std::ops::ControlFlow::Break(reason) = actor.handle(cmd).await {
4114 break reason;
4115 }
4116 }
4117 None => break EndReason::HostShutdown,
4119 },
4120 Some(req) = actor.secret_prompt_rx.recv() => actor.on_prompt_request(req),
4121 _ = queue.notified() => actor.on_queue_wake().await,
4122 _ = next_tick(&mut actor.subagent_tick) => {
4123 actor.publish_subagent_rows();
4124 actor.flush_turn_draft();
4125 }
4126 _ = park_timer(actor.park_deadline) => {
4127 if let std::ops::ControlFlow::Break(reason) = actor.park().await {
4128 break reason;
4129 }
4130 }
4131 _ = prompt_abandon_timer(actor.prompt_abandon_deadline) => {
4132 actor.on_prompt_abandon_deadline();
4133 }
4134 _ = super::cache_keepwarm::keepwarm_timer(actor.cache_keepwarm.deadline()) => {
4135 actor.on_cache_keepwarm_due().await;
4136 }
4137 res = super::cache_keepwarm::poll_keepwarm_ping(&mut actor.cache_keepwarm.task) => {
4138 actor.on_cache_keepwarm_done(res);
4139 }
4140 res = poll_compaction(&mut actor.compact) => actor.on_compaction_done(res).await,
4141 _ = ext_ready(&mut actor.ext_ready) => {
4142 actor.ext_ready = None;
4143 let id = (**actor.journal_id.load()).clone();
4144 let bus = Arc::clone(&actor.hook_bus);
4145 crate::extensions::loader::emit_session_start(&bus, &id).await;
4146 if actor.runtime.is_live() {
4147 actor.publish_view().await;
4148 }
4149 },
4150 _ = driver_tick_timer(actor.driver.is_some(), actor.driver_pending.is_some(), &mut actor.driver_tick_interval) => {
4151 actor.driver_tick().await;
4152 }
4153 _ = revoked_turn_timer(actor.revoked_turn_deadline) => {
4154 actor.finish_revoked_turn(false).await;
4155 }
4156 ev = next_stream_event(&mut actor.stream) => match ev {
4157 Some(ev) => actor.on_stream_event(ev).await,
4158 None if actor.turn_cancelled() => actor.finish_revoked_turn(true).await,
4160 None => {
4161 if actor.driver.as_ref().is_some_and(|d| d.awaiting_terminal) {
4164 let terminal = super::driver::capture_terminal(None, false);
4165 if let Some(terminal) = terminal {
4166 if let Some(driver) = actor.driver.as_mut() {
4167 let reason = super::driver::observe_terminal(driver, &actor.runtime, terminal);
4168 let sel = driver.selection.clone();
4174 let fb = driver.grant.feedback_enabled()
4175 .then(|| driver.completed_feedback.to_string());
4176 actor.emit(SessionEventWire::DriverTurnOutcome {
4177 outcome: crate::extensions::session_driver::Outcome::Blocked,
4178 selection: sel,
4179 feedback: fb,
4180 });
4181 if let Some(reason) = reason {
4182 actor.driver_revoke(&reason);
4183 }
4184 }
4185 }
4186 }
4187 actor.clear_stream();
4188 actor.emit_conversation();
4189 actor.announce_idle().await;
4190 }
4191 },
4192 }
4193 };
4194 self.0.finish(reason).await;
4195 }
4196}
4197
4198#[cfg(test)]
4199mod subagent_tick_tests {
4200 use super::keep_subagent_tick;
4201
4202 #[test]
4203 fn rows_keep_flowing_after_the_turn_while_a_worker_runs() {
4204 assert!(keep_subagent_tick(true, false), "during a turn, always");
4205 assert!(keep_subagent_tick(true, true));
4206 assert!(
4207 keep_subagent_tick(false, true),
4208 "after the turn, while a background worker runs"
4209 );
4210 assert!(
4211 !keep_subagent_tick(false, false),
4212 "stops once the turn is over and no worker runs"
4213 );
4214 }
4215}
4216
4217#[cfg(test)]
4218mod parked_evict_tests {
4219 use super::parked_evict_after_from;
4220 use super::prompt_abandon_timeout_from;
4221 use std::time::Duration;
4222
4223 #[test]
4224 fn parked_evict_defaults_to_one_hour_and_honours_never() {
4225 let d = 3600u64;
4227 assert_eq!(parked_evict_after_from(None, d), Some(Duration::from_secs(3600)));
4228 assert_eq!(parked_evict_after_from(Some("120"), d), Some(Duration::from_secs(120)));
4229 for never in ["never", "0", "off"] {
4230 assert_eq!(parked_evict_after_from(Some(never), d), None, "{never:?}");
4231 }
4232 assert_eq!(parked_evict_after_from(Some("junk"), d), Some(Duration::from_secs(3600)));
4233 assert_eq!(parked_evict_after_from(None, 42), Some(Duration::from_secs(42)));
4235 assert_eq!(parked_evict_after_from(None, 0), None);
4236 assert_eq!(parked_evict_after_from(Some("7"), 42), Some(Duration::from_secs(7)));
4237 }
4238
4239 #[test]
4240 fn prompt_abandon_defaults_to_one_hour_and_honours_never() {
4241 let d = 3600u64;
4243 assert_eq!(prompt_abandon_timeout_from(None, d), Some(Duration::from_secs(3600)));
4244 assert_eq!(prompt_abandon_timeout_from(Some("30"), d), Some(Duration::from_secs(30)));
4245 for never in ["never", "0", "off"] {
4247 assert_eq!(prompt_abandon_timeout_from(Some(never), d), None, "{never:?}");
4248 }
4249 assert_eq!(prompt_abandon_timeout_from(Some("junk"), d), Some(Duration::from_secs(3600)));
4251 assert_eq!(prompt_abandon_timeout_from(None, 42), Some(Duration::from_secs(42)));
4253 assert_eq!(prompt_abandon_timeout_from(None, 0), None);
4254 assert_eq!(prompt_abandon_timeout_from(Some("7"), 42), Some(Duration::from_secs(7)));
4255 }
4256}