Skip to content

Improve backport conflict resolution context and models - #8556

Open
hujc7 wants to merge 5 commits into
isaac-sim:mainfrom
hujc7:jichuanh/backport-resolver-context
Open

hujc7 wants to merge 5 commits into
isaac-sim:mainfrom
hujc7:jichuanh/backport-resolver-context

Conversation

@hujc7

@hujc7 hujc7 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The backport bot's resolver sent the model only the conflicted files and forced a full rewrite of each. The model could not see the PR's intent or release's layout, so it imported develop-only code: #8509 recreated install_ci/conftest.py fixtures and test_workspace.py from #8128, which release never received.

  • Models: default to azure/openai/gpt-6.1-sol, falling back to azure/anthropic/claude-opus-5-5. The NVIDIA_BACKPORT_MODEL / NVIDIA_BACKPORT_FALLBACK_MODEL repository variables still override.
  • Context: adds the source PR description, its full diff, and the cleanly merged paths.
  • Actions:
    • edit, write, keep_target or delete per conflicted path.
    • extra_edits for release's equivalent of code that exists only on develop.
    • blocked to fail with a reason instead of opening a wrong draft.
    • Every action carries a reason.
  • Tools and retries: read-only release-tree tools (read_file, list_dir, grep), capped at 10 calls. Up to 2 correction rounds when validation rejects a reply. Reasoning effort is medium.
  • Validation: backport.py validate-candidate --resolution_report accepts declared kept paths, which must equal release, and declared extra paths. All other checks are unchanged.
  • Failure report: when resolution fails or the model returns blocked, the bot comments on the source PR with the reason and the workflow run link, so a manual backport is not missed. The app token is created earlier so this step can use it.
  • Draft PR body:
    • Lists each path's resolution and reason.
    • Lists the cleanly merged paths for review.
    • States what validation actually checks; the previous text said "deterministic validation confirmed".

Validation

Ran this PR's resolver against the inference API on past conflicted backports, using azure/openai/gpt-6.1-sol at medium effort:

Source PR Bot PR (current resolver) This PR
#8233 #8509: imported develop-only fixtures and test_workspace.py Correct behavior on 2 of 2 trials: kept release's conftest.py, left test_workspace.py absent, and moved the ARM re-enable into the release uv_run test. It also restored the cleanly merged cartpole_training_smoke.py docstring, which a human had fixed by hand; its version is one docstring line off from the human fix.
#8467 #8507: kept an unrelated develop-only test Correct on trial 1, identical to the human fix

@hujc7
hujc7 marked this pull request as ready for review October 14, 2026 23:50
@hujc7
hujc7 requested a review from a team October 14, 2026 23:50
@hujc7

hujc7 commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

run-ci

@isaaclab-bot isaaclab-bot Bot added ci:run-docker Trigger the on-demand Docker and GPU CI workflow and removed ci:run-docker Trigger the on-demand Docker and GPU CI workflow labels Oct 14, 2026
@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[High impact] Adds AI model calls and GitHub token generation to backport automation.

Fix empty-file resolution before merging; valid backports can currently be rejected even when the model returns the right result.

Findings

  1. P1 Empty files cannot be resolved ▶
  2. P2 PR explanation is missing ▶
  3. P2 Editable text cannot be inspected ▶
  4. P2 Bad arguments stop recovery ▶
  5. P2 Unneeded backports report failure ▶
  6. P2 New checks lack regression tests ▶

Summary

The PR gives the backport resolver more context, release-tree tools, smaller edits, correction rounds, and a way to stop with a reason.

  • The backport model can now resolve conflicts using release-specific context.
  • Backport checks now allow declared kept and extra paths.
  • Failed backports now tell the source PR why manual work is needed.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Cherry-pick source change] --> B{Conflict?}
  B -->|No| C[Check exact patch and push release]
  B -->|Yes| D[Collect source change and conflicts]
  D --> E[Model reads release files]
  E --> F{Reply}
  F -->|Invalid| G[Request correction or try fallback]
  G --> E
  F -->|Blocked| H[Comment on source PR]
  F -->|Accepted| I[Apply edits and write report]
  I --> J[Run hooks and check candidate]
  J -->|Failure| H
  J -->|Pass| K[Continue cherry-pick and open draft]
Loading

Reviews (1) · Last reviewed commit: "Accept declared extra edits to cleanly m..." · Reviewed by Greptile

"source_parent": source_parent,
"source_commit": source,
"release_base": target,
"source_description": _git("show", "--no-patch", "--format=%B", source),

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.

P2 PR explanation is missing

source_description contains the commit message, not the PR description. The workflow supports merge commits and rebased PRs, whose messages can omit the PR's explanation. The prompt nevertheless tells the model it received that description, leaving it without context needed to choose the right release-side change.

Pass the actual PR body from the event or GitHub API. Keep the commit message as a separate field.

Comment on lines +438 to +439
if name == "read_file":
output = _git_bytes("show", f"{target}:{path}").decode("utf-8", errors="replace")

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.

P2 Editable text cannot be inspected

extra_edits search the current worktree, but read_file only shows the release version from before the cherry-pick. For a cleanly merged path, the model must reconstruct the current file to supply an exact search match. If the relevant source diff was truncated, it cannot inspect those applied changes at all.

Add a read-only way to inspect the current content used by extra_edits, while retaining access to the release version.

Comment on lines +479 to +480
if not isinstance(content, str) or not content:
raise ConflictResolutionError(f"model returned no text content for {path!r}")

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.

P1 Empty files cannot be resolved

_checked_content now rejects an empty string for both write and the result of edit. A source change can clear a file that release also modified, producing a conflict whose correct result is an empty tracked file. Neither action can express that result, and delete is rejected because the source still contains the file. Both models will fail even when they return the right content.

Allow empty strings while retaining the type, size, and NUL checks.

Suggested change
if not isinstance(content, str) or not content:
raise ConflictResolutionError(f"model returned no text content for {path!r}")
if not isinstance(content, str):
raise ConflictResolutionError(f"model returned non-text content for {path!r}")

Comment on lines +430 to +433
arguments = json.loads(function.get("arguments") or "{}")
except json.JSONDecodeError:
return "error: tool arguments are not valid JSON"
path = str(arguments.get("path") or "").strip("/")

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.

P2 Bad arguments stop recovery

json.loads can return a list, string, or number, but _run_tool immediately calls .get on the result. Model-generated arguments such as [] raise AttributeError instead of returning an error the model can correct. This also bypasses the fallback model and detailed failure report.

Check that the decoded arguments are an object before reading fields. Return a tool error for other shapes.

Suggested change
arguments = json.loads(function.get("arguments") or "{}")
except json.JSONDecodeError:
return "error: tool arguments are not valid JSON"
path = str(arguments.get("path") or "").strip("/")
arguments = json.loads(function.get("arguments") or "{}")
except json.JSONDecodeError:
return "error: tool arguments are not valid JSON"
if not isinstance(arguments, dict):
return "error: tool arguments must be a JSON object"
path = str(arguments.get("path") or "").strip("/")

Comment on lines 318 to +322
python3 ../automation/.github/scripts/backport.py validate-candidate \
--source_parent "$SOURCE_PARENT" \
--source "$SOURCE_SHA" \
--target "$TARGET_SHA"
--target "$TARGET_SHA" \
--resolution_report "$RUNNER_TEMP/backport-resolution.json"

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.

P2 Unneeded backports report failure

The new report accepts a resolution that keeps every path unchanged, but the next step always runs git cherry-pick --continue. When release already has the intended behavior and the model correctly chooses keep_target for every conflict, there are no changes to commit. Git rejects the empty cherry-pick, and the workflow incorrectly asks for a manual backport.

Detect this no-change result, end the cherry-pick, and report that no backport change was needed.

Comment on lines 98 to +108
missing_paths = source_paths - candidate_paths
changed_kept_paths = {path for path in kept_paths if _tree_entry(target, path) != _candidate_entry(candidate, path)}
if changed_kept_paths:
raise BackportValidationError(
"the candidate changes paths declared as kept at the release version: " + _format_paths(changed_kept_paths)
)
declared_paths = kept_paths | extra_paths
nonmatching_missing_paths = {
path for path in missing_paths if _tree_entry(source, path) != _candidate_entry(candidate, path)
path
for path in missing_paths - declared_paths
if _tree_entry(source, path) != _candidate_entry(candidate, path)

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.

P2 New checks lack regression tests

The new kept-path and extra-path exceptions have no automated tests. The reported offline checks cover successful historical resolutions, but not the checks that must still reject a bad candidate. Future changes could weaken these checks or reject valid backports unnoticed.

Add tests using temporary Git repositories to prove that:

  • Kept paths preserve both content and mode.
  • Undeclared extra paths fail.
  • A declared extra path can restore a cleanly merged file.

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!

@isaaclab-review-bot isaaclab-review-bot 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.

Isaac Lab Review Bot

Needs attention: Design and architecture (1), Implementation (2), Style consistency (1). Other reviewed categories look good.

  • PR description: The body mislabels the merge commit message as the PR description and a 300,000-character prefix as the full diff; correct both claims and add exact baseline/candidate commands and revisions for latency/cost figures.
  • Design and architecture: Release-tree tools cannot inspect the worktree content used for extra edits, so generated searches can target stale text; expose the actual edit basis read-only.
  • Scope and over-engineering: No over-engineering issues.
  • API: API changes look good.
  • Compatibility and deprecation: No breaking changes identified.
  • Implementation: Model-selected extra paths authorize their own validation exemption, allowing arbitrary existing repository files to be committed; enforce an independently derived allowlist before applying or validating extra edits.
  • Style consistency: Repeated numeric caps can drift from tool descriptions and summary behavior; define named module constants and interpolate them at each use.
  • Test quality: Test quality looks good.

Significant concerns. Posted 4 inline findings.

Automated review; human maintainers own approval decisions.

"source_parent": source_parent,
"source_commit": source,
"release_base": target,
"source_description": _git("show", "--no-patch", "--format=%B", source),

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.

🟡 Warning · Implementation — Commit message labeled PR description

%B of the merged commit is not the PR description for rebased merges or default squash messages, yet the prompt presents it as such. Pass github.event.pull_request.body instead.

if not isinstance(edit, dict) or set(edit) != {"path", "edits", "reason"}:
raise ConflictResolutionError("each extra edit must contain exactly path, edits, and reason")
path = edit["path"]
if not isinstance(path, str) or path in files or path in {item["path"] for item in validated_extra}:

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.

🟡 Warning · Implementation — Extra edits accept any repository path

Extra paths are only checked for existence and are exempted from validate-candidate's outside-the-PR check, so model-chosen edits to any file, including .github/ workflows, get committed. Restrict the allowed extra paths.

if path:
_validate_repository_path(path)
if name == "read_file":
output = _git_bytes("show", f"{target}:{path}").decode("utf-8", errors="replace")

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.

🟡 Warning · Design Architecture — read_file basis differs from extra edits

read_file returns the release blob, but extra edits apply to worktree content, which already carries the cleanly merged source change. Search text taken from this tool can fail the exactly-once check.

output = _git_bytes("show", f"{target}:{path}").decode("utf-8", errors="replace")
elif name == "list_dir":
entries = _git("ls-tree", "-r", "--name-only", target, "--", path).splitlines()
output = "\n".join(entries[:200]) + ("\n..." if len(entries) > 200 else "")

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.

🟡 Warning · Style Consistency — Hard-coded caps bypass module constants

list_dir's 200 and grep's 100 duplicate the numbers stated in their tool descriptions, and write_summary repeats 50 three times. Define _MAX_* constants beside the adjacent limits so the values cannot drift.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant