use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex as StdMutex};

use tokio::sync::{Mutex, mpsc, watch};
use tokio_util::sync::CancellationToken;

use super::persistence::{
    append_persisted_messages, append_persisted_messages_with_meta, append_persisted_turn_messages,
    finalize_turn_after_persistence,
};
use super::turn::{TurnBlock, TurnController, TurnSnapshot};
use super::turn_input::prepare_turn_runtime;
use crate::bus::{
    ChannelContext, ChatMessage, CompletionStatus, InboundMessage, MediaItem, MediaRef,
    MessageSource, OutboundMessage, SourceKind,
};
use crate::mcp::get_mcp_status;
use crate::storage::{ContextCheckpoint, Storage, StorageError};
use std::sync::Arc as StdArc;

pub(super) type MessagePersistSnapshot = (
    StdArc<Storage>,
    String,
    crate::storage::message::MessageMeta,
    crate::storage::session::SessionMeta,
);

const SESSION_QUEUE_CAPACITY: usize = 32;

fn outbound_session_metadata(session_id: &str) -> HashMap<String, String> {
    HashMap::from([("_session_id".to_string(), session_id.to_string())])
}

fn outbound_turn_metadata(
    session_id: &str,
    private_context: &HashMap<String, String>,
) -> HashMap<String, String> {
    let mut metadata = private_context.clone();
    metadata.insert("_session_id".to_string(), session_id.to_string());
    metadata
}

fn committed_turn_delta(
    session_id: &str,
    messages: Vec<crate::storage::message::MessageMeta>,
) -> crate::bus::CommittedTurnDelta {
    let history_revision = messages.last().map_or(0, |message| message.seq);
    let messages = messages
        .into_iter()
        .filter(|message| message.client_visibility == crate::bus::ClientVisibility::Visible)
        .map(|message| crate::bus::CommittedMessage {
            id: message.id,
            seq: message.seq,
            role: message.role,
            content: message.content,
            reasoning_content: message.reasoning_content,
            completion_status: message.completion_status,
            media_refs: message
                .media_refs
                .and_then(|refs| serde_json::from_str(&refs).ok())
                .unwrap_or_default(),
            created_at: message.created_at,
            tool_call_id: message.tool_call_id,
            tool_name: message.tool_name,
            tool_calls: message
                .tool_calls
                .and_then(|calls| serde_json::from_str(&calls).ok()),
            turn_origin: message.turn_origin,
        })
        .collect();
    crate::bus::CommittedTurnDelta {
        session_id: session_id.to_string(),
        history_revision,
        messages,
    }
}

tokio::task_local! {
    pub(super) static CURRENT_SOURCE_SESSION: Option<String>;
    pub(super) static CURRENT_TURN_ID: Option<String>;
    pub(super) static CURRENT_TURN_DELIVERIES: Option<Arc<std::sync::Mutex<Vec<PendingTurnDelivery>>>>;
}

pub(super) struct PendingTurnDelivery {
    pub content: String,
    pub media: Vec<MediaItem>,
}

fn take_pending_turn_deliveries(
    deliveries: &Arc<std::sync::Mutex<Vec<PendingTurnDelivery>>>,
) -> Vec<PendingTurnDelivery> {
    std::mem::take(
        &mut *deliveries
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner()),
    )
}

fn take_current_turn_deliveries() -> Vec<PendingTurnDelivery> {
    CURRENT_TURN_DELIVERIES
        .try_with(|deliveries| {
            deliveries
                .as_ref()
                .map(take_pending_turn_deliveries)
                .unwrap_or_default()
        })
        .unwrap_or_default()
}

fn collapse_pending_turn_deliveries(pending: Vec<PendingTurnDelivery>) -> (String, Vec<MediaRef>) {
    let fallback_content = pending
        .iter()
        .map(|delivery| delivery.content.trim())
        .filter(|content| !content.is_empty())
        .collect::<Vec<_>>()
        .join("\n\n");
    let mut media_refs = Vec::new();
    for media_ref in pending
        .into_iter()
        .flat_map(|delivery| delivery.media.into_iter().map(|media| media.to_media_ref()))
    {
        if !media_refs.iter().any(|existing: &MediaRef| {
            existing.path == media_ref.path && existing.media_type == media_ref.media_type
        }) {
            media_refs.push(media_ref);
        }
    }
    (fallback_content, media_refs)
}

fn attach_pending_to_message(
    message: &mut ChatMessage,
    fallback_content: &str,
    media_refs: &[MediaRef],
) {
    if message.content.trim().is_empty() && !fallback_content.is_empty() {
        message.content = fallback_content.to_string();
    }
    for media_ref in media_refs {
        if !message.media_refs.iter().any(|existing| {
            existing.path == media_ref.path && existing.media_type == media_ref.media_type
        }) {
            message.media_refs.push(media_ref.clone());
        }
    }
}

fn attach_pending_turn_deliveries(
    result: &mut crate::agent::AgentProcessResult,
    pending: Vec<PendingTurnDelivery>,
) {
    let (fallback_content, media_refs) = collapse_pending_turn_deliveries(pending);
    if media_refs.is_empty() && fallback_content.is_empty() {
        return;
    }

    attach_pending_to_message(&mut result.final_response, &fallback_content, &media_refs);

    if let Some(final_message) = result
        .emitted_messages
        .iter_mut()
        .rev()
        .find(|message| message.id == result.final_response.id)
    {
        final_message
            .content
            .clone_from(&result.final_response.content);
        final_message
            .media_refs
            .clone_from(&result.final_response.media_refs);
    }
}

fn media_items_from_refs(media_refs: &[MediaRef]) -> Vec<MediaItem> {
    media_refs.iter().map(MediaItem::from_media_ref).collect()
}

fn partial_assistant_with_pending_deliveries(
    snapshot: &TurnSnapshot,
    completion_status: CompletionStatus,
    pending: Vec<PendingTurnDelivery>,
) -> Option<ChatMessage> {
    let (fallback_content, media_refs) = collapse_pending_turn_deliveries(pending);
    let mut message = partial_assistant_message(snapshot, completion_status).or_else(|| {
        (!media_refs.is_empty()).then(|| {
            let mut message = ChatMessage::assistant("");
            message.id = snapshot.message_id.clone();
            message.turn_id = Some(snapshot.id.0.clone());
            message.completion_status = completion_status;
            message
        })
    })?;
    attach_pending_to_message(&mut message, &fallback_content, &media_refs);
    Some(message)
}

/// Result of handling a message - either an AI response or a command output
pub enum HandleResult {
    /// AI response to be sent as AssistantResponse
    AgentResponse(String),
    /// Command output to be sent as CommandExecuted
    CommandOutput(String),
    /// Agent processing spawned in background; response will be sent via bus
    AgentProcessing,
}
use crate::agent::system_prompt::build_system_prompt;
use crate::agent::{
    AgentError, AgentLoop, AgentTurnContext, CompactionReason, ContextBudget, ContextBudgetParts,
    ContextCompactor, ContextRequestKey, ContextUsageTracker, PreviousCheckpoint, SequencedMessage,
    TurnEmitter, TurnInput, TurnInputSource, TurnMailbox, context_request_digest,
};
use crate::channels::slash_command::parse_slash_command;
use crate::config::LLMProviderConfig;
use crate::config::{BrowserConfig, ContextCompactionConfig};
use crate::delivery::{TurnDeliveryHandle, TurnDeliveryService};

fn partial_assistant_message(
    snapshot: &TurnSnapshot,
    completion_status: CompletionStatus,
) -> Option<ChatMessage> {
    let mut assistant_segments = Vec::new();
    let mut reasoning_segments = Vec::new();
    let mut last_iteration = None;
    for block in &snapshot.blocks {
        match block {
            TurnBlock::Assistant {
                iteration, text, ..
            } if !text.is_empty() => {
                assistant_segments.push(text.as_str());
                last_iteration = Some(*iteration);
            }
            TurnBlock::Reasoning { text, .. } if !text.is_empty() => {
                reasoning_segments.push(text.as_str());
            }
            _ => {}
        }
    }
    if assistant_segments.is_empty() {
        return None;
    }

    let mut message = ChatMessage::assistant(assistant_segments.join("\n\n"));
    message.id = snapshot.message_id.clone();
    message.turn_id = Some(snapshot.id.0.clone());
    message.iteration = last_iteration;
    message.completion_status = completion_status;
    message.reasoning_content =
        (!reasoning_segments.is_empty()).then(|| reasoning_segments.join("\n\n"));
    Some(message)
}

fn terminal_fallback_content(snapshot: &TurnSnapshot) -> Option<String> {
    partial_assistant_message(snapshot, CompletionStatus::Interrupted)
        .map(|message| message.content)
        .or_else(|| {
            (snapshot.status == super::turn::TurnStatus::Failed)
                .then(|| "The response could not be delivered. Please try again.".to_string())
        })
}

async fn deliver_terminal_fallback(
    handle: TurnDeliveryHandle,
    bus: &MessageBus,
    target: &crate::channels::TurnTarget,
    controller: &TurnController,
) {
    let Err(error) = handle.wait().await else {
        return;
    };
    tracing::error!(channel = %target.channel, chat_id = %target.chat_id, error = %error, "Turn sink terminal delivery failed; using ordinary outbound fallback");
    let snapshot = controller.snapshot();
    let Some(content) = terminal_fallback_content(&snapshot) else {
        return;
    };
    let outbound = OutboundMessage {
        channel: target.channel.clone(),
        chat_id: target.chat_id.clone(),
        content,
        reply_to: target.reply_to.clone(),
        media: vec![],
        metadata: target.metadata.clone(),
        delivery: None,
    };
    if let Err(fallback_error) = bus.deliver_outbound(outbound).await {
        tracing::error!(channel = %target.channel, chat_id = %target.chat_id, error = %fallback_error, "Ordinary terminal fallback delivery failed");
    }
}

async fn fail_turn_with_partial(
    controller: &TurnController,
    session: &Arc<Mutex<Session>>,
    error: String,
) {
    let snapshot = controller.snapshot();
    let partial = partial_assistant_with_pending_deliveries(
        &snapshot,
        CompletionStatus::Interrupted,
        take_current_turn_deliveries(),
    );
    if let Some(partial) = partial {
        controller.begin_finalizing();
        if let Err(persistence_error) = append_persisted_messages(session, vec![partial]).await {
            controller.fail(format!(
                "{error}; failed to persist interrupted turn: {persistence_error}"
            ));
            return;
        }
    }
    controller.fail(error);
}

/// Persist the partial assistant output of a cancelled Turn and close it as
/// `Cancelled`.  Shared by the `/stop` select branch and the cooperative
/// `AgentError::Cancelled` return so both paths commit the same terminal
/// state without emitting a processing-error outbound message.
async fn persist_cancelled_turn(
    controller: &TurnController,
    session: &Arc<Mutex<Session>>,
    pending_deliveries: Vec<PendingTurnDelivery>,
) {
    let snapshot = controller.snapshot();
    if let Some(partial) = partial_assistant_with_pending_deliveries(
        &snapshot,
        CompletionStatus::Cancelled,
        pending_deliveries,
    ) {
        controller.begin_finalizing();
        match append_persisted_messages(session, vec![partial]).await {
            Ok(()) => {
                controller.cancel(Some("stopped by user".to_string()));
            }
            Err(error) => {
                tracing::error!(error = %error, "Failed to persist cancelled partial turn");
                controller.fail(format!("failed to persist cancelled turn: {error}"));
            }
        }
    } else {
        controller.cancel(Some("stopped by user".to_string()));
    }
}

#[cfg(test)]
mod cancelled_partial_tests {
    use super::*;
    use crate::agent::TurnEvent;
    use crate::bus::{MessageBus, OutboundDispatcher};
    use crate::channels::{Channel, ChannelError, ChannelManager, CliChatChannel};
    use crate::delivery::{ConversationWriteLocks, DeliveryError};
    use crate::task_supervisor::TaskSupervisor;

    #[derive(Default)]
    struct RecordingChannel {
        messages: Mutex<Vec<String>>,
    }

    #[async_trait::async_trait]
    impl Channel for RecordingChannel {
        fn name(&self) -> &str {
            "recording"
        }

        fn is_running(&self) -> bool {
            true
        }

        async fn start(&self, _bus: Arc<MessageBus>) -> Result<(), ChannelError> {
            Ok(())
        }

        async fn stop(&self) -> Result<(), ChannelError> {
            Ok(())
        }

        async fn send(&self, message: OutboundMessage) -> Result<(), ChannelError> {
            self.messages.lock().await.push(message.content);
            Ok(())
        }
    }

    #[test]
    fn turn_metadata_preserves_channel_cleanup_fields() {
        let forwarded = HashMap::from([
            ("feishu.message_id".to_string(), "message-1".to_string()),
            ("feishu.reaction_id".to_string(), "reaction-1".to_string()),
            ("_session_id".to_string(), "stale".to_string()),
        ]);

        let metadata = outbound_turn_metadata("session-1", &forwarded);

        assert_eq!(
            metadata.get("_session_id").map(String::as_str),
            Some("session-1")
        );
        assert_eq!(
            metadata.get("feishu.message_id").map(String::as_str),
            Some("message-1")
        );
        assert_eq!(
            metadata.get("feishu.reaction_id").map(String::as_str),
            Some("reaction-1")
        );
    }

    #[test]
    fn visible_partial_text_becomes_cancelled_persisted_message() {
        let (controller, emitter, _) = TurnController::start("session", "message-id");
        emitter
            .emit(TurnEvent::ReasoningDelta {
                iteration: 0,
                delta: "reason".into(),
            })
            .unwrap();
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: "first".into(),
            })
            .unwrap();
        emitter
            .emit(TurnEvent::TextSegmentFinished { iteration: 0 })
            .unwrap();
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 1,
                delta: "second".into(),
            })
            .unwrap();

        let message =
            partial_assistant_message(&controller.snapshot(), CompletionStatus::Cancelled).unwrap();
        assert_eq!(message.id, "message-id");
        assert_eq!(message.content, "first\n\nsecond");
        assert_eq!(message.reasoning_content.as_deref(), Some("reason"));
        assert_eq!(message.iteration, Some(1));
        assert_eq!(message.completion_status, CompletionStatus::Cancelled);
    }

    #[test]
    fn failed_terminal_without_visible_text_has_safe_fallback() {
        let (controller, _emitter, _) = TurnController::start("session", "message-id");
        controller.fail("provider response contained a secret");

        assert_eq!(
            terminal_fallback_content(&controller.snapshot()).as_deref(),
            Some("The response could not be delivered. Please try again.")
        );
    }

    #[tokio::test]
    async fn asynchronous_terminal_failure_uses_one_ordinary_fallback() {
        let bus = MessageBus::new(8);
        let cli = Arc::new(CliChatChannel::new());
        let channels = ChannelManager::with_bus(cli, bus.clone());
        let channel = Arc::new(RecordingChannel::default());
        channels
            .register_channel("recording", channel.clone())
            .await;
        let supervisor = TaskSupervisor::new();
        let dispatcher = OutboundDispatcher::new(
            bus.clone(),
            channels,
            supervisor.clone(),
            ConversationWriteLocks::default(),
            Arc::new(std::sync::atomic::AtomicUsize::new(0)),
        );
        let dispatcher_task = tokio::spawn(async move { dispatcher.run().await });

        let (controller, emitter, _) = TurnController::start("session", "message-id");
        emitter
            .emit(TurnEvent::TextDelta {
                iteration: 0,
                delta: "completed response".into(),
            })
            .unwrap();
        controller.complete(None);
        let (sender, completion) = tokio::sync::oneshot::channel();
        sender.send(Err(DeliveryError::FinalTimedOut)).unwrap();

        let target = crate::channels::TurnTarget {
            channel: "recording".to_string(),
            chat_id: "chat".to_string(),
            session_id: "session".to_string(),
            reply_to: None,
            metadata: HashMap::new(),
        };
        deliver_terminal_fallback(
            TurnDeliveryHandle { completion },
            &bus,
            &target,
            &controller,
        )
        .await;

        assert_eq!(
            channel.messages.lock().await.as_slice(),
            &["completed response"]
        );
        dispatcher_task.abort();
        let _ = dispatcher_task.await;
        supervisor.shutdown(std::time::Duration::from_secs(1)).await;
    }

    #[test]
    fn reasoning_only_cancel_does_not_create_assistant_history() {
        let (controller, emitter, _) = TurnController::start("session", "message-id");
        emitter
            .emit(TurnEvent::ReasoningDelta {
                iteration: 0,
                delta: "private".into(),
            })
            .unwrap();
        assert!(
            partial_assistant_message(&controller.snapshot(), CompletionStatus::Cancelled,)
                .is_none()
        );
    }

    #[test]
    fn pending_same_turn_media_is_attached_to_the_final_response() {
        let final_message = ChatMessage::assistant("截图已经准备好了");
        let final_id = final_message.id.clone();
        let mut result = crate::agent::AgentProcessResult {
            final_response: final_message.clone(),
            emitted_messages: vec![
                ChatMessage::tool("call-1", "send_message", "附件已加入当前回复"),
                final_message,
            ],
            total_tokens: None,
            usage: None,
            last_request_usage: None,
            last_request_digest: None,
        };

        attach_pending_turn_deliveries(
            &mut result,
            vec![PendingTurnDelivery {
                content: "这是百度首页截图".to_string(),
                media: vec![MediaItem::new("/tmp/baidu.png", "image")],
            }],
        );

        assert_eq!(result.final_response.content, "截图已经准备好了");
        assert_eq!(result.final_response.media_refs.len(), 1);
        assert_eq!(result.final_response.media_refs[0].path, "/tmp/baidu.png");
        let committed_final = result
            .emitted_messages
            .iter()
            .find(|message| message.id == final_id)
            .unwrap();
        assert_eq!(committed_final.media_refs.len(), 1);
        assert_eq!(committed_final.media_refs[0].path, "/tmp/baidu.png");
    }

    #[test]
    fn pending_media_survives_a_turn_without_partial_text() {
        let (controller, _emitter, _) = TurnController::start("session", "message-id");

        let message = partial_assistant_with_pending_deliveries(
            &controller.snapshot(),
            CompletionStatus::Interrupted,
            vec![PendingTurnDelivery {
                content: "这是已生成的截图".to_string(),
                media: vec![MediaItem::new("/tmp/baidu.png", "image")],
            }],
        )
        .unwrap();

        assert_eq!(message.id, "message-id");
        assert_eq!(message.content, "这是已生成的截图");
        assert_eq!(message.completion_status, CompletionStatus::Interrupted);
        assert_eq!(message.media_refs.len(), 1);
    }
}
use crate::bus::MessageBus;
use crate::providers::{LLMProvider, create_provider};
use crate::session::events::DialogInfo;
use crate::session::session_id::UnifiedSessionId;
use crate::skills::SkillsLoader;
use crate::tools::OutboundMessenger;
use crate::tools::SendMessageTool;
use crate::tools::{ToolExecutionContext, ToolRegistry, create_default_tools};

/// Session = 一个 dialog
/// 每个 Session 对应一个 UnifiedSessionId，有独立的 messages history
pub struct Session {
    pub id: UnifiedSessionId,
    pub title: String,
    pub created_at: i64,
    pub last_active_at: i64,
    pub message_count: i64,
    pub total_message_count: i64,

    messages: Vec<ChatMessage>,
    /// Durable sequence for every raw in-memory message, keyed by message ID.
    message_seqs: HashMap<String, i64>,
    seq_counter: i64,

    provider_config: LLMProviderConfig,
    provider: Arc<dyn LLMProvider>,
    tools: Arc<ToolRegistry>,
    context_compactor: ContextCompactor,
    context_usage: ContextUsageTracker,
    active_context_checkpoint: Option<ContextCheckpoint>,
    context_generation: i64,
    effective_context_window: usize,

    storage: Option<StdArc<Storage>>,
    routing_info: String,
    archived_at: Option<i64>,
    /// Timestamp (Unix ms) of the last consolidation.
    /// Messages before this time have been compressed into memory.
    pub last_consolidated_at: Option<i64>,
    pub last_compressed_message_at: Option<i64>,
    memory_manager: Arc<crate::memory::MemoryManager>,

    /// Task queue for per-session serial agent processing
    agent_tx: Option<mpsc::Sender<AgentTask>>,
    /// Monotonic admission order across steering and next-turn queue inputs.
    /// It is allocated while holding the Session mutex so worker cleanup can
    /// use that same lock as the send barrier.
    next_task_sequence: u64,
    /// Whether the session has an active Turn or queued local fallback
    /// tasks.  `/stop` clears it; the busy signal is observability-only
    /// (idle wait, active-turn count).  Cancellation itself is driven solely
    /// by `current_turn_token`.
    turn_busy: bool,
    /// Structured cancellation for the active Turn.  `/stop` cancels it and
    /// the token propagates through AgentLoop provider streams and tool
    /// batches.
    current_turn_token: Option<CancellationToken>,
    /// Latest durable inbox revision for this session.  The worker watches
    /// this to claim agent events; the value only merges wakes, the payload
    /// always comes from SQLite.
    agent_inbox_wake: watch::Sender<i64>,
    active_turn_emitter: Option<ActiveTurnEmitter>,
    /// Monotonic counter to detect stale workers
    worker_generation: u64,
    /// Prevents duplicate background title requests while the title is still default.
    title_generation_in_flight: bool,
    /// Monotonic counter for in-memory session mutations.
    ///
    /// Slow work such as memory recall, compression, and title generation runs
    /// outside the session lock. Workers capture this version before starting
    /// that work and verify it before committing results, so stale snapshots do
    /// not overwrite a session that was changed by a command such as /clear or
    /// /delete while the slow work was in flight.
    state_version: u64,
    /// Serializes durable mutations while allowing the session state mutex to
    /// be released during SQLite I/O.
    pub(super) persistence_lock: Arc<Mutex<()>>,
}

struct ActiveTurnEmitter {
    turn_id: String,
    emitter: TurnEmitter,
    /// Mailbox for user messages that should be injected into this turn at a
    /// safe agent-loop boundary.  The mailbox is shared with AgentLoop via
    /// `AgentTurnContext`; keeping it on the session handle makes admission
    /// atomic with `/stop` and worker cleanup.
    steering: Arc<TurnMailbox>,
    /// Original inbound tasks for accepted steering messages.  ChatMessage
    /// intentionally carries only durable history fields, so this side map
    /// preserves channel context and rich MediaItem metadata if a terminal
    /// turn has to fall back to a subsequent queued Turn.
    recovery: StdArc<StdMutex<HashMap<String, AgentTask>>>,
}

/// A task to be processed by the per-session agent worker
#[derive(Clone)]
struct AgentTask {
    channel: String,
    sender_id: String,
    chat_id: String,
    sequence: u64,
    client_message_id: Option<String>,
    content: String,
    received_at: i64,
    media: Vec<MediaItem>,
    channel_context: ChannelContext,
}

#[derive(Clone)]
struct SessionContextSnapshot {
    raw: Vec<SequencedMessage>,
    previous: Option<PreviousCheckpoint>,
    generation: i64,
}

fn chat_message_from_meta(message: crate::storage::message::MessageMeta) -> ChatMessage {
    ChatMessage {
        id: message.id,
        role: message.role,
        content: message.content,
        reasoning_content: message.reasoning_content,
        provider_state: message
            .provider_state
            .and_then(|state| crate::bus::ProviderReasoningState::from_json_lossy(&state)),
        turn_id: message.turn_id,
        iteration: message
            .iteration
            .and_then(|value| u32::try_from(value).ok()),
        completion_status: message.completion_status,
        client_visibility: message.client_visibility,
        turn_origin: message.turn_origin,
        media_refs: message
            .media_refs
            .map(|refs| serde_json::from_str(&refs).unwrap_or_default())
            .unwrap_or_default(),
        timestamp: message.created_at,
        tool_call_id: message.tool_call_id,
        tool_name: message.tool_name,
        tool_calls: message
            .tool_calls
            .and_then(|calls| serde_json::from_str(&calls).ok())
            .filter(|calls: &Vec<crate::providers::ToolCall>| !calls.is_empty()),
        source: message
            .source
            .and_then(|source| serde_json::from_str(&source).ok()),
    }
}

/// Build a typed steer input from a claimed inbox event.  The content is a
/// bounded, readable envelope for the model; the typed source and durable
/// event id are preserved for rendering and cancellation recovery.
fn steer_input_from_event(
    event: &crate::storage::agent_inbox::AgentInboxEventRecord,
    now: i64,
) -> TurnInput {
    use crate::agent::steering::InputDelivery;
    use crate::storage::agent_inbox::AgentEventType;
    let payload: serde_json::Value =
        serde_json::from_str(&event.payload_json).unwrap_or(serde_json::Value::Null);
    let run_id = event.run_id.clone().unwrap_or_default();
    let agent_id = payload
        .get("agent_id")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("unknown")
        .to_string();
    let (source, content) = match event.event_type {
        AgentEventType::Signal => {
            let severity = event.severity.clone().unwrap_or_else(|| "info".to_string());
            let summary = payload
                .get("summary")
                .and_then(serde_json::Value::as_str)
                .unwrap_or_default()
                .to_string();
            let mut content = format!(
                "[后台 Agent 信号] severity={severity}, agent={agent_id}, run={run_id}\n{summary}"
            );
            if let Some(details) = payload.get("details")
                && !details.is_null()
            {
                content.push_str("\n详情: ");
                content.push_str(&details.to_string());
            }
            (
                TurnInputSource::AgentSignal {
                    run_id: run_id.clone(),
                    agent_id: agent_id.clone(),
                },
                content,
            )
        }
        AgentEventType::Completion => {
            let status = payload
                .get("status")
                .and_then(serde_json::Value::as_str)
                .unwrap_or("completed");
            let mut content =
                format!("[后台 Agent 任务结果] status={status}, agent={agent_id}, run={run_id}");
            if let Some(error) = payload.get("error").and_then(serde_json::Value::as_str) {
                content.push_str(&format!("\n错误: {error}"));
            }
            (
                TurnInputSource::AgentCompletion {
                    run_id: run_id.clone(),
                    agent_id: agent_id.clone(),
                },
                content,
            )
        }
    };
    TurnInput {
        id: format!("steer:{}", event.id),
        sequence: 0,
        source,
        delivery: InputDelivery::Steer,
        content,
        media_refs: Vec::new(),
        durable_event_id: Some(event.id.clone()),
        received_at: now,
        message_source: None,
        lease_token: Some(event.lease_token.clone().unwrap_or_default()),
    }
}

/// Move terminally pending steering into the worker's local FIFO.  The
/// recovery map is consulted first so the original channel context and rich
/// media metadata survive a same-turn fallback; only legacy/test producers
/// need the ChatMessage-derived fallback.
///
/// Durable Agent steer entries are NOT requeued as user tasks: they are
/// returned as `(event_id, lease_token)` pairs so the caller releases them
/// back to the durable inbox (`pending`), where the queue lane will deliver
/// them as a continuation.
fn prepend_pending_steering(
    mailbox: &TurnMailbox,
    recovery: &StdArc<StdMutex<HashMap<String, AgentTask>>>,
    local_tasks: &mut VecDeque<AgentTask>,
    fallback_channel: &str,
    fallback_chat_id: &str,
    fallback_reply_to: &Option<String>,
    fallback_private: &HashMap<String, String>,
) -> Vec<(String, String)> {
    let take = mailbox.close_and_take_pending();
    for message in take.user_inputs.into_iter().rev() {
        let recovered = recovery
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
            .remove(&message.id);
        let task = recovered.unwrap_or_else(|| AgentTask {
            channel: message
                .message_source
                .as_ref()
                .and_then(|source| source.from_channel.clone())
                .unwrap_or_else(|| fallback_channel.to_string()),
            sender_id: message
                .message_source
                .as_ref()
                .and_then(|source| source.from_user_id.clone())
                .unwrap_or_else(|| "unknown".to_string()),
            chat_id: fallback_chat_id.to_string(),
            sequence: 0,
            client_message_id: Some(message.id.clone()),
            content: message.content,
            received_at: message.received_at,
            media: message
                .media_refs
                .into_iter()
                .map(|media| MediaItem::new(media.path, media.media_type))
                .collect(),
            channel_context: ChannelContext {
                reply_to: fallback_reply_to.clone(),
                private: fallback_private.clone(),
                durable_private: HashMap::new(),
            },
        });
        local_tasks.push_front(task);
    }
    recovery
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .clear();
    let mut leases = take.admitted_leases;
    leases.extend(take.reserved_leases);
    leases
}

/// Return admitted/leased Agent steer events to the durable inbox.  They
/// become `pending` again with an immediate retry so the queue lane picks
/// them up (steer reliably degrades to queue when the active Turn closes).
async fn release_steer_leases(storage: &Storage, leases: Vec<(String, String)>) {
    if leases.is_empty() {
        return;
    }
    let mut by_token: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();
    for (event_id, token) in leases {
        by_token.entry(token).or_default().push(event_id);
    }
    let now = chrono::Utc::now().timestamp_millis();
    for (token, ids) in by_token {
        if let Err(error) = storage
            .release_inbox_lease(&token, &ids, 0, Some("steer turn closed"), now)
            .await
        {
            tracing::warn!(error = %error, "failed to release steer events to pending");
        }
    }
}

fn pop_lowest_sequence(local_tasks: &mut VecDeque<AgentTask>) -> Option<AgentTask> {
    let index = local_tasks
        .iter()
        .enumerate()
        .min_by_key(|(_, task)| task.sequence)
        .map(|(index, _)| index)?;
    local_tasks.remove(index)
}

#[derive(Clone)]
struct AgentWorkerDeps {
    bus: Arc<MessageBus>,
    memory_manager: Arc<crate::memory::MemoryManager>,
    work_manager: Arc<crate::work::WorkManager>,
    skills_loader: Arc<SkillsLoader>,
    task_supervisor: crate::task_supervisor::TaskSupervisor,
    turn_delivery: TurnDeliveryService,
    execution_gate: Arc<crate::agent::gate::ExecutionGate>,
    /// Fairness policy: after this many consecutive user Turns a due inbox
    /// event must be processed before another user input.
    inbox_burst: usize,
    /// Fairness policy: a pending event older than this is forced before the
    /// next user Turn.
    inbox_wait_ms: i64,
    /// Delivery attempts before an inbox event is dead-lettered.
    inbox_max_attempts: i64,
}

#[derive(Clone)]
pub(super) struct SessionContextServices {
    pub(super) memory_manager: Arc<crate::memory::MemoryManager>,
    pub(super) compaction_config: ContextCompactionConfig,
}

impl Session {
    pub(super) async fn new(
        id: UnifiedSessionId,
        provider_config: LLMProviderConfig,
        tools: Arc<ToolRegistry>,
        storage: Option<StdArc<Storage>>,
        routing_info: String,
        title: String,
        context_services: SessionContextServices,
    ) -> Result<Self, AgentError> {
        let mut provider_box = create_provider(provider_config.clone())
            .map_err(|e| AgentError::Other(format!("provider creation error: {}", e)))?;
        if let Some(ref s) = storage {
            provider_box.set_storage(s.clone());
        }
        let provider: Arc<dyn LLMProvider> = Arc::from(provider_box);

        let context_compactor =
            ContextCompactor::new(provider.clone(), context_services.compaction_config);

        let now = chrono::Utc::now().timestamp_millis();

        Ok(Self {
            id: id.clone(),
            title,
            created_at: now,
            last_active_at: now,
            message_count: 0,
            total_message_count: 0,
            messages: Vec::new(),
            message_seqs: HashMap::new(),
            seq_counter: 1,
            provider_config: provider_config.clone(),
            provider: provider.clone(),
            tools,
            context_compactor,
            context_usage: ContextUsageTracker::default(),
            active_context_checkpoint: None,
            context_generation: 0,
            effective_context_window: provider_config.token_limit,
            storage,
            routing_info,
            archived_at: None,
            last_consolidated_at: None,
            last_compressed_message_at: None,
            memory_manager: context_services.memory_manager,
            agent_tx: None,
            next_task_sequence: 1,
            turn_busy: false,
            current_turn_token: None,
            agent_inbox_wake: watch::channel(0).0,
            active_turn_emitter: None,
            worker_generation: 0,
            title_generation_in_flight: false,
            state_version: 0,
            persistence_lock: Arc::new(Mutex::new(())),
        })
    }

    /// 从 Storage 恢复 Session
    pub(super) async fn from_storage(
        id: UnifiedSessionId,
        provider_config: LLMProviderConfig,
        tools: Arc<ToolRegistry>,
        storage: StdArc<Storage>,
        context_services: SessionContextServices,
    ) -> Result<Self, AgentError> {
        let session_meta = storage.get_session(&id.to_string()).await.map_err(|e| {
            AgentError::Other(format!("failed to load session from storage: {}", e))
        })?;

        let mut provider_box = create_provider(provider_config.clone())
            .map_err(|e| AgentError::Other(format!("provider creation error: {}", e)))?;
        provider_box.set_storage(storage.clone());
        let provider: Arc<dyn LLMProvider> = Arc::from(provider_box);

        let context_compactor =
            ContextCompactor::new(provider.clone(), context_services.compaction_config);
        let stored_messages = storage
            .load_messages(&id.to_string(), 0)
            .await
            .map_err(|e| AgentError::Other(format!("failed to load messages from storage: {e}")))?;
        let message_seqs = stored_messages
            .iter()
            .map(|message| (message.id.clone(), message.seq))
            .collect::<HashMap<_, _>>();
        let mut chat_messages = stored_messages
            .into_iter()
            .map(chat_message_from_meta)
            .collect::<Vec<_>>();
        repair_tool_call_chains(&mut chat_messages);

        let checkpoint_state = storage
            .load_context_checkpoint_state(&id.to_string())
            .await
            .map_err(|e| AgentError::Other(format!("failed to load context checkpoint: {e}")))?;
        let active_context_checkpoint = checkpoint_state.checkpoint.filter(|checkpoint| {
            let valid = !checkpoint.summary.trim().is_empty()
                && message_seqs
                    .values()
                    .any(|seq| *seq == checkpoint.first_retained_seq);
            if !valid {
                tracing::error!(
                    session_id = %id,
                    checkpoint_id = %checkpoint.id,
                    "Ignoring invalid context checkpoint and restoring raw history"
                );
            }
            valid
        });
        if checkpoint_state.active_checkpoint_id.is_some() && active_context_checkpoint.is_none() {
            tracing::error!(session_id = %id, "Active context checkpoint row is missing or invalid");
        }

        // seq_counter from actual DB max
        let max_seq = storage
            .get_max_message_seq(&id.to_string())
            .await
            .map_err(|e| AgentError::Other(format!("failed to load message sequence: {}", e)))?;
        let seq_counter = max_seq + 1;
        let total_message_count = session_meta.message_count;

        Ok(Self {
            id: id.clone(),
            title: session_meta.title,
            created_at: session_meta.created_at,
            last_active_at: session_meta.last_active_at,
            message_count: session_meta.message_count,
            total_message_count,
            messages: chat_messages,
            message_seqs,
            seq_counter,
            provider_config: provider_config.clone(),
            provider: provider.clone(),
            tools,
            context_compactor,
            context_usage: ContextUsageTracker::default(),
            active_context_checkpoint,
            context_generation: checkpoint_state.generation,
            effective_context_window: provider_config.token_limit,
            storage: Some(storage),
            routing_info: session_meta.routing_info.unwrap_or_default(),
            archived_at: session_meta.archived_at,
            last_consolidated_at: session_meta.last_consolidated_at,
            last_compressed_message_at: session_meta.last_compressed_message_at,
            memory_manager: context_services.memory_manager,
            agent_tx: None,
            next_task_sequence: 1,
            turn_busy: false,
            current_turn_token: None,
            agent_inbox_wake: watch::channel(0).0,
            active_turn_emitter: None,
            worker_generation: 0,
            title_generation_in_flight: false,
            state_version: 0,
            persistence_lock: Arc::new(Mutex::new(())),
        })
    }

    /// 获取 session ID
    pub fn session_id(&self) -> String {
        self.id.to_string()
    }

    pub(super) fn add_message_in_memory_with_version(
        &mut self,
        message: ChatMessage,
        persist: bool,
        advance_state_version: bool,
    ) -> Option<MessagePersistSnapshot> {
        let persist_snapshot = persist
            .then(|| self.prepare_message_persist_snapshot(&message))
            .flatten();
        let (seq, now) = persist_snapshot
            .as_ref()
            .map(|(_, _, message, _)| (message.seq, message.created_at))
            .unwrap_or_else(|| (self.seq_counter, chrono::Utc::now().timestamp_millis()));
        let applied =
            self.apply_prepared_message_in_memory(message, seq, now, advance_state_version);
        debug_assert!(applied, "fresh in-memory message snapshot must apply");
        persist_snapshot
    }

    pub(super) fn prepare_message_persist_snapshot(
        &self,
        message: &ChatMessage,
    ) -> Option<MessagePersistSnapshot> {
        let storage = self.storage.clone()?;
        let is_user = message.role == "user";
        let counts_as_user_input =
            is_user && message.client_visibility == crate::bus::ClientVisibility::Visible;
        let now = chrono::Utc::now().timestamp_millis();
        let seq = self.seq_counter;
        let msg_meta = crate::storage::message::MessageMeta {
            id: message.id.clone(),
            session_id: self.id.to_string(),
            seq,
            role: message.role.clone(),
            content: message.content.clone(),
            reasoning_content: message.reasoning_content.clone(),
            provider_state: message
                .provider_state
                .as_ref()
                .and_then(|state| serde_json::to_string(state).ok()),
            turn_id: message.turn_id.clone(),
            iteration: message.iteration.map(i64::from),
            completion_status: message.completion_status,
            client_visibility: message.client_visibility,
            turn_origin: message.turn_origin,
            media_refs: if message.media_refs.is_empty() {
                None
            } else {
                Some(serde_json::to_string(&message.media_refs).unwrap_or_default())
            },
            tool_call_id: message.tool_call_id.clone(),
            tool_name: message.tool_name.clone(),
            tool_calls: message
                .tool_calls
                .as_ref()
                .and_then(|tc| serde_json::to_string(tc).ok()),
            source: message
                .source
                .as_ref()
                .map(|s| serde_json::to_string(s).unwrap_or_default()),
            created_at: now,
        };
        let session_id = self.id.to_string();
        let session_meta = crate::storage::session::SessionMeta {
            id: session_id.clone(),
            channel: self.id.channel.clone(),
            chat_id: self.id.chat_id.clone(),
            dialog_id: self.id.dialog_id.clone(),
            title: self.title.clone(),
            created_at: self.created_at,
            last_active_at: now,
            message_count: self.message_count + i64::from(counts_as_user_input),
            routing_info: if self.routing_info.is_empty() {
                None
            } else {
                Some(self.routing_info.clone())
            },
            archived_at: self.archived_at,
            deleted_at: None,
            last_consolidated_at: self.last_consolidated_at,
            last_compressed_message_at: self.last_compressed_message_at,
        };
        Some((storage, session_id, msg_meta, session_meta))
    }

    pub(super) fn apply_prepared_message_in_memory(
        &mut self,
        message: ChatMessage,
        seq: i64,
        now: i64,
        advance_state_version: bool,
    ) -> bool {
        if self.seq_counter != seq || self.contains_message_id(&message.id) {
            return false;
        }
        let is_user = message.role == "user";
        let counts_as_user_input =
            is_user && message.client_visibility == crate::bus::ClientVisibility::Visible;
        self.message_seqs.insert(message.id.clone(), seq);
        self.messages.push(message);
        self.seq_counter += 1;
        self.total_message_count += 1;
        if counts_as_user_input {
            self.message_count += 1;
        }
        self.last_active_at = now;
        if advance_state_version {
            self.state_version = self.state_version.wrapping_add(1);
        }
        true
    }

    pub(super) fn contains_message_id(&self, message_id: &str) -> bool {
        self.message_seqs.contains_key(message_id)
    }

    /// Roll back messages that were appended in memory but whose atomic
    /// persistence failed. This is only called while holding the session lock,
    /// so the suffix check also protects against removing unrelated messages.
    pub(super) fn rollback_message_suffix_with_version(
        &mut self,
        message_ids: &[String],
        advance_state_version: bool,
    ) {
        if message_ids.is_empty() || self.messages.len() < message_ids.len() {
            return;
        }
        let start = self.messages.len() - message_ids.len();
        if self.messages[start..]
            .iter()
            .zip(message_ids)
            .any(|(message, id)| &message.id != id)
        {
            tracing::error!(session_id = %self.id, "Refusing to roll back a non-matching message suffix");
            return;
        }

        let removed_user_messages = self.messages[start..]
            .iter()
            .filter(|message| message.role == "user")
            .count() as i64;
        for message in &self.messages[start..] {
            self.message_seqs.remove(&message.id);
        }
        self.messages.truncate(start);
        self.seq_counter -= message_ids.len() as i64;
        self.total_message_count -= message_ids.len() as i64;
        self.message_count -= removed_user_messages;
        if advance_state_version {
            self.state_version = self.state_version.wrapping_add(1);
        }
    }

    pub(super) fn owns_active_turn(&self, turn_id: &str) -> bool {
        self.active_turn_emitter
            .as_ref()
            .is_some_and(|active| active.turn_id == turn_id)
    }

    fn allocate_task_sequence(&mut self) -> u64 {
        let sequence = self.next_task_sequence;
        self.next_task_sequence = self.next_task_sequence.wrapping_add(1).max(1);
        sequence
    }

    #[cfg(test)]
    pub(super) fn state_version_for_test(&self) -> u64 {
        self.state_version
    }

    #[cfg(test)]
    pub(super) fn set_active_turn_for_test(&mut self, turn_id: &str) {
        let (_controller, emitter, _) = TurnController::start(self.id.to_string(), "test-message");
        self.active_turn_emitter = Some(ActiveTurnEmitter {
            turn_id: turn_id.to_string(),
            emitter,
            steering: TurnMailbox::new_shared(),
            recovery: StdArc::new(StdMutex::new(HashMap::new())),
        });
    }

    /// 获取消息历史
    pub fn get_history(&self) -> &[ChatMessage] {
        &self.messages
    }

    pub fn create_user_message(&self, content: &str, media_refs: Vec<MediaRef>) -> ChatMessage {
        if media_refs.is_empty() {
            ChatMessage::user(content)
        } else {
            ChatMessage::user_with_media(content, media_refs)
        }
    }

    pub fn create_user_message_with_source(
        &self,
        content: &str,
        media_refs: Vec<MediaRef>,
        source: MessageSource,
    ) -> ChatMessage {
        let mut message = ChatMessage::user_with_source(content, source);
        message.media_refs = media_refs;
        message
    }

    fn session_meta_snapshot(
        &self,
    ) -> Option<(StdArc<Storage>, crate::storage::session::SessionMeta)> {
        let storage = self.storage.clone()?;
        let meta = crate::storage::session::SessionMeta {
            id: self.id.to_string(),
            channel: self.id.channel.clone(),
            chat_id: self.id.chat_id.clone(),
            dialog_id: self.id.dialog_id.clone(),
            title: self.title.clone(),
            created_at: self.created_at,
            last_active_at: self.last_active_at,
            message_count: self.message_count,
            routing_info: if self.routing_info.is_empty() {
                None
            } else {
                Some(self.routing_info.clone())
            },
            archived_at: self.archived_at,
            deleted_at: None,
            last_consolidated_at: self.last_consolidated_at,
            last_compressed_message_at: self.last_compressed_message_at,
        };
        Some((storage, meta))
    }

    /// 检查是否需要自动生成 title（5 条用户消息后）
    pub fn should_generate_title(&self) -> bool {
        self.title == "新对话" && self.message_count >= 5
    }

    fn title_prompt_snapshot(&self) -> Option<String> {
        if !self.should_generate_title() {
            return None;
        }

        Some(format!(
            r#"给定以下对话历史，生成一个简短的会话标题（5-15 个中文字符），概括这个对话的核心内容或用户的主要需求。只返回一个标题，不要解释。

历史：
{}"#,
            self.messages
                .iter()
                .filter(|m| m.role == "user" || m.role == "assistant")
                .take(20)
                .map(|m| format!("[{}]: {}", m.role, m.content))
                .collect::<Vec<_>>()
                .join("\n")
        ))
    }

    fn apply_generated_title(&mut self, title: String) -> bool {
        if title.is_empty() || !self.should_generate_title() {
            return false;
        }

        self.title = title;
        self.state_version = self.state_version.wrapping_add(1);
        true
    }

    fn context_snapshot(&self) -> SessionContextSnapshot {
        let mut raw = self
            .messages
            .iter()
            .filter_map(|message| {
                self.message_seqs
                    .get(&message.id)
                    .copied()
                    .map(|seq| SequencedMessage {
                        seq,
                        message: message.clone(),
                    })
            })
            .collect::<Vec<_>>();
        raw.sort_by_key(|entry| entry.seq);
        SessionContextSnapshot {
            raw,
            previous: self.active_context_checkpoint.as_ref().map(|checkpoint| {
                PreviousCheckpoint {
                    id: checkpoint.id.clone(),
                    summary: checkpoint.summary.clone(),
                    first_retained_seq: checkpoint.first_retained_seq,
                }
            }),
            generation: self.context_generation,
        }
    }

    fn projected_history(&self) -> Vec<ChatMessage> {
        let snapshot = self.context_snapshot();
        crate::agent::context_compaction::project_history(&snapshot.raw, snapshot.previous.as_ref())
    }

    fn apply_context_checkpoint(&mut self, checkpoint: ContextCheckpoint) {
        if checkpoint.generation < self.context_generation {
            return;
        }
        self.context_generation = checkpoint.generation;
        self.last_compressed_message_at = Some(checkpoint.created_at);
        self.active_context_checkpoint = Some(checkpoint);
        self.context_usage.invalidate();
    }

    /// 获取 provider_config 引用
    pub fn provider_config(&self) -> &LLMProviderConfig {
        &self.provider_config
    }

    /// 创建一个临时的 AgentLoop 实例来处理消息
    pub fn create_agent(&self) -> Result<AgentLoop, AgentError> {
        let threshold = ContextBudget::new(
            self.effective_context_window,
            self.context_compactor.config(),
        )
        .threshold;
        Ok(AgentLoop::with_provider_and_tools(
            self.provider.clone(),
            self.tools.clone(),
            self.provider_config.max_tool_iterations,
            self.provider_config.model_id.clone(),
            self.provider_config.workspace_dir.clone(),
            self.provider_config.input_types.clone(),
        )
        .with_context_window(self.effective_context_window)
        .with_context_trim_threshold(threshold))
    }

    /// 构建系统提示词（包含 AgentLoop 的基础提示词 + skills + memory）
    pub fn build_system_prompt(&self, skills_prompt: &str) -> String {
        let base_prompt = build_system_prompt(
            &self.provider_config.workspace_dir,
            &self.provider_config.model_id,
            &self.tools,
        );

        if skills_prompt.trim().is_empty() {
            base_prompt
        } else {
            format!("{}\n\n{}", base_prompt, skills_prompt)
        }
    }

    /// 将当前 session 导出为 markdown 文档并保存到文件
    pub fn dump_to_file(&self, system_prompt: &str) -> std::io::Result<String> {
        use chrono::Local;
        use std::fs;
        use std::io::Write;

        let md = self.dump_as_markdown_with_system_prompt(system_prompt);

        // Create dumps directory under workspace
        let dumps_dir = self.provider_config.workspace_dir.join("dumps");
        fs::create_dir_all(&dumps_dir)?;

        // Generate filename based on session info
        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
        let filename = format!("{}_{}_{}.md", self.id.channel, self.id.chat_id, timestamp);
        let filepath = dumps_dir.join(&filename);

        // Write to file
        let mut file = fs::File::create(&filepath)?;
        file.write_all(md.as_bytes())?;

        Ok(filepath.to_string_lossy().to_string())
    }

    /// 将当前 session 导出为 markdown 文档（纯内存版本）
    pub fn dump_as_markdown(&self) -> String {
        use chrono::{DateTime, Local};

        let now = Local::now().format("%Y-%m-%d %H:%M:%S");

        let mut md = String::new();
        md.push_str("# Session Dump\n\n");
        md.push_str(&format!("- **Session ID**: `{}`\n", self.id));
        md.push_str(&format!("- **Channel**: `{}`\n", self.id.channel));
        md.push_str(&format!("- **Chat ID**: `{}`\n", self.id.chat_id));
        md.push_str(&format!("- **Dialog ID**: `{}`\n", self.id.dialog_id));
        md.push_str(&format!("- **Message Count**: {}\n", self.messages.len()));
        md.push_str(&format!(
            "- **Model**: `{}`\n",
            self.provider_config.model_id
        ));
        md.push_str(&format!("- **Exported At**: {}\n", now));
        md.push_str("\n---\n\n");

        md.push_str("## Conversation History\n\n");

        for (i, msg) in self.messages.iter().enumerate() {
            let role = match msg.role.as_str() {
                "system" => "System",
                "user" => "User",
                "assistant" => "Assistant",
                "tool" => "Tool",
                r => r,
            };

            let timestamp = if msg.timestamp > 0 {
                DateTime::from_timestamp_millis(msg.timestamp)
                    .map(|dt| {
                        dt.with_timezone(&Local)
                            .format("%Y-%m-%d %H:%M:%S")
                            .to_string()
                    })
                    .unwrap_or_default()
            } else {
                String::new()
            };

            md.push_str(&format!("### [{:03}] {} {}\n\n", i + 1, role, timestamp));
            md.push_str("```\n");

            if let Some(ref tool_calls) = msg.tool_calls {
                md.push_str("[Tool Calls]\n");
                for tc in tool_calls {
                    md.push_str(&format!("- {}: {:?}\n", tc.name, tc.arguments));
                }
            }

            if let Some(ref tool_name) = msg.tool_name {
                md.push_str(&format!("[Tool: {}]\n", tool_name));
            }

            if let Some(ref tool_call_id) = msg.tool_call_id {
                md.push_str(&format!("[Tool Call ID: {}]\n", tool_call_id));
            }

            md.push_str(&msg.content);
            md.push_str("\n```\n\n");

            if !msg.media_refs.is_empty() {
                md.push_str(&format!("**Media**: {:?}\n\n", msg.media_refs));
            }
        }

        md
    }

    /// 将当前 session 导出为 markdown 文档（包含系统提示词）
    pub fn dump_as_markdown_with_system_prompt(&self, system_prompt: &str) -> String {
        use chrono::{DateTime, Local};

        let now = Local::now().format("%Y-%m-%d %H:%M:%S");

        let mut md = String::new();
        md.push_str("# Session Dump\n\n");
        md.push_str(&format!("- **Session ID**: `{}`\n", self.id));
        md.push_str(&format!("- **Channel**: `{}`\n", self.id.channel));
        md.push_str(&format!("- **Chat ID**: `{}`\n", self.id.chat_id));
        md.push_str(&format!("- **Dialog ID**: `{}`\n", self.id.dialog_id));
        md.push_str(&format!("- **Message Count**: {}\n", self.messages.len()));
        md.push_str(&format!(
            "- **Model**: `{}`\n",
            self.provider_config.model_id
        ));
        md.push_str(&format!("- **Exported At**: {}\n", now));
        md.push_str("\n---\n\n");

        // System Prompt Section
        md.push_str("## System Prompt (Injected to Model)\n\n");
        md.push_str("```\n");
        md.push_str(system_prompt);
        md.push_str("\n```\n\n");
        md.push_str("---\n\n");

        md.push_str("## Conversation History\n\n");

        for (i, msg) in self.messages.iter().enumerate() {
            let role = match msg.role.as_str() {
                "system" => "System",
                "user" => "User",
                "assistant" => "Assistant",
                "tool" => "Tool",
                r => r,
            };

            let timestamp = if msg.timestamp > 0 {
                DateTime::from_timestamp_millis(msg.timestamp)
                    .map(|dt| {
                        dt.with_timezone(&Local)
                            .format("%Y-%m-%d %H:%M:%S")
                            .to_string()
                    })
                    .unwrap_or_default()
            } else {
                String::new()
            };

            md.push_str(&format!("### [{:03}] {} {}\n\n", i + 1, role, timestamp));
            md.push_str("```\n");

            if let Some(ref tool_calls) = msg.tool_calls {
                md.push_str("[Tool Calls]\n");
                for tc in tool_calls {
                    md.push_str(&format!("- {}: {:?}\n", tc.name, tc.arguments));
                }
            }

            if let Some(ref tool_name) = msg.tool_name {
                md.push_str(&format!("[Tool: {}]\n", tool_name));
            }

            if let Some(ref tool_call_id) = msg.tool_call_id {
                md.push_str(&format!("[Tool Call ID: {}]\n", tool_call_id));
            }

            md.push_str(&msg.content);
            md.push_str("\n```\n\n");

            if !msg.media_refs.is_empty() {
                md.push_str(&format!("**Media**: {:?}\n\n", msg.media_refs));
            }
        }

        md
    }
}

/// Repair damaged tool call chains after restoring from storage.
/// Handles cases where the gateway crashed mid-loop, leaving assistant
/// tool_calls without corresponding tool result messages.
fn repair_tool_call_chains(messages: &mut [ChatMessage]) {
    let mut i = 0;
    while i < messages.len() {
        let calls = match &messages[i].tool_calls {
            Some(calls) if !calls.is_empty() => calls.clone(),
            _ => {
                i += 1;
                continue;
            }
        };

        if messages[i].role != "assistant" {
            i += 1;
            continue;
        }

        // Collect expected tool call IDs
        let expected_ids: std::collections::HashSet<&str> =
            calls.iter().map(|c| c.id.as_str()).collect();
        let expected_count = expected_ids.len();

        // Check following messages for matching tool results (same tool_call_id)
        let mut found = 0;
        let mut j = i + 1;
        while j < messages.len() && found < expected_count {
            if messages[j].role == "tool" {
                if let Some(ref tc_id) = messages[j].tool_call_id
                    && expected_ids.contains(tc_id.as_str())
                {
                    found += 1;
                }
            } else if messages[j].role == "user" || messages[j].role == "assistant" {
                // Next user/assistant message — stop scanning, chain is broken
                break;
            }
            j += 1;
        }

        if found < expected_count {
            // Incomplete chain: remove tool_calls and add interruption note
            tracing::warn!(
                found,
                expected = expected_count,
                "Repairing incomplete tool call chain — gateway restart likely interrupted execution"
            );
            let old_content = std::mem::take(&mut messages[i].content);
            messages[i].content = format!(
                "{}\n\n[Tool calls ({}): {} — execution interrupted by gateway restart]",
                old_content,
                expected_count,
                calls
                    .iter()
                    .map(|c| c.name.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            );
            messages[i].tool_calls = None;
        }

        i += 1;
    }
}

fn tool_definition_tokens(tools: &ToolRegistry) -> usize {
    serde_json::to_string(&tools.get_definitions())
        .map(|definitions| crate::agent::context_compaction::estimate_text_tokens(&definitions))
        .unwrap_or_default()
}

fn context_request_key(
    provider: &str,
    model: &str,
    generation: i64,
    last_sent_raw_seq: i64,
    messages: &[ChatMessage],
    tools: &ToolRegistry,
) -> ContextRequestKey {
    let tool_signature = serde_json::to_string(&tools.get_definitions()).unwrap_or_default();
    ContextRequestKey {
        provider: provider.to_string(),
        model: model.to_string(),
        generation,
        last_sent_raw_seq,
        request_digest: context_request_digest(messages, &tool_signature),
    }
}

fn observed_context_request_key(session: &Session, request_digest: u64) -> ContextRequestKey {
    ContextRequestKey {
        provider: session.provider_config.name.clone(),
        model: session.provider_config.model_id.clone(),
        generation: session.context_generation,
        last_sent_raw_seq: session.seq_counter,
        request_digest,
    }
}

async fn commit_compaction_candidate(
    session: &Arc<Mutex<Session>>,
    candidate: crate::agent::CompactionCandidate,
) -> Result<ContextCheckpoint, AgentError> {
    let expected_generation = candidate.base_generation;
    let (storage, session_id, provider_kind, model, memory_manager) = {
        let guard = session.lock().await;
        (
            guard.storage.clone().ok_or_else(|| {
                AgentError::Other("context checkpoint storage unavailable".to_string())
            })?,
            guard.id.to_string(),
            guard.provider_config.provider_type.clone(),
            guard.provider_config.model_id.clone(),
            guard.memory_manager.clone(),
        )
    };
    let now = chrono::Utc::now().timestamp_millis();
    let new_checkpoint = crate::storage::NewContextCheckpoint {
        id: uuid::Uuid::new_v4().to_string(),
        parent_checkpoint_id: candidate.parent_checkpoint_id,
        summary: candidate.summary,
        first_retained_seq: candidate.first_retained_seq,
        source_max_seq: candidate.source_max_seq,
        trigger_reason: candidate.reason.as_str().to_string(),
        provider_kind,
        model,
        tokens_before: i64::try_from(candidate.tokens_before).unwrap_or(i64::MAX),
        tokens_after: i64::try_from(candidate.tokens_after).unwrap_or(i64::MAX),
        degraded: candidate.degraded,
        created_at: now,
    };
    let checkpoint = storage
        .commit_context_checkpoint(&session_id, expected_generation, &new_checkpoint)
        .await
        .map_err(|error| {
            AgentError::Other(format!("failed to commit context checkpoint: {error}"))
        })?;
    {
        let mut guard = session.lock().await;
        guard.apply_context_checkpoint(checkpoint.clone());
    }
    tracing::info!(
        session_id = %session_id,
        checkpoint_id = %checkpoint.id,
        generation = checkpoint.generation,
        trigger_reason = %checkpoint.trigger_reason,
        tokens_before = checkpoint.tokens_before,
        tokens_after = checkpoint.tokens_after,
        first_retained_seq = checkpoint.first_retained_seq,
        degraded = checkpoint.degraded,
        "Context checkpoint committed"
    );

    if !checkpoint.degraded {
        let key = format!("context_checkpoint:{}", checkpoint.id);
        if let Err(error) = memory_manager
            .store(
                &key,
                &checkpoint.summary,
                crate::memory::MemoryCategory::Timeline,
                Some(&session_id),
                Some(0.3),
            )
            .await
        {
            tracing::warn!(
                checkpoint_id = %checkpoint.id,
                error = %error,
                "Failed to store committed context checkpoint as Timeline"
            );
        }
    }
    Ok(checkpoint)
}

#[derive(Debug, Clone, Copy)]
struct CompactRequest {
    reason: CompactionReason,
    context_window_override: Option<usize>,
    degraded_only: bool,
    memory_tokens: usize,
    active_plan_tokens: usize,
}

struct CompactOutcome {
    projected_history: Vec<ChatMessage>,
    checkpoint: Option<ContextCheckpoint>,
    semantic_attempted: bool,
    retained_turn_count: usize,
}

/// The single orchestration path for manual, automatic, and overflow
/// compaction. It owns snapshotting, complete-request measurement, candidate
/// generation, checkpoint CAS, and fresh post-commit reprojection.
async fn compact_session_context<F>(
    session: &Arc<Mutex<Session>>,
    request: CompactRequest,
    assemble_request: F,
) -> Result<CompactOutcome, AgentError>
where
    F: Fn(Vec<ChatMessage>) -> Vec<ChatMessage>,
{
    let (
        snapshot,
        compactor,
        context_usage,
        context_window,
        tool_tokens,
        provider,
        model,
        tool_signature,
    ) = {
        let guard = session.lock().await;
        (
            guard.context_snapshot(),
            guard.context_compactor.clone(),
            guard.context_usage.clone(),
            request
                .context_window_override
                .unwrap_or(guard.effective_context_window),
            tool_definition_tokens(&guard.tools),
            guard.provider_config.name.clone(),
            guard.provider_config.model_id.clone(),
            serde_json::to_string(&guard.tools.get_definitions()).unwrap_or_default(),
        )
    };
    let original_projection = crate::agent::context_compaction::project_history(
        &snapshot.raw,
        snapshot.previous.as_ref(),
    );
    let complete_request = assemble_request(original_projection.clone());
    let request_key = ContextRequestKey {
        provider,
        model,
        generation: snapshot.generation,
        last_sent_raw_seq: snapshot.raw.last().map_or(0, |entry| entry.seq),
        request_digest: context_request_digest(&complete_request, &tool_signature),
    };
    let observed_context_tokens =
        context_usage.estimate_request(&request_key, &complete_request, tool_tokens);
    let history_total = crate::agent::estimate_tokens(&original_projection);
    let fixed_tokens = observed_context_tokens.saturating_sub(history_total);
    let current_input_and_media = original_projection
        .iter()
        .rposition(|message| message.role == "user")
        .map(|index| crate::agent::estimate_tokens(&original_projection[index..=index]))
        .unwrap_or_default();
    let tools_part = tool_tokens.min(fixed_tokens);
    let base_system_tokens = complete_request
        .first()
        .filter(|message| message.role == "system")
        .map(|message| crate::agent::estimate_tokens(std::slice::from_ref(message)))
        .unwrap_or_default()
        .min(fixed_tokens.saturating_sub(tools_part));
    let mut dynamic_tokens = fixed_tokens
        .saturating_sub(tools_part)
        .saturating_sub(base_system_tokens);
    let memory_tokens = request.memory_tokens.min(dynamic_tokens);
    dynamic_tokens = dynamic_tokens.saturating_sub(memory_tokens);
    let active_plan_tokens = request.active_plan_tokens.min(dynamic_tokens);
    dynamic_tokens = dynamic_tokens.saturating_sub(active_plan_tokens);
    let budget_parts = ContextBudgetParts {
        system_and_skills: base_system_tokens.saturating_add(dynamic_tokens),
        tools: tools_part,
        memory: memory_tokens,
        active_plan: active_plan_tokens,
        history: history_total.saturating_sub(current_input_and_media),
        current_input_and_media,
    };
    let candidate = if request.degraded_only {
        compactor.compact_degraded(
            &snapshot.raw,
            snapshot.previous.as_ref(),
            context_window,
            &budget_parts,
            Some(observed_context_tokens),
        )?
    } else {
        compactor
            .compact(
                &snapshot.raw,
                snapshot.previous.as_ref(),
                request.reason,
                context_window,
                &budget_parts,
                Some(observed_context_tokens),
            )
            .await?
    };
    let Some(mut candidate) = candidate else {
        return Ok(CompactOutcome {
            projected_history: original_projection,
            checkpoint: None,
            semantic_attempted: false,
            retained_turn_count: 0,
        });
    };
    candidate.base_generation = snapshot.generation;
    let semantic_attempted = candidate.semantic_attempted;
    let retained_turn_count = candidate.retained_turn_count;
    let checkpoint = commit_compaction_candidate(session, candidate).await?;

    // Raw messages may have been appended while the summary Provider call was
    // in flight. Never return the candidate's stale vector; project again from
    // the committed checkpoint and the current append-only raw log.
    let projected_history = {
        let mut guard = session.lock().await;
        if let Some(window) = request.context_window_override {
            guard.effective_context_window = window;
        }
        guard.projected_history()
    };
    Ok(CompactOutcome {
        projected_history,
        checkpoint: Some(checkpoint),
        semantic_attempted,
        retained_turn_count,
    })
}

/// SessionManager 管理所有 Session，按 channel_name 路由
#[derive(Clone)]
pub struct SessionManager {
    inner: Arc<Mutex<SessionManagerInner>>,
    provider_config: LLMProviderConfig,
    tools: Arc<ToolRegistry>,
    skills_loader: Arc<SkillsLoader>,
    pub(super) storage: Arc<Storage>,
    pub(super) bus: Arc<MessageBus>,
    memory_manager: Arc<crate::memory::MemoryManager>,
    work_manager: Arc<crate::work::WorkManager>,
    agent_catalog: Arc<crate::agent::AgentCatalog>,
    execution_gate: Arc<crate::agent::gate::ExecutionGate>,
    agent_coordinator: Option<Arc<crate::agent::AgentCoordinator>>,
    inbox_notifier: Arc<crate::agent::AgentInboxNotifier>,
    agent_projection_hub: Arc<crate::agent::AgentProjectionHub>,
    inbox_burst: usize,
    inbox_wait_ms: i64,
    inbox_max_attempts: i64,
    task_supervisor: crate::task_supervisor::TaskSupervisor,
    turn_delivery: TurnDeliveryService,
    reload: crate::gateway::reload::ReloadHandle,
    health: Arc<crate::health::HealthService>,
    context_compaction: ContextCompactionConfig,
}

/// Gateway-owned runtime services shared by all Session workers.
pub struct SessionManagerServices {
    bus: Arc<MessageBus>,
    memory_manager: Arc<crate::memory::MemoryManager>,
    task_supervisor: crate::task_supervisor::TaskSupervisor,
    turn_delivery: TurnDeliveryService,
    reload: crate::gateway::reload::ReloadHandle,
    admission: crate::gateway::reload::RuntimeAdmission,
}

pub struct AgentCatalogPreparation {
    pub provider_profiles: HashMap<String, LLMProviderConfig>,
    pub providers: HashMap<String, crate::config::ProviderConfig>,
    pub models: HashMap<String, crate::config::ModelConfig>,
    pub workspace_dir: std::path::PathBuf,
    pub config: crate::config::AgentOrchestrationConfig,
    pub config_dir: std::path::PathBuf,
    pub runtime_generation: u64,
}

impl SessionManagerServices {
    pub fn new(
        bus: Arc<MessageBus>,
        memory_manager: Arc<crate::memory::MemoryManager>,
        task_supervisor: crate::task_supervisor::TaskSupervisor,
        turn_delivery: TurnDeliveryService,
        reload: crate::gateway::reload::ReloadHandle,
    ) -> Self {
        Self {
            bus,
            memory_manager,
            task_supervisor,
            turn_delivery,
            reload,
            admission: crate::gateway::reload::RuntimeAdmission::open(),
        }
    }

    pub(crate) fn with_admission(
        mut self,
        admission: crate::gateway::reload::RuntimeAdmission,
    ) -> Self {
        self.admission = admission;
        self
    }
}

struct SessionManagerInner {
    /// Sessions keyed by UnifiedSessionId.to_string()
    sessions: HashMap<String, Arc<Mutex<Session>>>,
    /// Current active session per channel:chat_id
    current_sessions: HashMap<String, String>,
}

/// 斜杠命令定义
#[derive(Debug, Clone)]
pub struct SlashCommand {
    /// 命令名称
    pub name: &'static str,
    /// 命令描述
    pub description: &'static str,
    /// 命令别名（触发词）
    pub aliases: &'static [&'static str],
}

impl SlashCommand {
    /// 检查给定内容是否匹配此命令
    pub fn matches(&self, content: &str) -> bool {
        let trimmed = content.trim();
        self.aliases
            .iter()
            .any(|&alias| trimmed == alias || trimmed.starts_with(&format!("{} ", alias)))
    }
}

/// Session 支持的斜杠命令列表
pub static SLASH_COMMANDS: &[SlashCommand] = &[
    SlashCommand {
        name: "new",
        description: "创建新对话",
        aliases: &["/new"],
    },
    SlashCommand {
        name: "sessions",
        description: "列出最近对话",
        aliases: &["/sessions"],
    },
    SlashCommand {
        name: "switch",
        description: "切换到指定对话",
        aliases: &["/switch"],
    },
    SlashCommand {
        name: "rename",
        description: "重命名当前对话",
        aliases: &["/rename"],
    },
    SlashCommand {
        name: "delete",
        description: "删除当前对话",
        aliases: &["/delete"],
    },
    SlashCommand {
        name: "compact",
        description: "手动触发上下文压缩",
        aliases: &["/compact"],
    },
    SlashCommand {
        name: "info",
        description: "显示当前对话信息",
        aliases: &["/info"],
    },
    SlashCommand {
        name: "dump",
        description: "保存当前对话为 markdown 文档",
        aliases: &["/dump"],
    },
    SlashCommand {
        name: "?",
        description: "显示帮助",
        aliases: &["/?", "/help"],
    },
    SlashCommand {
        name: "mcp",
        description: "显示 MCP 服务状态和工具列表",
        aliases: &["/mcp"],
    },
    SlashCommand {
        name: "health",
        description: "检查 PicoBot 运行依赖",
        aliases: &["/health"],
    },
    SlashCommand {
        name: "stop",
        description: "停止当前正在执行的任务并清空消息队列",
        aliases: &["/stop"],
    },
    SlashCommand {
        name: "queue",
        description: "等待当前任务完成后处理消息",
        aliases: &["/queue"],
    },
    SlashCommand {
        name: "todo",
        description: "查看、完成或取消当前任务计划",
        aliases: &["/todo"],
    },
    SlashCommand {
        name: "reload",
        description: "重新加载配置",
        aliases: &["/reload"],
    },
];

fn resolve_slash_command(command: &str) -> Option<&'static SlashCommand> {
    let command = command.strip_prefix('/').unwrap_or(command);
    SLASH_COMMANDS.iter().find(|candidate| {
        candidate.name == command
            || candidate
                .aliases
                .iter()
                .any(|alias| alias.strip_prefix('/') == Some(command))
    })
}

/// Result of one steer admission pass over the active Turn.
enum SteerAdmission {
    /// Events were activated into the active Turn's mailbox.
    Activated,
    /// Events were claimed but could not be admitted and returned to
    /// the queue lane (or the active Turn disappeared).
    ReleasedToQueue,
    /// No steer events were due.
    NothingClaimed,
}

impl SessionManager {
    fn worker_deps(&self) -> AgentWorkerDeps {
        AgentWorkerDeps {
            bus: self.bus.clone(),
            memory_manager: self.memory_manager.clone(),
            work_manager: self.work_manager.clone(),
            skills_loader: self.skills_loader.clone(),
            task_supervisor: self.task_supervisor.clone(),
            turn_delivery: self.turn_delivery.clone(),
            execution_gate: self.execution_gate.clone(),
            inbox_burst: self.inbox_burst,
            inbox_wait_ms: self.inbox_wait_ms,
            inbox_max_attempts: self.inbox_max_attempts,
        }
    }

    pub fn new(
        provider_config: LLMProviderConfig,
        context_compaction: ContextCompactionConfig,
        catalog_preparation: AgentCatalogPreparation,
        storage: Arc<Storage>,
        services: SessionManagerServices,
        browser_config: Option<BrowserConfig>,
        health: Arc<crate::health::HealthService>,
    ) -> Result<Self, AgentError> {
        let SessionManagerServices {
            bus,
            memory_manager,
            task_supervisor,
            turn_delivery,
            reload,
            admission,
        } = services;
        let mut skills_loader = SkillsLoader::new();
        skills_loader.load_skills();
        skills_loader.set_workspace_skills_dir(provider_config.workspace_dir.clone());
        let skills_loader = Arc::new(skills_loader);

        let work_manager = Arc::new(crate::work::WorkManager::new(storage.clone()));
        let tools = Arc::new(
            create_default_tools(
                skills_loader.clone(),
                memory_manager.clone(),
                work_manager.clone(),
                None, // SubAgentManager created below
                browser_config.as_ref(),
                health.clone(),
                provider_config.workspace_dir.clone(),
            )
            .map_err(|error| AgentError::Other(format!("failed to create tools: {error}")))?,
        );

        // Release built-in Agent definitions (like built-in skills) before
        // the catalog loads; existing user files are never overwritten.
        crate::agent::builtin::install_builtin_agents(
            &catalog_preparation.config_dir,
            &catalog_preparation.provider_profiles,
        );
        let agent_catalog = Arc::new(
            crate::agent::AgentCatalog::load(
                &catalog_preparation.config,
                &catalog_preparation.config_dir,
                &catalog_preparation.provider_profiles,
                &catalog_preparation.providers,
                &catalog_preparation.models,
                &catalog_preparation.workspace_dir,
                &tools,
                &skills_loader,
                catalog_preparation.runtime_generation,
            )
            .map_err(|error| AgentError::Other(format!("failed to load Agent catalog: {error}")))?,
        );
        let execution_gate = crate::agent::gate::ExecutionGate::new(&catalog_preparation.config);

        // Create SubAgentManager and register DelegateTool
        let sub_agent_manager = Arc::new(
            crate::agent::SubAgentManager::new(
                provider_config.clone(),
                tools.clone(),
                Some(storage.clone()),
                Some(skills_loader.clone()),
            )
            .with_catalog(agent_catalog.clone())
            .with_execution_gate(execution_gate.clone())
            .with_work_manager(work_manager.clone()),
        );
        let mut delegate_tool = crate::tools::DelegateTool::new(sub_agent_manager.clone());
        let inbox_notifier = crate::agent::AgentInboxNotifier::new();
        let agent_projection_hub = Arc::new(crate::agent::AgentProjectionHub::new());
        let coordinator = crate::agent::AgentCoordinator::new(
            storage.clone(),
            sub_agent_manager.clone(),
            work_manager.clone(),
            inbox_notifier.clone(),
            agent_projection_hub.clone(),
            execution_gate.clone(),
            admission.clone(),
            task_supervisor.clone(),
            catalog_preparation.runtime_generation,
            &catalog_preparation.config,
        );
        tools.register(crate::tools::AgentTaskTool::new(coordinator.clone()));
        delegate_tool = delegate_tool.with_coordinator(coordinator.clone());
        sub_agent_manager.bind_coordinator(&coordinator);
        let agent_coordinator = Some(coordinator);
        tools.register(delegate_tool);
        tools.register(crate::tools::ReloadConfigTool::new(reload.clone()));

        Ok(Self {
            inner: Arc::new(Mutex::new(SessionManagerInner {
                sessions: HashMap::new(),
                current_sessions: HashMap::new(),
            })),
            provider_config,
            tools,
            skills_loader,
            storage,
            bus,
            memory_manager,
            work_manager,
            agent_catalog,
            execution_gate,
            agent_coordinator,
            inbox_notifier,
            agent_projection_hub,
            inbox_burst: catalog_preparation.config.max_user_turn_burst_before_inbox,
            inbox_wait_ms: (catalog_preparation.config.max_inbox_wait_secs * 1000) as i64,
            inbox_max_attempts: i64::from(catalog_preparation.config.max_inbox_delivery_attempts),
            task_supervisor,
            turn_delivery,
            reload,
            health,
            context_compaction,
        })
    }

    /// Register the send_message tool (requires self in Arc)
    pub fn register_outbound_tool(self: &Arc<Self>, available_channels: Vec<String>) {
        let messenger: Arc<dyn OutboundMessenger> = self.clone();
        self.tools
            .register(SendMessageTool::new(messenger, available_channels));
    }

    pub fn tools(&self) -> Arc<ToolRegistry> {
        self.tools.clone()
    }

    pub fn agent_catalog(&self) -> Arc<crate::agent::AgentCatalog> {
        self.agent_catalog.clone()
    }

    /// The durable coordinator, present only when orchestration is enabled.
    pub fn agent_coordinator(&self) -> Option<Arc<crate::agent::AgentCoordinator>> {
        self.agent_coordinator.clone()
    }

    /// Bind this SessionManager as the late-bound wake target of the inbox
    /// notifier.  Called once after the manager is placed in an Arc.
    pub fn bind_inbox_wake(self: &Arc<Self>) {
        let weak: std::sync::Weak<dyn crate::agent::AgentInboxWakeTarget> =
            Arc::downgrade(self) as std::sync::Weak<dyn crate::agent::AgentInboxWakeTarget>;
        self.inbox_notifier.bind(weak);
    }

    pub fn skills_loader(&self) -> Arc<SkillsLoader> {
        self.skills_loader.clone()
    }

    pub fn projection_hub(&self) -> Arc<crate::agent::AgentProjectionHub> {
        self.agent_projection_hub.clone()
    }

    pub fn work_manager(&self) -> Arc<crate::work::WorkManager> {
        self.work_manager.clone()
    }

    /// 获取所有可用的斜杠命令
    pub fn get_slash_commands(&self) -> &[SlashCommand] {
        SLASH_COMMANDS
    }

    /// 执行斜杠命令
    /// 返回 (新session_id, 响应消息)
    pub async fn execute_slash_command(
        &self,
        command: &str,
        args: Option<&str>,
        channel: &str,
        chat_id: &str,
        current_session_id: Option<&UnifiedSessionId>,
    ) -> Result<(Option<UnifiedSessionId>, String), AgentError> {
        let cmd = resolve_slash_command(command)
            .ok_or_else(|| AgentError::Other(format!("Unknown command: {}", command)))?;

        tracing::info!(cmd = %cmd.name, args = ?args, "Executing slash command");

        match cmd.name {
            "new" => {
                let title = args.map(|s| s.to_string());
                let (new_id, title) = self
                    .create_session(channel, chat_id, title.as_deref(), String::new())
                    .await?;
                Ok((Some(new_id), format!("新对话 '{}' 已创建。", title)))
            }
            "delete" => {
                if let Some(sid) = current_session_id {
                    self.delete_dialog(sid).await?;
                }
                let (new_id, _title) = self
                    .create_session(channel, chat_id, None, String::new())
                    .await?;
                Ok((Some(new_id), "对话已删除。新对话已创建。".to_string()))
            }
            "compact" => {
                if let Some(sid) = current_session_id {
                    let session = self.get_or_create_session(sid).await?;
                    let skills_prompt = self.skills_loader.build_skills_prompt();
                    let system_prompt = {
                        let session_guard = session.lock().await;
                        if session_guard.turn_busy {
                            return Ok((
                                None,
                                "A Turn is active. Run /compact after it finishes, or use /stop first."
                                    .to_string(),
                            ));
                        }
                        session_guard.build_system_prompt(&skills_prompt)
                    };
                    let outcome = compact_session_context(
                        &session,
                        CompactRequest {
                            reason: CompactionReason::Manual,
                            context_window_override: None,
                            degraded_only: false,
                            memory_tokens: 0,
                            active_plan_tokens: 0,
                        },
                        |mut history| {
                            history.insert(0, ChatMessage::system(system_prompt.clone()));
                            history
                        },
                    )
                    .await?;
                    let Some(checkpoint) = outcome.checkpoint else {
                        return Ok((None, "Nothing to compact.".to_string()));
                    };
                    Ok((
                        None,
                        format!(
                            "Context compacted: {tokens_before} → {tokens_after} tokens. Kept {retained_turn_count} recent turns from raw message seq {first_retained_seq}. Raw history is unchanged. Checkpoint {}.",
                            checkpoint.id,
                            tokens_before = checkpoint.tokens_before,
                            tokens_after = checkpoint.tokens_after,
                            retained_turn_count = outcome.retained_turn_count,
                            first_retained_seq = checkpoint.first_retained_seq,
                        ),
                    ))
                } else {
                    Ok((None, "No active conversation to compress.".to_string()))
                }
            }
            "info" => {
                if let Some(sid) = current_session_id {
                    let stats = self.get_session_stats(sid).await?;
                    let output = if args.is_some_and(|value| value.trim() == "--json") {
                        serde_json::to_string_pretty(&stats).map_err(|error| {
                            AgentError::Other(format!("failed to serialize session info: {error}"))
                        })?
                    } else {
                        stats.render_text()
                    };
                    Ok((None, output))
                } else {
                    Ok((None, "No active session.".to_string()))
                }
            }
            "dump" => {
                if let Some(sid) = current_session_id {
                    let session = self.get_or_create_session(sid).await?;
                    let session_guard = session.lock().await;

                    // Build the same system prompt that would be injected to the model
                    let skills_prompt = self.skills_loader.build_skills_prompt();
                    let system_prompt = session_guard.build_system_prompt(&skills_prompt);

                    let filepath = session_guard
                        .dump_to_file(&system_prompt)
                        .map_err(|e| AgentError::Other(format!("Failed to save dump: {}", e)))?;
                    Ok((None, format!("Session dump saved to: {}", filepath)))
                } else {
                    Ok((None, "No active session.".to_string()))
                }
            }
            "sessions" => {
                let (dialogs, _current) = self.list_dialogs(channel, chat_id, false).await?;
                if dialogs.is_empty() {
                    Ok((None, "暂无对话记录。".to_string()))
                } else {
                    let lines: Vec<String> = dialogs
                        .iter()
                        .map(|d| {
                            let current = if current_session_id
                                .map(|s| s.dialog_id == d.session_id.dialog_id)
                                .unwrap_or(false)
                            {
                                " [当前]"
                            } else {
                                ""
                            };
                            format!(
                                "- {} ({}){} — {}",
                                d.session_id.dialog_id,
                                d.title,
                                current,
                                chrono::DateTime::from_timestamp_millis(d.last_active_at)
                                    .map(|dt| dt
                                        .with_timezone(&chrono::Local)
                                        .format("%m-%d %H:%M")
                                        .to_string())
                                    .unwrap_or_default()
                            )
                        })
                        .collect();
                    Ok((None, format!("最近对话：\n{}", lines.join("\n"))))
                }
            }
            "switch" => {
                let dialog_id = args
                    .ok_or_else(|| AgentError::Other("Usage: /switch <dialog_id>".to_string()))?;
                let new_id = self.switch_dialog(channel, chat_id, dialog_id).await?;
                Ok((None, format!("已切换到对话：{}", new_id.dialog_id)))
            }
            "rename" => {
                let title =
                    args.ok_or_else(|| AgentError::Other("Usage: /rename <新标题>".to_string()))?;
                if let Some(sid) = current_session_id {
                    self.rename_dialog(sid, title).await?;
                    Ok((None, format!("对话已重命名为：{}", title)))
                } else {
                    Ok((None, "No active session.".to_string()))
                }
            }
            "?" => {
                let lines: Vec<String> = SLASH_COMMANDS
                    .iter()
                    .map(|c| format!("  {}  - {}", c.aliases.join(", "), c.description))
                    .collect();
                Ok((None, format!("可用命令：\n{}", lines.join("\n"))))
            }
            "mcp" => {
                let servers = get_mcp_status();
                if servers.is_empty() {
                    return Ok((None, "未配置 MCP 服务。".to_string()));
                }
                let lines: Vec<String> = servers
                    .iter()
                    .map(|s| {
                        let status = if s.connected {
                            format!("✅ 已连接 ({})", s.transport)
                        } else {
                            format!("❌ 连接失败: {}", s.error.as_deref().unwrap_or("未知错误"))
                        };
                        let tool_lines: Vec<String> = s
                            .tools
                            .iter()
                            .map(|t| {
                                let desc = if t.description.is_empty() {
                                    "无描述".to_string()
                                } else {
                                    t.description.chars().take(60).collect::<String>()
                                };
                                format!("    - {}: {}", t.name, desc)
                            })
                            .collect();
                        let tools_section = if tool_lines.is_empty() {
                            String::new()
                        } else {
                            format!("\n{}", tool_lines.join("\n"))
                        };
                        format!("{} {}{}", s.name, status, tools_section)
                    })
                    .collect();
                Ok((None, format!("MCP 服务：\n\n{}", lines.join("\n\n"))))
            }
            "health" => {
                let report = self.health.check().await;
                Ok((None, report.render_text()))
            }
            "queue" => {
                let sid = current_session_id
                    .ok_or_else(|| AgentError::Other("no active session".to_string()))?;
                let content = args.map(str::trim).unwrap_or_default();
                if content.is_empty() {
                    return Err(AgentError::Other("Usage: /queue <message>".to_string()));
                }
                let session = self.get_or_create_session(sid).await?;
                let task = AgentTask {
                    channel: channel.to_string(),
                    sender_id: "cli".to_string(),
                    chat_id: chat_id.to_string(),
                    sequence: 0,
                    client_message_id: None,
                    content: content.to_string(),
                    received_at: chrono::Utc::now().timestamp_millis(),
                    media: Vec::new(),
                    channel_context: ChannelContext::default(),
                };
                self.enqueue_agent_task(session, sid.clone(), task).await?;
                Ok((None, "消息已加入队列，将在当前任务完成后处理。".to_string()))
            }
            "stop" => {
                let sid = current_session_id
                    .ok_or_else(|| AgentError::Other("no active session".to_string()))?;
                let session = self.get_or_create_session(sid).await?;
                let mut stop_leases: Option<crate::agent::steering::MailboxTake> = None;
                let msgs = {
                    let mut guard = session.lock().await;
                    let mut msgs: Vec<String> = Vec::new();
                    if guard.turn_busy {
                        guard.turn_busy = false;
                        msgs.push("当前任务已发送停止信号。".to_string());
                    }
                    if let Some(token) = guard.current_turn_token.take() {
                        token.cancel();
                    }
                    if let Some(active_turn) = guard.active_turn_emitter.take() {
                        // Closing the mailbox makes an in-flight ordinary
                        // input race resolve to the next queue (or an
                        // explicit queue-full response), rather than being
                        // accepted after `/stop` has invalidated the turn.
                        // `/stop` intentionally discards accepted-but-not-yet
                        // injected user steering, matching its queue-clearing
                        // semantics.  Durable Agent steer events are NOT
                        // discarded: they are released back to `pending` so
                        // the queue lane can still deliver them.
                        stop_leases = Some(active_turn.steering.close_and_take_pending());
                        active_turn
                            .recovery
                            .lock()
                            .unwrap_or_else(|poisoned| poisoned.into_inner())
                            .clear();
                        active_turn.emitter.deactivate();
                    }
                    if guard.agent_tx.take().is_some() {
                        msgs.push("消息队列已清空。".to_string());
                    }
                    guard.worker_generation = guard.worker_generation.wrapping_add(1);
                    guard.state_version = guard.state_version.wrapping_add(1);
                    msgs
                };

                // Cancel all running background sub-agent tasks for this session
                // after releasing the session lock.  Named durable runs are
                // cancelled with suppress_continuation so no continuation Turn
                // restarts after an explicit stop.
                if let Some(coordinator) = self.agent_coordinator.as_ref()
                    && let Err(error) = coordinator
                        .cancel_session(&sid.to_string(), "stopped by user")
                        .await
                {
                    tracing::warn!(error = %error, "Failed to cancel durable agent runs on /stop");
                }
                // Durable steer events admitted to the stopped Turn return to
                // `pending`; the queue lane delivers them later.  They are
                // never silently discarded by `/stop`.
                let stop_leases = stop_leases.unwrap_or_default();
                let mut leases = stop_leases.admitted_leases;
                leases.extend(stop_leases.reserved_leases);
                release_steer_leases(&self.storage, leases).await;
                let resp = if msgs.is_empty() {
                    "没有正在执行的任务或队列。".to_string()
                } else {
                    msgs.join(" ")
                };
                Ok((None, resp))
            }
            "todo" => {
                let sid = current_session_id
                    .ok_or_else(|| AgentError::Other("no active session".to_string()))?;
                let action = args.map(str::trim).filter(|value| !value.is_empty());
                match action {
                    Some("cancel") => self
                        .work_manager
                        .close_plan(&sid.to_string(), "cancelled", None)
                        .await
                        .map(|plan| (None, format!("任务计划已取消：{}", plan.objective)))
                        .map_err(|error| AgentError::Other(error.to_string())),
                    Some("done") => self
                        .work_manager
                        .close_plan(&sid.to_string(), "completed", None)
                        .await
                        .map(|plan| (None, format!("任务计划已完成：{}", plan.objective)))
                        .map_err(|error| AgentError::Other(error.to_string())),
                    Some(_) => Err(AgentError::Other("Usage: /todo [done|cancel]".to_string())),
                    None => match self.work_manager.active_plan(&sid.to_string()).await {
                        Ok(Some(plan)) => {
                            let mut lines = vec![format!(
                                "任务计划：{}（version {}）",
                                plan.objective, plan.version
                            )];
                            for item in plan.items {
                                let icon = match item.status.as_str() {
                                    "completed" => "✓",
                                    "in_progress" => "●",
                                    "blocked" => "!",
                                    _ => "○",
                                };
                                lines.push(format!(
                                    "{icon} {} [{}] {}",
                                    item.id, item.status, item.title
                                ));
                            }
                            Ok((None, lines.join("\n")))
                        }
                        Ok(None) => Ok((None, "当前 session 没有 active plan。".to_string())),
                        Err(error) => Err(AgentError::Other(error.to_string())),
                    },
                }
            }
            "reload" => self
                .reload
                .request()
                .await
                .map(|accepted| (None, accepted.message))
                .map_err(|error| AgentError::Other(error.to_string())),
            _ => Err(AgentError::Other(format!(
                "未知命令：/{}。输入 /? 获取帮助。",
                cmd.name
            ))),
        }
    }

    /// Wait until all interactive session Turns have reached a terminal state.
    /// A reload uses this to avoid cancelling the Turn that requested it.
    pub async fn wait_until_idle(&self, timeout: std::time::Duration) -> bool {
        let deadline = tokio::time::Instant::now() + timeout;
        let mut idle_since = None;
        loop {
            let sessions: Vec<_> = {
                let inner = self.inner.lock().await;
                inner.sessions.values().cloned().collect()
            };
            let mut busy = false;
            for session in sessions {
                let session = session.lock().await;
                let queued = session
                    .agent_tx
                    .as_ref()
                    .is_some_and(|sender| sender.capacity() < sender.max_capacity());
                if session.turn_busy || queued {
                    busy = true;
                    break;
                }
            }
            if busy {
                idle_since = None;
            } else {
                let since = idle_since.get_or_insert_with(tokio::time::Instant::now);
                if since.elapsed() >= std::time::Duration::from_millis(100) {
                    return true;
                }
            }
            if tokio::time::Instant::now() >= deadline {
                return false;
            }
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        }
    }

    /// Number of live sessions currently tracked by the manager.
    pub async fn session_count(&self) -> usize {
        self.inner.lock().await.sessions.len()
    }

    /// Number of sessions with an actively executing Turn.
    pub async fn active_turn_count(&self) -> usize {
        let sessions: Vec<_> = {
            let inner = self.inner.lock().await;
            inner.sessions.values().cloned().collect()
        };
        let mut count = 0;
        for session in sessions {
            let session = session.lock().await;
            if session.turn_busy {
                count += 1;
            }
        }
        count
    }

    pub async fn create_session(
        &self,
        channel: &str,
        chat_id: &str,
        title: Option<&str>,
        routing_info: String,
    ) -> Result<(UnifiedSessionId, String), AgentError> {
        let dialog_id = crate::util::short_id();
        let unified_id = UnifiedSessionId::new(channel, chat_id, &dialog_id);
        let session_id_str = unified_id.to_string();

        let title = title
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .unwrap_or_else(|| "新对话".to_string());

        // Write to Storage first
        let now = chrono::Utc::now().timestamp_millis();
        let meta = crate::storage::session::SessionMeta {
            id: session_id_str.clone(),
            channel: channel.to_string(),
            chat_id: chat_id.to_string(),
            dialog_id: dialog_id.clone(),
            title: title.clone(),
            created_at: now,
            last_active_at: now,
            message_count: 0,
            routing_info: if routing_info.is_empty() {
                None
            } else {
                Some(routing_info.clone())
            },
            archived_at: None,
            deleted_at: None,
            last_consolidated_at: None,
            last_compressed_message_at: None,
        };
        self.storage.upsert_session(&meta).await.map_err(|e| {
            AgentError::Other(format!("failed to create session in storage: {}", e))
        })?;

        let session = Session::new(
            unified_id.clone(),
            self.provider_config.clone(),
            self.tools.clone(),
            Some(self.storage.clone()),
            routing_info,
            title.clone(),
            SessionContextServices {
                memory_manager: self.memory_manager.clone(),
                compaction_config: self.context_compaction.clone(),
            },
        )
        .await?;

        let arc = Arc::new(Mutex::new(session));
        let inner = &mut *self.inner.lock().await;
        inner.sessions.insert(session_id_str.clone(), arc.clone());
        // Set as current session for this channel:chat_id
        let chat_scope = format!("{}:{}", channel, chat_id);
        inner.current_sessions.insert(chat_scope, session_id_str);

        Ok((unified_id, title))
    }

    pub async fn get_or_create_session(
        &self,
        unified_id: &UnifiedSessionId,
    ) -> Result<Arc<Mutex<Session>>, AgentError> {
        let session_id_str = unified_id.to_string();
        if let Some(session) = self
            .inner
            .lock()
            .await
            .sessions
            .get(&session_id_str)
            .cloned()
        {
            return Ok(session);
        }

        // Perform storage/provider I/O without holding the global registry lock.
        let session = match self.storage.get_session(&session_id_str).await {
            Ok(meta) => {
                tracing::debug!(session_id = %session_id_str, last_active_at = %meta.last_active_at, message_count = %meta.message_count, "Restoring session from Storage");
                Session::from_storage(
                    unified_id.clone(),
                    self.provider_config.clone(),
                    self.tools.clone(),
                    self.storage.clone(),
                    SessionContextServices {
                        memory_manager: self.memory_manager.clone(),
                        compaction_config: self.context_compaction.clone(),
                    },
                )
                .await?
            }
            Err(StorageError::NotFound(_)) => {
                let now = chrono::Utc::now().timestamp_millis();
                let meta = crate::storage::session::SessionMeta {
                    id: session_id_str.clone(),
                    channel: unified_id.channel.clone(),
                    chat_id: unified_id.chat_id.clone(),
                    dialog_id: unified_id.dialog_id.clone(),
                    title: "新对话".to_string(),
                    created_at: now,
                    last_active_at: now,
                    message_count: 0,
                    routing_info: None,
                    archived_at: None,
                    deleted_at: None,
                    last_consolidated_at: None,
                    last_compressed_message_at: None,
                };
                self.storage.upsert_session(&meta).await.map_err(|e| {
                    AgentError::Other(format!("failed to create session in storage: {}", e))
                })?;
                Session::new(
                    unified_id.clone(),
                    self.provider_config.clone(),
                    self.tools.clone(),
                    Some(self.storage.clone()),
                    String::new(),
                    "新对话".to_string(),
                    SessionContextServices {
                        memory_manager: self.memory_manager.clone(),
                        compaction_config: self.context_compaction.clone(),
                    },
                )
                .await?
            }
            Err(e) => {
                return Err(AgentError::Other(format!(
                    "failed to look up session in storage: {}",
                    e
                )));
            }
        };

        let arc = Arc::new(Mutex::new(session));
        // Another caller may have completed the same load while I/O was in
        // progress. Keep the already-published instance as the single source
        // of truth.
        let inner = &mut *self.inner.lock().await;
        if let Some(existing) = inner.sessions.get(&session_id_str) {
            return Ok(existing.clone());
        }
        inner.sessions.insert(session_id_str.clone(), arc.clone());
        let chat_scope = format!("{}:{}", unified_id.channel, unified_id.chat_id);
        inner.current_sessions.insert(chat_scope, session_id_str);
        Ok(arc)
    }

    pub async fn create_dialog(
        &self,
        channel: &str,
        chat_id: &str,
        title: Option<&str>,
    ) -> Result<(UnifiedSessionId, String), AgentError> {
        self.create_session(channel, chat_id, title, String::new())
            .await
    }

    pub async fn get_current_dialog(
        &self,
        channel: &str,
        chat_id: &str,
    ) -> Result<Option<UnifiedSessionId>, AgentError> {
        let chat_scope = format!("{}:{}", channel, chat_id);
        let current = {
            self.inner
                .lock()
                .await
                .current_sessions
                .get(&chat_scope)
                .cloned()
        };

        let Some(current) = current else {
            return Ok(None);
        };

        match self.storage.get_session(&current).await {
            Ok(_) => Ok(UnifiedSessionId::parse(&current)),
            Err(StorageError::NotFound(_)) => Ok(None),
            Err(e) => Err(AgentError::Other(format!("storage error: {}", e))),
        }
    }

    pub async fn switch_dialog(
        &self,
        channel: &str,
        chat_id: &str,
        dialog_id: &str,
    ) -> Result<UnifiedSessionId, AgentError> {
        let unified_id = UnifiedSessionId::new(channel, chat_id, dialog_id);
        // Ensure session is loaded into memory
        self.get_or_create_session(&unified_id).await?;
        // Update current session tracking
        let mut inner = self.inner.lock().await;
        let chat_scope = format!("{}:{}", channel, chat_id);
        inner
            .current_sessions
            .insert(chat_scope, unified_id.to_string());
        Ok(unified_id)
    }

    pub async fn get_dialog_history(
        &self,
        session_id: &UnifiedSessionId,
        limit: u32,
    ) -> Result<Vec<crate::storage::message::MessageMeta>, AgentError> {
        let session_id = session_id.to_string();
        self.storage
            .get_session(&session_id)
            .await
            .map_err(|e| AgentError::Other(format!("failed to load dialog: {e}")))?;
        self.storage
            .load_recent_session_messages(&session_id, limit)
            .await
            .map_err(|e| AgentError::Other(format!("failed to load dialog history: {e}")))
    }

    pub async fn get_task_plan(
        &self,
        session_id: &UnifiedSessionId,
    ) -> Result<Option<crate::work::TaskPlan>, AgentError> {
        let session_id = session_id.to_string();
        self.storage
            .get_session(&session_id)
            .await
            .map_err(|error| AgentError::Other(format!("failed to load dialog: {error}")))?;
        self.work_manager
            .plan_for_session(&session_id)
            .await
            .map_err(|error| AgentError::Other(format!("failed to load task plan: {error}")))
    }

    pub async fn get_session_stats(
        &self,
        session_id: &UnifiedSessionId,
    ) -> Result<crate::session::SessionStats, AgentError> {
        let session = self.get_or_create_session(session_id).await?;
        let (
            title,
            provider,
            model,
            user_message_count,
            history_message_count,
            created_at,
            last_active_at,
            configured_window,
            effective_window,
            configured_reserve_tokens,
            reserve_tokens,
            configured_keep_recent_tokens,
            effective_keep_recent_tokens,
            threshold_tokens,
            used_tokens,
            usage_observed,
            checkpoint,
            context_generation,
        ) = {
            let guard = session.lock().await;
            let history = guard.projected_history();
            let skills_prompt = self.skills_loader.build_skills_prompt();
            let mut request = history.clone();
            request.insert(
                0,
                ChatMessage::system(guard.build_system_prompt(&skills_prompt)),
            );
            let budget = ContextBudget::new(
                guard.effective_context_window,
                guard.context_compactor.config(),
            );
            let request_key = context_request_key(
                &guard.provider_config.name,
                &guard.provider_config.model_id,
                guard.context_generation,
                guard.seq_counter,
                &request,
                &guard.tools,
            );
            let used_tokens = guard.context_usage.estimate_request(
                &request_key,
                &request,
                tool_definition_tokens(&guard.tools),
            );
            (
                guard.title.clone(),
                guard.provider_config.name.clone(),
                guard.provider_config.model_id.clone(),
                u64::try_from(guard.message_count).unwrap_or_default(),
                u64::try_from(history.len()).unwrap_or(u64::MAX),
                guard.created_at,
                guard.last_active_at,
                guard.provider_config.token_limit,
                guard.effective_context_window,
                budget.configured_reserve,
                budget.effective_reserve,
                budget.configured_keep_recent,
                budget.effective_keep_recent,
                budget.threshold,
                used_tokens,
                guard.context_usage.has_observation_for(&request_key),
                guard.active_context_checkpoint.clone(),
                guard.context_generation,
            )
        };
        let totals = self
            .storage
            .get_session_usage_totals(&session_id.to_string())
            .await
            .map_err(|error| AgentError::Other(format!("failed to load session usage: {error}")))?;
        let effective_window = u64::try_from(effective_window).unwrap_or(u64::MAX);
        let used_tokens = u64::try_from(used_tokens).unwrap_or(u64::MAX);

        Ok(crate::session::SessionStats {
            session_id: session_id.to_string(),
            title,
            provider,
            model,
            user_message_count,
            history_message_count,
            lifetime_usage: crate::session::LifetimeUsage {
                input_tokens: totals.prompt_tokens,
                output_tokens: totals.completion_tokens,
                total_tokens: totals.total_tokens,
                cached_input_tokens: totals.cached_input_tokens,
                request_count: totals.request_count,
                turn_count: totals.turn_count,
                tracked_since: totals.tracked_since,
            },
            context: crate::session::ContextUsage {
                configured_window_tokens: u64::try_from(configured_window).unwrap_or(u64::MAX),
                effective_window_tokens: effective_window,
                configured_reserve_tokens: u64::try_from(configured_reserve_tokens)
                    .unwrap_or(u64::MAX),
                reserve_tokens: u64::try_from(reserve_tokens).unwrap_or(u64::MAX),
                configured_keep_recent_tokens: u64::try_from(configured_keep_recent_tokens)
                    .unwrap_or(u64::MAX),
                effective_keep_recent_tokens: u64::try_from(effective_keep_recent_tokens)
                    .unwrap_or(u64::MAX),
                used_tokens,
                remaining_tokens: effective_window.saturating_sub(used_tokens),
                compression_threshold_tokens: u64::try_from(threshold_tokens).unwrap_or(u64::MAX),
                source: if usage_observed {
                    crate::session::ContextUsageSource::Observed
                } else {
                    crate::session::ContextUsageSource::Estimated
                },
                last_observed_prompt_tokens: totals.last_prompt_tokens,
                observed_at: totals.last_observed_at,
                active_checkpoint_id: checkpoint.as_ref().map(|value| value.id.clone()),
                checkpoint_generation: u64::try_from(context_generation).unwrap_or_default(),
                checkpoint_tokens_before: checkpoint
                    .as_ref()
                    .and_then(|value| u64::try_from(value.tokens_before).ok()),
                checkpoint_tokens_after: checkpoint
                    .as_ref()
                    .and_then(|value| u64::try_from(value.tokens_after).ok()),
                checkpoint_degraded: checkpoint.is_some_and(|value| value.degraded),
            },
            created_at,
            last_active_at,
            updated_at: chrono::Utc::now().timestamp_millis(),
        })
    }

    /// Durable Agent run projection page for management/WebSocket clients.
    pub async fn get_agent_runs(
        &self,
        session_id: &UnifiedSessionId,
        cursor: Option<String>,
        limit: u32,
    ) -> Result<crate::session::SessionEvent, AgentError> {
        let cursor = cursor.as_deref().and_then(|cursor| {
            let (created_at, id) = cursor.split_once(':')?;
            Some((created_at.parse::<i64>().ok()?, id.to_string()))
        });
        let (revision, runs, next_cursor) = match self.agent_coordinator.as_ref() {
            Some(coordinator) => coordinator
                .list_runs_for_session(&session_id.to_string(), cursor, i64::from(limit))
                .await
                .map_err(|error| AgentError::Other(error.to_string()))?,
            None => (0, Vec::new(), None),
        };
        Ok(crate::session::SessionEvent::AgentRuns {
            session_id: session_id.clone(),
            revision,
            runs,
            next_cursor,
        })
    }

    /// One durable Agent run projection.
    pub async fn get_agent_run(
        &self,
        session_id: &UnifiedSessionId,
        run_id: &str,
    ) -> Result<crate::session::SessionEvent, AgentError> {
        let (revision, run) = match self.agent_coordinator.as_ref() {
            Some(coordinator) => coordinator
                .get_run_for_session(&session_id.to_string(), run_id)
                .await
                .map_err(|error| AgentError::Other(error.to_string()))?,
            None => (0, None),
        };
        Ok(crate::session::SessionEvent::AgentRun {
            session_id: session_id.clone(),
            revision,
            run,
        })
    }

    pub async fn list_dialogs(
        &self,
        channel: &str,
        chat_id: &str,
        include_archived: bool,
    ) -> Result<(Vec<DialogInfo>, Option<String>), AgentError> {
        let metas = self
            .storage
            .list_sessions(channel, chat_id, 100, include_archived)
            .await
            .map_err(|e| AgentError::Other(format!("failed to list dialogs: {}", e)))?;
        let current_dialog_id = self
            .get_current_dialog(channel, chat_id)
            .await?
            .map(|sid| sid.dialog_id);

        let dialogs: Vec<DialogInfo> = metas
            .into_iter()
            .map(|meta| DialogInfo {
                session_id: UnifiedSessionId::new(channel, chat_id, &meta.dialog_id),
                title: meta.title,
                created_at: meta.created_at,
                last_active_at: meta.last_active_at,
                message_count: meta.message_count,
                archived_at: meta.archived_at,
            })
            .collect();

        Ok((dialogs, current_dialog_id))
    }

    pub async fn rename_dialog(
        &self,
        session_id: &UnifiedSessionId,
        title: &str,
    ) -> Result<(), AgentError> {
        // Update in-memory session
        let session = self.get_or_create_session(session_id).await?;
        let persistence_lock = { session.lock().await.persistence_lock.clone() };
        let _persistence_guard = persistence_lock.lock().await;
        let meta_snapshot = {
            let mut session_guard = session.lock().await;
            session_guard.title = title.to_string();
            session_guard.state_version = session_guard.state_version.wrapping_add(1);
            session_guard.session_meta_snapshot()
        };
        if let Some((storage, meta)) = meta_snapshot {
            storage
                .upsert_session(&meta)
                .await
                .map_err(|e| AgentError::Other(format!("failed to rename dialog: {}", e)))?;
        }
        Ok(())
    }

    pub async fn delete_dialog(&self, session_id: &UnifiedSessionId) -> Result<(), AgentError> {
        let session_id_str = session_id.to_string();
        let session = self.get_or_create_session(session_id).await?;
        let persistence_lock = { session.lock().await.persistence_lock.clone() };
        let _persistence_guard = persistence_lock.lock().await;

        // Cancel durable Agent runs and dead-letter inbox events first; the
        // audit rows remain visible on the management surface.
        if let Some(coordinator) = self.agent_coordinator.as_ref() {
            let _ = coordinator
                .cancel_session(&session_id_str, "session deleted")
                .await;
            let _ = self
                .storage
                .dead_letter_agent_events(
                    &session_id_str,
                    "session_deleted",
                    chrono::Utc::now().timestamp_millis(),
                )
                .await;
        }

        // Soft delete from Storage
        self.storage
            .soft_delete_session(&session_id_str)
            .await
            .map_err(|e| AgentError::Other(format!("failed to delete dialog: {}", e)))?;

        // Remove from memory and current sessions
        let mut inner = self.inner.lock().await;
        inner.sessions.remove(&session_id_str);
        let chat_scope = format!("{}:{}", session_id.channel, session_id.chat_id);
        inner.current_sessions.remove(&chat_scope);

        Ok(())
    }

    pub async fn archive_dialog(&self, session_id: &UnifiedSessionId) -> Result<(), AgentError> {
        let session_id_str = session_id.to_string();
        let session = self.get_or_create_session(session_id).await?;
        let persistence_lock = { session.lock().await.persistence_lock.clone() };
        let _persistence_guard = persistence_lock.lock().await;
        if let Some(coordinator) = self.agent_coordinator.as_ref() {
            let _ = coordinator
                .cancel_session(&session_id_str, "session archived")
                .await;
            let _ = self
                .storage
                .dead_letter_agent_events(
                    &session_id_str,
                    "session_archived",
                    chrono::Utc::now().timestamp_millis(),
                )
                .await;
        }
        self.storage
            .archive_session(&session_id_str)
            .await
            .map_err(|e| AgentError::Other(format!("failed to archive dialog: {}", e)))?;

        let mut inner = self.inner.lock().await;
        inner.sessions.remove(&session_id_str);
        let chat_scope = format!("{}:{}", session_id.channel, session_id.chat_id);
        if inner
            .current_sessions
            .get(&chat_scope)
            .is_some_and(|id| id == &session_id_str)
        {
            inner.current_sessions.remove(&chat_scope);
        }

        Ok(())
    }

    pub async fn clear_dialog_history(
        &self,
        session_id: &UnifiedSessionId,
    ) -> Result<(), AgentError> {
        self.clear_session_history(session_id).await
    }

    /// Get or activate a specific session by its full UnifiedSessionId.
    /// Returns an error if the session does not exist in storage.
    /// If the session was expired from memory but still in storage,
    /// it will be restored (reactivated).
    pub async fn get_or_activate_session(
        &self,
        unified_id: &UnifiedSessionId,
    ) -> Result<Arc<Mutex<Session>>, AgentError> {
        let session_id_str = unified_id.to_string();
        match self.storage.get_session(&session_id_str).await {
            Ok(_) => self.get_or_create_session(unified_id).await,
            Err(StorageError::NotFound(_)) => Err(AgentError::Other(format!(
                "session not found: {}",
                unified_id
            ))),
            Err(e) => Err(AgentError::Other(format!("storage error: {}", e))),
        }
    }

    pub(super) async fn resolve_dialog_id(
        &self,
        channel: &str,
        chat_id: &str,
    ) -> Result<UnifiedSessionId, AgentError> {
        let chat_scope = format!("{}:{}", channel, chat_id);
        let current_id = {
            self.inner
                .lock()
                .await
                .current_sessions
                .get(&chat_scope)
                .cloned()
        };

        if let Some(ref current_id) = current_id {
            match self.storage.get_session(current_id).await {
                Ok(_) => {
                    if let Some(parsed) = UnifiedSessionId::parse(current_id) {
                        return Ok(parsed);
                    }
                }
                Err(StorageError::NotFound(_)) => {}
                Err(e) => {
                    return Err(AgentError::Other(format!(
                        "failed to resolve current session: {}",
                        e
                    )));
                }
            }
        }

        match self
            .storage
            .find_most_recent_session(channel, chat_id)
            .await
        {
            Ok(Some(meta)) => Ok(UnifiedSessionId::new(channel, chat_id, &meta.dialog_id)),
            Ok(None) => {
                let (new_id, _) = self
                    .create_session(channel, chat_id, None, String::new())
                    .await?;
                Ok(new_id)
            }
            Err(e) => Err(AgentError::Other(format!(
                "failed to find recent session: {}",
                e
            ))),
        }
    }

    pub(super) async fn restore_origin_dialog(&self, origin_id: &str, target: &UnifiedSessionId) {
        let Some(origin) = UnifiedSessionId::parse(origin_id) else {
            tracing::warn!(origin_id, "Ignoring malformed source session id");
            return;
        };
        if origin.channel == target.channel
            && origin.chat_id == target.chat_id
            && origin.dialog_id != target.dialog_id
        {
            self.inner
                .lock()
                .await
                .current_sessions
                .insert(target.chat_scope(), origin_id.to_string());
        }
    }

    /// Send a system notification (no LLM triggered).
    ///
    /// Flow:
    /// 1. Resolve target session (resolve_dialog_id)
    /// 2. Write assistant message with source tag to history
    /// 3. Publish OutboundMessage via bus to target channel
    pub async fn send_notification(
        &self,
        channel: &str,
        chat_id: &str,
        content: &str,
        system_name: &str,
        task_id: Option<&str>,
    ) -> Result<(), AgentError> {
        let unified_id = self.resolve_dialog_id(channel, chat_id).await?;
        let session = self.get_or_create_session(&unified_id).await?;
        let source = MessageSource {
            kind: SourceKind::SystemNotification,
            from_channel: None,
            from_session: None,
            from_user_id: None,
            system_name: Some(system_name.to_string()),
            task_id: task_id.map(|s| s.to_string()),
            from_run_id: None,
            from_agent_id: None,
        };
        let msg = ChatMessage::assistant_with_source(content, source);
        append_persisted_messages(&session, vec![msg])
            .await
            .map_err(|e| AgentError::Other(format!("persist error: {}", e)))?;

        let outbound = OutboundMessage {
            channel: channel.to_string(),
            chat_id: chat_id.to_string(),
            content: content.to_string(),
            reply_to: None,
            media: vec![],
            metadata: outbound_session_metadata(&unified_id.to_string()),
            delivery: None,
        };
        self.bus
            .deliver_outbound(outbound)
            .await
            .map_err(|e| AgentError::Other(format!("bus publish error: {}", e)))?;

        Ok(())
    }

    pub async fn handle_message(
        &self,
        inbound: &InboundMessage,
    ) -> Result<HandleResult, AgentError> {
        let channel = inbound.channel.as_str();
        let sender_id = inbound.sender_id.as_str();
        let chat_id = inbound.chat_id.as_str();
        let content = inbound.content.as_str();
        let unified_id = self.resolve_dialog_id(channel, chat_id).await?;
        tracing::debug!(unified_id = %unified_id, "handle_message resolved unified_id");
        let session = self.get_or_create_session(&unified_id).await?;

        CURRENT_SOURCE_SESSION
            .scope(Some(unified_id.to_string()), async {
                // Check for slash command
                if let Some((cmd_name, cmd_args)) = parse_slash_command(content) {
                    // `/queue` is an input-routing primitive rather than a
                    // normal slash command.  Preserve the original inbound
                    // metadata/media while stripping only the command
                    // prefix.  Unlike steering, queued input is never
                    // admitted to the active turn.
                    if cmd_name == "queue" {
                        let queued_content = cmd_args.trim();
                        if queued_content.is_empty() && inbound.media.is_empty() {
                            return Ok(HandleResult::CommandOutput(
                                "Usage: /queue <message>".to_string(),
                            ));
                        }
                        let task = AgentTask {
                            channel: channel.to_string(),
                            sender_id: sender_id.to_string(),
                            chat_id: chat_id.to_string(),
                            sequence: 0,
                            client_message_id: inbound
                                .client_message_id
                                .clone()
                                .filter(|id| !id.trim().is_empty()),
                            content: queued_content.to_string(),
                            received_at: inbound.received_at,
                            media: inbound.media.clone(),
                            channel_context: inbound.channel_context.clone(),
                        };
                        return self
                            .enqueue_agent_task(session.clone(), unified_id.clone(), task)
                            .await;
                    }

                    let result = self
                        .execute_slash_command(
                            cmd_name,
                            if cmd_args.is_empty() {
                                None
                            } else {
                                Some(cmd_args)
                            },
                            channel,
                            chat_id,
                            Some(&unified_id),
                        )
                        .await;

                    return match result {
                        Ok((_new_session_id, response)) => {
                            Ok(HandleResult::CommandOutput(response))
                        }
                        Err(e) => Ok(HandleResult::CommandOutput(e.to_string())),
                    };
                }

                // Normal message: enqueue to per-session worker for serial processing.
                let mut task = AgentTask {
                    channel: channel.to_string(),
                    sender_id: sender_id.to_string(),
                    chat_id: chat_id.to_string(),
                    sequence: 0,
                    client_message_id: inbound
                        .client_message_id
                        .clone()
                        .filter(|id| !id.trim().is_empty()),
                    content: content.to_string(),
                    received_at: inbound.received_at,
                    media: inbound.media.clone(),
                    channel_context: inbound.channel_context.clone(),
                };

                // While a turn is active, ordinary input is steering by
                // default.  The mailbox operation is synchronized with
                // `/stop`: if it is full or already closed, fall back to the
                // bounded session queue instead of dropping the input.
                let unified_str = unified_id.to_string();
                let mut guard = session.lock().await;
                if task.sequence == 0 {
                    task.sequence = guard.allocate_task_sequence();
                }
                if let Some(active) = guard.active_turn_emitter.as_ref() {
                    let media_refs: Vec<MediaRef> =
                        inbound.media.iter().map(MediaItem::to_media_ref).collect();
                    let source = MessageSource {
                        kind: SourceKind::UserInput,
                        from_channel: Some(channel.to_string()),
                        from_session: None,
                        from_user_id: Some(sender_id.to_string()),
                        system_name: None,
                        task_id: None,
                        from_run_id: None,
                        from_agent_id: None,
                    };
                    let mut message =
                        guard.create_user_message_with_source(content, media_refs, source);
                    if let Some(id) = inbound
                        .client_message_id
                        .as_deref()
                        .filter(|id| !id.trim().is_empty())
                    {
                        message.id = id.to_string();
                    }
                    if let Some(id) = inbound
                        .client_message_id
                        .as_deref()
                        .filter(|id| !id.trim().is_empty())
                    {
                        message.id = id.to_string();
                    }
                    message.timestamp = inbound.received_at;
                    let message_id = message.id.clone();
                    let mut recovery_task = task.clone();
                    recovery_task.client_message_id = Some(message_id.clone());
                    active
                        .recovery
                        .lock()
                        .unwrap_or_else(|poisoned| poisoned.into_inner())
                        .insert(message_id.clone(), recovery_task);
                    let input = TurnInput::user(
                        message_id.clone(),
                        message.content.clone(),
                        message.media_refs.clone(),
                        message.source.clone(),
                        message.timestamp,
                    );
                    match active.steering.try_push_user(input) {
                        Ok(()) => {
                            return Ok(HandleResult::AgentProcessing);
                        }
                        Err(_) => {
                            active
                                .recovery
                                .lock()
                                .unwrap_or_else(|poisoned| poisoned.into_inner())
                                .remove(&message_id);
                        }
                    }
                }

                self.enqueue_agent_task_locked(&mut guard, &session, &unified_str, task)
            })
            .await
    }

    /// Enqueue one user task on the session's bounded serial worker.  This is
    /// shared by ordinary messages, `/queue`, and the reliable fallback when
    /// steering admission races with mailbox closure/full capacity.
    async fn enqueue_agent_task(
        &self,
        session: Arc<Mutex<Session>>,
        unified_id: UnifiedSessionId,
        task: AgentTask,
    ) -> Result<HandleResult, AgentError> {
        let unified_str = unified_id.to_string();
        let mut guard = session.lock().await;
        self.enqueue_agent_task_locked(&mut guard, &session, &unified_str, task)
    }

    fn enqueue_agent_task_locked(
        &self,
        guard: &mut Session,
        session: &Arc<Mutex<Session>>,
        unified_str: &str,
        mut task: AgentTask,
    ) -> Result<HandleResult, AgentError> {
        if task.sequence == 0 {
            task.sequence = guard.allocate_task_sequence();
        }
        let needs_spawn =
            guard.agent_tx.is_none() || guard.agent_tx.as_ref().is_some_and(|tx| tx.is_closed());
        if needs_spawn {
            guard.agent_tx = None;
            guard.turn_busy = false;
            guard.current_turn_token = None;
            guard.worker_generation = guard.worker_generation.wrapping_add(1);
            let generation = guard.worker_generation;
            let (tx, rx) = mpsc::channel(SESSION_QUEUE_CAPACITY);
            guard.agent_tx = Some(tx);
            spawn_agent_worker(
                rx,
                session.clone(),
                self.worker_deps(),
                generation,
                unified_str.to_string(),
            );
        }
        let Some(agent_tx) = guard.agent_tx.as_ref() else {
            return Err(AgentError::Other(
                "agent worker queue was not initialized".to_string(),
            ));
        };
        if let Err(error) = agent_tx.try_send(task) {
            if matches!(error, mpsc::error::TrySendError::Full(_)) {
                tracing::warn!(session_id = %unified_str, capacity = SESSION_QUEUE_CAPACITY, "Session queue is full");
                return Ok(HandleResult::CommandOutput(
                    "当前对话消息队列已满，请稍后重试。".to_string(),
                ));
            }

            // The worker can exit between spawn and send.  Recreate the
            // worker under the same lock and retry the recovered task once.
            let task = error.into_inner();
            guard.agent_tx = None;
            guard.turn_busy = false;
            guard.current_turn_token = None;
            guard.worker_generation = guard.worker_generation.wrapping_add(1);
            let generation = guard.worker_generation;
            let (tx, rx) = mpsc::channel(SESSION_QUEUE_CAPACITY);
            guard.agent_tx = Some(tx);
            spawn_agent_worker(
                rx,
                session.clone(),
                self.worker_deps(),
                generation,
                unified_str.to_string(),
            );
            guard
                .agent_tx
                .as_ref()
                .ok_or_else(|| {
                    AgentError::Other("agent worker queue was not initialized".to_string())
                })?
                .try_send(task)
                .map_err(|_| {
                    AgentError::Other("agent worker spawn+send failed irrecoverably".to_string())
                })?;
        }
        Ok(HandleResult::AgentProcessing)
    }
}

async fn generate_title(
    session: Arc<Mutex<Session>>,
    provider: Arc<dyn LLMProvider>,
    prompt: String,
) -> Result<(), AgentError> {
    use crate::providers::{ChatCompletionRequest, ChatCompletionResponse, Message};

    let request = ChatCompletionRequest {
        messages: vec![Message::user(prompt)],
        temperature: Some(0.3),
        max_tokens: Some(20),
        tools: None,
    };

    let response: ChatCompletionResponse = provider
        .chat(request)
        .await
        .map_err(|e| AgentError::Other(format!("LLM call failed: {}", e)))?;
    let title = response.content.trim().to_string();

    let persistence_lock = { session.lock().await.persistence_lock.clone() };
    let _persistence_guard = persistence_lock.lock().await;
    let meta_snapshot = {
        let mut guard = session.lock().await;
        if guard.apply_generated_title(title) {
            guard.session_meta_snapshot()
        } else {
            None
        }
    };

    if let Some((storage, meta)) = meta_snapshot {
        storage
            .upsert_session(&meta)
            .await
            .map_err(|e| AgentError::Other(format!("failed to persist title: {}", e)))?;
    }

    Ok(())
}

async fn schedule_title_generation(
    session: Arc<Mutex<Session>>,
    supervisor: crate::task_supervisor::TaskSupervisor,
    session_id: &str,
) {
    let title_job = {
        let mut guard = session.lock().await;
        if guard.title_generation_in_flight {
            None
        } else {
            guard.title_prompt_snapshot().map(|prompt| {
                guard.title_generation_in_flight = true;
                (guard.provider.clone(), prompt)
            })
        }
    };
    let Some((provider, prompt)) = title_job else {
        return;
    };

    let title_session = session.clone();
    let task_session = session.clone();
    let spawned = supervisor.spawn(format!("session-title:{session_id}"), async move {
        if let Err(error) = generate_title(title_session, provider, prompt).await {
            tracing::warn!(error = %error, "Failed to generate session title");
        }
        task_session.lock().await.title_generation_in_flight = false;
    });
    if !spawned {
        session.lock().await.title_generation_in_flight = false;
    }
}

fn spawn_agent_worker(
    mut task_rx: mpsc::Receiver<AgentTask>,
    session: Arc<Mutex<Session>>,
    deps: AgentWorkerDeps,
    worker_gen: u64,
    unified_str: String,
) {
    let AgentWorkerDeps {
        bus,
        memory_manager,
        work_manager,
        skills_loader,
        task_supervisor,
        turn_delivery,
        execution_gate,
        inbox_burst,
        inbox_wait_ms,
        inbox_max_attempts,
    } = deps;
    let worker_supervisor = task_supervisor.clone();
    task_supervisor.spawn(format!("session-worker:{unified_str}"), async move {
        let unified_for_source = unified_str.clone();
        let _scope = CURRENT_SOURCE_SESSION.scope(Some(unified_for_source), async {
            // Steering that cannot be consumed by the current turn is
            // prepended here.  This local deque is deliberately ahead of the
            // channel queue: messages admitted after the mailbox closed must
            // not overtake an earlier accepted steering message, and moving
            // pending inputs here cannot fail due to channel capacity.
            let mut local_tasks = VecDeque::new();
            // Durable inbox wake lane: the value only merges wakes; event
            // payloads always come from SQLite claims.
            let mut inbox_wake_rx = {
                let session_guard = session.lock().await;
                session_guard.agent_inbox_wake.subscribe()
            };
            let mut consecutive_user_turns = 0usize;
            'tasks: loop {
                // Drain user tasks first so we can tell whether the session
                // is idle.  Admission sequence numbers are allocated while
                // holding the Session lock; draining everything currently
                // visible before selecting the smallest sequence keeps a
                // terminal fallback from overtaking an earlier `/queue` task.
                while let Ok(task) = task_rx.try_recv() {
                    local_tasks.push_back(task);
                }
                let has_user_backlog = !local_tasks.is_empty();

                // Fairness: when user work is queued it runs first, but a due
                // inbox event must still preempt it once the user burst
                // budget is exhausted or the oldest pending event has waited
                // too long.  When idle (no user backlog) a due event is
                // claimed immediately, so a background result is delivered as
                // soon as its run finishes.  The next pending due time also
                // arms a timer so a released event is re-claimed after its
                // retry backoff without needing a wake.
                let mut next_due_at = None;
                let storage = {
                    let guard = session.lock().await;
                    guard.storage.clone()
                };
                if let Some(storage) = storage {
                    let now = chrono::Utc::now().timestamp_millis();
                    let oldest_due = crate::storage::Storage::oldest_pending_due(
                        &storage,
                        &unified_str,
                        now,
                    )
                    .await
                    .unwrap_or(None);
                    next_due_at = crate::storage::Storage::next_pending_due_at(
                        &storage,
                        &unified_str,
                    )
                    .await
                    .unwrap_or(None);
                    let age_exceeded = oldest_due
                        .is_some_and(|created_at| now - created_at >= inbox_wait_ms);
                    let burst_exceeded = consecutive_user_turns >= inbox_burst;
                    let force = oldest_due.is_some()
                        && (!has_user_backlog || burst_exceeded || age_exceeded);
                    if force
                        && let Ok(Some(lease)) = crate::storage::Storage::claim_inbox_batch(
                            &storage,
                            &unified_str,
                            now,
                            60_000,
                            8,
                            32 * 1024,
                            None,
                        )
                        .await
                    {
                        let claimed = run_inbox_continuation(
                            &session,
                            &turn_delivery,
                            &execution_gate,
                            lease,
                            worker_gen,
                            &unified_str,
                            inbox_max_attempts,
                        )
                        .await;
                        if claimed {
                            consecutive_user_turns = 0;
                            continue 'tasks;
                        }
                        // Claim raced with a user message; fall through to the
                        // user lane below.
                    }
                }

                let task = if let Some(task) = pop_lowest_sequence(&mut local_tasks) {
                    task
                } else {
                    let has_due = next_due_at.is_some();
                    let due_sleep = async move {
                        if let Some(due) = next_due_at {
                            let now = chrono::Utc::now().timestamp_millis();
                            let remaining = (due - now).max(0) as u64;
                            tokio::time::sleep(tokio::time::Duration::from_millis(remaining)).await;
                        }
                    };
                    match tokio::select! {
                        task = task_rx.recv() => task,
                        _ = inbox_wake_rx.changed() => {
                            // A durable event landed while idle; loop to
                            // re-check the inbox before waiting for a user.
                            continue 'tasks;
                        }
                        _ = due_sleep, if has_due => {
                            // A released event's retry backoff elapsed; loop
                            // to re-claim it.
                            continue 'tasks;
                        }
                    } {
                        Some(task) => task,
                        None => break,
                    }
                };
                let task_chan = task.channel.clone();
                let task_cid = task.chat_id.clone();
                let task_metadata = task.channel_context.private.clone();
                let task_reply_to = task.channel_context.reply_to.clone();
                // Phase 1: capture a stable session snapshot under lock.
                // Memory recall and compression happen outside this block so
                // /stop and other commands are not blocked behind slow I/O or
                // LLM-backed compaction.
                let skills_prompt = skills_loader.build_skills_prompt();
                let user_message = {
                    let guard = session.lock().await;
                    if guard.worker_generation != worker_gen {
                        return;
                    }
                    let media_refs: Vec<MediaRef> =
                        task.media.iter().map(MediaItem::to_media_ref).collect();
                    let source = MessageSource {
                        kind: SourceKind::UserInput,
                        from_channel: Some(task.channel.clone()),
                        from_session: None,
                        from_user_id: Some(task.sender_id.clone()),
                        system_name: None,
                        task_id: None,
                        from_run_id: None,
                        from_agent_id: None,
                    };
                    let mut message =
                        guard.create_user_message_with_source(&task.content, media_refs, source);
                    if let Some(id) = task
                        .client_message_id
                        .as_deref()
                        .filter(|id| !id.trim().is_empty())
                    {
                        message.id = id.to_string();
                    }
                    message.timestamp = task.received_at;
                    message
                };
                if let Err(e) = append_persisted_messages(&session, vec![user_message]).await {
                    tracing::error!(error = %e, "Failed to persist user message");
                    let err_outbound = OutboundMessage {
                        channel: task_chan.clone(),
                        chat_id: task_cid.clone(),
                        content: "Failed to save your message, please try again.".to_string(),
                        reply_to: task_reply_to.clone(),
                        media: vec![],
                        metadata: outbound_turn_metadata(&unified_str, &task_metadata),
                        delivery: None,
                    };
                    let _ = bus.publish_outbound(err_outbound).await;
                    continue 'tasks;
                }
                // Remember the channel's durable delivery context (thread/
                // root identity) so later continuation Turns can reuse it.
                // One-shot reply/reaction ids never reach this column.
                if !task.channel_context.durable_private.is_empty()
                    && let Some(storage) = session.lock().await.storage.clone()
                {
                    let durable_json = serde_json::json!(task.channel_context.durable_private)
                        .to_string();
                    if let Err(e) = storage
                        .update_session_delivery_context(
                            &unified_str,
                            &durable_json,
                            chrono::Utc::now().timestamp_millis(),
                        )
                        .await
                    {
                        tracing::warn!(error = %e, "Failed to persist durable delivery context");
                    }
                }

                let (
                    agent,
                    system_prompt_out,
                    base_version,
                    turn_token,
                    turn_controller,
                    turn_emitter,
                    turn_receiver,
                    initial_turn,
                    steering,
                    recovery,
                ) = {
                    let mut guard = session.lock().await;

                    if guard.worker_generation != worker_gen {
                        return; // stale worker
                    }

                    let agent = match guard.create_agent() {
                        Ok(a) => a,
                        Err(e) => {
                            tracing::error!(error = %e, "Failed to create agent");
                            let err_outbound = OutboundMessage {
                                channel: task_chan.clone(),
                                chat_id: task_cid.clone(),
                                content: "Agent creation failed, please try again."
                                    .to_string(),
                                reply_to: task_reply_to.clone(),
                                media: vec![],
                                metadata: outbound_turn_metadata(&unified_str, &task_metadata),
                                delivery: None,
                            };
                            let _ = bus.publish_outbound(err_outbound).await;
                            continue 'tasks;
                        }
                    };

                    let turn_token = CancellationToken::new();

                    if guard.worker_generation != worker_gen {
                        return; // /stop replaced us
                    }
                    guard.turn_busy = true;
                    guard.current_turn_token = Some(turn_token.clone());

                    // Install the active-turn handle before memory recall and
                    // context preparation.  This closes the historical race
                    // where a message received during slow preparation was
                    // queued as a new turn instead of steering the running
                    // one.
                    let (turn_controller, turn_emitter, turn_receiver) =
                        TurnController::start(
                            unified_str.clone(),
                            uuid::Uuid::new_v4().to_string(),
                        );
                    let initial_turn = turn_controller.snapshot();
                    let steering = TurnMailbox::new_shared();
                    let recovery = StdArc::new(StdMutex::new(HashMap::new()));
                    guard.active_turn_emitter = Some(ActiveTurnEmitter {
                        turn_id: initial_turn.id.0.clone(),
                        emitter: turn_emitter.clone(),
                        steering: steering.clone(),
                        recovery: recovery.clone(),
                    });

                    (
                        agent,
                        guard.build_system_prompt(&skills_prompt),
                        guard.state_version,
                        turn_token.clone(),
                        turn_controller,
                        turn_emitter,
                        turn_receiver,
                        initial_turn,
                        steering,
                        recovery,
                    )
                }; // lock released

                let runtime_context = prepare_turn_runtime(
                    memory_manager.clone(),
                    work_manager.clone(),
                    &unified_str,
                    &task.content,
                    system_prompt_out,
                )
                .await;
                let (runtime_memory_tokens, runtime_plan_tokens) = runtime_context.budget_hints();
                let compaction_result = compact_session_context(
                    &session,
                    CompactRequest {
                        reason: CompactionReason::Auto,
                        context_window_override: None,
                        degraded_only: false,
                        memory_tokens: runtime_memory_tokens,
                        active_plan_tokens: runtime_plan_tokens,
                    },
                    |history| runtime_context.assemble(history),
                )
                .await;
                let (history_projection, semantic_compaction_attempted) = match compaction_result {
                    Ok(outcome) => (outcome.projected_history, outcome.semantic_attempted),
                    Err(error) => {
                        tracing::warn!(error = %error, "Automatic context compaction failed");
                        (session.lock().await.projected_history(), true)
                    }
                };
                let history_out = runtime_context.assemble(history_projection);
                let meta_snapshot = {
                    let mut guard = session.lock().await;
                    let storage = guard.storage.clone();
                    if guard.worker_generation != worker_gen {
                        let mut take = steering.close_and_take_pending();
                        recovery
                            .lock()
                            .unwrap_or_else(|poisoned| poisoned.into_inner())
                            .clear();
                        if guard
                            .active_turn_emitter
                            .as_ref()
                            .is_some_and(|active| active.turn_id == initial_turn.id.0)
                            && let Some(active) = guard.active_turn_emitter.take()
                        {
                            let active_take = active.steering.close_and_take_pending();
                            take.admitted_leases.extend(active_take.admitted_leases);
                            take.reserved_leases.extend(active_take.reserved_leases);
                            active
                                .recovery
                                .lock()
                                .unwrap_or_else(|poisoned| poisoned.into_inner())
                                .clear();
                            active.emitter.deactivate();
                        }
                        drop(guard);
                        let mut leases = take.admitted_leases;
                        leases.extend(take.reserved_leases);
                        if let Some(storage) = storage {
                            release_steer_leases(&storage, leases).await;
                        }
                        turn_emitter.deactivate();
                        turn_controller.cancel(Some(
                            "session changed before turn preparation completed".to_string(),
                        ));
                        return;
                    }
                    if guard.state_version != base_version {
                        tracing::warn!(
                            session_id = %guard.id,
                            "Session changed while preparing agent history; dropping stale task"
                        );
                        guard.turn_busy = false;
                        guard.current_turn_token = None;
                        let mut released = Vec::new();
                        if guard
                            .active_turn_emitter
                            .as_ref()
                            .is_some_and(|active| active.turn_id == initial_turn.id.0)
                            && let Some(active) = guard.active_turn_emitter.take()
                        {
                            let leases = prepend_pending_steering(
                                &active.steering,
                                &active.recovery,
                                &mut local_tasks,
                                &task_chan,
                                &task_cid,
                                &task_reply_to,
                                &task_metadata,
                            );
                            released.extend(leases);
                            active.emitter.deactivate();
                        }
                        drop(guard);
                        if let Some(storage) = storage.clone() {
                            release_steer_leases(&storage, released).await;
                        }
                        turn_emitter.deactivate();
                        turn_controller.cancel(Some(
                            "session changed while preparing agent history".to_string(),
                        ));
                        continue 'tasks;
                    }
                    guard.last_consolidated_at = Some(chrono::Utc::now().timestamp_millis());
                    guard.session_meta_snapshot()
                };
                if let Some((storage, meta)) = meta_snapshot
                    && let Err(e) = storage.upsert_session(&meta).await
                {
                    tracing::warn!(error = %e, "Failed to persist session meta after compression");
                }
                let active_turn_id = initial_turn.id.0.clone();
                let turn_target = crate::channels::TurnTarget {
                    channel: task_chan.clone(),
                    chat_id: task_cid.clone(),
                    session_id: unified_str.clone(),
                    reply_to: task_reply_to.clone(),
                    metadata: outbound_turn_metadata(&unified_str, &task_metadata),
                };
                let delivery_handle = match turn_delivery
                    .start(turn_target.clone(), turn_receiver)
                    .await
                {
                    Ok(handle) => Some(handle),
                    Err(error) => {
                        tracing::debug!(
                            channel = %task_chan,
                            error = %error,
                            "Live turn delivery unavailable; using ordinary final delivery"
                        );
                        None
                    }
                };
                let live_delivery_started = delivery_handle.is_some();
                {
                    let mut guard = session.lock().await;
                    let storage = guard.storage.clone();
                    if guard.worker_generation != worker_gen || guard.state_version != base_version {
                        let mut released = Vec::new();
                        if guard
                            .active_turn_emitter
                            .as_ref()
                            .is_some_and(|active| active.turn_id == active_turn_id)
                            && let Some(active) = guard.active_turn_emitter.take()
                        {
                            let leases = prepend_pending_steering(
                                &active.steering,
                                &active.recovery,
                                &mut local_tasks,
                                &task_chan,
                                &task_cid,
                                &task_reply_to,
                                &task_metadata,
                            );
                            released.extend(leases);
                            active.emitter.deactivate();
                        }
                        turn_emitter.deactivate();
                        turn_controller.cancel(Some(
                            "session changed before model execution".to_string(),
                        ));
                        guard.turn_busy = false;
                        guard.current_turn_token = None;
                        drop(guard);
                        if let Some(storage) = storage {
                            release_steer_leases(&storage, released).await;
                        }
                        continue 'tasks;
                    }
                }
                let agent_turn = AgentTurnContext::new_with_steering(
                    initial_turn.id.0.clone(),
                    initial_turn.message_id.clone(),
                    turn_emitter,
                    steering.clone(),
                );

                // Phase 2 + 3: LLM call with cancellation
                let session2 = session.clone();
                let bus2 = bus.clone();
                let chan2 = task_chan.clone();
                let cid2 = task_cid.clone();
                let unified_str2 = unified_str.clone();
                let task_metadata2 = task_metadata.clone();
                let task_reply_to2 = task_reply_to.clone();
                let title_supervisor = worker_supervisor.clone();
                let commit_delivery = turn_delivery.clone();
                let commit_target = turn_target.clone();
                let turn_lifecycle = &turn_controller;
                let pending_turn_deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
                let scoped_turn_deliveries = pending_turn_deliveries.clone();
                let steering_for_process = steering.clone();
                let process_gate = execution_gate.clone();
                let turn_token_for_process = turn_token.clone();
                let process_future = async move {
                    let response_session_id = unified_str2.clone();
                    let tool_context = ToolExecutionContext::for_session(&response_session_id)
                        .with_turn_id(agent_turn.turn_id.clone())
                        .with_cancellation(turn_token_for_process)
                        .with_execution_gate(process_gate.clone());
                    let process_result = agent
                        .process_streaming_with_context(
                            history_out.clone(),
                            agent_turn.clone(),
                            tool_context.clone(),
                        )
                        .await;
                    let mut result = match process_result {
                        Ok(r) => r,
                        Err(AgentError::ContextOverflow {
                            message,
                            parsed_window,
                            tool_progress: false,
                        }) => {
                            let new_window = {
                                let guard = session2.lock().await;
                                parsed_window.unwrap_or(guard.effective_context_window)
                            };
                            tracing::warn!(
                                new_window,
                                error = %message,
                                "Context overflow before tool progress — compacting and retrying once"
                            );
                            let retry_outcome = compact_session_context(
                                &session2,
                                CompactRequest {
                                    reason: CompactionReason::Overflow,
                                    context_window_override: Some(new_window),
                                    degraded_only: semantic_compaction_attempted,
                                    memory_tokens: runtime_memory_tokens,
                                    active_plan_tokens: runtime_plan_tokens,
                                },
                                |history| runtime_context.assemble(history),
                            )
                            .await;
                            let retry_outcome = match retry_outcome {
                                Ok(outcome) if outcome.checkpoint.is_some() => outcome,
                                Ok(_) => {
                                    tracing::error!(
                                        "Overflow recovery found no compressible context"
                                    );
                                    fail_turn_with_partial(
                                        turn_lifecycle,
                                        &session2,
                                        "context overflow handling found no compressible history"
                                            .to_string(),
                                    )
                                    .await;
                                    return;
                                }
                                Err(e) => {
                                    tracing::error!(error = %e, "Retry compression failed");
                                    fail_turn_with_partial(
                                        turn_lifecycle,
                                        &session2,
                                        format!("context overflow handling failed: {e}"),
                                    )
                                    .await;
                                    let err_outbound = OutboundMessage {
                                        channel: chan2,
                                        chat_id: cid2,
                                        content: "Context overflow handling failed.".to_string(),
                                        reply_to: task_reply_to2.clone(),
                                        media: vec![],
                                        metadata: outbound_turn_metadata(
                                            &response_session_id,
                                            &task_metadata2,
                                        ),
                                        delivery: None,
                                    };
                                    if !live_delivery_started {
                                        let _ = bus2.publish_outbound(err_outbound).await;
                                    }
                                    return;
                                }
                            };
                            let retry_agent = {
                                let guard = session2.lock().await;
                                tracing::warn!(
                                    new_window,
                                    "Context overflow checkpoint committed; issuing the only formal retry"
                                );
                                match guard.create_agent() {
                                    Ok(agent) => agent,
                                    Err(error) => {
                                        drop(guard);
                                        fail_turn_with_partial(
                                            turn_lifecycle,
                                            &session2,
                                            format!("context overflow retry agent failed: {error}"),
                                        )
                                        .await;
                                        return;
                                    }
                                }
                            };
                            let retry_history =
                                runtime_context.assemble(retry_outcome.projected_history);

                            match retry_agent
                                .process_streaming_with_context(
                                    retry_history,
                                    agent_turn.clone(),
                                    tool_context,
                                )
                                .await
                            {
                                Ok(r) => r,
                                Err(AgentError::Cancelled) => {
                                    persist_cancelled_turn(
                                        turn_lifecycle,
                                        &session2,
                                        take_current_turn_deliveries(),
                                    )
                                    .await;
                                    return;
                                }
                                Err(e) => {
                                    tracing::error!(
                                        error = %e,
                                        "Agent retry after overflow failed"
                                    );
                                    fail_turn_with_partial(
                                        turn_lifecycle,
                                        &session2,
                                        e.to_string(),
                                    )
                                    .await;
                                    let err_outbound = OutboundMessage {
                                        channel: chan2,
                                        chat_id: cid2,
                                        content: format!("Processing error: {}", e),
                                        reply_to: task_reply_to2.clone(),
                                        media: vec![],
                                        metadata: outbound_turn_metadata(
                                            &response_session_id,
                                            &task_metadata2,
                                        ),
                                        delivery: None,
                                    };
                                    if !live_delivery_started {
                                        let _ = bus2.publish_outbound(err_outbound).await;
                                    }
                                    return;
                                }
                            }
                        }
                        Err(AgentError::Cancelled) => {
                            persist_cancelled_turn(
                                turn_lifecycle,
                                &session2,
                                take_current_turn_deliveries(),
                            )
                            .await;
                            return;
                        }
                        Err(e) => {
                            tracing::error!(error = %e, "Agent processing error");
                            fail_turn_with_partial(
                                turn_lifecycle,
                                &session2,
                                e.to_string(),
                            )
                            .await;
                            let err_outbound = OutboundMessage {
                                channel: chan2,
                                chat_id: cid2,
                                content: format!("Processing error: {}", e),
                                reply_to: task_reply_to2.clone(),
                                media: vec![],
                                metadata: outbound_turn_metadata(
                                    &response_session_id,
                                    &task_metadata2,
                                ),
                                delivery: None,
                            };
                            if !live_delivery_started {
                                let _ = bus2.publish_outbound(err_outbound).await;
                            }
                            return;
                        }
                    };

                    let pending = take_current_turn_deliveries();
                    attach_pending_turn_deliveries(&mut result, pending);
                    let response_media =
                        media_items_from_refs(&result.final_response.media_refs);
                    let response_content = result.final_response.content;
                    let usage = result.usage;
                    let last_prompt_tokens = result
                        .last_request_usage
                        .as_ref()
                        .map(|value| value.prompt_tokens);
                    let last_request_digest = result.last_request_digest;
                    let (provider_name, model_name) = {
                        let guard = session2.lock().await;
                        let storage = guard.storage.clone();
                        if guard.worker_generation != worker_gen {
                            // A generation change is the explicit `/stop` /
                            // worker-replacement boundary.  User steering is
                            // discarded like the command path does, but
                            // durable Agent steer events return to `pending`.
                            let take = steering_for_process.close_and_take_pending();
                            drop(guard);
                            let mut leases = take.admitted_leases;
                            leases.extend(take.reserved_leases);
                            if let Some(storage) = storage {
                                release_steer_leases(&storage, leases).await;
                            }
                            turn_lifecycle.cancel(Some(
                                "session changed before turn commit".to_string(),
                            ));
                            return;
                        }
                        if guard.state_version != base_version {
                            // A version-only mutation (for example /clear)
                            // invalidates this commit but does not carry the
                            // explicit stop/discard semantics.  Requeue the
                            // accepted steering for the next local Turn.
                            steering_for_process.restore_drained();
                            turn_lifecycle.cancel(Some(
                                "session changed before turn commit".to_string(),
                            ));
                            return;
                        }
                        (
                            guard.provider_config.name.clone(),
                            guard.provider_config.model_id.clone(),
                        )
                    };
                    let usage_record = usage.as_ref().map(|turn_usage| {
                        crate::storage::TurnUsageRecord {
                            session_id: response_session_id.clone(),
                            turn_id: agent_turn.turn_id.clone(),
                            provider: provider_name,
                            model: model_name,
                            usage: turn_usage.clone(),
                            last_prompt_tokens: last_prompt_tokens.unwrap_or_default(),
                            created_at: chrono::Utc::now().timestamp_millis(),
                        }
                    });
                    let emitted_messages = result.emitted_messages;
                    // Steer events admitted to this Turn are consumed in the
                    // same transaction as the Turn commit: the client never
                    // observes history without the event consumption.
                    let steer_consumption = {
                        let event_ids = steering_for_process.durable_in_flight_ids();
                        if event_ids.is_empty() {
                            None
                        } else {
                            Some(crate::storage::agent_inbox::SteerConsumption {
                                event_ids,
                                admitted_turn_id: agent_turn.turn_id.clone(),
                            })
                        }
                    };
                    let response = match finalize_turn_after_persistence(
                        turn_lifecycle,
                        usage.clone(),
                        async {
                            if let Some(steer) = steer_consumption.as_ref()
                                && let Some(usage_record) = usage_record.clone()
                            {
                                let steer = steer.clone();
                                crate::session::persistence::append_persisted_turn_messages_with_steer(
                                    &session2,
                                    emitted_messages,
                                    usage_record,
                                    steer,
                                )
                                .await
                            } else if let Some(usage_record) = usage_record {
                                append_persisted_turn_messages(
                                    &session2,
                                    emitted_messages,
                                    usage_record,
                                )
                                .await
                            } else {
                                append_persisted_messages_with_meta(&session2, emitted_messages)
                                    .await
                            }
                        },
                    )
                    .await
                    {
                        Ok(committed_messages) => {
                            // AgentLoop keeps drained steering in the
                            // mailbox until the whole Turn batch is durable.
                            steering_for_process.commit_drained();
                            let mut guard = session2.lock().await;
                            if let (Some(request_digest), Some(prompt_tokens)) =
                                (last_request_digest, last_prompt_tokens)
                            {
                                let request_key =
                                    observed_context_request_key(&guard, request_digest);
                                guard
                                    .context_usage
                                    .record(request_key, Some(prompt_tokens));
                            } else {
                                guard.context_usage.invalidate();
                            }
                            Some((response_content, response_media, committed_messages))
                        }
                        Err(e) => {
                            tracing::error!(error = %e, "Failed to atomically persist agent turn");
                            // Restore the in-memory steering batch so an
                            // accepted user input is retried as a subsequent
                            // Turn instead of silently disappearing.
                            steering_for_process.restore_drained();
                            None
                        }
                    };

                    let Some((response, response_media, committed_messages)) = response else {
                        let err_outbound = OutboundMessage {
                            channel: chan2,
                            chat_id: cid2,
                            content: "Failed to save the agent response, please try again."
                                .to_string(),
                            reply_to: task_reply_to2.clone(),
                            media: vec![],
                            metadata: outbound_turn_metadata(
                                &response_session_id,
                                &task_metadata2,
                            ),
                            delivery: None,
                        };
                        if !live_delivery_started {
                            let _ = bus2.publish_outbound(err_outbound).await;
                        }
                        return;
                    };

                    let delta = committed_turn_delta(&response_session_id, committed_messages);
                    let commit_presents_media = match commit_delivery
                        .commit(&commit_target, delta)
                        .await
                    {
                        Ok(presents_media) => presents_media,
                        Err(error) => {
                            tracing::warn!(error = %error, "Failed to publish committed turn delta");
                            false
                        }
                    };

                    schedule_title_generation(
                        session2.clone(),
                        title_supervisor,
                        &response_session_id,
                    )
                    .await;

                    if !live_delivery_started {
                        let outbound = OutboundMessage {
                            channel: chan2,
                            chat_id: cid2,
                            content: response,
                            reply_to: task_reply_to2.clone(),
                            media: response_media,
                            metadata: outbound_turn_metadata(
                                &response_session_id,
                                &task_metadata2,
                            ),
                            delivery: None,
                        };
                        let _ = bus2.publish_outbound(outbound).await;
                    } else if !commit_presents_media && !response_media.is_empty() {
                        // The live sink already delivered the text. Deliver
                        // only the final reply artifacts to avoid duplicating
                        // that text on channels whose committed-history event
                        // is not itself user-visible.
                        let outbound = OutboundMessage {
                            channel: chan2,
                            chat_id: cid2,
                            content: String::new(),
                            reply_to: task_reply_to2.clone(),
                            media: response_media,
                            metadata: outbound_turn_metadata(
                                &response_session_id,
                                &task_metadata2,
                            ),
                            delivery: None,
                        };
                        let _ = bus2.publish_outbound(outbound).await;
                    }
                };
                let process_future =
                    CURRENT_TURN_ID.scope(Some(active_turn_id.clone()), process_future);
                let process_future = CURRENT_TURN_DELIVERIES
                    .scope(Some(scoped_turn_deliveries), process_future);

                tokio::select! {
                    () = process_future => {}
                    _ = turn_token.cancelled() => {
                        // Cancelled by `/stop`, which took and cancelled the
                        // token; the terminal state is persisted here.
                        persist_cancelled_turn(
                            &turn_controller,
                            &session,
                            take_pending_turn_deliveries(&pending_turn_deliveries),
                        )
                        .await;
                    }
                }

                if let Some(handle) = delivery_handle {
                    deliver_terminal_fallback(
                        handle,
                        &bus,
                        &turn_target,
                        &turn_controller,
                    )
                    .await;
                }

                // AgentLoop closes its mailbox on terminal/error and leaves
                // any not-yet-injected messages pending.  Transfer those
                // accepted inputs to the ordinary bounded queue so they are
                // processed by a subsequent Turn instead of being lost.
                // `/stop` calls `close_and_take_pending` itself, therefore
                // its intentionally discarded inputs do not reach here.
                let released = prepend_pending_steering(
                    &steering,
                    &recovery,
                    &mut local_tasks,
                    &task_chan,
                    &task_cid,
                    &task_reply_to,
                    &task_metadata,
                );

                // Clean up
                if !released.is_empty() {
                    let storage = { session.lock().await.storage.clone() };
                    if let Some(storage) = storage {
                        release_steer_leases(&storage, released).await;
                    }
                }
                let mut guard = session.lock().await;
                if guard
                    .active_turn_emitter
                    .as_ref()
                    .is_some_and(|active| active.turn_id == active_turn_id)
                    && let Some(active) = guard.active_turn_emitter.take()
                {
                    active.steering.close();
                    active
                        .recovery
                        .lock()
                        .unwrap_or_else(|poisoned| poisoned.into_inner())
                        .clear();
                    active.emitter.deactivate();
                }
                if guard.worker_generation == worker_gen {
                    guard.current_turn_token = None;
                    consecutive_user_turns = consecutive_user_turns.saturating_add(1);
                    // Keep the session observable as busy while local
                    // fallback tasks are waiting for their next Turn.
                    // `/stop` can still invalidate this generation before
                    // the worker starts the next task.
                    guard.turn_busy = !local_tasks.is_empty();
                }
            }
        }).await;
    });
}

/// Execute one durable inbox batch as a continuation Turn of the root Agent.
///
/// The trigger message is hidden (never shown to clients), the tool set is
/// read-only, and the terminal persistence is atomic with event consumption:
/// a crash or failure releases the lease instead of losing the event.
/// Returns true when a continuation was processed (events consumed or
/// released for retry).
#[allow(clippy::too_many_arguments)]
async fn run_inbox_continuation(
    session: &Arc<Mutex<Session>>,
    turn_delivery: &TurnDeliveryService,
    execution_gate: &Arc<crate::agent::gate::ExecutionGate>,
    lease: crate::storage::agent_inbox::InboxLease,
    worker_gen: u64,
    unified_str: &str,
    inbox_max_attempts: i64,
) -> bool {
    let event_ids: Vec<String> = lease.events.iter().map(|event| event.id.clone()).collect();
    let exhausted = lease
        .events
        .iter()
        .any(|event| event.attempt_count >= inbox_max_attempts);

    /// Release the lease for retry, or dead-letter events that exhausted
    /// their delivery attempts so they become diagnosable instead of looping
    /// forever.
    async fn release_or_dead_letter(
        storage: &StdArc<crate::storage::Storage>,
        token: &str,
        event_ids: &[String],
        exhausted: bool,
        error: &str,
        now: i64,
    ) {
        if exhausted {
            let dead = storage
                .dead_letter_leased_events(token, event_ids, error, now)
                .await
                .unwrap_or(0);
            tracing::warn!(
                dead_lettered = dead,
                error,
                "Inbox event dead-lettered after exhausting delivery attempts"
            );
        } else {
            let _ = storage
                .release_inbox_lease(token, event_ids, 30_000, Some(error), now)
                .await;
        }
    }
    let token = lease.token.clone();
    let now = chrono::Utc::now().timestamp_millis();

    // Validate the worker generation before doing any work; `/stop` may have
    // replaced this worker while the lease was being claimed.
    let (provider_config, provider, full_tools, history_base) = {
        let guard = session.lock().await;
        if guard.worker_generation != worker_gen || guard.archived_at.is_some() {
            return false;
        }
        (
            guard.provider_config.clone(),
            guard.provider.clone(),
            guard.tools.clone(),
            guard.projected_history(),
        )
    };
    let Some(storage) = ({ session.lock().await.storage.clone() }) else {
        return false;
    };

    // Read-only continuation registry: inspect results and read files, but
    // never write, message, delegate or schedule.
    let tools = {
        let scoped = crate::tools::ToolRegistry::new();
        for name in crate::storage::agent_inbox::continuation_tool_names() {
            if let Some(tool) = full_tools.get(name) {
                scoped.register_raw(name.to_string(), tool);
            }
        }
        Arc::new(scoped)
    };
    let (controller, _emitter, _receiver) =
        TurnController::start(unified_str.to_string(), uuid::Uuid::new_v4().to_string());
    let turn_id = controller.snapshot().id.0.clone();
    let turn_token = CancellationToken::new();
    // Make `/stop` cancel this continuation like any active Turn.
    session.lock().await.current_turn_token = Some(turn_token.clone());
    let tool_context = ToolExecutionContext::for_session(unified_str)
        .with_turn_id(turn_id.clone())
        .with_cancellation(turn_token.clone())
        .with_execution_gate(execution_gate.clone());
    let trigger = crate::storage::agent_inbox::build_continuation_trigger(&lease.events, now);

    let agent = AgentLoop::with_provider_and_tools(
        provider.clone(),
        tools.clone(),
        12,
        provider_config.model_id.clone(),
        provider_config.workspace_dir.clone(),
        provider_config.input_types.clone(),
    )
    .with_context_window(provider_config.token_limit);

    let mut history = history_base;
    history.insert(
        0,
        ChatMessage::system(crate::agent::system_prompt::build_system_prompt(
            &provider_config.workspace_dir,
            &provider_config.model_id,
            &tools,
        )),
    );
    history.push(trigger.clone());

    let result = tokio::select! {
        result = agent.process_with_context(history, tool_context.clone()) => result,
        _ = turn_token.cancelled() => Err(crate::agent::AgentError::Cancelled),
    };

    let usage = result.as_ref().ok().and_then(|result| result.usage.clone());
    let last_prompt_tokens = result.as_ref().ok().and_then(|result| {
        result
            .last_request_usage
            .as_ref()
            .map(|value| value.prompt_tokens)
    });
    let emitted_messages = match result {
        Ok(result) => result.emitted_messages,
        Err(error) => {
            tracing::warn!(session_id = %unified_str, error = %error, "Continuation agent failed; releasing lease");
            controller.fail(format!("continuation failed: {error}"));
            release_or_dead_letter(
                &storage,
                &token,
                &event_ids,
                exhausted,
                &error.to_string(),
                now,
            )
            .await;
            return true;
        }
    };

    // Persist atomically: hidden trigger + assistant/tool messages + usage +
    // event consumption in one transaction.  Failure rolls back the in-memory
    // suffix and releases the lease for a retry.
    let committed = {
        let mut guard = session.lock().await;
        if guard.worker_generation != worker_gen || guard.archived_at.is_some() {
            drop(guard);
            let _ = storage
                .release_inbox_lease(&token, &event_ids, 5_000, Some("worker replaced"), now)
                .await;
            controller.cancel(Some(
                "worker replaced before continuation commit".to_string(),
            ));
            return true;
        }
        let mut all_metas: Vec<crate::storage::message::MessageMeta> = Vec::new();
        let mut session_meta = None;
        let mut added_ids = Vec::new();
        for message in std::iter::once(trigger).chain(emitted_messages) {
            if let Some((_, _, meta, meta_session)) =
                guard.add_message_in_memory_with_version(message, true, true)
            {
                added_ids.push(meta.id.clone());
                session_meta = Some(meta_session);
                all_metas.push(meta);
            }
        }
        let Some(session_meta) = session_meta else {
            return true;
        };
        let Some(trigger_meta) = all_metas.first().cloned() else {
            return true;
        };
        let usage_record = usage
            .as_ref()
            .map(|turn_usage| crate::storage::TurnUsageRecord {
                session_id: unified_str.to_string(),
                turn_id: turn_id.clone(),
                provider: provider.name().to_string(),
                model: provider.model_id().to_string(),
                usage: turn_usage.clone(),
                last_prompt_tokens: last_prompt_tokens.unwrap_or_default(),
                created_at: now,
            });
        match storage
            .commit_continuation_turn(
                unified_str,
                &trigger_meta,
                &all_metas[1..],
                &session_meta,
                usage_record.as_ref(),
                &token,
                &event_ids,
                now,
            )
            .await
        {
            Ok(committed) => committed,
            Err(error) => {
                guard.rollback_message_suffix_with_version(&added_ids, true);
                controller.fail(format!("continuation persistence failed: {error}"));
                release_or_dead_letter(
                    &storage,
                    &token,
                    &event_ids,
                    exhausted,
                    &error.to_string(),
                    now,
                )
                .await;
                return true;
            }
        }
    };

    controller.complete(usage);
    session.lock().await.current_turn_token = None;
    let delta = committed_turn_delta(unified_str, committed);
    // Deliver through the session's own channel binding: the WebSocket/WebUI
    // projection receives the committed history while external channel
    // delivery stays out of the continuation path.  When the channel
    // declared a durable delivery context (thread/root identity), reuse it
    // so a continuation lands in the same conversation thread; one-shot
    // `reply_to` values are never reused.
    if let Some(session_id) = UnifiedSessionId::parse(unified_str) {
        let durable_context = storage
            .get_session_delivery_context(unified_str)
            .await
            .unwrap_or(None);
        let mut metadata = outbound_turn_metadata(unified_str, &HashMap::new());
        if let Some(context_json) = durable_context
            && let Ok(context) = serde_json::from_str::<HashMap<String, String>>(&context_json)
        {
            metadata.extend(context);
        }
        let target = crate::channels::TurnTarget {
            channel: session_id.channel,
            chat_id: session_id.chat_id,
            session_id: unified_str.to_string(),
            reply_to: None,
            metadata,
        };
        let _ = turn_delivery.commit(&target, delta).await;
    }
    tracing::info!(session_id = %unified_str, events = event_ids.len(), "Continuation Turn committed");
    true
}

#[async_trait::async_trait]
impl crate::agent::AgentInboxWakeTarget for SessionManager {
    async fn wake_agent_inbox(&self, session_id: &str, revision: i64) {
        let session = { self.inner.lock().await.sessions.get(session_id).cloned() };
        let Some(session) = session else {
            // No live session: the event stays pending in SQLite and is
            // claimed when the session is next opened.
            return;
        };
        {
            let guard = session.lock().await;
            if guard.archived_at.is_some() {
                return;
            }
            let has_active_turn = guard.active_turn_emitter.is_some();
            drop(guard);
            // A live Turn accepts steer events through the two-phase
            // admission; everything that cannot be admitted stays pending
            // for the queue lane.
            if has_active_turn {
                self.try_steer_inbox_events(&session, session_id, revision)
                    .await;
            }
        }
        let mut guard = session.lock().await;
        // Share the worker creation path with user enqueue so a wake can
        // start a worker for an idle session.
        if guard.agent_tx.is_none() || guard.agent_tx.as_ref().is_some_and(|tx| tx.is_closed()) {
            guard.agent_tx = None;
            guard.worker_generation = guard.worker_generation.wrapping_add(1);
            let generation = guard.worker_generation;
            let (tx, rx) = mpsc::channel(SESSION_QUEUE_CAPACITY);
            guard.agent_tx = Some(tx);
            drop(guard);
            spawn_agent_worker(
                rx,
                session.clone(),
                self.worker_deps(),
                generation,
                session_id.to_string(),
            );
            let _ = session.lock().await.agent_inbox_wake.send_replace(revision);
            return;
        }
        let _ = guard.agent_inbox_wake.send_replace(revision);
    }
}

impl SessionManager {
    /// Two-phase steer admission (design §15.3): claim `pending → leased`
    /// without the Session lock, reserve agent-lane capacity in the active
    /// Turn mailbox, persist `leased → admitted` with the Turn id, and only
    /// then activate the reservation into the drainable queue.  Any failure
    /// releases the lease so the event degrades reliably to the queue lane;
    /// a lost release is covered by lease expiry.
    async fn try_steer_inbox_events(
        &self,
        session: &Arc<Mutex<Session>>,
        session_id: &str,
        revision: i64,
    ) -> SteerAdmission {
        let storage = { session.lock().await.storage.clone() };
        let Some(storage) = storage else {
            return SteerAdmission::NothingClaimed;
        };
        let now = chrono::Utc::now().timestamp_millis();
        let Ok(Some(lease)) = crate::storage::Storage::claim_inbox_batch(
            &storage,
            session_id,
            now,
            60_000,
            8,
            32 * 1024,
            Some(crate::storage::agent_inbox::AgentEventDelivery::Steer),
        )
        .await
        else {
            return SteerAdmission::NothingClaimed;
        };
        let token = lease.token.clone();

        // Step 2: reserve under the Session lock for the current accepting
        // Turn.  A closed/full mailbox rejects the whole batch; the events
        // stay pending for the queue lane.
        let (turn_id, mailbox) = {
            let guard = session.lock().await;
            let Some(active) = guard.active_turn_emitter.as_ref() else {
                drop(guard);
                let leases: Vec<_> = lease
                    .events
                    .iter()
                    .map(|event| (event.id.clone(), token.clone()))
                    .collect();
                release_steer_leases(&storage, leases).await;
                let _ = session.lock().await.agent_inbox_wake.send_replace(revision);
                return SteerAdmission::ReleasedToQueue;
            };
            (active.turn_id.clone(), active.steering.clone())
        };
        let mut rejected = Vec::new();
        let mut reserved = Vec::new();
        for event in &lease.events {
            let input = steer_input_from_event(event, now);
            match mailbox.try_reserve_steer(input, token.clone()) {
                Ok(()) => {
                    reserved.push(event.id.clone());
                }
                Err(_) => rejected.push((event.id.clone(), token.clone())),
            }
        }
        if !reserved.is_empty() {
            release_steer_leases(&storage, rejected).await;
            rejected = Vec::new();
            // Step 3: durable admission without the Session lock.
            let mut admitted = Vec::new();
            for event_id in &reserved {
                if storage
                    .admit_inbox_event(event_id, &token, &turn_id, now)
                    .await
                    .unwrap_or(false)
                {
                    admitted.push((event_id.clone(), token.clone()));
                } else {
                    rejected.push((event_id.clone(), token.clone()));
                }
            }
            // Step 4: activate only when the same Turn/generation is still
            // accepting; otherwise release the admitted events back to
            // `pending` and wake the queue lane.
            let still_active = {
                let guard = session.lock().await;
                guard
                    .active_turn_emitter
                    .as_ref()
                    .is_some_and(|active| active.turn_id == turn_id && !active.steering.is_closed())
            };
            if still_active {
                mailbox.activate_reserved();
                return SteerAdmission::Activated;
            }
            rejected.extend(admitted);
            release_steer_leases(&storage, rejected).await;
            let _ = session.lock().await.agent_inbox_wake.send_replace(revision);
            SteerAdmission::ReleasedToQueue
        } else {
            release_steer_leases(&storage, rejected).await;
            SteerAdmission::ReleasedToQueue
        }
    }
}

impl SessionManager {
    pub async fn clear_session_history(
        &self,
        unified_id: &UnifiedSessionId,
    ) -> Result<(), AgentError> {
        let session = self.get_or_create_session(unified_id).await?;
        let persistence_lock = { session.lock().await.persistence_lock.clone() };
        let _persistence_guard = persistence_lock.lock().await;
        let (storage, session_id) = {
            let session_guard = session.lock().await;
            (session_guard.storage.clone(), session_guard.id.to_string())
        };
        if let Some(storage) = storage.as_ref() {
            storage
                .clear_messages(&session_id)
                .await
                .map_err(|e| AgentError::Other(format!("failed to clear messages: {e}")))?;
        }
        let meta_snapshot = {
            let mut session_guard = session.lock().await;
            session_guard.messages.clear();
            session_guard.message_seqs.clear();
            session_guard.seq_counter = 1;
            session_guard.total_message_count = 0;
            session_guard.message_count = 0;
            session_guard.last_consolidated_at = None;
            session_guard.last_compressed_message_at = None;
            session_guard.active_context_checkpoint = None;
            session_guard.context_generation = session_guard.context_generation.saturating_add(1);
            session_guard.context_usage.invalidate();
            session_guard.state_version = session_guard.state_version.wrapping_add(1);
            session_guard.session_meta_snapshot()
        };
        if let Some((storage, meta)) = meta_snapshot {
            storage.upsert_session(&meta).await.map_err(|e| {
                AgentError::Other(format!("failed to persist cleared session: {}", e))
            })?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod slash_command_tests {
    use super::{
        AgentTask, SLASH_COMMANDS, pop_lowest_sequence, prepend_pending_steering,
        resolve_slash_command,
    };
    use crate::agent::steering::{SteeringPushError, TurnInput, TurnMailbox};
    use crate::bus::{ChannelContext, ChatMessage, MediaItem};
    use std::collections::{HashMap, VecDeque};
    use std::sync::{Arc, Mutex};

    #[test]
    fn aliases_resolve_to_their_canonical_command() {
        assert_eq!(
            resolve_slash_command("?").map(|command| command.name),
            Some("?")
        );
        assert_eq!(
            resolve_slash_command("help").map(|command| command.name),
            Some("?")
        );
        assert_eq!(
            resolve_slash_command("/help").map(|command| command.name),
            Some("?")
        );
        assert_eq!(
            resolve_slash_command("reload").map(|command| command.name),
            Some("reload")
        );
        assert_eq!(
            resolve_slash_command("health").map(|command| command.name),
            Some("health")
        );
        assert!(resolve_slash_command("unknown").is_none());
    }

    #[test]
    fn queue_is_advertised_and_resolves_to_the_routing_command() {
        assert!(SLASH_COMMANDS.iter().any(|command| command.name == "queue"));
        assert_eq!(
            resolve_slash_command("/queue").map(|command| command.name),
            Some("queue")
        );
    }

    #[test]
    fn steering_capacity_error_is_reliable_fallback_signal() {
        let mailbox = TurnMailbox::with_limits(1, usize::MAX, 8, usize::MAX);
        mailbox
            .try_push_user(TurnInput::user("first", "first", Vec::new(), None, 1_000))
            .unwrap();
        let error = mailbox
            .try_push_user(TurnInput::user("second", "second", Vec::new(), None, 1_000))
            .unwrap_err();
        assert!(matches!(error, SteeringPushError::Full));
        assert_eq!(mailbox.len(), 1);
    }

    #[test]
    fn terminal_pending_steering_keeps_original_route_and_media() {
        let mailbox = TurnMailbox::new_shared();
        let recovery = Arc::new(Mutex::new(HashMap::new()));
        let mut message = ChatMessage::user("inspect the upload");
        message.id = "client-steer-1".to_string();
        message.media_refs.push(crate::bus::MediaRef {
            path: "/tmp/upload.bin".to_string(),
            media_type: "file".to_string(),
        });
        let original = AgentTask {
            channel: "feishu".to_string(),
            sender_id: "user-7".to_string(),
            chat_id: "chat-9".to_string(),
            sequence: 7,
            client_message_id: Some(message.id.clone()),
            content: message.content.clone(),
            received_at: message.timestamp,
            media: vec![MediaItem::new("/tmp/upload.bin", "file")],
            channel_context: ChannelContext {
                reply_to: Some("reply-1".to_string()),
                private: HashMap::from([(String::from("thread"), String::from("root-1"))]),
                durable_private: HashMap::new(),
            },
        };
        recovery
            .lock()
            .unwrap()
            .insert(message.id.clone(), original);
        mailbox
            .try_push_user(TurnInput::user(
                message.id.clone(),
                message.content.clone(),
                message.media_refs.clone(),
                message.source.clone(),
                message.timestamp,
            ))
            .unwrap();

        let mut local_tasks = VecDeque::new();
        prepend_pending_steering(
            &mailbox,
            &recovery,
            &mut local_tasks,
            "cli",
            "fallback-chat",
            &None,
            &HashMap::new(),
        );
        let recovered = local_tasks.pop_front().unwrap();
        assert_eq!(recovered.channel, "feishu");
        assert_eq!(recovered.sender_id, "user-7");
        assert_eq!(recovered.chat_id, "chat-9");
        assert_eq!(
            recovered.channel_context.reply_to.as_deref(),
            Some("reply-1")
        );
        assert_eq!(
            recovered.channel_context.private.get("thread").unwrap(),
            "root-1"
        );
        assert_eq!(recovered.media[0].path, "/tmp/upload.bin");
        assert!(mailbox.is_closed());
        assert!(recovery.lock().unwrap().is_empty());
    }

    #[test]
    fn pending_and_channel_tasks_are_selected_by_admission_sequence() {
        let mailbox = TurnMailbox::new_shared();
        let recovery = Arc::new(Mutex::new(HashMap::new()));
        let mut pending = ChatMessage::user("pending");
        pending.id = "pending-id".to_string();
        let template = AgentTask {
            channel: "cli".to_string(),
            sender_id: "user".to_string(),
            chat_id: "chat".to_string(),
            sequence: 10,
            client_message_id: Some(pending.id.clone()),
            content: pending.content.clone(),
            received_at: pending.timestamp,
            media: Vec::new(),
            channel_context: ChannelContext::default(),
        };
        recovery
            .lock()
            .unwrap()
            .insert(pending.id.clone(), template.clone());
        mailbox
            .try_push_user(TurnInput::user(
                pending.id.clone(),
                pending.content.clone(),
                Vec::new(),
                None,
                pending.timestamp,
            ))
            .unwrap();

        let mut local_tasks = VecDeque::from([
            AgentTask {
                sequence: 30,
                ..template.clone()
            },
            AgentTask {
                sequence: 20,
                ..template
            },
        ]);
        prepend_pending_steering(
            &mailbox,
            &recovery,
            &mut local_tasks,
            "cli",
            "chat",
            &None,
            &HashMap::new(),
        );
        assert_eq!(pop_lowest_sequence(&mut local_tasks).unwrap().sequence, 10);
        assert_eq!(pop_lowest_sequence(&mut local_tasks).unwrap().sequence, 20);
        assert_eq!(pop_lowest_sequence(&mut local_tasks).unwrap().sequence, 30);
    }

    #[test]
    fn stop_style_close_discards_pending_steering() {
        let mailbox = TurnMailbox::new_shared();
        mailbox
            .try_push_user(TurnInput::user("d", "discard me", Vec::new(), None, 1_000))
            .unwrap();
        assert_eq!(mailbox.close_and_take_pending().len(), 1);
        assert!(mailbox.is_closed());
        assert!(mailbox.take_pending().is_empty());
    }
}
