Skip to content

fix(git): attribute inferred branches to the captured span - #3084

Open
devin-ai-integration[bot] wants to merge 4 commits into
masterfrom
devin/1791139759-single-git-evidence-span
Open

devin-ai-integration[bot] wants to merge 4 commits into
masterfrom
devin/1791139759-single-git-evidence-span

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Removes the second Git evidence span per session: a reflog-inferred branch is now attributed to the ingest/hook captured span it overlaps, tagged BranchProvenance::Inferred, instead of being written as a parallel source=backfill span.
  • GitEvidenceWriter::replace_backfill_session retracts inference independently: it deletes inferred-only spans and clears an inferred branch from a captured span (back to branch=None, Captured). Relations derived from either are deleted too.
  • GIT_CORRELATION_SCHEMA_VERSION 7 → 8 (span record shape changed; older stores get the existing typed reset, same as the 6 → 7 bump in 505f1a1).
  • Follow-up commit e6650ab addresses the Devin Review findings: a conflicting captured branch keeps the inferred span standalone, inference-tagged spans are bounded by their evidence window, revisit preserves captured commit evidence, and folding spans absorb the segment window/event count so inference coverage survives in time-filtered queries.

Closes #3052. Related: #2980, #3024.

Motivation / design decision

Verified on master 7aa78775ab: ingest capture writes transcript: spans with source=ingest, branch=None. Backfill writes backfill: spans for the same window with branch=Some("main") from the reflog. merge_span requires equal source and branch, so the two never merge. Every reader then has to reconcile two rows (span_hit groups them, span_count=2).

Options considered:

  1. Write the inferred branch into the captured branch field. fix(sessions): order git catch-up by change sequence #2980 rejected this: inference would become indistinguishable from capture and could not be retracted.
  2. Merge the two spans when reading. This adds a merge layer on top of the special case without removing the special case.
  3. One span with a per-span branch provenance tag (chosen). SessionGitSpan gains branch_provenance: Captured | Inferred. branch keeps one meaning: the effective branch that readers and indexes use, so the SQL branch column, span_matches_ref, scan_targets and commit attribution are unchanged. SessionGitSpan::captured_branch() returns only what the host observed.

Why each constraint still holds:

  • fix(sessions): order git catch-up by change sequence #2980 (captured fact never replaced by inference): infer_captured_branch only fills a captured span whose branch is None. It never overwrites an explicit captured branch. Capture matching (transcript_spans_from_observations) compares on captured_branch(), and merge_span also requires equal provenance. So a later explicit observation can't merge into, or be hidden by, an inferred attribution. The regression test asserts captured_branch() == None while the reader-visible branch is main.
  • Git evidence misses sessions rewritten in place on reimport #3024 (revisits replace inference): replace_backfill_session still runs before every re-derivation. Each inference it removes is either a whole inferred span or the Inferred tag on a captured span. The captured span stays. in_place_history_rewrites_revisit_and_retract_old_inference passes unchanged. The revisit marker is still the git_history_session_change journal.
  • Inference with no captured span: history-only sessions, or a branch segment that contradicts an inference already on the captured span (for example, two reflog branches in one window), still produce a standalone backfill: span. Nothing is dropped.

Hits keep reporting "backfill" in sources when the branch was inferred, so callers can still tell where the branch came from. The only reader-visible change is that span_count drops from 2 to 1.

Changes

  • crates/tracedecay-sessions/src/runtime/git_correlation.rs: BranchProvenance, SessionGitSpan::branch_provenance, captured_branch(), span_hit sources, schema version 8.
  • .../git_correlation/attribution.rs: infer_captured_branch (fold absorbs the segment window and refuses on any differently-named branch); capture matching on captured_branch() plus evidence-bounded merging for inference-tagged spans; merge_span requires equal provenance.
  • .../git_correlation/rows.rs: apply folds incoming backfill spans first. replace_backfill_session clears inferred attribution and deletes only inference-derived relations on surviving captured spans. The shared upsert_span helper replaces the inline insert.
  • Fixture literals updated in sessions tests and tracedecay-dashboard-api/src/loom_api.rs.

Test plan

  • New regression test runtime::git_correlation::backfill::bounded::tests::reflog_inference_attributes_the_captured_span_and_retracts_alone. On master it fails with capture and inference must share one span: left: 2, right: 1; it passes with this change. It checks that one row is stored, sessions_for(branch=main) returns span_count=1 and sources=["backfill","ingest"], captured_branch() is None, and after a revisit that moves the session out of the worktree the captured span survives with no branch and no relations derived from the inferred branch.
  • Four regression tests for the review fixes, each verified red on the pre-fix code and green after: conflicting_captured_branch_keeps_the_inferred_span, folded_inference_absorbs_the_segment_window, branchless_capture_beyond_an_inferred_window_opens_its_own_span, revisit_keeps_captured_commit_evidence_on_the_surviving_span (run via scripts/require-exact-test.sh).
  • cargo test -p tracedecay-sessions --lib: 554 passed
  • cargo test -p tracedecay-global-db -p tracedecay-dashboard-api, tracedecay-session-runtime --test session_store_read_cost
  • tracedecay suites (--features test-helpers,test-transport): runtime_acceptance_suite canonical_git, session_suite git, transcript_ingest_suite sessions_for, mcp_suite git_correlation/workflow_query, dashboard_api_test loom/delivery
  • cargo fmt --all -- --check, cargo clippy -p tracedecay-sessions -p tracedecay-dashboard-api --all-targets -- -D warnings, python3 scripts/linux-test-partitions.py check, node scripts/lint-commit-range.mjs --repository . origin/master HEAD
  • Full cargo nextest run --workspace: left to CI

Checklist

  • CHANGELOG.md updated: not edited by hand; release tooling generates it from commits
  • No secrets, credentials, or .env files included
  • Breaking changes documented: schema version bump 7 → 8 (existing typed reset path)

Link to Devin session: https://app.devin.ai/sessions/1f0d1bf1427c4efcabbcdbd01e63cebc
Open in Devin Desktop: https://app.devin.ai/desktop/session/1f0d1bf1427c4efcabbcdbd01e63cebc?variant=devin
Requested by: @ScriptedAlchemy


Devin Review

Reflog backfill no longer writes a second span beside the ingest capture
span of the same session window. The inferred branch is folded onto the
captured span with BranchProvenance::Inferred, so readers see one span,
the captured branch stays null (#2980), and replace_backfill_session
clears only the inference on revisit (#3024).

Refs #3052

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@changeset-bot

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ee6caa8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Fixed in e6650ab. infer_captured_branch now returns false when any overlapping span names a different branch — captured or inferred — so the incoming inferred span is kept via merge_span instead of being swallowed. Fold still absorbs the segment window/event count for agreeing spans. Regression test: runtime::git_correlation::backfill::bounded::tests::conflicting_captured_branch_keeps_the_inferred_span (red on the old code, green now).

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Fixed in e6650ab. An inference-tagged span is now bounded by its evidence: a branchless observation merges only when it lands inside [first_ts, last_ts]; a later observation inside the merge gap but outside the covered window opens its own captured span instead of stretching the inferred branch over time the reflog segment did not cover. Regression test: runtime::git_correlation::backfill::bounded::tests::branchless_capture_beyond_an_inferred_window_opens_its_own_span.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Fixed in e6650ab. Revisit now splits removed backfill spans (every linked record dies with the deleted span) from cleared captured spans that keep their identity (only inference-derived records — ReflogOverlap, and the TimeOverlap the inferred branch admitted — are deleted). Captured ToolResult/HostEvent/HeadObservation evidence survives on the surviving span, and post-revisit derivation re-links TimeOverlap from the span's own window. Regression test: runtime::git_correlation::backfill::bounded::tests::revisit_keeps_captured_commit_evidence_on_the_surviving_span.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Also addressed in e6650ab: the review's flag on infer_captured_branch (folding replaced the reflog segment's timestamps with capture timestamps). Folding spans now absorb the segment's window and event count — first_ts.min, last_ts.max, event_count.max — so nothing the inference covered is dropped from time-filtered sessions_for queries or dashboard timelines; the span's window is the union of its captured and inferred evidence. Regression test: runtime::git_correlation::backfill::bounded::tests::folded_inference_absorbs_the_segment_window.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Resolved in ee6caa8 — merge_span now unions capture_window.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session runtime records two Git evidence spans per session that never merge

1 participant