1use crate::skills::keybinds::KeybindRegistry;
7use crate::skills::registry::CommandRegistry;
8use crate::{latest_session, resolve_session, EngineHost, HostOpts, Result, Runtime, Session};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12pub struct EngineOpts {
14 pub continue_session: Option<Option<String>>,
15 pub system: Option<String>,
16 pub prompt_manifest: Option<std::path::PathBuf>,
17 pub profile: Option<String>,
20 pub no_extensions: bool,
21}
22
23pub struct BackgroundTasks {
25 watcher_shutdown: Arc<std::sync::atomic::AtomicBool>,
26 watcher_task: tokio::task::JoinHandle<()>,
27 socket_shutdown: Arc<std::sync::atomic::AtomicBool>,
28 socket_task: tokio::task::JoinHandle<()>,
29 #[allow(dead_code)] session_socket_path: String,
31 session_id: String,
32 hook_bus: Arc<crate::extensions::hooks::HookBus>,
35 #[allow(dead_code)]
41 log_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
42}
43
44impl BackgroundTasks {
45 pub fn shutdown(&self) {
47 self.watcher_shutdown
48 .store(true, std::sync::atomic::Ordering::Release);
49 self.socket_shutdown
50 .store(true, std::sync::atomic::Ordering::Release);
51 crate::events::registry::unregister_session(&self.session_id);
52 if let Ok(handle) = tokio::runtime::Handle::try_current() {
54 let hook_bus = Arc::clone(&self.hook_bus);
55 let session_id = self.session_id.clone();
56 handle.spawn(async move {
57 hook_bus.clear_session_injection(&session_id).await;
58 });
59 }
60 }
61}
62
63impl Drop for BackgroundTasks {
64 fn drop(&mut self) {
65 self.watcher_shutdown
66 .store(true, std::sync::atomic::Ordering::Relaxed);
67 self.socket_shutdown
68 .store(true, std::sync::atomic::Ordering::Relaxed);
69 self.watcher_task.abort();
70 self.socket_task.abort();
71 }
72}
73
74pub struct EngineBoot {
76 pub runtime: Runtime,
77 pub config: crate::SynapsConfig,
78 pub no_extensions: bool,
81 pub session: Session,
82 pub api_messages: Vec<crate::SharedMessage>,
83 pub total_input_tokens: u64,
84 pub total_output_tokens: u64,
85 pub session_cost: f64,
86 pub continued: bool,
87 pub continue_info: Option<ContinueInfo>,
88 pub registry: Arc<CommandRegistry>,
89 pub keybind_registry: Arc<std::sync::RwLock<KeybindRegistry>>,
94 pub mcp_server_count: usize,
95 pub system_prompt_path: std::path::PathBuf,
96 pub ext_manager: Arc<RwLock<crate::extensions::manager::ExtensionManager>>,
97 pub background: BackgroundTasks,
99}
100
101pub struct ContinueInfo {
103 pub session_id: String,
104 pub resolved_via: Option<String>, pub query: String,
106 pub compaction_notice: Option<String>,
108}
109
110pub async fn boot(opts: EngineOpts) -> Result<EngineBoot> {
113 let host = EngineHost::boot_and_install(HostOpts {
119 profile: opts.profile.clone(),
120 no_extensions: opts.no_extensions,
121 })
122 .await?;
123 let mut runtime = host.foreground_runtime().await?;
126 let config: crate::SynapsConfig = (**host.config()).clone();
127
128 let sb = resolve_session_and_prompt(
129 &mut runtime,
130 &opts.continue_session,
131 opts.system.as_deref(),
132 opts.prompt_manifest.as_deref(),
133 )?;
134
135 let registry = Arc::clone(host.command_registry());
138 let keybind_registry = Arc::clone(host.keybind_registry());
139 let mcp_server_count = host.mcp_server_count();
140
141 let system_prompt_path = crate::config::resolve_read_path("system.md");
142
143 let background = spawn_session_background(&runtime, &sb.session)?;
147
148 finish_session_setup(&mut runtime, &config, &sb.session, None, IndexRecord::Start);
149
150 let ext_manager = Arc::clone(host.ext_manager());
152
153 if mcp_server_count > 0 {
154 tracing::info!(
155 "{} MCP servers available (use connect_mcp_server to activate)",
156 mcp_server_count
157 );
158 }
159
160 Ok(EngineBoot {
161 runtime,
162 config,
163 no_extensions: opts.no_extensions,
164 session: sb.session,
165 api_messages: sb.api_messages,
166 total_input_tokens: sb.total_input_tokens,
167 total_output_tokens: sb.total_output_tokens,
168 session_cost: sb.session_cost,
169 continued: sb.continued,
170 continue_info: sb.continue_info,
171 registry,
172 keybind_registry,
173 mcp_server_count,
174 system_prompt_path,
175 ext_manager,
176 background,
177 })
178}
179
180pub(crate) fn resolve_session_and_prompt(
183 runtime: &mut Runtime,
184 continue_session: &Option<Option<String>>,
185 system: Option<&str>,
186 prompt_manifest: Option<&std::path::Path>,
187) -> Result<SessionBootResult> {
188 let sb = resolve_or_create_session(runtime, continue_session)?;
191 runtime.set_session_id(Some(sb.session.id.clone()));
192 runtime.reset_context_continuation(&sb.session.id, &sb.api_messages);
195
196 let legacy_prompt = crate::config::resolve_system_prompt(system);
198 if let Some(path) = prompt_manifest {
199 let raw = std::fs::read_to_string(path)
200 .map_err(|_| crate::RuntimeError::Config("prompt manifest is unavailable".into()))?;
201 let manifest = agent_core::prompt::PromptManifest::parse(&raw)
202 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
203 let registry = manifest
204 .registry(path.parent())
205 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
206 let model = crate::orchestration::canonical_foreground_identity(runtime.model())
207 .map_err(|e| crate::RuntimeError::Config(format!("invalid foreground model: {e}")))?;
208 let context = agent_core::prompt::SelectionContext::new(model.clone(), None)
209 .map_err(|e| crate::RuntimeError::Config(e.to_string()))?;
210 let catalog = crate::orchestration::OrchestrationRuntime::trusted_catalog(
211 &model,
212 manifest.delegation_catalog_candidates(),
213 )
214 .map_err(|error| crate::RuntimeError::Config(error.into()))?;
215 let delegation_policy = manifest
216 .delegation_policy(model.clone(), &catalog)
217 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
218 let delegation_policy_digest = delegation_policy.as_ref().map(|policy| policy.digest());
219 if let Some(policy) = delegation_policy {
220 runtime.install_orchestration(Arc::new(
221 crate::orchestration::OrchestrationRuntime::new(policy),
222 ));
223 } else {
224 runtime.install_orchestration(Arc::new(
225 crate::orchestration::OrchestrationRuntime::baseline(model.clone(), 8, 64)
226 .map_err(|error| crate::RuntimeError::Config(error.into()))?,
227 ));
228 }
229 let user = system
230 .map(|_| {
231 agent_core::prompt::resolved_system_prompt_as_user_module(legacy_prompt.clone())
232 })
233 .transpose()
234 .map_err(|e| crate::RuntimeError::Config(e.to_string()))?;
235 let stack =
236 agent_core::prompt::compile_prompt_stack(&manifest, ®istry, &context, user.clone())
237 .map_err(|e| {
238 crate::RuntimeError::Config(format!("invalid prompt manifest: {e}"))
239 })?;
240 runtime
241 .apply_prompt_stack(stack)
242 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
243 runtime.retain_prompt_reload_source(
244 path.to_path_buf(),
245 context,
246 user,
247 delegation_policy_digest,
248 );
249 } else {
250 let foreground = crate::orchestration::canonical_foreground_identity(runtime.model())
251 .map_err(|e| crate::RuntimeError::Config(format!("invalid foreground model: {e}")))?;
252 runtime.install_orchestration(Arc::new(
253 crate::orchestration::OrchestrationRuntime::baseline(foreground, 8, 64)
254 .map_err(|error| crate::RuntimeError::Config(error.into()))?,
255 ));
256 runtime.set_system_prompt(legacy_prompt);
257 }
258
259 Ok(sb)
260}
261
262pub(crate) fn spawn_session_background(
265 runtime: &Runtime,
266 session: &Session,
267) -> Result<BackgroundTasks> {
268 let watcher_shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false));
270 let watcher_task = {
271 let inbox_dir = crate::config::base_dir().join("inbox");
272 let event_queue = runtime.event_queue().clone();
273 let shutdown = watcher_shutdown.clone();
274 tokio::spawn(async move {
275 crate::events::watch_inbox(inbox_dir, event_queue, shutdown).await;
276 })
277 };
278
279 let abort_tasks = |ws: &Arc<std::sync::atomic::AtomicBool>,
281 wt: &tokio::task::JoinHandle<()>| {
282 ws.store(true, std::sync::atomic::Ordering::Relaxed);
283 wt.abort();
284 };
285
286 let socket_shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false));
288 let session_socket_path = crate::events::registry::socket_path_for_session(&session.id);
289 let socket_task = crate::events::socket::listen_session_socket(
290 session_socket_path.clone(),
291 runtime.event_queue().clone(),
292 socket_shutdown.clone(),
293 );
294 let session_registration = crate::events::registry::SessionRegistration {
295 kind: crate::events::registry::REGISTRATION_KIND.to_string(),
296 session_id: session.id.clone(),
297 name: session.name.clone(),
298 socket_path: session_socket_path.clone(),
299 pid: std::process::id(),
300 started_at: chrono::Utc::now(),
301 };
302 if let Err(e) = crate::events::registry::register_session(&session_registration) {
303 abort_tasks(&watcher_shutdown, &watcher_task);
304 socket_shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
305 socket_task.abort();
306 return Err(crate::core::error::RuntimeError::Session(format!(
311 "failed to register session {}: {}",
312 session_registration.session_id, e
313 )));
314 }
315
316 Ok(BackgroundTasks {
317 watcher_shutdown,
318 watcher_task,
319 socket_shutdown,
320 socket_task,
321 session_socket_path,
322 session_id: session.id.clone(),
323 hook_bus: Arc::clone(runtime.hook_bus()),
324 log_guard: None,
326 })
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub(crate) enum IndexRecord {
333 Start,
334 Skip,
335}
336
337pub(crate) fn finish_session_setup(
340 runtime: &mut Runtime,
341 config: &crate::SynapsConfig,
342 session: &Session,
343 cwd: Option<std::path::PathBuf>,
344 index_record: IndexRecord,
345) {
346 runtime.set_turn_budget(crate::runtime::budget::TurnBudget::from_config(
349 crate::runtime::budget::TurnRole::Foreground,
350 &config.turn_budgets,
351 ));
352
353 if index_record == IndexRecord::Start {
362 let mut index_record =
363 crate::core::session_index::SessionIndexRecord::start(&session.id);
364 index_record.model = Some(session.model.clone());
365 index_record.profile = crate::core::config::get_profile();
366 index_record.cwd = cwd.or_else(|| std::env::current_dir().ok());
367 if let Err(err) = crate::core::session_index::append_record(&index_record) {
368 tracing::warn!("failed to append session start index record: {}", err);
369 }
370 }
371
372}
373
374pub(crate) struct SessionBootResult {
376 pub(crate) session: Session,
377 pub(crate) api_messages: Vec<crate::SharedMessage>,
378 pub(crate) total_input_tokens: u64,
379 pub(crate) total_output_tokens: u64,
380 pub(crate) session_cost: f64,
381 pub(crate) continued: bool,
382 pub(crate) continue_info: Option<ContinueInfo>,
383}
384
385pub fn lock_session(
395 id: &str,
396 continued: bool,
397 kind: &str,
398) -> Result<Option<agent_core::session_lock::SessionLock>> {
399 let dir = agent_core::session_lock::sessions_dir();
400 let holder = agent_core::session_lock::LockHolder {
401 pid: std::process::id(),
402 kind: kind.to_string(),
403 };
404 match agent_core::session_lock::SessionLock::try_acquire(&dir, id, holder) {
405 Ok(lock) => Ok(Some(lock)),
406 Err(
407 e @ (agent_core::session_lock::SessionLockError::Held { .. }
408 | agent_core::session_lock::SessionLockError::CompactedInto { .. }),
409 ) if continued => Err(crate::RuntimeError::Session(e.to_string())),
410 Err(e) => {
411 tracing::warn!(session = %id, "session lock: {e}");
412 Ok(None)
413 }
414 }
415}
416
417pub async fn recover_turn_draft(conv: &mut crate::engine::session::ConversationState) {
432 let dir = agent_core::session_lock::sessions_dir();
433 let id = conv.session.id.clone();
434 let read = {
435 let (dir, id) = (dir.clone(), id.clone());
436 tokio::task::spawn_blocking(move || {
437 agent_core::core::session_draft::read_turn_draft(&dir, &id)
438 })
439 .await
440 .map_err(std::io::Error::other)
441 .and_then(|r| r)
442 };
443 let recovered = match read {
444 Ok(None) => return,
445 Ok(Some(draft)) => {
446 crate::engine::interrupt::recover_crashed_turn(&mut conv.api_messages, &draft)
447 }
448 Err(e) => {
449 tracing::warn!(session = %id, "unreadable turn draft, removing it: {e}");
452 false
453 }
454 };
455 if recovered {
456 tracing::warn!(
457 session = %id,
458 "session was interrupted mid-turn by an unexpected stop; recovered"
459 );
460 if !conv.save().await {
461 tracing::warn!(
462 session = %id,
463 "could not save the recovered history; keeping the turn draft"
464 );
465 return;
466 }
467 } else {
468 tracing::info!(session = %id, "removing a stale turn draft");
471 }
472 let removed = tokio::task::spawn_blocking(move || {
473 agent_core::core::session_draft::remove_turn_draft(&dir, &id)
474 })
475 .await;
476 if let Ok(Err(e)) | Err(e) = removed.map_err(std::io::Error::other) {
477 tracing::warn!(session = %conv.session.id, "failed to remove turn draft: {e}");
478 }
479}
480
481fn resolve_or_create_session(
482 runtime: &mut Runtime,
483 continue_session: &Option<Option<String>>,
484) -> Result<SessionBootResult> {
485 match continue_session {
486 Some(ref maybe_id) => {
487 let raw_session = match maybe_id {
488 Some(ref id) => resolve_session(id).map_err(|e| {
489 crate::error::RuntimeError::Tool(format!(
490 "Failed to load session '{}': {}",
491 id, e
492 ))
493 })?,
494 None => latest_session().map_err(|e| {
495 crate::error::RuntimeError::Tool(format!("No sessions to continue: {}", e))
496 })?,
497 };
498 let resolved = agent_core::session::follow_compaction_chain(raw_session)
501 .map_err(|e| crate::error::RuntimeError::Session(e.to_string()))?;
502 let compaction_notice = resolved.compaction_notice;
503 let mut session = resolved.session;
504 runtime.set_model(session.model.clone());
505 if let Some(clamp) = runtime.restore_session_reasoning(&session.thinking_level) {
510 tracing::warn!(
511 from = %clamp.from,
512 to = %clamp.to,
513 model = %session.model,
514 "saved session thinking level not supported by model; clamped"
515 );
516 session.thinking_level = runtime.thinking_level().to_string();
517 }
518 if let Some(ref sp) = session.system_prompt {
519 runtime.set_system_prompt(sp.clone());
520 }
521
522 let continue_info = {
523 let (resolved_via, query) = match maybe_id {
524 Some(ref q) => {
525 let via = if *q != session.id {
526 if crate::chain::load_chain(q).is_ok() {
527 Some("chain".to_string())
528 } else if agent_core::session::find_session_by_name(q).is_ok() {
529 Some("name".to_string())
530 } else if compaction_notice.is_some() {
531 Some("compacted".to_string())
532 } else {
533 None
534 }
535 } else {
536 None
537 };
538 (via, q.clone())
539 }
540 None => {
541 let via = compaction_notice.as_ref().map(|_| "compacted".to_string());
542 (via, session.id.clone())
543 }
544 };
545 if maybe_id.is_some() || compaction_notice.is_some() {
548 Some(ContinueInfo {
549 session_id: session.id.clone(),
550 resolved_via,
551 query,
552 compaction_notice: compaction_notice.clone(),
553 })
554 } else {
555 None
556 }
557 };
558
559 if crate::engine::interrupt::migrate_legacy_abort_context(
567 &mut session.api_messages,
568 &mut session.abort_context,
569 ) {
570 tracing::info!(
571 session = %session.id,
572 "migrated a legacy abort-context recap to an interruption marker"
573 );
574 }
575 Ok(SessionBootResult {
580 api_messages: session.api_messages.clone(),
581 total_input_tokens: session.total_input_tokens,
582 total_output_tokens: session.total_output_tokens,
583 session_cost: session.session_cost,
584 continued: true,
585 continue_info,
586 session,
587 })
588 }
589 None => {
590 let session = Session::new(
591 runtime.model(),
592 runtime.thinking_level(),
593 runtime.system_prompt(),
594 );
595 Ok(SessionBootResult {
596 session,
597 api_messages: Vec::new(),
598 total_input_tokens: 0,
599 total_output_tokens: 0,
600 session_cost: 0.0,
601 continued: false,
602 continue_info: None,
603 })
604 }
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use agent_core::reasoning::ReasoningLevel;
612
613 async fn saved_session(first: &str) -> Session {
614 let mut session = Session::new("claude-sonnet-4-5", "low", None);
615 session.api_messages = vec![std::sync::Arc::new(
616 serde_json::json!({"role": "user", "content": first}),
617 )];
618 session.save().await.unwrap();
619 session
620 }
621
622 #[tokio::test]
627 #[serial_test::serial(synaps_base_dir)]
628 async fn loading_never_folds_or_touches_the_draft() {
629 use agent_core::core::session_draft::{read_turn_draft, write_turn_draft, TurnDraft};
630 let _base = crate::test_env::BaseDirGuard::new();
631 let dir = agent_core::session_lock::sessions_dir();
632 let session = saved_session("do X").await;
633 let draft = TurnDraft {
634 base_len: 1,
635 partial_text: "partial".into(),
636 };
637 write_turn_draft(&dir, &session.id, &draft).unwrap();
638
639 let mut runtime = Runtime::new_headless();
640 let sb = resolve_or_create_session(&mut runtime, &Some(Some(session.id.clone()))).unwrap();
641 assert_eq!(sb.api_messages.len(), 1, "history exactly as saved");
642 assert_eq!(
643 read_turn_draft(&dir, &session.id).unwrap(),
644 Some(draft),
645 "the draft is left for the lock holder"
646 );
647 }
648
649 #[tokio::test]
651 #[serial_test::serial(synaps_base_dir)]
652 async fn the_lock_holder_recovers_saves_then_removes_the_draft() {
653 use agent_core::core::session_draft::{read_turn_draft, write_turn_draft, TurnDraft};
654 let _base = crate::test_env::BaseDirGuard::new();
655 let dir = agent_core::session_lock::sessions_dir();
656 let session = saved_session("do X").await;
657 let id = session.id.clone();
658 write_turn_draft(
659 &dir,
660 &id,
661 &TurnDraft {
662 base_len: 1,
663 partial_text: "partial".into(),
664 },
665 )
666 .unwrap();
667
668 let mut conv = crate::engine::session::ConversationState::from_resumed(session);
669 recover_turn_draft(&mut conv).await;
670 assert_eq!(conv.api_messages.len(), 3, "partial reply + marker");
671 let on_disk = Session::load(&id).unwrap();
672 assert_eq!(on_disk.api_messages, conv.api_messages, "saved before removal");
673 assert_eq!(read_turn_draft(&dir, &id).unwrap(), None, "draft removed");
674
675 recover_turn_draft(&mut conv).await;
677 assert_eq!(conv.api_messages.len(), 3);
678 }
679
680 #[tokio::test]
683 #[serial_test::serial(synaps_base_dir)]
684 async fn an_unreadable_draft_is_removed_and_history_kept() {
685 let _base = crate::test_env::BaseDirGuard::new();
686 let dir = agent_core::session_lock::sessions_dir();
687 let session = saved_session("do X").await;
688 let path = dir.join(format!("{}.turn", session.id));
689 std::fs::write(&path, b"{not json").unwrap();
690 let mut conv = crate::engine::session::ConversationState::from_resumed(session);
691 recover_turn_draft(&mut conv).await;
692 assert_eq!(conv.api_messages.len(), 1);
693 assert!(!path.exists());
694 }
695
696 #[cfg(unix)]
699 #[tokio::test]
700 #[serial_test::serial(synaps_base_dir)]
701 async fn a_failed_recovery_save_keeps_the_draft() {
702 use agent_core::core::session_draft::{read_turn_draft, write_turn_draft, TurnDraft};
703 let _base = crate::test_env::BaseDirGuard::new();
704 let dir = agent_core::session_lock::sessions_dir();
705 let session = saved_session("do X").await;
706 let id = session.id.clone();
707 let draft = TurnDraft {
708 base_len: 1,
709 partial_text: "partial".into(),
710 };
711 write_turn_draft(&dir, &id, &draft).unwrap();
712 let snapshot = dir.join(format!("{id}.json"));
715 std::fs::remove_file(&snapshot).unwrap();
716 std::fs::create_dir(&snapshot).unwrap();
717 std::fs::write(snapshot.join("occupied"), b"x").unwrap();
718
719 let mut conv = crate::engine::session::ConversationState::from_resumed(session);
720 recover_turn_draft(&mut conv).await;
721 assert_eq!(read_turn_draft(&dir, &id).unwrap(), Some(draft), "draft kept");
722 }
723
724 #[tokio::test]
727 #[serial_test::serial(synaps_base_dir)]
728 async fn boot_hosts_refuse_to_continue_a_session_held_elsewhere() {
729 let _base = crate::test_env::BaseDirGuard::new();
730 let held = lock_session("20260930-000000-held", true, "tui").unwrap();
731 assert!(held.is_some());
732 let err = lock_session("20260930-000000-held", true, "rpc").unwrap_err();
733 assert!(err.to_string().contains("20260930-000000-held"), "{err}");
734 assert!(lock_session("20260930-000000-held", false, "rpc").unwrap().is_none());
736 drop(held);
737 assert!(lock_session("20260930-000000-held", true, "rpc").unwrap().is_some());
738
739 let mut old = Session::new("claude-sonnet-4-5", "low", None);
741 old.api_messages = vec![std::sync::Arc::new(
742 serde_json::json!({"role": "user", "content": "before compaction"}),
743 )];
744 old.compacted_into = Some("20260930-000001-next".into());
745 old.save().await.unwrap();
746 let err = lock_session(&old.id, true, "rpc").unwrap_err();
747 assert!(err.to_string().contains("20260930-000001-next"), "{err}");
748 }
749
750 #[test]
753 fn continue_path_restores_thinking_level_from_session() {
754 let mut runtime = Runtime::new_headless();
755
756 let thinking_level_str = "ultra";
758 if let Some(level) = ReasoningLevel::parse(thinking_level_str) {
759 runtime.set_reasoning_level_explicit(level);
760 }
761
762 assert_eq!(
763 runtime.reasoning_level(),
764 ReasoningLevel::Ultra,
765 "thinking level must be restored from session on --continue"
766 );
767 assert!(
768 runtime.is_reasoning_explicit(),
769 "restored thinking level must be marked explicit so set_model won't overwrite it"
770 );
771 }
772
773 #[test]
774 fn continue_path_restores_max_level() {
775 let mut runtime = Runtime::new_headless();
776 let thinking_level_str = "max";
777 if let Some(level) = ReasoningLevel::parse(thinking_level_str) {
778 runtime.set_reasoning_level_explicit(level);
779 }
780 assert_eq!(runtime.reasoning_level(), ReasoningLevel::Max);
781 }
782
783 #[test]
784 fn continue_path_restores_off_level() {
785 let mut runtime = Runtime::new_headless();
786 let thinking_level_str = "off";
787 if let Some(level) = ReasoningLevel::parse(thinking_level_str) {
788 runtime.set_reasoning_level_explicit(level);
789 }
790 assert_eq!(runtime.reasoning_level(), ReasoningLevel::Off);
791 }
792
793 #[test]
796 fn default_config_is_dark_no_checkpoint_tool_legacy_binding() {
797 let runtime = Runtime::new_headless();
798 assert!(
800 !runtime.memory_backend_exclusive(),
801 "default config must NOT be exclusive (Axel); it must be legacy"
802 );
803 let tools = runtime.tools_shared();
805 let registry = tools.blocking_read();
806 let has_checkpoint = registry
807 .iter_tools_sorted()
808 .iter()
809 .any(|t| t.name() == "context_checkpoint");
810 assert!(
811 !has_checkpoint,
812 "context_checkpoint must not appear in the default tool set"
813 );
814 }
815}