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
11 changes: 11 additions & 0 deletions src/agents/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,17 @@ fn doctor_check_session_ingest(dc: &mut DoctorCounters, project_path: &Path) {
let health = tokio::task::block_in_place(|| {
handle.block_on(async {
let db = crate::sessions::cursor::open_project_session_db(project_path).await?;
let placeholder_paths = db.literal_workspace_placeholder_transcript_paths(10).await;
if !placeholder_paths.is_empty() {
dc.warn(&format!(
"Cursor transcript ingest has {} path(s) with a literal workspace placeholder; \
Cursor did not expand `${{workspaceFolder}}`, so session recall will miss those transcripts",
placeholder_paths.len(),
));
for path in &placeholder_paths {
dc.info(&format!(" - {path}"));
}
}
Some(db.session_ingest_health().await)
})
});
Expand Down
78 changes: 73 additions & 5 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::collections::HashMap;
use std::fmt::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::os::unix::net::UnixStream as StdUnixStream;
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(unix)]
Expand Down Expand Up @@ -293,11 +295,7 @@ pub fn uninstall_service(stop: bool) -> Result<PathBuf> {
}

pub fn service_status(socket_path: &Path) -> String {
let socket_state = if socket_path.exists() {
"present"
} else {
"missing"
};
let socket_state = daemon_socket_state(socket_path);
format!(
"service: {}\nsocket: {} ({})\nlogs: journalctl --user -u {} -f\n",
systemd_user_service_path().map_or_else(
Expand All @@ -310,6 +308,28 @@ pub fn service_status(socket_path: &Path) -> String {
)
}

#[cfg(unix)]
fn daemon_socket_state(socket_path: &Path) -> &'static str {
if !socket_path.exists() {
return "missing";
}
match StdUnixStream::connect(socket_path) {
Ok(_) => "connectable",
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => "stale",
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => "present but not accessible",
Err(_) => "present but unreachable",
}
}

#[cfg(not(unix))]
fn daemon_socket_state(socket_path: &Path) -> &'static str {
if socket_path.exists() {
"present"
} else {
"missing"
}
}

fn format_daemon_log_line(event: &str, fields: &[(&str, String)]) -> String {
let mut line = format!("[tracedecay] event={}", quote_log_value(event));
for (key, value) in fields {
Expand Down Expand Up @@ -1602,6 +1622,54 @@ mod tests {
assert!(status.contains("logs: journalctl --user -u tracedecay.service -f"));
}

#[cfg(unix)]
#[test]
fn service_status_reports_missing_socket() {
let dir = TempDir::new().expect("temp dir");
let socket = dir.path().join("missing.sock");

let status = super::service_status(&socket);

assert!(
status.contains(&format!("socket: {} (missing)", socket.display())),
"status should report missing socket, got:\n{status}"
);
}

#[cfg(unix)]
#[test]
fn service_status_reports_unconnectable_socket_file() {
let dir = TempDir::new().expect("temp dir");
let socket = dir.path().join("unconnectable.sock");
std::fs::write(&socket, "").expect("unconnectable socket placeholder");

let status = super::service_status(&socket);

assert!(
status.contains(&format!("socket: {} (stale)", socket.display()))
|| status.contains(&format!(
"socket: {} (present but unreachable)",
socket.display()
)),
"status should report an unconnectable socket, got:\n{status}"
);
}

#[cfg(unix)]
#[test]
fn service_status_reports_connectable_socket() {
let dir = TempDir::new().expect("temp dir");
let socket = dir.path().join("daemon.sock");
let _listener = std::os::unix::net::UnixListener::bind(&socket).expect("bind socket");

let status = super::service_status(&socket);

assert!(
status.contains(&format!("socket: {} (connectable)", socket.display())),
"status should report connectable socket, got:\n{status}"
);
}

#[cfg(unix)]
#[test]
fn scheduler_task_start_log_uses_task_key_and_project() {
Expand Down
63 changes: 44 additions & 19 deletions src/db/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,31 +213,56 @@ impl Database {
&self,
query: &str,
limit: usize,
path_prefix: Option<&str>,
) -> Result<Vec<DependencyImportUse>> {
let query = query.trim();
if query.is_empty() || limit == 0 {
return Ok(Vec::new());
}
let like_pattern = format!("%{query}%");
let mut rows = self
.conn()
.query(
"SELECT name, signature, file_path, start_line
FROM nodes
WHERE kind = 'use'
AND signature LIKE ?1
AND name NOT LIKE './%'
AND name NOT LIKE '../%'
AND name NOT LIKE '/%'
ORDER BY file_path ASC, start_line ASC
LIMIT ?2",
params![like_pattern.as_str(), limit.saturating_mul(4) as i64],
)
.await
.map_err(|e| TraceDecayError::Database {
message: format!("failed to query dependency import uses: {e}"),
operation: "dependency_import_uses".to_string(),
})?;
let limit = limit.saturating_mul(4) as i64;
let mut rows = if let Some(prefix) = path_prefix {
let with_slash = if prefix.ends_with('/') {
prefix.to_string()
} else {
format!("{prefix}/")
};
let prefix_like = format!("{with_slash}%");
self.conn()
.query(
"SELECT name, signature, file_path, start_line
FROM nodes
WHERE kind = 'use'
AND signature LIKE ?1
AND name NOT LIKE './%'
AND name NOT LIKE '../%'
AND name NOT LIKE '/%'
AND (file_path = ?2 OR file_path LIKE ?3)
ORDER BY file_path ASC, start_line ASC
LIMIT ?4",
params![like_pattern.as_str(), prefix, prefix_like.as_str(), limit],
)
.await
} else {
self.conn()
.query(
"SELECT name, signature, file_path, start_line
FROM nodes
WHERE kind = 'use'
AND signature LIKE ?1
AND name NOT LIKE './%'
AND name NOT LIKE '../%'
AND name NOT LIKE '/%'
ORDER BY file_path ASC, start_line ASC
LIMIT ?2",
params![like_pattern.as_str(), limit],
)
.await
}
.map_err(|e| TraceDecayError::Database {
message: format!("failed to query dependency import uses: {e}"),
operation: "dependency_import_uses".to_string(),
})?;

let mut imports = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| TraceDecayError::Database {
Expand Down
88 changes: 58 additions & 30 deletions src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,49 +368,77 @@ fn classify_registry_storage(
if store.storage_mode != "profile_sharded" {
return None;
}
let artifacts = registry_store_artifacts(profile_root, store);
if artifacts
.iter()
.any(|artifacts| artifacts.graph_db_path.exists())
{
Some(DoctorStorageStatus::ProfileSharded)
} else if artifacts
.iter()
.any(|artifacts| artifacts.manifest_path.is_some())
{
Some(DoctorStorageStatus::ManifestReconstructable)
} else if artifacts.is_empty() {
None
} else {
Some(DoctorStorageStatus::Stale)
}
}

#[derive(Debug, Clone)]
struct RegistryStoreArtifacts {
graph_db_path: PathBuf,
manifest_path: Option<PathBuf>,
}

fn registry_store_artifacts(
profile_root: &Path,
store: &crate::global_db::StoreInstanceRecord,
) -> Vec<RegistryStoreArtifacts> {
if store.storage_mode != "profile_sharded" {
return Vec::new();
}
let store_relpath = registry_relpath(&store.store_relpath);
let manifest_relpath = store
.manifest_relpath
.as_ref()
.map(|relpath| registry_relpath(relpath));
let mut resolved_any_root = false;
let mut manifest_exists = false;
let mut artifacts = Vec::new();
for profile_root in registry_profile_roots(profile_root) {
let Ok(data_root) =
crate::storage::StoreArtifactPath::resolve(&profile_root, &store_relpath)
else {
continue;
};
resolved_any_root = true;
let data_root = data_root.absolute_path();
if data_root
.join(crate::config::db_filename(&data_root))
.exists()
{
return Some(DoctorStorageStatus::ProfileSharded);
}
manifest_exists |= manifest_relpath.as_ref().map_or_else(
|| {
data_root
.join(crate::storage::STORE_MANIFEST_FILENAME)
.is_file()
},
|relpath| {
[&profile_root, &data_root].iter().any(|root| {
crate::storage::StoreArtifactPath::resolve(root, relpath)
.ok()
.is_some_and(|path| path.absolute_path().is_file())
})
},
);
}
if manifest_exists {
Some(DoctorStorageStatus::ManifestReconstructable)
} else if resolved_any_root {
Some(DoctorStorageStatus::Stale)
} else {
None
artifacts.push(RegistryStoreArtifacts {
graph_db_path: data_root.join(crate::config::db_filename(&data_root)),
manifest_path: registry_manifest_path(
&profile_root,
&data_root,
manifest_relpath.as_deref(),
),
});
}
artifacts
}

fn registry_manifest_path(
profile_root: &Path,
data_root: &Path,
manifest_relpath: Option<&Path>,
) -> Option<PathBuf> {
if let Some(relpath) = manifest_relpath {
return [profile_root, data_root].iter().find_map(|root| {
crate::storage::StoreArtifactPath::resolve(root, relpath)
.ok()
.map(|path| path.absolute_path())
.filter(|path| path.is_file())
});
}
let path = data_root.join(crate::storage::STORE_MANIFEST_FILENAME);
path.is_file().then_some(path)
}

fn registry_relpath(value: &str) -> PathBuf {
Expand Down
2 changes: 1 addition & 1 deletion src/extraction/typescript_extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1080,7 +1080,7 @@ impl TypeScriptExtractor {

// Unresolved Uses reference.
state.unresolved_refs.push(UnresolvedRef {
from_node_id: id,
from_node_id: id.clone(),
reference_name: name,
reference_kind: EdgeKind::Uses,
line: start_line,
Expand Down
36 changes: 36 additions & 0 deletions src/global_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,42 @@ impl GlobalDb {
health
}

/// Returns tracked transcript paths that still contain an unresolved
/// workspace placeholder. Cursor should expand `${workspaceFolder}` before
/// a transcript path is persisted; if it reaches the session DB literally,
/// catch-up and recall will look at a non-existent path.
pub async fn literal_workspace_placeholder_transcript_paths(
&self,
limit: usize,
) -> Vec<String> {
if limit == 0 {
return Vec::new();
}
let Ok(mut rows) = self
.conn
.query(
"SELECT DISTINCT transcript_path FROM sessions
WHERE transcript_path IS NOT NULL
AND transcript_path != ''
AND (transcript_path LIKE '%${workspaceFolder}%'
OR transcript_path LIKE '%$workspaceFolder%')
ORDER BY transcript_path
LIMIT ?1",
params![i64::try_from(limit).unwrap_or(i64::MAX)],
)
.await
else {
return Vec::new();
};
let mut paths = Vec::new();
while let Ok(Some(row)) = rows.next().await {
if let Ok(path) = row.get::<String>(0) {
paths.push(path);
}
}
paths
}

/// Canonical registry key for a project path. Falls back to the lossy path
/// string when canonicalization fails (e.g. the path no longer exists) so
/// upserts and lookups always agree on a single key per project, instead of
Expand Down
Loading
Loading