Skip to content

Resolve each session's repository before uploading it for backfill - #260

Open
gowshik450526511 wants to merge 1 commit into
stagingfrom
fix-backfill-project
Open

Resolve each session's repository before uploading it for backfill#260
gowshik450526511 wants to merge 1 commit into
stagingfrom
fix-backfill-project

Conversation

@gowshik450526511

@gowshik450526511 gowshik450526511 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Companion to ai-gateway-data#2681. Ship that one first — it plumbs the cwd this map is keyed on.

Why this has to happen client-side

A replayed session lands with no repository, so backfilled work can't be attributed to a repo at all. #2681 recovers the working directory from the transcript, but a project isn't in the transcript — it comes from git's origin remote, and by the time a session is replayed the backend is nowhere near the checkout.

unbound.py::_get_project resolves it by running git -C <cwd> remote get-url origin. That only works on the machine holding the checkout. So the resolution has to happen here, in the uploader, while we're still standing on it.

What changed

Each session payload gains a projects map of cwd -> "org/repo", which coding_tools_backfill_service.resolve_record_projects() stamps onto the parsed records.

  • _backfill_remote_path mirrors unbound.py::_github_remote_path rather than matching on github.com, so a self-hosted Enterprise remote resolves the same way. Handles both the SSH scp form and the scheme form.
  • Output is lowercased "<org>/<repo>" — the exact shape _get_project emits, so a replayed row is comparable to a live one rather than merely similar.
  • One git call per distinct checkout per run, cached across every transcript. The cache stores failures too, so a deleted directory isn't retried once per session. A 5,000-session backfill costs a handful of git invocations, not thousands.
  • The cwd lookup checks the payload / data nesting Codex and Copilot use, matching the server's _entry_cwd.

Applied to all five uploaders: claude-code, codex, copilot, and both mdm variants. The server-side resolver handles every tool type, so fixing one tree would have left the others blind.

Nothing is required of the server

A backend that doesn't read projects ignores it. A session where nothing resolves omits the key entirely, so it's indistinguishable from an older client. Either order of deployment is safe — though reader-first means data starts landing the moment devices update.

Deliberately not inferred

A cwd that doesn't resolve gets no project rather than a guessed one. A wrong repository is worse than a missing one: it attributes someone's work to another team, and nothing downstream could tell it was a guess. The server-side half enforces the same rule.

Tests

claude-code/hooks/test_setup.py40 passed, 8 new covering: SSH/HTTPS/Enterprise URL forms, unparseable remotes, the lowercased org/repo output, a plain folder yielding nothing, git failure never raising, the per-directory cache (asserting exactly one call per distinct cwd), failures being cached too, and malformed entries.

Full repo: augment/hooks 133, codex/hooks 33, copilot/hooks 25, binary/tests 887 — all pass. Three failures remain and are identical on staging, verified by stashing: test_long_fields_are_capped_so_the_body_fits_the_pipe, test_helper_is_byte_identical_across_trees, test_keys_limited_to_identity_fields.

Also verified live: resolving this repo's own directory returns websentry-ai/setup.

Reviewer note

test_setup.py is CRLF in this repo. I rewrote it with LF on the first pass, which showed as a 1,102-line diff; the endings are restored, and the diff is now the 84 added lines only. Worth a glance at the raw diff to confirm.

🤖 Generated with Claude Code

https://claude.ai/code/session_016P7LvdubpsPtgR5vSenSSi

Greptile Summary

The PR resolves each historical session’s working directories to lowercase repository identifiers before upload, caching Git lookups across the run and applying the behavior to Claude Code, Codex, Copilot, and managed variants.

  • Adds origin-URL parsing and local git remote get-url origin resolution.
  • Adds a per-run cwd-to-project cache, including negative results.
  • Includes project maps in session payloads only when at least one cwd resolves.
  • Adds Claude setup tests for parsing, normalization, caching, and failure handling.

Confidence Score: 4/5

The PR should not merge until hostless Git remotes are rejected consistently with live repository resolution; silent failure handling is also worth improving.

A valid file:// origin is currently converted into a fabricated organization/repository and uploaded as authoritative attribution, while repository-resolution exceptions provide no actionable diagnostics.

Files Needing Attention: claude-code/hooks/setup.py, claude-code/hooks/mdm/setup.py, codex/hooks/setup.py, codex/hooks/mdm/setup.py, copilot/hooks/setup.py

Important Files Changed

Filename Overview
claude-code/hooks/setup.py Adds cached repository resolution to user backfill; hostless file remotes can be misattributed and resolution exceptions lack diagnostics.
claude-code/hooks/mdm/setup.py Mirrors repository resolution in managed backfill while preserving per-user collection, with the same parsing and diagnostic issues.
codex/hooks/setup.py Adds cwd project maps and shared caching to Codex backfill, duplicating the hostless-remote and silent-error behavior.
codex/hooks/mdm/setup.py Applies the same project-resolution logic to managed Codex sessions.
copilot/hooks/setup.py Adds cached repository attribution to Copilot backfill with the same parser contract divergence.
claude-code/hooks/test_setup.py Adds focused project-resolution tests but does not cover hostless file:// origins or diagnostic behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    T[Transcript entries] --> C[Extract distinct cwd values]
    C --> K{Cached?}
    K -->|Yes| M[Reuse cached project]
    K -->|No| G[git remote get-url origin]
    G --> P[Parse and lowercase org/repo]
    P --> S[Store result including failure]
    M --> U[Add projects map to session]
    S --> U
    U --> B[Upload backfill payload]
Loading

Reviews (1): Last reviewed commit: "Resolve each session's repository before..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used (6)

A replayed session lands with no repository, so backfilled work cannot be
attributed to a repo at all. The companion change in ai-gateway-data
recovers the working directory from the transcript, but a project is not in
the transcript: it comes from git's origin remote, and by the time a session
is replayed the backend is nowhere near the checkout.

So the resolution has to happen here, while the code is still standing on the
machine where the repos exist. Each session payload gains a `projects` map of
cwd -> "org/repo", which coding_tools_backfill_service stamps onto the parsed
records.

_backfill_remote_path mirrors unbound.py::_github_remote_path rather than
matching on github.com, so a self-hosted Enterprise remote resolves the same
way. Output is lowercased "<org>/<repo>", the shape _get_project already
emits, so a replayed row is comparable to a live one rather than merely
similar.

One git call per distinct checkout per run, cached across every transcript --
including the ones that fail to resolve, so a deleted directory is not
retried once per session. A 5,000-session backfill costs a handful of git
invocations.

Applied to all five uploaders. The cwd lookup checks the payload / data
nesting Codex and Copilot use, matching the server's _entry_cwd.

Nothing is required of the server: a backend that does not read `projects`
ignores it, and a session where nothing resolves omits the key entirely, so
it is indistinguishable from an older client.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016P7LvdubpsPtgR5vSenSSi
@gowshik450526511
gowshik450526511 requested a review from a team August 21, 2026 21:30
@cursor

cursor Bot commented Aug 21, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

Comment on lines +868 to +875
path = _backfill_remote_path(result.stdout.strip())
if not path:
return None
parts = path.split('/')
if len(parts) < 2:
return None
org = parts[0][:-4] if parts[0].endswith('.git') else parts[0]
repo = parts[1][:-4] if parts[1].endswith('.git') else parts[1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Hostless remotes fabricate projects

If a checkout uses a hostless origin such as file:///srv/git/x, this parser treats the local path components as srv/git, causing replayed work to be attributed to a fabricated project instead of leaving the project unset as the live resolver does.

Comment on lines +879 to +880
except Exception:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Resolution failures lack diagnostics

The broad exception handler silently discards Git availability, timeout, invalid-directory, and decoding errors, leaving operators unable to distinguish these failures from an intentionally unresolvable checkout when project attribution is missing.

Context Used: P0 — Critical (must block merge)
Django / Backend ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Findings

🟡 Hostless file:// remotes fabricate org/repo attribution

claude-code/hooks/setup.py:842 (same logic in claude-code/hooks/mdm/setup.py, codex/hooks/setup.py, codex/hooks/mdm/setup.py, copilot/hooks/setup.py)

Impact: For origins like file:///srv/git/x, _backfill_remote_path strips the scheme and treats path segments as org/repo (e.g. srv/git), uploading a fabricated project map—contradicting the PR’s “no guess / wrong repo is worse than missing” rule and misattributing replayed sessions.

Fix: Reject hostless scheme URLs (file://, etc.) before path parsing, matching live unbound.py::_get_project behavior; add a test case for file:///… expecting no project.

Flagged by: Lead, Greptile


Notes (not raised as findings): Gitleaks — no secrets. Claude — no issues. Semgrep pickle/file-permission hits are on unchanged lines outside this diff. subprocess.run(['git', '-C', cwd, ...]) uses a list argv (no shell injection); cwd is transcript-sourced on the local machine during backfill (expected trust boundary).


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head f6b9dd1b · 2026-08-21T21:41Z

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.

2 participants