Skip to content

feat: attribute turn diffs so git-only changes stop polluting changed files - #4249

Closed
t3dotgg wants to merge 4 commits into
mainfrom
feat/attribution-aware-turn-diffs
Closed

feat: attribute turn diffs so git-only changes stop polluting changed files#4249
t3dotgg wants to merge 4 commits into
mainfrom
feat/attribution-aware-turn-diffs

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 22, 2026

Copy link
Copy Markdown
Member

Problem

The changed-files card and per-turn inline diff are derived from full-tree checkpoint snapshots (git diff snap(N-1) snap(N)). That answers "what bytes changed between turn boundaries" — but the card is a review surface, and the moment the agent runs a tree-rewriting git command the two diverge:

  • git pull origin main → hundreds of already-reviewed upstream files listed as agent changes
  • git checkout other-branch → the entire branch delta appears
  • git cherry-pick / git rebase → content authored elsewhere shows up as new

Approach

Keep the snapshot mechanism exactly as-is (it powers revert and is ground truth) and add a read-side attribution layer. We don't need to observe what the agent did — git history records it after the fact:

agent-authored delta = tree delta (snapshots) − foreign-history delta (HEAD movement)
  • Checkpoint commits now record head=<oid> and branch=<name> in their commit message (no schema/ref changes; legacy checkpoints degrade to today's behavior).
  • attributeCheckpointDiff classifies each path in a checkpoint tree delta: a path is git only when its exact blob transition (mode + oid both sides, --no-renames) also appears in the HEAD-movement delta and no commit authored during the turn touched it. Commits are considered turn-authored when their author date ≥ the baseline checkpoint's commit time — cherry-pick/rebase preserve author dates, so replayed foreign commits are correctly demoted, while the agent committing its own edits stays "agent". Merge commits use the combined diff (-c) so only conflict resolutions count as agent work.
  • Fast path: if HEAD didn't move during the turn (the vast majority), no extra git calls beyond two rev-parses at capture, and behavior is identical to today.
  • Fail direction: every ambiguity resolves to "agent" (shown). Attribution errors, truncated output, legacy checkpoints, or pathological commit counts degrade to the unfiltered diff — attribution can add noise but never hides agent work, and never blocks the checkpoint pipeline.

Surfaces

  • Changed-files card: lists agent-authored files only, plus a muted ⎇ Updated via git — N files row for demoted changes (with aggregate +/− stats). A git-only turn shows just that row instead of disappearing.
  • Diff panel: git-attributed file sections are dropped from the patch by default; a banner offers "Show them" / "Hide them" (new includeGitChanges input on getTurnDiff / getFullThreadDiff).
  • OrchestrationCheckpointFile gains an optional origin: "agent" | "git" field.

Testing

  • Unit tests for message metadata round-trip, raw-diff parsing, and path classification (CheckpointAttribution.test.ts)
  • Integration tests against real git repos: no-HEAD-move fast path, branch switch + edit in one turn, own-commit turns, cherry-pick of a pre-existing commit, legacy checkpoints (CheckpointStore.test.ts)
  • Patch filtering tests incl. deletions and binary sections (Diffs.test.ts), partition tests (turnDiffTree.test.ts)
  • Full suites green: server (1551), web (1378), contracts (179), client-runtime (269); typecheck and lint clean

Plan write-up: https://syizveepeikc.postplan.dev

🤖 Generated with Claude Code


Note

Medium Risk
Non-trivial git history heuristics and new diff filtering affect what users see; ambiguous cases degrade to unfiltered diffs and legacy checkpoints skip attribution rather than blocking checkpoints.

Overview
Turn diffs and changed-files views no longer treat every checkpoint tree delta as agent work when git rewrote the tree (pull, checkout, cherry-pick, rebase). A read-side attribution layer classifies paths as agent vs git, while snapshot capture/revert stays unchanged.

Server: Checkpoint commits embed HEAD oid + branch in commit messages. New attributeCheckpointDiff (VCS → CheckpointStore) compares checkpoint tree deltas to HEAD-movement deltas and turn-authored commits (patch-id disambiguation for cherry-picks/amends). getTurnDiff / getFullThreadDiff run attribution after diffCheckpoints, strip git-attributed file sections by default, accept includeGitChanges, and return optional gitFileCount aligned with the rendered patch. filterUnifiedDiffFiles / path parsing handles renames, binaries, and quoted paths. CheckpointReactor tags turn summary files with optional origin (best-effort; failures leave files unattributed).

Contracts & client: Optional origin on checkpoint files; diff APIs gain includeGitChanges and gitFileCount.

Web: Changed-files card lists agent files only plus a compact “Updated via git” row; diff panel shows a show/hide git changes banner per turn scope.

Reviewed by Cursor Bugbot for commit bcebb79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Filter git-attributed file changes from turn diffs to reduce noise in changed files view

  • Checkpoint commits now record HEAD commit OID and branch metadata, enabling GitVcsDriver.attributeCheckpointDiff to classify each changed path as 'agent' or 'git' by comparing tree and history deltas, with conservative fallback to null.
  • CheckpointDiffQuery applies this attribution to filter out git-attributed sections from unified diffs by default; callers pass includeGitChanges: true to see them.
  • CheckpointReactor attaches a per-file origin field to turn summary files, and MessagesTimeline collapses git-origin files into a separate 'Updated via git' row.
  • The diff panel shows a banner when git-attributed files are hidden, with a toggle to reveal them.
  • Behavioral Change: existing checkpoint commits lack the new HEAD metadata, so attribution returns null for legacy checkpoints and all files are treated as agent-authored.

Macroscope summarized bcebb79.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 133e6086-1c6f-453c-a2e4-e240e58ce87a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/attribution-aware-turn-diffs

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 22, 2026
Comment thread apps/server/src/checkpointing/Diffs.ts
Comment thread apps/server/src/vcs/GitVcsDriver.ts
Comment thread apps/web/src/components/DiffPanel.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/server/src/vcs/GitVcsDriver.ts
Comment thread apps/server/src/checkpointing/Diffs.ts
Comment thread apps/server/src/vcs/GitVcsDriver.ts
@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

New feature adding git attribution logic with 3 unresolved medium-severity bugs identified (unborn HEAD handling, past-dated commits, mobile compatibility). The complexity of the attribution algorithm and its impact on what diffs users see warrants human review.

You can customize Macroscope's approvability policy. Learn more.

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 22, 2026
Comment thread apps/server/src/vcs/GitVcsDriver.ts Outdated
Comment thread apps/server/src/vcs/GitVcsDriver.ts
Comment thread apps/server/src/checkpointing/CheckpointDiffQuery.ts
Comment thread apps/server/src/checkpointing/Diffs.ts Outdated
Comment thread apps/server/src/checkpointing/Diffs.ts Outdated
Comment thread apps/server/src/checkpointing/CheckpointDiffQuery.ts
t3dotgg and others added 4 commits July 23, 2026 13:43
… files

The changed-files card and per-turn inline diff are derived from full-tree
checkpoint snapshots, so any tree-rewriting git command the agent runs
(pull, checkout, cherry-pick, rebase) shows up as hundreds of "changed
files" the agent never authored.

This adds a read-side attribution layer on top of the existing checkpoint
mechanism (capture, storage, and revert are untouched):

- Checkpoint commits now record the HEAD oid and branch in their commit
  message. Legacy checkpoints without metadata degrade to today's behavior.
- attributeCheckpointDiff classifies each path in a checkpoint tree delta:
  a path is "git" only when its exact blob transition also appears in the
  HEAD-movement delta for the turn and no commit authored during the turn
  touched it. Every ambiguity resolves to "agent" (shown), so
  misclassification can add noise but never hide agent work. Merge commits
  use the combined diff so only conflict resolutions count as agent work.
- The checkpoint reactor tags each file with an origin; attribution
  failures degrade to unattributed files and never block the pipeline.
- getTurnDiff/getFullThreadDiff drop git-attributed file sections from the
  patch unless the new includeGitChanges input is set.
- The changed-files card lists agent files only, with a muted
  "Updated via git — N files" row for demoted changes. The diff panel
  shows a banner with a show/hide toggle so the full tree delta stays one
  click away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resolveDiffSectionPath scanned line-by-line and returned the `--- a/`
  source path before ever seeing `+++ b/`, so rename sections resolved to
  the pre-image path and could be mis-filtered (macroscope, bugbot). Now
  resolves the post-image path in a full pass with the source as fallback,
  plus a rename filtering test.
- `commit --amend` of a pre-turn commit preserved the old author date, so
  amended agent content was misattributed to history and hidden — the one
  invariant violation both bots flagged. Commits authored before the turn
  but committed during it are now disambiguated by patch-id: an identical
  patch elsewhere in the repo (branches/remotes/reflog — reflog catches
  pre-rebase originals) means replayed pre-existing content (demote);
  otherwise it's an amend (show). Integration test included.
- Per-commit diff-tree now passes --root so agent-authored root commits on
  orphan branches contribute their paths (macroscope).
- includeGitChanges is scoped to the diff panel selection key so revealing
  git changes on one turn no longer leaks into other turns/threads
  (macroscope).
- Git-only turns render a compact card with just the "Updated via git" row
  instead of a "0 changed files" header with dead controls (macroscope).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iltering, and banner drift

- patch-id --stable strips whitespace, so a whitespace-only amend collided
  with its pre-amend reflog original and was demoted as a replay — hiding
  agent work (macroscope, bugbot, codex). Switched to --verbatim and added
  an integration test amending a whitespace-only change.
- Binary and mode-only sections have no ---/+++ lines, so they always
  survived filtering even when git-attributed (codex). resolveDiffSectionPath
  now falls back to rename/copy metadata and then the `diff --git` header
  (with C-style quoted-path decoding); genuinely ambiguous headers are still
  kept. Tests for binary, mode-only, quoted, spaced, rename-only, and
  ambiguous sections.
- The diff panel banner was driven by the turn summary's origin tags while
  the patch was filtered by an independent attribution pass, so the two
  could disagree after partial failures (codex). getTurnDiff and
  getFullThreadDiff now return gitFileCount from the same pass that filtered
  the patch, and the banner uses that.

Codex also flagged attribution cost (re-running per diff-query variant);
deferred — all calls are plumbing on OIDs, the fast path covers turns
without HEAD movement, and caching by checkpoint pair can come later if
real-world latency shows up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ch sections

- Git C-style quoting octal-escapes raw UTF-8 bytes; decoding each escape
  with String.fromCharCode produced mojibake, so attribution lookups missed
  quoted non-ASCII paths and their sections were never filtered (macroscope,
  bugbot). stripGitPathQuoting now decodes into a byte buffer and UTF-8
  decodes the result.
- gitFileCount counted every git-attributed path in the attribution map,
  overstating hidden files when a path had no section in the patch (e.g.
  whitespace-only changes elided by ignoreWhitespace) and double-counting
  renames from the --no-renames map (macroscope, bugbot). It now counts
  git-attributed sections actually present in the patch, so the banner
  matches what the toggle can reveal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotgg force-pushed the feat/attribution-aware-turn-diffs branch from f2c7b15 to bcebb79 Compare July 23, 2026 20:46

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bcebb79. Configure here.

// no git-origin paths".
if (fromMetadata.headOid === toMetadata.headOid || fromMetadata.headOid === null) {
return new Map<string, "agent" | "git">();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unborn HEAD skips git attribution

Medium Severity

The fast path treats any turn that starts with unborn HEAD (head=none) as having no git-origin changes, including when HEAD later points at a real commit. A git pull / git checkout of existing history into a fresh repo therefore keeps every path as agent work, so the changed-files card and diff panel still show the foreign tree as agent edits.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcebb79. Configure here.

turnAuthoredCommits.push(commit);
} else if (committedAt >= fromCommitTime) {
rewrittenCommits.push(commit);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Past-dated agent commits demoted to git

Medium Severity

Turn commit classification only keeps commits with author or committer time at/after the baseline checkpoint. An agent commit whose author and committer dates both precede that baseline falls into neither bucket, so matching HEAD-movement blob transitions are labeled git and can be filtered out of the review diff, contrary to the fail-toward-agent rule.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcebb79. Configure here.

fromCheckpointRef,
toCheckpointRef,
diff: rawDiff,
includeGitChanges: input.includeGitChanges ?? false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mobile diffs hide git changes silently

Medium Severity

getTurnDiff / getFullThreadDiff now default includeGitChanges to false, so history-driven sections are stripped for every client. Mobile still builds review file lists from that patch and never sends includeGitChanges or offers a reveal control, so a git-only turn (pull, checkout, rebase) renders as an empty diff with no explanation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcebb79. Configure here.

@juliusmarminge

Copy link
Copy Markdown
Member

Closing in favor of #2829 (orchestration V2).

#2829 deletes the V1 orchestration layer this PR builds on — apps/server/src/orchestration/**, provider/Layers/*Adapter.ts and provider/Services/** are removed and replaced by apps/server/src/orchestration-v2/**, with the IPC surface renamed to ORCHESTRATION_V2_WS_METHODS. The files this PR touches either no longer exist or are rewritten, so it can't be rebased — it would need reimplementing against the V2 adapters.

This is not a judgement on the change itself. Several of these are real gaps we still want fixed; the base just moved out from under them.

Once #2829 merges, please rebase onto main, port the change to the V2 equivalent, and reopen (or open a fresh PR). Ping me and I'll prioritise the review.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants