//! Example: two `JsonlBackend`s side by side, demonstrating boot-from-config, //! priority-ordered read routing, write fanout, and a health snapshot. //! //! Layout: //! - `primary` → `read_priority = 0`, `failure_mode = required` (default) //! - `mirror` → `read_priority = 10`, `failure_mode = best_effort` //! //! The primary is the default write target (lowest priority among //! Required+write-active backends). `append_messages` fans out inline to the //! mirror too. Reads walk the registrations in priority order, so the primary //! answers first; if it is missing a session, the walk falls through to the //! mirror. //! //! Run with: //! //! cargo run --package dirigent_archivist --example multi_backend use std::sync::Arc; use chrono::Utc; use dirigent_archivist::coordinator::Archivist; use dirigent_archivist::registry::{ArchivesConfig, BackendRegistry}; use dirigent_archivist::types::{ MessageRecord, RegisterConnectorRequest, RegisterSessionRequest, }; use uuid::Uuid; #[tokio::main] async fn main() -> anyhow::Result<()> { let dir_a = tempfile::tempdir()?; let dir_b = tempfile::tempdir()?; // Build a two-archive config entirely from TOML so the example doubles as // a faithful demonstration of the config surface. let cfg_src = format!( r#" [[archives]] name = "primary" type = "jsonl" read_priority = 0 [archives.params] path = "{a}" [[archives]] name = "mirror" type = "jsonl" failure_mode = "best_effort" read_priority = 10 [archives.params] path = "{b}" "#, a = dir_a.path().to_string_lossy().replace('\\', "/"), b = dir_b.path().to_string_lossy().replace('\\', "/"), ); let cfg: ArchivesConfig = toml::from_str(&cfg_src)?; let registry = BackendRegistry::with_jsonl(); let archivist = Arc::new(Archivist::from_config(cfg, ®istry, None).await?); println!("\n=== Multi-backend Archivist example ===\n"); println!("Boot complete. Archives (ordered by read_priority):"); for s in archivist.list_archives_with_health().await { println!( " - name={:<8} type={:<6} priority={:<3} enabled={} write_active={} failure_mode={:?} health={:?}", s.name, s.type_name, s.read_priority, s.enabled, s.write_active, s.failure_mode, s.health, ); } // ------------------------------------------------------------------ // Register a connector. The primary owns the canonical record; fanout // mirrors it to the secondary. // ------------------------------------------------------------------ let connector_resp = archivist .register_connector( RegisterConnectorRequest { r#type: "Example".into(), title: "multi-backend demo".into(), client_native_id: "example://multi_backend".into(), custom_uid: None, metadata: serde_json::json!({ "demo": true }), fingerprint: None, }, None, ) .await?; let connector_uid = connector_resp.connector_uid; println!( "\nRegistered connector: uid={} status={:?}", connector_uid, connector_resp.status ); // ------------------------------------------------------------------ // Register a session under that connector. `register_session` writes // the mapping and `session.json` on the primary first, then fans out // to any enabled secondaries. // ------------------------------------------------------------------ let session_resp = archivist .register_session( RegisterSessionRequest { connector_uid, native_session_id: "demo-session-1".into(), title: Some("multi-backend demo session".into()), custom_scroll_id: None, metadata: serde_json::json!({ "model": "demo" }), completeness: Default::default(), parent_scroll_id: None, is_subagent: false, continuation: None, agent_id: None, subagent_type: None, spawning_tool_use_id: None, }, None, ) .await?; let scroll_id = session_resp.scroll_id; println!( "Registered session: scroll_id={} status={:?}", scroll_id, session_resp.status ); // ------------------------------------------------------------------ // Append a couple of messages. `append_messages` writes to the primary // and then fans out inline to the mirror. // ------------------------------------------------------------------ let user_msg = MessageRecord { version: 1, message_id: Uuid::now_v7(), session: scroll_id, parent_id: None, ts: Utc::now(), role: "user".into(), author: Some("alice".into()), content_md: "Hello from the multi-backend example!".into(), content_parts: None, attachments: vec![], metadata: serde_json::json!({}), }; let asst_msg = MessageRecord { version: 1, message_id: Uuid::now_v7(), session: scroll_id, parent_id: Some(user_msg.message_id), ts: Utc::now(), role: "assistant".into(), author: Some("claude".into()), content_md: "Greetings. I have been written to two archives.".into(), content_parts: None, attachments: vec![], metadata: serde_json::json!({}), }; archivist .append_messages(scroll_id, vec![user_msg, asst_msg], None) .await?; println!("\nAppended 2 messages — fanned out to primary + mirror."); // ------------------------------------------------------------------ // Read path: the priority walk tries the primary first (priority=0). // It finds the session there and never consults the mirror. // ------------------------------------------------------------------ let meta = archivist.get_session_metadata(scroll_id, None).await?; println!( "\nRead session via priority walk: title={:?} completeness={:?}", meta.title, meta.completeness ); println!( "Read cache size after read: {}", archivist.read_cache_size().await ); let messages = archivist.get_messages(scroll_id, None).await?; println!("Read {} message(s) from the archive:", messages.len()); for m in &messages { println!(" - [{}] {}", m.role, m.content_md); } // ------------------------------------------------------------------ // Final health snapshot. Both backends should still be Available and // have no queued writes (both run Inline write policies by default). // ------------------------------------------------------------------ println!("\nFinal health snapshot:"); for s in archivist.list_archives_with_health().await { println!( " - {:<8} health={:?} queue_depth={:?} last_error={:?}", s.name, s.health, s.queue_depth, s.last_error ); } // Clean shutdown drains any queued writer tasks. Both backends here run // Inline, so this is effectively a no-op but remains the correct API. archivist.shutdown().await?; println!("\nShutdown complete."); Ok(()) }