lean-ctx-server 3.12.1

The LeanCTX MCP server and its tools on top of LeanCTX | OSS.
Documentation
// SPDX-License-Identifier: Apache-2.0

//! Post-dispatch persistence: checkpoints, context IR and the ledger.

#[allow(clippy::wildcard_imports)]
use super::*;

pub(in crate::server) async fn record_checkpoint(
    server: &LeanCtxServer,
    intent: Option<McpCheckpointIntent>,
    result: &mut CallToolResult,
) -> lean_ctx::core::execution_lifecycle::StageDisposition {
    let Some(intent) = intent else {
        return lean_ctx::core::execution_lifecycle::StageDisposition::Skipped(
            "terminal result has no checkpoint cadence",
        );
    };
    if !intent.enabled {
        return lean_ctx::core::execution_lifecycle::StageDisposition::Skipped(
            "checkpoint disabled for tool",
        );
    }
    if !server.increment_and_check() {
        return lean_ctx::core::execution_lifecycle::StageDisposition::Skipped(
            "checkpoint not due",
        );
    }
    let Some(checkpoint) = server.auto_checkpoint().await else {
        return lean_ctx::core::execution_lifecycle::StageDisposition::Skipped(
            "checkpoint unavailable",
        );
    };
    if intent.output_visible && lean_ctx::core::protocol::meta_visible() {
        if let Ok(checkpoint) = policy_guard::protect_result("ctx_session", &checkpoint) {
            result.content.push(ContentBlock::text(format!(
                "--- AUTO CHECKPOINT ---\n{checkpoint}"
            )));
        }
    }
    lean_ctx::core::execution_lifecycle::StageDisposition::Applied
}

pub(in crate::server) async fn record_context_ir(
    server: &LeanCtxServer,
    processed: &McpProcessed,
) -> lean_ctx::core::execution_lifecycle::StageDisposition {
    let (Some(ir), Some(intent)) = (&server.context_ir, processed.ir.as_ref()) else {
        return lean_ctx::core::execution_lifecycle::StageDisposition::Skipped(
            "Context IR unavailable for terminal or binary result",
        );
    };
    let input = lean_ctx::core::context_ir::RecordIrInput {
        kind: intent.kind.clone(),
        tool: &intent.tool,
        client_name: None,
        agent_id: None,
        path: intent.path.as_deref(),
        command: intent.command.as_deref(),
        pattern: intent.pattern.as_deref(),
        input_tokens: intent.input_tokens,
        output_tokens: intent.output_tokens,
        duration: intent.duration,
        content_excerpt: &intent.content_excerpt,
    };
    ir.write().await.record(input);
    lean_ctx::core::execution_lifecycle::StageDisposition::Applied
}

pub(in crate::server) async fn record_ledger(
    server: &LeanCtxServer,
    processed: &McpProcessed,
) -> Result<lean_ctx::core::execution_lifecycle::StageDisposition, ErrorData> {
    let mut applied = false;
    if let Some(intent) = processed.ledger.as_ref() {
        applied = true;
        let result = std::panic::AssertUnwindSafe(async {
            let active_task = {
                let session = server.session.read().await;
                session.task.as_ref().map(|task| task.description.clone())
            };
            let mut ledger = server.ledger.write().await;
            let overlay = lean_ctx::core::context_overlay::OverlayStore::load_project(
                &std::path::PathBuf::from(intent.project_root.as_deref().unwrap_or(".")),
            );
            let gate_result = context_gate::post_dispatch_record_with_task(
                &intent.read_path,
                &intent.mode_used,
                intent.output_tokens,
                intent.sent_tokens,
                &mut ledger,
                &overlay,
                active_task.as_deref(),
                intent.project_root.as_deref(),
            );
            let entry = ledger.read_entry(&intent.read_path);
            drop(ledger);
            lean_ctx::core::context_ledger::ContextLedger::persist_read(entry).await?;
            if let Some(hint) = &gate_result.eviction_hint {
                tracing::debug!("deferred eviction hint: {hint}");
            }
            if intent.wants_elicitation
                && let Some(hint) = &gate_result.elicitation_hint
            {
                tracing::debug!("deferred elicitation hint: {hint}");
            }
            if gate_result.resource_changed
                && let Some(peer) = server.peer.read().await.as_ref()
            {
                notifications::send_resource_updated(peer, notifications::RESOURCE_URI_SUMMARY)
                    .await;
            }
            Ok::<(), String>(())
        })
        .catch_unwind()
        .await;
        result
            .map_err(|_| ErrorData::internal_error("ledger write panicked", None))?
            .map_err(|error| ErrorData::internal_error(error, None))?;
    }
    if let Some(intent) = processed.receipt.as_ref() {
        applied = true;
        server
            .record_receipt_and_cost(
                &intent.name,
                intent.args.as_ref(),
                intent.action.as_deref(),
                &intent.result_text,
                intent.output_token_count,
            )
            .await;
    }
    if applied {
        Ok(lean_ctx::core::execution_lifecycle::StageDisposition::Applied)
    } else {
        Ok(
            lean_ctx::core::execution_lifecycle::StageDisposition::Skipped(
                "no ledger or receipt intent",
            ),
        )
    }
}