From fb08e8138d9f470b10ef26050eb6714bee03b662 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:36:51 +0000 Subject: [PATCH] Tolerate a missing reflog end cut in trace normalizer A mutating git command whose reflog end cut never arrives no longer fails the whole trace ingest. This happens when a temp repo is cleaned up before the exit hook delivers its cut. The family key stays recoverable from the worktree, so the command now records with low confidence instead of hitting a hard error in finalize_root_exit. This mirrors how the daemon already tolerates a working directory that vanished mid-operation (is_missing_working_dir_error). Generated-By: PostHog Desktop Task-Id: fb1ea41b-8b9e-406e-bff5-0b25f963e123 --- src/daemon/trace_normalizer.rs | 58 +++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/daemon/trace_normalizer.rs b/src/daemon/trace_normalizer.rs index 505b4a7..4f9cf91 100644 --- a/src/daemon/trace_normalizer.rs +++ b/src/daemon/trace_normalizer.rs @@ -891,10 +891,23 @@ impl TraceNormalizer { // Clone/init can resolve into a family only after the repository exists at exit. // In that flow there is no stable pre-command reflog cut to diff against. } else { - return Err(AutterError::Generic(format!( - "missing reflog end cut for mutating command sid={} primary={:?} family={}", - pending.root_sid, primary_command, family - ))); + // The exit hook delivered no reflog end cut. This normally means a + // temp repo was cleaned up before the hook ran, so the cut never + // reached the daemon. The family key is still recoverable from the + // worktree, so record the command with low confidence rather than + // rejecting the whole trace. This mirrors how the daemon tolerates + // a working directory that vanished mid-operation. + observability::log_message( + "missing reflog end cut for mutating command; recording with low confidence", + "warn", + Some(serde_json::json!({ + "component": "trace_normalizer", + "phase": "finalize_root_exit", + "root_sid": pending.root_sid, + "primary_command": primary_command, + "family": family.0, + })), + ); } } @@ -2495,4 +2508,41 @@ mod tests { Some("main") ); } + + #[test] + fn mutating_command_without_reflog_end_cut_degrades_to_low_confidence() { + let backend = Arc::new(MockBackend::default()); + let mut normalizer = TraceNormalizer::new(backend); + let temp = tempfile::tempdir().expect("create tempdir"); + let repo = temp.path().join("repo"); + fs::create_dir_all(repo.join(".git")).expect("create git dir"); + + // A checkout whose temp repo is cleaned up before the exit hook delivers + // its reflog end cut. The family key is still recoverable from the + // worktree, so the command must record with low confidence, not error. + let start = serde_json::json!({ + "event":"start", + "sid":"checkout-no-end-cut", + "ts":1, + "argv":["git","checkout","feature"], + "worktree":repo + }); + let exit = serde_json::json!({ + "event":"exit", + "sid":"checkout-no-end-cut", + "ts":2, + "code":0 + }); + + assert!(normalizer.ingest_payload(&start).unwrap().is_none()); + let cmd = normalizer + .ingest_payload(&exit) + .expect("missing reflog end cut should not block normalization") + .expect("exit payload should emit a normalized command"); + + assert_eq!(cmd.primary_command.as_deref(), Some("checkout")); + assert!(matches!(cmd.scope, CommandScope::Family(_))); + assert_eq!(cmd.confidence, Confidence::Low); + assert!(cmd.ref_changes.is_empty()); + } }