Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ grafeo-core = { version = "=0.5.42", default-features = false, features = ["lpg"
grafeo-engine = { version = "=0.5.42", default-features = false, features = ["lpg", "gql", "algos", "wal", "grafeo-file", "vector-index"] }
grafeo-storage = { version = "=0.5.42", default-features = false, features = ["wal"] }
rusqlite = { version = "0.40.1", default-features = false, features = ["backup", "cache"] }
# Pinned once: the MCP git handlers and the root's edit/lifecycle readers must
# resolve the same object database and status implementation, so a feature
# added for one is never missing for the other.
gix = { version = "=0.86.0", default-features = false, features = ["revision", "blob-diff", "parallel", "sha1", "sha256", "status"] }
tracedecay-semantic-contracts = { path = "crates/tracedecay-semantic-contracts", version = "0.1.0" }

[profile.bench]
Expand Down
9 changes: 8 additions & 1 deletion crates/tracedecay-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ source-analysis = [
]

[dependencies]
gix.workspace = true
glob = "0.3"
hotpath.workspace = true
ignore = { version = "0.4", optional = true }
Expand All @@ -50,15 +51,21 @@ tracedecay-application = { path = "../tracedecay-application", version = "0.1.0"
tracedecay-contracts = { path = "../tracedecay-contracts", version = "0.1.0" }
tracedecay-code-extraction = { path = "../tracedecay-code-extraction", version = "0.1.0", default-features = false }
tracedecay-code-index = { path = "../tracedecay-code-index", version = "0.1.0", default-features = false, features = ["lite"] }
tracedecay-code-index-runtime = { path = "../tracedecay-code-index-runtime", version = "0.1.0" }
tracedecay-daemon-protocol = { path = "../tracedecay-daemon-protocol", version = "0.1.0" }
tracedecay-domain = { path = "../tracedecay-domain", version = "0.1.0" }
tracedecay-framing = { path = "../tracedecay-framing", version = "0.1.0" }
tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0" }
tracedecay-graph-query = { path = "../tracedecay-graph-query", version = "0.1.0" }
tracedecay-hooks = { path = "../tracedecay-hooks", version = "0.1.0" }
tracedecay-privacy = { path = "../tracedecay-privacy", version = "0.1.0" }
tracedecay-query = { path = "../tracedecay-query", version = "0.1.0" }
tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0" }
tracedecay-session-memory = { path = "../tracedecay-session-memory", version = "0.1.0" }
tracedecay-session-temporal-store = { path = "../tracedecay-session-temporal-store", version = "0.1.0" }
tracedecay-sessions = { path = "../tracedecay-sessions", version = "0.1.0" }
tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" }
tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = "0.1.0" }
tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1.0" }
tracing = "0.1"
tree-sitter = { version = "0.26", optional = true }
Expand All @@ -70,5 +77,5 @@ tokio = { version = "1", features = ["full", "test-util"] }
tracedecay-daemon-protocol = { path = "../tracedecay-daemon-protocol", version = "0.1.0", features = ["test-helpers"] }
tracedecay-framing = { path = "../tracedecay-framing", version = "0.1.0", features = ["test-helpers"] }
tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0", features = ["test-helpers"] }
tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0", features = ["test-helpers"] }
tracedecay-sessions = { path = "../tracedecay-sessions", version = "0.1.0", features = ["test-helpers"] }
tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" }
74 changes: 74 additions & 0 deletions crates/tracedecay-mcp/src/broker_stream_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ use tracedecay_framing::{
};
use tracedecay_session_memory::context::CancellationToken;

use crate::lifecycle::ProjectServerResponseLifecycle;
use crate::server::{RmcpSelectedProjectResponseAuthority, RmcpWorkDeliverySettlement};
use tracedecay_domain::errors::TraceDecayError;

use crate::{ErrorCode, JsonRpcDecodeError, JsonRpcResponse, McpTransport};

/// Response-revocation authority retained for one selected project server.
Expand Down Expand Up @@ -51,6 +55,47 @@ pub trait BrokerWorkDeliverySettlement: Send + Sync {
);
}

impl BrokerResponseLifecycle for ProjectServerResponseLifecycle {
fn response_revoked(&self) -> &CancellationToken {
ProjectServerResponseLifecycle::response_revoked(self)
}
}

impl<L: BrokerSelectedResponseLease + 'static> BrokerSelectedResponseAuthority
for RmcpSelectedProjectResponseAuthority<L>
{
fn take_response(
&self,
id: Option<&serde_json::Value>,
) -> std::io::Result<Option<Box<dyn BrokerSelectedResponseLease>>> {
self.take(id)
.map(|lease| lease.map(|lease| Box::new(lease) as Box<dyn BrokerSelectedResponseLease>))
.map_err(selected_response_io_error)
}
}

fn selected_response_io_error(error: TraceDecayError) -> std::io::Error {
std::io::Error::other(error)
}

impl BrokerWorkDeliverySettlement for RmcpWorkDeliverySettlement {
fn attempt_for_request(
&self,
request: &serde_json::Value,
) -> Option<tracedecay_domain::DeliverySettlementAttemptV1> {
RmcpWorkDeliverySettlement::attempt_for_request(self, request)
}

fn settle(
&self,
attempt: tracedecay_domain::DeliverySettlementAttemptV1,
outcome: tracedecay_domain::DeliverySettlementOutcomeV1,
drop_reason: Option<tracedecay_domain::DeliveryDropReasonV1>,
) {
RmcpWorkDeliverySettlement::settle(self, attempt, outcome, drop_reason);
}
}

pub struct BrokerStreamTransport {
// Every daemon read of this transport races something else in a
// `tokio::select!` — draining, an owner open, a completed handler. The
Expand Down Expand Up @@ -549,3 +594,32 @@ impl rmcp::transport::Transport<rmcp::RoleServer> for BrokerStreamTransport {
Ok(())
}
}

#[cfg(test)]
mod selected_response_error_tests {
use super::*;

#[test]
fn io_boundary_retains_typed_project_route_classification() {
let error = TraceDecayError::project_route(
"project_route_unavailable",
true,
"selected response authority is warming",
);

let error = selected_response_io_error(error);
let source = error
.get_ref()
.and_then(|source| source.downcast_ref::<TraceDecayError>())
.expect("I/O error must retain the typed TraceDecay source");

assert_eq!(
source.project_route_context(),
Some((
"project_route_unavailable",
true,
"selected response authority is warming",
))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,35 @@ use tracedecay_contracts::retrieval::{

use tracedecay_domain::errors::{Result, TraceDecayError};
use tracedecay_graph_query::VerifiedGraphQuery;
use tracedecay_mcp::tools::render::{self, Md};

pub(super) fn should_check_external_import_hint(result_count: usize, limit: usize) -> bool {
use crate::tool_context::McpToolContext;
use crate::tools::render::{self, Md};

pub fn should_check_external_import_hint(result_count: usize, limit: usize) -> bool {
result_count == 0 || result_count < limit.clamp(1, 20)
}

pub(super) fn lazy_indexing_requested(args: &Value) -> bool {
pub fn lazy_indexing_requested(args: &Value) -> bool {
args.get("lazy_index_ignored_dependencies")
.and_then(Value::as_bool)
.unwrap_or(false)
}

#[hotpath::measure(future = true, label = "mcp.search.import_hint.total")]
pub(super) async fn external_import_hint(
/// Advisory external-import evidence for a sparse search.
///
/// The bound context carries the graph's admitted scope plus the caller's
/// deadline and cancellation, so this read cannot outlive the request or reach
/// a graph admitted for another checkout.
#[hotpath::measure(label = "mcp.search.import_hint.total")]
pub fn external_import_hint(
ctx: &McpToolContext<'_>,
graph: &VerifiedGraphQuery,
query: &str,
limit: usize,
scope_prefix: Option<&str>,
deadline: Option<&tracedecay_contracts::Deadline>,
cancellation: Option<&tracedecay_contracts::CancellationSignal>,
) -> Result<Option<Value>> {
let candidates = hotpath::measure_block!("mcp.search.import_hint.scan", {
ignored_dependency_candidates(graph, query, limit, scope_prefix, deadline, cancellation)?
ignored_dependency_candidates(ctx, graph, query, limit, scope_prefix)?
});
if candidates.is_empty() {
return Ok(None);
Expand All @@ -51,7 +57,7 @@ pub(super) async fn external_import_hint(
})))
}

pub(super) fn unavailable_evidence(error: &TraceDecayError) -> PrimitiveUnavailableEvidenceV1 {
pub fn unavailable_evidence(error: &TraceDecayError) -> PrimitiveUnavailableEvidenceV1 {
let (reason_code, retryable, detail) =
if let Some((reason_code, retryable, detail)) = error.project_route_context() {
(reason_code, retryable, detail.to_owned())
Expand All @@ -72,21 +78,19 @@ pub(super) fn unavailable_evidence(error: &TraceDecayError) -> PrimitiveUnavaila
}
}

pub(super) fn unavailable_hint(error: &TraceDecayError) -> Value {
pub fn unavailable_hint(error: &TraceDecayError) -> Value {
json!(unavailable_evidence(error))
}

#[hotpath::measure(label = "mcp.search.import_admit.total")]
pub(super) async fn admit_verified_ignored_dependency(
pub async fn admit_verified_ignored_dependency(
ctx: &McpToolContext<'_>,
admission: Option<&dyn CodeIndexIgnoredDependencyAdmissionPortV1>,
graph: &VerifiedGraphQuery,
query: &str,
scope_prefix: Option<&str>,
deadline: Option<&tracedecay_contracts::Deadline>,
cancellation: Option<&tracedecay_contracts::CancellationSignal>,
) -> Result<()> {
let candidates =
ignored_dependency_candidates(graph, query, 1, scope_prefix, deadline, cancellation)?;
let candidates = ignored_dependency_candidates(ctx, graph, query, 1, scope_prefix)?;
let Some(import) = candidates.first() else {
return Ok(());
};
Expand Down Expand Up @@ -158,13 +162,15 @@ pub(super) async fn admit_verified_ignored_dependency(
}

fn ignored_dependency_candidates(
ctx: &McpToolContext<'_>,
graph: &VerifiedGraphQuery,
query: &str,
limit: usize,
scope_prefix: Option<&str>,
deadline: Option<&tracedecay_contracts::Deadline>,
cancellation: Option<&tracedecay_contracts::CancellationSignal>,
) -> Result<Vec<CodeIndexImportEvidenceV1>> {
ctx.verify_graph_scope(graph)?;
let cancellation = ctx.cancellation();
let deadline = ctx.deadline();
if cancellation.is_some_and(tracedecay_contracts::CancellationSignal::is_cancelled) {
return Err(TraceDecayError::project_route(
"code-graph-cancelled",
Expand All @@ -190,7 +196,7 @@ fn generation_advanced() -> TraceDecayError {
)
}

pub(super) fn append_external_import_hint_md(md: &mut Md, value: &Value) {
pub fn append_external_import_hint_md(md: &mut Md, value: &Value) {
let Some(hint) = value.get("external_import_hint") else {
return;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,12 @@ pub(crate) async fn collect_affected_test_files<D: AffectedTestDependents + ?Siz
}

#[hotpath::measure(future = true, label = "mcp.git.affected.total")]
pub(crate) async fn handle_affected(
cg: &TraceDecay,
pub async fn handle_affected(
ctx: &McpToolContext<'_>,
graph: &tracedecay_graph_query::VerifiedGraphQuery,
args: Value,
) -> Result<ToolResult> {
ctx.verify_graph_scope(graph)?;
let files = require_string_array_arg(&args, "files")?;
let max_depth = clamped_depth_arg(&args, "depth", 5, 10);

Expand Down Expand Up @@ -173,7 +174,7 @@ pub(crate) async fn handle_affected(
);

Ok(generic_tool_result(
Some(cg.project_root()),
Some(ctx.project_root()),
&args,
&output,
touched_files,
Expand Down Expand Up @@ -245,7 +246,7 @@ mod tests {
let mut visited = HashSet::new();
let mut queue = std::collections::VecDeque::new();
for file in files {
if crate::tracedecay::is_test_file(file) {
if tracedecay_code_index::is_test_file(file) {
affected.insert(file.clone());
}
if visited.insert(file.clone()) {
Expand All @@ -260,7 +261,7 @@ mod tests {
if !visited.insert(dependent.clone()) {
continue;
}
if crate::tracedecay::is_test_file(dependent) {
if tracedecay_code_index::is_test_file(dependent) {
affected.insert(dependent.clone());
} else {
queue.push_back((dependent.clone(), depth + 1));
Expand Down
Loading
Loading