Skip to content
Open
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: 5 additions & 1 deletion src/daemon/git_watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,14 @@ impl GitWatcher {
None => crate::global_db::GlobalDb::open().await,
};
if let Some(db) = db {
let open_options = daemon_open_options(&self.inner);
let projects = db.code_projects_seen_within(window, cap).await;
for record in projects {
let root = PathBuf::from(&record.canonical_root);
if root.is_dir() {
if root.is_dir()
&& root.join(".git").exists()
&& TraceDecay::has_initialized_store_with_options(&root, &open_options).await
Comment on lines +319 to +321

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the watcher cap after rejecting stale rows

code_projects_seen_within(window, cap) applies its SQL LIMIT before these new validity checks, so recent stale or projectless rows still consume the entire watcher budget. If the newest watch_max_projects rows are invalid, initialized Git projects that are only slightly older are never examined or watched after daemon startup, even though no watcher slots were actually used. Fetch enough candidates to fill the cap after filtering, or move equivalent validity filtering ahead of the limit.

Useful? React with 👍 / 👎.

{
self.ensure_watching(&root).await;
}
}
Expand Down
74 changes: 74 additions & 0 deletions src/daemon/git_watch/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,80 @@ async fn disabled_watcher_never_registers() {
assert!(watcher.health_report().await.is_empty());
}

#[tokio::test]
async fn spawn_skips_recent_registry_rows_without_an_initialized_store() {
let _profile = crate::config::PinnedUserDataDir::new();
let profile_root = crate::storage::default_profile_root().unwrap();
let global_db_path = profile_root.join("global.db");
let global_db = crate::global_db::GlobalDb::open_at(&global_db_path)
.await
.expect("open isolated global registry");

let valid = temp_repo();
crate::storage::write_enrollment_marker(
valid.path(),
&crate::storage::EnrollmentMarker {
project_id: "proj_valid_watch".to_string(),
storage_mode: crate::storage::StorageMode::ProfileSharded,
},
)
.expect("write valid enrollment marker");
let layout = crate::storage::resolve_layout_for_current_profile(valid.path())
.expect("resolve valid project layout");
std::fs::create_dir_all(layout.graph_db_path.parent().unwrap())
.expect("create valid graph directory");
std::fs::write(&layout.graph_db_path, b"").expect("create valid graph marker");
global_db
.upsert_code_project("proj_valid_watch", valid.path(), None, None, Some("main"))
.await
.expect("register valid project");

let invalid = tempfile::tempdir().unwrap();
crate::storage::write_enrollment_marker(
invalid.path(),
&crate::storage::EnrollmentMarker {
project_id: "proj_invalid_watch".to_string(),
storage_mode: crate::storage::StorageMode::ProfileSharded,
},
)
.expect("write stale enrollment marker");
let invalid_layout = crate::storage::resolve_layout_for_current_profile(invalid.path())
.expect("resolve stale project layout");
std::fs::create_dir_all(invalid_layout.graph_db_path.parent().unwrap())
.expect("create stale graph directory");
std::fs::write(&invalid_layout.graph_db_path, b"").expect("create stale graph marker");
global_db
.upsert_code_project(
"proj_invalid_watch",
invalid.path(),
None,
None,
Some("main"),
)
.await
.expect("register stale directory-only project");

let watcher = GitWatcher::new(fast_watch_config());
watcher.spawn(Some(global_db_path)).await;

let watched = watcher
.health_report()
.await
.into_iter()
.map(|(path, _)| path)
.collect::<HashSet<_>>();
assert!(
watched.contains(&valid.path().canonicalize().unwrap()),
"an initialized registered project must still be watched"
);
assert!(
!watched.contains(&invalid.path().canonicalize().unwrap()),
"a stale registry row for an existing non-project directory must not start a watcher"
);

watcher.shutdown().await;
}

#[tokio::test]
async fn shutdown_cancels_and_joins_watcher_tasks() {
let repo = temp_repo();
Expand Down
41 changes: 31 additions & 10 deletions src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,31 @@ fn proxy_serve_handshake(
let path = sanitize_serve_path_arg(path_arg);
let explicit_path = path.is_some();
let mut project_path = if explicit_path {
crate::config::resolve_path(path)
Some(crate::config::resolve_path(path))
} else {
crate::config::resolve_path_with_discovery(None)
original_cwd.and_then(crate::config::discover_project_root)
};
Comment on lines 188 to 190

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain registry-only initialized CWD routing

Using only synchronous filesystem discovery makes an initialized profile-sharded project invisible when its local enrollment marker is missing but its store remains resolvable through the global registry—a supported state handled by discover_project_root_with_identity. For a non-Git project, or a Git project with TRACEDECAY_SYNC_AUTO_INIT=false, auto_init_root cannot restore the path, so a no---path client now sends a projectless handshake and project tools fail unless that MCP host happens to provide usable initialize roots. Previously the CWD fallback was still sent to the daemon, which could resolve the registered store; preserve a daemon-side identity lookup for this case rather than dropping the path outright.

Useful? React with 👍 / 👎.


let initialized = TraceDecay::is_initialized(&project_path);
let auto_init_root = (!initialized && crate::config::load_sync_config(&project_path).auto_init)
.then(|| crate::worktree::git_worktree_root(&project_path))
.flatten();
let initialized = project_path
.as_deref()
.is_some_and(TraceDecay::is_initialized);
let auto_init_candidate = project_path.as_deref().or(original_cwd);
let auto_init_root = auto_init_candidate
.filter(|candidate| !initialized && crate::config::load_sync_config(candidate).auto_init)
.and_then(crate::worktree::git_worktree_root);
Comment on lines +195 to +198

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check auto-init config at the Git root

When discovery-mode serve starts in a subdirectory of an uninitialized Git repository, this filter loads configuration from that subdirectory before git_worktree_root resolves the repository root. A root-level .tracedecay/config.json with sync.auto_init=false is therefore missed, the default true is used, and the daemon is allowed to initialize a repository whose configuration explicitly opts out. Resolve the Git root first and evaluate its sync configuration before setting allow_init.

Useful? React with 👍 / 👎.

if let Some(root) = auto_init_root.as_ref() {
project_path.clone_from(root);
project_path = Some(root.clone());
}

let scope_prefix = serve_scope_prefix(original_cwd, &project_path);
let telemetry_timings = timings || crate::config::load_telemetry_config(&project_path).timings;
let scope_prefix = project_path
.as_deref()
.and_then(|project_path| serve_scope_prefix(original_cwd, project_path));
let telemetry_timings = timings
|| project_path
.as_deref()
.is_some_and(|path| crate::config::load_telemetry_config(path).timings);
Comment on lines +206 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve timings after initialize-root routing

For a projectless CWD that is later routed to a real project through MCP initialize.roots, this expression permanently sets the handshake's timings flag to false unless --timings was passed. apply_daemon_initialize_route updates only the project path and allow_init, while run_daemon_connection_with_timings passes this false value as an override; consequently the routed project's telemetry.timings setting—including its default true—can never enable response timing annotations for this flow. The routed project configuration needs to be applied after initialize-root resolution rather than treating the initially absent path as an opt-out.

Useful? React with 👍 / 👎.

let mut handshake = crate::daemon::DaemonHandshake::for_current_client(
Some(project_path),
project_path,
scope_prefix,
telemetry_timings,
auto_init_root.is_some(),
Expand Down Expand Up @@ -234,6 +242,19 @@ pub const DEGRADED_SERVE_STDERR_MARKER: &str =
mod tests {
use super::*;

#[test]
fn projectless_cwd_does_not_become_a_daemon_project() {
let _profile = crate::config::PinnedUserDataDir::new();
let cwd = tempfile::tempdir().unwrap();

let handshake = proxy_serve_handshake(None, Some(cwd.path()), false)
.expect("build projectless proxy handshake");

assert_eq!(handshake.project_path, None);
assert!(!handshake.allow_init);
assert!(handshake.allow_initialize_root_routing);
}

#[tokio::test]
async fn direct_project_open_fails_closed() {
let path = Path::new("/tmp/tracedecay-direct-open-must-not-run");
Expand Down