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
Original file line number Diff line number Diff line change
Expand Up @@ -1566,8 +1566,8 @@ fn a_replacement_daemon_waits_for_its_predecessors_authority_lock() {
refusal.downcast_ref::<String>().map(String::as_str),
Some(
format!(
"refusing to replace a live test daemon at {} or its authority lock",
common::daemon_socket_path(&home_path).display()
"a stopped test daemon still owns an endpoint or authority lock in {}",
home_path.join(".tracedecay").display()
)
.as_str()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ use std::process::Output;

use tracedecay_agent_hosts::agents::host_bundle::{HostComponentV1, HostKindV1};
#[cfg(unix)]
use tracedecay_daemon_identity::current_daemon_connection;
#[cfg(unix)]
use tracedecay_daemon_protocol::DaemonEndpoint;
#[cfg(target_os = "linux")]
use tracedecay_daemon_protocol::unix_socket_path_within_limit;
#[cfg(unix)]
use tracedecay_runtime_core::test_executable::write_executable_script;

#[cfg(unix)]
Expand Down Expand Up @@ -387,13 +393,21 @@ impl ProfileDaemon {
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
let daemon = Self(command.spawn().unwrap());
let socket = cli.profile.join("daemon.sock");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while std::os::unix::net::UnixStream::connect(&socket).is_err() {
// Deep worker paths can move the socket outside the profile. Follow
// the same profile-owned authority that production clients discover.
while !current_daemon_connection(&cli.profile).is_ok_and(|connection| {
match connection.endpoint() {
DaemonEndpoint::Unix(socket) => {
std::os::unix::net::UnixStream::connect(socket).is_ok()
}
DaemonEndpoint::Loopback(address) => std::net::TcpStream::connect(address).is_ok(),
}
}) {
assert!(
std::time::Instant::now() < deadline,
"the profile daemon never listened at {}",
socket.display()
"the registered daemon never listened for profile {}",
cli.profile.display()
);
std::thread::sleep(std::time::Duration::from_millis(25));
}
Expand Down Expand Up @@ -864,7 +878,12 @@ fn doctor_observes_copilots_vscode_settings_without_failing_on_them() {
#[cfg(target_os = "linux")]
#[test]
fn doctor_warns_on_detected_hosts_without_a_tracedecay_integration() {
let cli = IsolatedCli::new();
let mut cli = IsolatedCli::new();
cli.profile = cli.home.path().join("profile".repeat(20));
assert!(
!unix_socket_path_within_limit(&cli.profile.join("daemon.sock")),
"this journey must exercise production's short endpoint for a deep profile"
);
install_cline(&cli);
fs::create_dir_all(cli.home.path().join(".config/zed")).unwrap();
fs::create_dir_all(cli.home.path().join(".gemini/antigravity")).unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ async fn shutdown_joins_a_worker_suppressing_a_terminal_publication_park() {
let fixture = GitFixture::new(ALPHA_LIB_V1);
let store = TempDir::new().expect("store root");
let registry = mounted_registry(&fixture, &store).await;
// Seating precedes the worker tail that can replace its park state.
super::settle_text_projection(&registry, fixture.path()).await;
assert!(
registry
.plant_terminal_publication_authority_park_for_test(
Expand Down
49 changes: 36 additions & 13 deletions crates/tracedecay-mcp/src/server/read_coalescing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use serde_json::Value;
use tracedecay_domain::canonical_text::canonical_framed_sha256_bytes;
use tracedecay_tool_catalog::ApplicationSurfaceOperation;

use crate::ToolResult;
use crate::{ToolResult, tool_result_has_semantic_error};
use tracedecay_runtime_core::weak_registry::WeakRegistry;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
Expand Down Expand Up @@ -152,6 +152,11 @@ impl ReadFlight {

impl ReadFlightLeader {
pub fn complete(mut self, result: ToolResult) -> ToolResult {
// Refusals carry the caller's identity and invocation controls. Dropping
// this unfinished flight wakes followers to run their own dispatch.
if tool_result_has_semantic_error(&result) {
return result;
}
self.finished = true;
self.remove_registration();
// Deregistration closes this flight to new followers (a later
Expand Down Expand Up @@ -199,11 +204,17 @@ pub fn tool_allows_identical_read_coalescing(
tool_name: &str,
tool_is_read_only: impl FnOnce(&str) -> bool,
) -> bool {
// Every owner side effect runs per call; identical calls never share one.
if ApplicationSurfaceOperation::from_tool_name(tool_name)
.and_then(ApplicationSurfaceOperation::owner_side_effect)
.is_some()
{
// The key identifies graph data, not an application invocation's authority
// or cancellation. Every non-graph call must reach its own executor.
let Some(operation) = ApplicationSurfaceOperation::from_tool_name(tool_name) else {
return false;
};
if !operation.is_graph_tool() || operation.owner_side_effect().is_some() {
return false;
}
// A view records usage under this caller's request identity before reading
// the resulting summary, so every caller must execute the handler.
if operation == ApplicationSurfaceOperation::SkillView {
return false;
}
if matches!(
Expand Down Expand Up @@ -233,7 +244,14 @@ fn read_flight_key(
arguments: &Value,
scope_prefix: Option<&str>,
) -> ReadFlightKey {
let arguments = serde_json::to_vec(arguments).unwrap_or_default();
// Routing assigns correlation identities per connection/request. They
// control cancellation, not eligible reads' inputs. Only successful reads
// are shared; a refusal makes every caller dispatch with its own identity.
let mut arguments = arguments.clone();
if let Some(arguments) = arguments.as_object_mut() {
arguments.remove("__mcp_request_id");
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
let arguments = serde_json::to_vec(&arguments).unwrap_or_default();
let scope_present: &[u8] = if scope_prefix.is_some() { b"1" } else { b"0" };
let scope = scope_prefix.unwrap_or_default();
ReadFlightKey(canonical_framed_sha256_bytes(
Expand All @@ -259,17 +277,17 @@ mod tests {
let coalescer = IdenticalReadCoalescer::default();
let leader = match coalescer.claim(
"graph-main",
"tracedecay_search",
&json!({"query": "DaemonEngine"}),
"tracedecay_files",
&json!({"path": "src", "__mcp_request_id": "connection-a:1"}),
None,
) {
ReadFlightClaim::Leader(leader) => leader,
ReadFlightClaim::Follower(_) => panic!("first caller must lead"),
};
let follower = match coalescer.claim(
"graph-main",
"tracedecay_search",
&json!({"query": "DaemonEngine"}),
"tracedecay_files",
&json!({"path": "src", "__mcp_request_id": "connection-b:1"}),
None,
) {
ReadFlightClaim::Follower(follower) => follower,
Expand Down Expand Up @@ -421,14 +439,19 @@ mod tests {
"tracedecay_dependency_depth",
"tracedecay_health",
"tracedecay_dsm",
"tracedecay_configuration_get",
"tracedecay_project_list",
"tracedecay_fact_store_search",
"tracedecay_source_outline",
"tracedecay_skill_view",
] {
assert!(
!tool_allows_identical_read_coalescing(controlled_read, |_| true),
"{controlled_read} has caller-specific cancellation and deadline controls"
"{controlled_read} must execute with each caller's own identity and controls"
);
}
assert!(tool_allows_identical_read_coalescing(
"tracedecay_source_outline",
"tracedecay_files",
|_| true,
));
assert!(!tool_allows_identical_read_coalescing(
Expand Down
52 changes: 24 additions & 28 deletions crates/tracedecay/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,29 @@ fn daemon_authority_lock_held(profile_root: &Path) -> bool {
}
}

/// Waits for a stopped daemon's inherited endpoint and authority handles to
/// close before a fixture starts a replacement or opens the store itself.
/// Reaping the leader does not reap descendants that still hold these handles.
pub fn wait_for_stopped_daemon(home: &Path) {
let profile_root = canonical_existing_path(home).join(".tracedecay");
poll_until(
Instant::now() + PREDECESSOR_DAEMON_VACATE_TIMEOUT,
Duration::from_millis(25),
|| {
let live = daemon_is_connectable(home);
#[cfg(unix)]
let live = live || daemon_authority_lock_held(&profile_root);
(!live).then_some(())
},
|| {
format!(
"a stopped test daemon still owns an endpoint or authority lock in {}",
profile_root.display()
)
},
);
}

fn spawn_tracedecay_daemon_process(
home: &Path,
binary: &Path,
Expand Down Expand Up @@ -1237,34 +1260,7 @@ fn spawn_tracedecay_daemon_process(
// endpoint go quiet; this wait only covers the kernel's leftover. The
// authority lock has the same tail, and `stop_managed_daemon` unlinks the
// socket, so only the lock shows such a predecessor's child is still alive.
poll_until(
Instant::now() + PREDECESSOR_DAEMON_VACATE_TIMEOUT,
Duration::from_millis(25),
|| {
#[cfg(unix)]
let live = std::os::unix::net::UnixStream::connect(&socket_path).is_ok()
|| daemon_authority_lock_held(&profile_root);
#[cfg(not(unix))]
let live = portable_daemon_connectable();
(!live).then_some(())
},
|| {
#[cfg(unix)]
{
format!(
"refusing to replace a live test daemon at {} or its authority lock",
socket_path.display()
)
}
#[cfg(not(unix))]
{
format!(
"refusing to replace a live test daemon recorded at {}",
authority_path.display()
)
}
},
);
wait_for_stopped_daemon(home);

let mut command = Command::new(binary);
apply_tracedecay_home_env(&mut command, home);
Expand Down
Loading
Loading