Skip to content

Commit 99f4fcd

Browse files
authored
Merge pull request #131 from TeamCadenceAI/fix/session-discovery-gaps
fix(discovery): resolve Claude desktop sessions with parent-directory CWD
2 parents 6fe7684 + c03aae2 commit 99f4fcd

2 files changed

Lines changed: 344 additions & 30 deletions

File tree

src/main.rs

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,43 @@ async fn session_log_content_async(log: &agents::SessionLog) -> Option<String> {
779779
}
780780
}
781781

782+
/// Maximum number of candidate directories to try when resolving a repo
783+
/// from transcript content. Prevents excessive `git rev-parse` calls when
784+
/// a transcript references many unique directories.
785+
const TRANSCRIPT_CWD_MAX_CANDIDATES: usize = 20;
786+
787+
/// Attempt to resolve a session's repo root by scanning transcript content
788+
/// for file paths when the recorded `cwd` doesn't resolve to a git repo.
789+
///
790+
/// This is a fallback for sessions (typically Claude desktop app) where the
791+
/// manifest's `cwd` points to a parent directory rather than a specific repo.
792+
/// Reads the transcript, extracts absolute file paths from tool-call inputs,
793+
/// and tries to resolve each to a git repo root.
794+
///
795+
/// Caps the number of candidates tried at [`TRANSCRIPT_CWD_MAX_CANDIDATES`]
796+
/// to bound the cost of git subprocess calls.
797+
async fn resolve_repo_from_transcript(
798+
log: &agents::SessionLog,
799+
repo_root_cache: &std::collections::HashMap<String, git::RepoRootResolution>,
800+
) -> Option<git::RepoRootResolution> {
801+
let content = session_log_content_async(log).await?;
802+
let candidate_cwds = scanner::extract_candidate_cwds_from_transcript(&content);
803+
804+
for candidate_cwd in candidate_cwds.iter().take(TRANSCRIPT_CWD_MAX_CANDIDATES) {
805+
// Check cache first.
806+
if let Some(cached) = repo_root_cache.get(candidate_cwd.as_str()) {
807+
return Some(cached.clone());
808+
}
809+
810+
let cwd_path = std::path::Path::new(&candidate_cwd);
811+
if let Ok(resolution) = git::resolve_repo_root_with_fallbacks(cwd_path).await {
812+
return Some(resolution);
813+
}
814+
}
815+
816+
None
817+
}
818+
782819
/// Parse a duration string like "7d", "30d", "1d" into seconds.
783820
///
784821
/// Currently only supports the `<N>d` format (number of days).
@@ -1437,36 +1474,55 @@ async fn run_backfill_inner_with_invocation(
14371474
let resolved = match git::resolve_repo_root_with_fallbacks(cwd_path).await {
14381475
Ok(resolution) => resolution,
14391476
Err(diagnostics) => {
1440-
::tracing::warn!(
1441-
event = "session_discovery_skipped",
1442-
file = file_path.as_str(),
1443-
session_id = ?metadata.session_id,
1444-
cwd = cwd.as_str(),
1445-
requested_cwd = diagnostics.requested_cwd.to_string_lossy().to_string(),
1446-
reason = "repo_root_lookup_failed",
1447-
error = ?diagnostics.direct_error,
1448-
cwd_exists = diagnostics.cwd_exists,
1449-
nearest_existing_ancestor = ?diagnostics
1450-
.nearest_existing_ancestor
1451-
.map(|path| path.to_string_lossy().to_string()),
1452-
ancestor_error = ?diagnostics.ancestor_error,
1453-
candidate_repo_names = ?diagnostics.candidate_repo_names,
1454-
candidate_owner_repo_roots = ?diagnostics
1455-
.candidate_owner_repo_roots
1456-
.into_iter()
1457-
.map(|path| path.to_string_lossy().to_string())
1458-
.collect::<Vec<_>>(),
1459-
matched_worktree_owner_repo_root = ?diagnostics
1460-
.matched_worktree_owner_repo_root
1461-
.map(|path| path.to_string_lossy().to_string()),
1462-
matched_worktree_path = ?diagnostics
1463-
.matched_worktree_path
1464-
.map(|path| path.to_string_lossy().to_string()),
1465-
);
1466-
if let Some(ref pb) = progress {
1467-
pb.inc(1);
1477+
// Fallback: scan transcript content for file paths that
1478+
// reveal the actual working directory. This handles cases
1479+
// like Claude desktop sessions where the manifest's `cwd`
1480+
// points to a parent directory (e.g. ~/Documents/GitHub)
1481+
// rather than the specific repo.
1482+
let transcript_resolution =
1483+
resolve_repo_from_transcript(log, &repo_root_cache).await;
1484+
1485+
if let Some(resolution) = transcript_resolution {
1486+
::tracing::info!(
1487+
event = "session_cwd_resolved_from_transcript",
1488+
file = file_path.as_str(),
1489+
session_id = ?metadata.session_id,
1490+
original_cwd = cwd.as_str(),
1491+
resolved_repo = resolution.repo_root.to_string_lossy().to_string(),
1492+
);
1493+
resolution
1494+
} else {
1495+
::tracing::warn!(
1496+
event = "session_discovery_skipped",
1497+
file = file_path.as_str(),
1498+
session_id = ?metadata.session_id,
1499+
cwd = cwd.as_str(),
1500+
requested_cwd = diagnostics.requested_cwd.to_string_lossy().to_string(),
1501+
reason = "repo_root_lookup_failed",
1502+
error = ?diagnostics.direct_error,
1503+
cwd_exists = diagnostics.cwd_exists,
1504+
nearest_existing_ancestor = ?diagnostics
1505+
.nearest_existing_ancestor
1506+
.map(|path| path.to_string_lossy().to_string()),
1507+
ancestor_error = ?diagnostics.ancestor_error,
1508+
candidate_repo_names = ?diagnostics.candidate_repo_names,
1509+
candidate_owner_repo_roots = ?diagnostics
1510+
.candidate_owner_repo_roots
1511+
.into_iter()
1512+
.map(|path| path.to_string_lossy().to_string())
1513+
.collect::<Vec<_>>(),
1514+
matched_worktree_owner_repo_root = ?diagnostics
1515+
.matched_worktree_owner_repo_root
1516+
.map(|path| path.to_string_lossy().to_string()),
1517+
matched_worktree_path = ?diagnostics
1518+
.matched_worktree_path
1519+
.map(|path| path.to_string_lossy().to_string()),
1520+
);
1521+
if let Some(ref pb) = progress {
1522+
pb.inc(1);
1523+
}
1524+
continue;
14681525
}
1469-
continue;
14701526
}
14711527
};
14721528
repo_root_cache.insert(cwd.clone(), resolved.clone());
@@ -1801,10 +1857,26 @@ async fn upload_incremental_sessions_globally(
18011857
let resolved_repo = if let Some(cached) = repo_root_cache.get(&cwd) {
18021858
cached.clone()
18031859
} else {
1804-
let resolved = git::resolve_repo_root_with_fallbacks(std::path::Path::new(&cwd))
1860+
let mut resolved = git::resolve_repo_root_with_fallbacks(std::path::Path::new(&cwd))
18051861
.await
18061862
.ok()
18071863
.map(|resolution| resolution.repo_root);
1864+
1865+
// Fallback: scan transcript for file paths when CWD doesn't
1866+
// resolve to a repo (e.g. Claude desktop app parent-dir CWD).
1867+
if resolved.is_none()
1868+
&& let Some(content) = session_log_content_async(&parsed.log).await
1869+
{
1870+
let candidates = scanner::extract_candidate_cwds_from_transcript(&content);
1871+
for candidate_cwd in candidates.iter().take(TRANSCRIPT_CWD_MAX_CANDIDATES) {
1872+
let cwd_path = std::path::Path::new(&candidate_cwd);
1873+
if let Ok(resolution) = git::resolve_repo_root_with_fallbacks(cwd_path).await {
1874+
resolved = Some(resolution.repo_root);
1875+
break;
1876+
}
1877+
}
1878+
}
1879+
18081880
repo_root_cache.insert(cwd.clone(), resolved.clone());
18091881
resolved
18101882
};

0 commit comments

Comments
 (0)