From 63c67129423eefe9a843105aa1129a6ae3ab74b3 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:52:29 +0000 Subject: [PATCH] fix(daemon): stop logging vanished clone/init target as an error After a successful clone or init, finalize_root_exit resolves the new repo's family key. When the target directory is already gone (an expected race for agent staging clones and throwaway test repos), common_dir_for_repo_path finds no repo and the branch logged via observability::log_error, which the telemetry bridge turns into a user-facing $exception. The code already keeps the best worktree hint and continues, so attribution is never affected. Treat the vanished target as a benign outcome: log it at warn level (a Message envelope, not an Error), mirroring the vanished-working-dir and missing-reflog guards in the same function. Move the absolute path out of the message and into structured context so occurrences group into one issue instead of fragmenting one per directory. paths: src/daemon/trace_normalizer.rs Generated-By: PostHog Desktop Task-Id: 8656b2be-b314-4b57-ae05-63890fc0b543 --- src/daemon/trace_normalizer.rs | 81 +++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/src/daemon/trace_normalizer.rs b/src/daemon/trace_normalizer.rs index 4f9cf91..db37610 100644 --- a/src/daemon/trace_normalizer.rs +++ b/src/daemon/trace_normalizer.rs @@ -965,7 +965,7 @@ impl TraceNormalizer { } let mut resolved = false; - let mut last_error: Option<(PathBuf, AutterError)> = None; + let mut last_failed_target: Option = None; for candidate in candidates { if let Some(common_dir) = common_dir_for_repo_path(&candidate) { let resolved_family = FamilyKey::new( @@ -981,13 +981,7 @@ impl TraceNormalizer { resolved = true; break; } else { - last_error = Some(( - candidate.clone(), - AutterError::Generic(format!( - "failed to resolve clone/init target family from filesystem: {}", - candidate.display() - )), - )); + last_failed_target = Some(candidate.clone()); } } @@ -996,13 +990,23 @@ impl TraceNormalizer { if let Some(target) = target_from_def_repo.or(target_from_argv) { pending.worktree = Some(target); } - if let Some((target, error)) = last_error { - observability::log_error( - &error, + if let Some(target) = last_failed_target { + // A clone/init target that no longer exists at finalize is an + // expected race, not a bug: the directory (an agent plugin + // staging clone, or a throwaway test repo) was removed the moment + // git exited. The best worktree hint is already kept above, so + // attribution is unaffected. Log below error level, the same way + // the vanished-working-dir guard above does, and keep the + // absolute path in structured context so occurrences group into + // one issue instead of one per directory. + observability::log_message( + "clone/init target family unresolved at finalize; target directory removed before daemon finalized", + "warn", Some(serde_json::json!({ "component": "trace_normalizer", "phase": "resolve_clone_or_init_target_family", "root_sid": pending.root_sid, + "primary_command": primary_command, "target": target, })), ); @@ -2109,6 +2113,61 @@ mod tests { assert!(matches!(cmd.scope, CommandScope::Family(_))); } + #[test] + fn clone_target_vanished_before_finalize_keeps_worktree_hint_and_does_not_error() { + // The clone target directory is removed before the daemon finalizes the + // trace (an expected race for staging clones and throwaway test repos). + // Family resolution fails, but finalize must still succeed, keep the best + // worktree hint, and fall back to Global scope rather than erroring. + let backend = Arc::new(MockBackend::default()); + let mut normalizer = TraceNormalizer::new(backend); + let temp = tempfile::tempdir().expect("create tempdir"); + let outer = temp.path().join("outer"); + let clone_dir = outer.join("nested").join("vanished-clone"); + fs::create_dir_all(clone_dir.parent().expect("clone parent")).expect("create clone parent"); + + let start = serde_json::json!({ + "event":"start", + "sid":"clone-vanished", + "ts":1, + "argv":["git","clone","ssh://example/repo.git","nested/vanished-clone"], + "worktree":outer + }); + let def_repo = serde_json::json!({ + "event":"def_repo", + "sid":"clone-vanished", + "ts":2, + "worktree":clone_dir + }); + let cmd_name = serde_json::json!({ + "event":"cmd_name", + "sid":"clone-vanished", + "ts":3, + "name":"clone" + }); + let exit = serde_json::json!({ + "event":"exit", + "sid":"clone-vanished", + "ts":4, + "code":0 + }); + + assert!(normalizer.ingest_payload(&start).unwrap().is_none()); + assert!(normalizer.ingest_payload(&def_repo).unwrap().is_none()); + assert!(normalizer.ingest_payload(&cmd_name).unwrap().is_none()); + + // The target repo never materializes on disk, mirroring a directory that + // vanished the moment git exited. + let cmd = normalizer + .ingest_payload(&exit) + .expect("clone finalize should not error when the target vanished") + .expect("clone should still emit a normalized command"); + + assert_eq!(cmd.primary_command.as_deref(), Some("clone")); + assert_eq!(cmd.worktree.as_ref(), Some(&clone_dir)); + assert!(matches!(cmd.scope, CommandScope::Global)); + } + #[test] fn clone_prefers_target_family_over_source_cwd_family() { let backend = Arc::new(MockBackend::default());