Skip to content

Feat/persist inline comments#2358

Open
avidspartan1 wants to merge 9 commits intoThe-PR-Agent:mainfrom
avidspartan1:feat/persist-inline-comments
Open

Feat/persist inline comments#2358
avidspartan1 wants to merge 9 commits intoThe-PR-Agent:mainfrom
avidspartan1:feat/persist-inline-comments

Conversation

@avidspartan1
Copy link
Copy Markdown

Resolves #2357 and #2037 (and likely other dupes of "Persist Inline Comments")

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Persist inline comments with deduplication and auto feedback disable improvements

✨ Enhancement 🧪 Tests 🐞 Bug fix 📝 Documentation

Grey Divider

Walkthroughs

Description
• **Persistent inline comments with deduplication**: Implemented core deduplication algorithm in
  inline_comments_dedup.py using stable content-derived hashing to identify and update existing
  inline comments across PR runs
• **GitHub provider support**: Added get_bot_review_comments(), edit_review_comment(), and
  thread resolution methods using GraphQL; enhanced publish_code_suggestions() with deduplication
  logic supporting off/update/skip modes
• **GitLab provider support**: Implemented equivalent persistent inline comment functionality with
  MR discussion fetching and thread resolution capabilities
• **Configuration options**: Added persistent_inline_comments (update/skip/off modes),
  resolve_outdated_inline_comments, and claude_extended_thinking_models_override settings
• **Base provider interface**: Extended git_provider.py with new methods for inline comment
  management and fixed get_user_description() parsing bug
• **Documentation**: Added comprehensive guide explaining deduplication mechanism, identity rules,
  and configuration
• **Auto feedback disable simplification**: Unified auto feedback disable logic across all webhook
  servers (GitHub, GitLab, Bitbucket, Gitea, Azure DevOps) and GitHub Action runner
• **Add docs integration**: Added original_suggestion field to documentation suggestions for
  deduplication support
• **Extensive test coverage**: Added 769 lines of GitLab tests, 634 lines of GitHub tests, plus core
  algorithm, integration, and configuration validation tests
Diagram
flowchart LR
  A["PR Suggestions<br/>GitHub/GitLab"] -->|"Deduplication<br/>Algorithm"| B["inline_comments_dedup.py<br/>Stable Hashing"]
  B -->|"Match & Update"| C["Existing Bot<br/>Comments"]
  C -->|"Resolve/Unresolve<br/>Threads"| D["Updated PR<br/>with Persistent<br/>Comments"]
  E["Configuration<br/>persistent_inline_comments<br/>resolve_outdated_inline_comments"] -.->|"Controls Behavior"| B
  F["GitHub/GitLab<br/>Providers"] -->|"Fetch & Edit"| C
Loading

Grey Divider

File Changes

1. tests/unittest/test_gitlab_inline_dedup.py 🧪 Tests +769/-0

GitLab inline comment deduplication test suite

• Comprehensive test suite for GitLab inline comment deduplication with 769 lines covering
 off/update/skip modes
• Tests for get_bot_review_comments filtering by bot identity and handling multi-line notes
• Tests for edit_review_comment and thread resolution/unresolution functionality
• Tests for outdated comment resolution pass with various edge cases

tests/unittest/test_gitlab_inline_dedup.py


2. tests/unittest/test_github_inline_dedup.py 🧪 Tests +634/-0

GitHub inline comment deduplication test suite

• Comprehensive test suite for GitHub inline comment deduplication with 634 lines
• Tests for persistent comment modes (off/update/skip) and GraphQL-backed comment retrieval
• Tests for thread resolution/unresolution mutations and pagination handling
• Tests for outdated pass and structured hash-based comment matching

tests/unittest/test_github_inline_dedup.py


3. pr_agent/git_providers/github_provider.py ✨ Enhancement +267/-13

GitHub persistent inline comments with deduplication

• Added get_bot_review_comments() using GraphQL to fetch existing bot review comments with thread
 resolution state
• Implemented edit_review_comment() and thread mutation methods (resolve_review_thread,
 unresolve_review_thread)
• Enhanced publish_code_suggestions() with persistent inline comment deduplication logic
 supporting off/update/skip modes
• Added outdated pass to auto-resolve threads for suggestions no longer emitted

pr_agent/git_providers/github_provider.py


View more (19)
4. tests/unittest/test_inline_comments_dedup.py 🧪 Tests +296/-0

Inline comment deduplication core logic tests

• Tests for marker generation with stable hashing based on file, label, and content
• Tests for marker extraction, appending, and building marker indices
• Tests for persistent mode normalization and code normalization utilities
• Tests for structured suggestions with improved_code field and prose fallback logic

tests/unittest/test_inline_comments_dedup.py


5. pr_agent/git_providers/gitlab_provider.py ✨ Enhancement +192/-3

GitLab persistent inline comments with deduplication

• Implemented get_bot_review_comments() to fetch bot's inline comments from MR discussions
• Added edit_review_comment() and thread resolution methods (resolve_review_thread,
 unresolve_review_thread)
• Enhanced publish_code_suggestions() with persistent inline comment deduplication matching GitHub
 implementation
• Added outdated pass to auto-resolve threads for suggestions no longer emitted

pr_agent/git_providers/gitlab_provider.py


6. pr_agent/algo/inline_comments_dedup.py ✨ Enhancement +210/-0

Inline comment deduplication core algorithm module

• Core module for stable marker generation using content-derived hashing (file, label,
 improved_code)
• Implements structured-first, prose-fallback deduplication strategy with version tags
• Provides utilities for marker extraction, appending, indexing, and resolved body formatting
• Supports three persistent modes: off, update, skip with configurable outdated comment resolution

pr_agent/algo/inline_comments_dedup.py


7. tests/unittest/test_disable_auto_feedback.py 🧪 Tests +129/-0

Auto feedback disable feature tests

• Tests for disable_auto_feedback configuration flag in GitHub App push trigger
• Tests for GitHub Action runner respecting the disable flag and skipping automatic tools
• Verifies that PRAgent.handle_request is not called when auto feedback is disabled

tests/unittest/test_disable_auto_feedback.py


8. pr_agent/git_providers/git_provider.py ✨ Enhancement +49/-2

Base provider interface for inline comment deduplication

• Added base class methods get_bot_review_comments(), edit_review_comment(),
 resolve_review_thread(), unresolve_review_thread() with default no-op implementations
• Fixed get_user_description() to correctly find next header position using find() with start
 offset
• Provides interface contract for providers implementing persistent inline comment deduplication

pr_agent/git_providers/git_provider.py


9. tests/unittest/test_git_provider_description.py 🧪 Tests +91/-0

Git provider description parsing test

• Test for get_user_description() correctly extracting user section before generated headers
• Verifies that "Generated by PR Agent" header does not interfere with user description parsing

tests/unittest/test_git_provider_description.py


10. tests/unittest/test_add_docs_inline_dedup.py 🧪 Tests +41/-0

Add docs inline deduplication integration test

• Test verifying that PRAddDocs.push_inline_docs() emits markerable original_suggestion field
• Validates that generated markers can be created from documentation suggestions

tests/unittest/test_add_docs_inline_dedup.py


11. pr_agent/algo/__init__.py ⚙️ Configuration changes +10/-1

Extended Claude extended thinking model support

• Extended CLAUDE_EXTENDED_THINKING_MODELS list with additional Claude Sonnet 4.6 model variants
• Added support for vertex_ai and bedrock deployment variants with regional endpoints

pr_agent/algo/init.py


12. pr_agent/servers/gitlab_webhook.py 🐞 Bug fix +2/-2

GitLab webhook auto feedback disable simplification

• Simplified auto feedback disable check by removing commands_conf == "pr_commands" condition
• Now skips auto commands whenever disable_auto_feedback is true, regardless of command type

pr_agent/servers/gitlab_webhook.py


13. tests/unittest/test_inline_comments_dedup_constants.py 🧪 Tests +27/-0

Inline comment deduplication constants validation tests

• Smoke tests for resolved marker format and detectability
• Tests for base GitProvider default implementations returning False

tests/unittest/test_inline_comments_dedup_constants.py


14. pr_agent/tools/pr_add_docs.py ✨ Enhancement +10/-1

Add docs original suggestion field for deduplication

• Added original_suggestion field to documentation suggestions for deduplication support
• Includes file, line numbers, label, content, and improved_code for marker generation

pr_agent/tools/pr_add_docs.py


15. pr_agent/servers/bitbucket_app.py 🐞 Bug fix +2/-2

Bitbucket app auto feedback disable simplification

• Simplified auto feedback disable check by removing commands_conf == "pr_commands" condition
• Now skips auto commands whenever disable_auto_feedback is true

pr_agent/servers/bitbucket_app.py


16. pr_agent/servers/gitea_app.py 🐞 Bug fix +2/-2

Gitea app auto feedback disable simplification

• Simplified auto feedback disable check by removing commands_conf == "pr_commands" condition
• Now skips auto commands whenever disable_auto_feedback is true

pr_agent/servers/gitea_app.py


17. pr_agent/servers/github_app.py 🐞 Bug fix +2/-2

GitHub app auto feedback disable simplification

• Simplified auto feedback disable check by removing commands_conf == "pr_commands" condition
• Now skips auto commands whenever disable_auto_feedback is true

pr_agent/servers/github_app.py


18. pr_agent/servers/azuredevops_server_webhook.py 🐞 Bug fix +2/-2

Azure DevOps auto feedback disable simplification

• Simplified auto feedback disable check by removing commands_conf == "pr_commands" condition
• Now skips auto commands whenever disable_auto_feedback is true

pr_agent/servers/azuredevops_server_webhook.py


19. pr_agent/algo/ai_handlers/litellm_ai_handler.py ✨ Enhancement +3/-2

Claude extended thinking models override configuration

• Added support for claude_extended_thinking_models_override configuration to allow custom model
 lists
• Falls back to built-in CLAUDE_EXTENDED_THINKING_MODELS when override is empty

pr_agent/algo/ai_handlers/litellm_ai_handler.py


20. pr_agent/servers/github_action_runner.py ✨ Enhancement +4/-0

GitHub Action runner auto feedback disable check

• Added check for disable_auto_feedback configuration to skip automatic tools in GitHub Actions
• Logs info message and returns early when auto feedback is disabled

pr_agent/servers/github_action_runner.py


21. docs/docs/tools/improve.md 📝 Documentation +40/-0

Document inline comment deduplication feature and configuration

• Added documentation for two new configuration parameters: persistent_inline_comments and
 resolve_outdated_inline_comments
• Added comprehensive section "How inline-comment deduplication works" explaining the deduplication
 mechanism, identity rules, and behavior across runs
• Documented the structured hash approach using file and normalized improved_code, with prose
 fallback when no code is available
• Explained invariant behaviors (prose paraphrase, reindentation, upstream commits) and what still
 causes comment splitting

docs/docs/tools/improve.md


22. pr_agent/settings/configuration.toml ⚙️ Configuration changes +13/-0

Add inline comment persistence and deduplication configuration options

• Added claude_extended_thinking_models_override configuration parameter to allow overriding the
 built-in list of Claude models for extended thinking
• Added persistent_inline_comments configuration with three modes: "update" (default), "skip", and
 "off" to control inline suggestion deduplication
• Added resolve_outdated_inline_comments configuration (default true) to automatically resolve
 inline-comment threads when suggestions are no longer emitted
• Included detailed comments explaining the purpose and behavior of each new configuration option

pr_agent/settings/configuration.toml


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 29, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (3) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. resolve_outdated bool-cast misparses📘 Rule violation ☼ Reliability
Description
resolve_outdated_inline_comments is coerced via bool(raw) which treats any non-empty string
(e.g., "false") as True, causing silent misconfiguration. This violates the requirement to
validate/normalize configuration inputs at boundaries.
Code

pr_agent/git_providers/github_provider.py[R723-728]

+        mode = normalize_persistent_mode(
+            get_settings().pr_code_suggestions.get("persistent_inline_comments", PERSISTENT_MODE_OFF)
+        )
+        resolve_outdated = bool(
+            get_settings().pr_code_suggestions.get("resolve_outdated_inline_comments", True)
+        )
Evidence
Compliance ID 24 requires config inputs be validated/normalized; the new code uses
bool(get_settings()...get(...)), which is not a safe normalization for string-sourced config
values and can invert intended behavior without warning.

pr_agent/git_providers/github_provider.py[723-728]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolve_outdated_inline_comments` is normalized with `bool(raw)`, which misparses string config values (e.g., env var `"false"` becomes `True`).
## Issue Context
This flag is user/operator-controlled configuration and should accept common boolean representations with targeted warnings/errors on invalid values.
## Fix Focus Areas
- pr_agent/git_providers/github_provider.py[723-728]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. edit_review_comment() called with None📘 Rule violation ☼ Reliability
Description
The code uses existing.get("id") / c.get("id") without validating presence/type before editing
comments or tracking IDs, but upstream API fields like GitHub databaseId can be missing (None).
This can lead to invalid API calls (e.g., /pulls/comments/None) and incorrect dedup/outdated
behavior.
Code

pr_agent/git_providers/github_provider.py[R644-808]

+                        out.append({
+                            "id": c.get("databaseId"),
+                            "thread_id": thread_id,
+                            "body": c.get("body") or "",
+                            "path": c.get("path"),
+                            "line": c.get("line"),
+                            "start_line": c.get("startLine"),
+                            "is_resolved": is_resolved,
+                        })
+                cursor = page_info.get("endCursor")
+                if not page_info.get("hasNextPage") or not cursor:
+                    break
+            return out
+        except Exception as e:
+            get_logger().warning(f"Failed to list GitHub review comments via GraphQL: {e}")
+            return []
+
+    def edit_review_comment(self, comment_id, body: str) -> bool:
+        try:
+            body = self.limit_output_characters(body, self.max_comment_chars)
+            self.pr._requester.requestJsonAndCheck(
+                "PATCH",
+                f"{self.base_url}/repos/{self.repo}/pulls/comments/{comment_id}",
+                input={"body": body},
+            )
+            return True
+        except Exception as e:
+            get_logger().warning(f"Failed to edit GitHub review comment {comment_id}: {e}")
+            return False
+
+    _RESOLVE_THREAD_MUTATION = """
+    mutation($threadId:ID!) {
+      resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } }
+    }
+    """
+
+    _UNRESOLVE_THREAD_MUTATION = """
+    mutation($threadId:ID!) {
+      unresolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } }
+    }
+    """
+
+    def _run_thread_mutation(self, query: str, comment: dict) -> bool:
+        thread_id = comment.get("thread_id")
+        if not thread_id:
+            return False
+        try:
+            _, data = self.pr._requester.requestJsonAndCheck(
+                "POST",
+                self._graphql_url(),
+                input={"query": query, "variables": {"threadId": thread_id}},
+            )
+            if not data or data.get("errors"):
+                get_logger().warning(
+                    f"GitHub thread mutation errors for {thread_id}: {(data or {}).get('errors')}"
+                )
+                return False
+            return True
+        except Exception as e:
+            get_logger().warning(f"GitHub thread mutation failed for {thread_id}: {e}")
+            return False
+
+    def resolve_review_thread(self, comment: dict) -> bool:
+        return self._run_thread_mutation(self._RESOLVE_THREAD_MUTATION, comment)
+
+    def unresolve_review_thread(self, comment: dict) -> bool:
+        return self._run_thread_mutation(self._UNRESOLVE_THREAD_MUTATION, comment)
+    def publish_code_suggestions(self, code_suggestions: list) -> bool:
+        """
+        Publishes code suggestions as review comments on the PR.
+
+        When `pr_code_suggestions.persistent_inline_comments` is 'update' (default)
+        or 'skip', a stable marker is embedded in each body so subsequent runs
+        can recognize and update (or skip) the existing comment rather than
+        creating duplicates.
+        """
    code_suggestions_validated = self.validate_comments_inside_hunks(code_suggestions)
+        mode = normalize_persistent_mode(
+            get_settings().pr_code_suggestions.get("persistent_inline_comments", PERSISTENT_MODE_OFF)
+        )
+        resolve_outdated = bool(
+            get_settings().pr_code_suggestions.get("resolve_outdated_inline_comments", True)
+        )
+
+        existing_index: dict[str, list[dict]] = {}
+        if mode != PERSISTENT_MODE_OFF:
+            try:
+                existing_index = build_marker_index(self.get_bot_review_comments())
+            except Exception as e:
+                get_logger().warning(f"persistent_inline_comments: fetch failed, falling back to create-new: {e}")
+                existing_index = {}
+
+        reused_comment_ids: set[int] = set()
+        post_parameters_list = []
    for suggestion in code_suggestions_validated:
-            body = suggestion['body']
-            relevant_file = suggestion['relevant_file']
-            relevant_lines_start = suggestion['relevant_lines_start']
-            relevant_lines_end = suggestion['relevant_lines_end']
+            body = suggestion["body"]
+            relevant_file = suggestion["relevant_file"]
+            relevant_lines_start = suggestion["relevant_lines_start"]
+            relevant_lines_end = suggestion["relevant_lines_end"]
        if not relevant_lines_start or relevant_lines_start == -1:
            get_logger().exception(
                f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
            continue
-
        if relevant_lines_end < relevant_lines_start:
-                get_logger().exception(f"Failed to publish code suggestion, "
-                                  f"relevant_lines_end is {relevant_lines_end} and "
-                                  f"relevant_lines_start is {relevant_lines_start}")
+                get_logger().exception(
+                    f"Failed to publish code suggestion, "
+                    f"relevant_lines_end is {relevant_lines_end} and "
+                    f"relevant_lines_start is {relevant_lines_start}")
            continue
+            if mode != PERSISTENT_MODE_OFF:
+                marker = generate_marker(suggestion.get("original_suggestion") or suggestion)
+                if marker:
+                    body = append_marker(body, marker)
+                    marker_hash = marker[len(MARKER_PREFIX):-len(MARKER_SUFFIX)]
+                    existing = find_comment_by_location(
+                        existing_index.get(marker_hash, []),
+                        relevant_file,
+                        relevant_lines_start,
+                        relevant_lines_end,
+                    )
+                    if existing is not None:
+                        if mode == PERSISTENT_MODE_SKIP:
+                            if resolve_outdated and (
+                                existing.get("is_resolved")
+                                or RESOLVED_BODY_MARKER in (existing.get("body") or "")
+                            ):
+                                if self.edit_review_comment(existing.get("id"), body):
+                                    reused_comment_ids.add(existing.get("id"))
+                                    if existing.get("is_resolved"):
+                                        self.unresolve_review_thread(existing)
+                                    continue
+                                get_logger().info(
+                                    f"persistent_inline_comments=skip: reopen failed for {existing.get('id')}; "
+                                    f"falling back to create-new"
+                                )
+                            else:
+                                reused_comment_ids.add(existing.get("id"))
+                                get_logger().info(
+                                    f"persistent_inline_comments=skip: existing comment {existing.get('id')} "
+                                    f"on {relevant_file}; not re-posting")
+                                continue
+                        else:
+                            # mode == update
+                            if self.edit_review_comment(existing.get("id"), body):
+                                reused_comment_ids.add(existing.get("id"))
+                                # If we previously auto-resolved this thread but the
+                                # suggestion is back, unresolve it.
+                                if resolve_outdated and existing.get("is_resolved"):
+                                    self.unresolve_review_thread(existing)
+                                continue
+                            get_logger().info(
+                                f"persistent_inline_comments=update: edit failed for {existing.get('id')}; "
+                                f"falling back to create-new")
+                    elif mode == PERSISTENT_MODE_SKIP:
+                        # No same-location match, so allow a new inline comment and
+                        # let the outdated pass resolve any stale thread with this marker.
+                        pass
+
        if relevant_lines_end > relevant_lines_start:
            post_parameters = {
                "body": body,
Evidence
Compliance ID 25 requires defensive access/type checks for variable external structures; the code
records databaseId directly (possibly None) and later passes existing.get("id")/c.get("id")
into edit calls and a set[int] without guards.

pr_agent/git_providers/github_provider.py[644-652]
pr_agent/git_providers/github_provider.py[774-797]
pr_agent/git_providers/github_provider.py[827-837]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The GitHub provider assumes external comment dicts always have a valid integer `id` and uses it for API calls and in `reused_comment_ids`. If `databaseId` is missing (`None`) the code can call `edit_review_comment(None, ...)` and/or add `None` into the ID set.
## Issue Context
`databaseId` is external API data and may be absent depending on permissions/server response. Dedup/outdated logic should fail safely when IDs are missing.
## Fix Focus Areas
- pr_agent/git_providers/github_provider.py[644-652]
- pr_agent/git_providers/github_provider.py[774-797]
- pr_agent/git_providers/github_provider.py[827-837]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Resolve marker not guaranteed🐞 Bug ≡ Correctness
Description
In the outdated-pass, the provider resolves a review thread before ensuring the comment body was
successfully updated with the RESOLVED_BODY_MARKER. If edit_review_comment fails after a successful
resolve, a reviewer who later unresolves the thread cannot be detected as an opt-out and the bot may
re-resolve it in later runs.
Code

pr_agent/git_providers/github_provider.py[R835-837]

+                        if not self.resolve_review_thread(c):
+                            continue
+                        self.edit_review_comment(c.get("id"), format_resolved_body(c.get("body") or ""))
Evidence
The outdated-pass resolves first and then edits the body, but ignores whether the body update
succeeded. The opt-out/idempotency design relies on RESOLVED_BODY_MARKER being present in the body
to detect manual unresolve and to skip future auto-resolves.

pr_agent/git_providers/github_provider.py[823-841]
pr_agent/git_providers/github_provider.py[661-672]
pr_agent/algo/inline_comments_dedup.py[21-29]
pr_agent/algo/inline_comments_dedup.py[190-200]
docs/docs/tools/improve.md[312-314]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The outdated-pass resolves a thread and then attempts to edit the comment body to append the auto-resolve note + `RESOLVED_BODY_MARKER`. If the edit fails, the thread is resolved without the marker, breaking the documented “manual unresolve opts out” behavior.
### Issue Context
`RESOLVED_BODY_MARKER` is the durable signal used to detect prior auto-resolution (and respect manual unresolve). The current implementation does not verify that the marker was written.
### Fix Focus Areas
- pr_agent/git_providers/github_provider.py[823-841]
- pr_agent/git_providers/gitlab_provider.py[863-881]
### Suggested fix
Implement a best-effort transactional flow per outdated comment:
1) Attempt to write the resolved body (with marker) first OR resolve first but **if body edit fails**, immediately attempt to `unresolve_review_thread` (best-effort) so you don’t leave the thread resolved without the marker.
2) Check return values from `edit_review_comment` and log when the marker write fails.
3) Only treat the outdated comment as auto-resolved when the marker write succeeded (so manual unresolve can be honored later).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Override allowlist not validated📘 Rule violation ☼ Reliability
Description
claude_extended_thinking_models_override is consumed without validating its type/shape, so a
misconfigured value (e.g., a string) will be treated as an iterable and produce an incorrect model
allowlist. This violates the requirement to validate/normalize configuration inputs at the boundary
and can silently disable extended-thinking eligibility.
Code

pr_agent/algo/ai_handlers/litellm_ai_handler.py[R150-152]

+        # Models that support extended thinking (config override replaces the built-in list when non-empty)
+        override = get_settings().config.get("claude_extended_thinking_models_override", []) or []
+        self.claude_extended_thinking_models = list(override) if override else CLAUDE_EXTENDED_THINKING_MODELS
Evidence
PR Compliance ID 20 requires validating/normalizing config inputs. The new code reads
claude_extended_thinking_models_override and applies list(override) without checking that
override is a list of model-id strings, so invalid types can change runtime behavior unexpectedly.

pr_agent/algo/ai_handlers/litellm_ai_handler.py[150-152]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`claude_extended_thinking_models_override` is not validated before being used, so invalid types (e.g., a string) can lead to incorrect allowlist behavior.
## Issue Context
Config inputs should be validated/normalized at boundaries; unknown/invalid values should warn and fall back to safe defaults.
## Fix Focus Areas
- pr_agent/algo/ai_handlers/litellm_ai_handler.py[150-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. repo_context_max_lines no warning 📘 Rule violation ◔ Observability ⭐ New
Description
Invalid repo_context_max_lines values silently fall back to 500 without any targeted warning,
making misconfiguration hard to detect. This conflicts with the requirement to emit targeted
warnings/errors when config inputs are invalid.
Code

pr_agent/algo/repo_context.py[R10-15]

+    max_lines = get_settings().config.get("repo_context_max_lines", 500)
+    try:
+        max_lines = max(0, int(max_lines))
+    except (TypeError, ValueError):
+        max_lines = 500
+
Evidence
PR Compliance ID 20 requires invalid configuration values trigger targeted warnings/errors; the new
code catches parsing errors and falls back to 500 with no warning/log.

pr_agent/algo/repo_context.py[10-15]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`repo_context_max_lines` falls back to `500` on invalid input without logging a targeted warning.

## Issue Context
Operators need actionable diagnostics when configuration inputs are malformed.

## Fix Focus Areas
- pr_agent/algo/repo_context.py[10-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Unresolve failure ignored 🐞 Bug ☼ Reliability ⭐ New
Description
When re-emitting a suggestion that matched an existing resolved thread,
GithubProvider/GitLabProvider call unresolve_review_thread() but ignore its boolean result and
proceed as if reopening succeeded. If unresolve fails (permissions/API error), the thread can remain
resolved even though the suggestion was updated, with no log signal and no fallback behavior.
Code

pr_agent/git_providers/github_provider.py[R824-828]

+                                if self.edit_review_comment(existing_id, body):
+                                    reused_comment_ids.add(existing_id)
+                                    if existing.get("is_resolved"):
+                                        self.unresolve_review_thread(existing)
+                                    continue
Evidence
GitProvider.unresolve_review_thread is documented to return True/False, but the GitHub and GitLab
providers do not check the return value in the “reopen” paths (they immediately continue after
editing), so a failed unresolve is silently ignored.

pr_agent/git_providers/git_provider.py[382-389]
pr_agent/git_providers/github_provider.py[819-848]
pr_agent/git_providers/gitlab_provider.py[805-834]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When a previously auto-resolved thread is re-emitted, the provider attempts to unresolve it but ignores the return value. If the unresolve fails, the code continues as if successful and does not emit any warning or fallback behavior.

### Issue Context
`unresolve_review_thread()` is explicitly a boolean-returning operation per the base provider contract. The existing code treats it as best-effort but provides no observability and no behavioral fallback when it fails.

### Fix Focus Areas
- pr_agent/git_providers/github_provider.py[819-848]
- pr_agent/git_providers/gitlab_provider.py[805-834]
- pr_agent/git_providers/git_provider.py[382-389]

### Suggested fix
- Check the return value of `unresolve_review_thread(existing)` in both providers.
- If it returns False:
 - log a warning including the provider thread/comment id, and
 - consider a fallback (e.g., do not `continue` so the code can create a new inline comment thread, or keep current behavior but at least log and/or remove the resolved-body marker if present).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Whitespace breaks model override🐞 Bug ≡ Correctness
Description
claude_extended_thinking_models_override entries are validated with model.strip() but stored
untrimmed, while eligibility later uses exact string equality (`model in
self.claude_extended_thinking_models`). This can silently disable extended-thinking for an
otherwise-correct override like [" anthropic/claude-sonnet-4-6 "].
Code

pr_agent/algo/ai_handlers/litellm_ai_handler.py[R150-165]

+        # Models that support extended thinking (config override replaces the built-in list when non-empty)
+        override = get_settings().config.get("claude_extended_thinking_models_override", []) or []
+        if override and not isinstance(override, list):
+            get_logger().warning(
+                "Invalid claude_extended_thinking_models_override in config; expected a list of model names. "
+                "Falling back to the built-in Claude extended-thinking model list."
+            )
+            override = []
+        elif override and not all(isinstance(model, str) and model.strip() for model in override):
+            get_logger().warning(
+                "Invalid claude_extended_thinking_models_override in config; "
+                "expected a list of model name strings. "
+                "Falling back to the built-in Claude extended-thinking model list."
+            )
+            override = []
+        self.claude_extended_thinking_models = list(override) if override else CLAUDE_EXTENDED_THINKING_MODELS
Evidence
The override validation checks model.strip() only for non-emptiness, but does not normalize the
stored strings; later, extended-thinking is gated by an exact membership check against the stored
list, so whitespace differences prevent matches.

pr_agent/algo/ai_handlers/litellm_ai_handler.py[150-165]
pr_agent/algo/ai_handlers/litellm_ai_handler.py[382-384]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`claude_extended_thinking_models_override` accepts model strings with leading/trailing whitespace and keeps them as-is, but later checks model eligibility via exact string equality. This can prevent extended-thinking from activating when operators include whitespace in config values.
### Issue Context
The validation currently only uses `model.strip()` for truthiness, not normalization.
### Fix Focus Areas
- pr_agent/algo/ai_handlers/litellm_ai_handler.py[150-165]
### Implementation direction
- After validating `override` is a list of non-empty strings, normalize it, e.g.:
- `override = [m.strip() for m in override]`
- Optionally drop duplicates while preserving order.
- Keep the existing “override replaces defaults when non-empty” behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (6)
8. App author match too loose 🐞 Bug ⛨ Security
Description
GitHub App deployments consider a comment bot-authored when the configured app name is a substring
of the author login, which can misclassify other users/bots and cause dedup updates/resolves on the
wrong review threads.
Code

pr_agent/git_providers/github_provider.py[R638-642]

+                        if self.deployment_type == "app":
+                            same_author = bool(our_app_name) and our_app_name in login
+                        elif self.deployment_type == "user":
+                            same_author = bool(bot_user_id) and login == bot_user_id
+                        if not same_author:
Evidence
The new dedup/resolution logic depends on get_bot_review_comments filtering to only the bot’s
comments; substring matching can select other authors unintentionally.

pr_agent/git_providers/github_provider.py[607-653]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`get_bot_review_comments()` uses substring matching (`our_app_name in login`) to decide whether a review comment belongs to the GitHub App bot. This can match unintended authors and lead to editing/resolving the wrong threads.
### Issue Context
For GitHub Apps, the bot login is typically exactly `{app_name}[bot]`.
### Fix Focus Areas
- pr_agent/git_providers/github_provider.py[607-653]
### Suggested fix
Replace substring matching with a stricter check, e.g.:
- `login == f"{our_app_name}[bot]"` (case-insensitive)
Optionally also allow exact equality to `our_app_name` to support any nonstandard deployments, but avoid substring containment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Dedup fallback default mismatch 🐞 Bug ☼ Reliability
Description
If persistent_inline_comments is absent from settings, providers fall back to mode "off" even though
docs describe "update" as the default, so dedup can silently disable in deployments with
incomplete/custom config loading.
Code

pr_agent/git_providers/github_provider.py[R723-725]

+        mode = normalize_persistent_mode(
+            get_settings().pr_code_suggestions.get("persistent_inline_comments", PERSISTENT_MODE_OFF)
+        )
Evidence
The providers pass PERSISTENT_MODE_OFF as the default to .get(), and normalize_persistent_mode(None)
returns 'off', while docs describe 'update' as the default behavior when enabled.

pr_agent/git_providers/github_provider.py[723-725]
pr_agent/git_providers/gitlab_provider.py[765-767]
pr_agent/algo/inline_comments_dedup.py[203-210]
docs/docs/tools/improve.md[308-310]
docs/docs/tools/improve.md[356-358]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `persistent_inline_comments` is missing from config/settings, the code defaults to `off`, but docs state the default is `update`. This can silently disable dedup for setups that don’t load the new key.
### Issue Context
This is an edge-case/defaulting mismatch, not a failure when configuration.toml is used as shipped.
### Fix Focus Areas
- pr_agent/git_providers/github_provider.py[723-725]
- pr_agent/git_providers/gitlab_provider.py[765-767]
- pr_agent/algo/inline_comments_dedup.py[203-210]
### Suggested fix
Pick one:
- Make the fallback default `update` (change the `.get(..., ...)` default and/or have `normalize_persistent_mode(None)` return `update`).
- Or, if you intentionally want fail-closed behavior, update the docs to explicitly say the default is `update` only when the key is present, otherwise it defaults to `off`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. publish_code_suggestions does too much 📘 Rule violation ⚙ Maintainability
Description
The updated publish_code_suggestions now mixes publishing, deduplication, comment updating, and an
“outdated pass” thread-resolution workflow in one method. This violates the single-responsibility
guideline and increases maintenance and regression risk.
Code

pr_agent/git_providers/github_provider.py[R712-808]

+    def publish_code_suggestions(self, code_suggestions: list) -> bool:
+        """
+        Publishes code suggestions as review comments on the PR.
+
+        When `pr_code_suggestions.persistent_inline_comments` is 'update' (default)
+        or 'skip', a stable marker is embedded in each body so subsequent runs
+        can recognize and update (or skip) the existing comment rather than
+        creating duplicates.
+        """
  code_suggestions_validated = self.validate_comments_inside_hunks(code_suggestions)
+        mode = normalize_persistent_mode(
+            get_settings().pr_code_suggestions.get("persistent_inline_comments", PERSISTENT_MODE_OFF)
+        )
+        resolve_outdated = bool(
+            get_settings().pr_code_suggestions.get("resolve_outdated_inline_comments", True)
+        )
+
+        existing_index: dict[str, list[dict]] = {}
+        if mode != PERSISTENT_MODE_OFF:
+            try:
+                existing_index = build_marker_index(self.get_bot_review_comments())
+            except Exception as e:
+                get_logger().warning(f"persistent_inline_comments: fetch failed, falling back to create-new: {e}")
+                existing_index = {}
+
+        reused_comment_ids: set[int] = set()
+        post_parameters_list = []
  for suggestion in code_suggestions_validated:
-            body = suggestion['body']
-            relevant_file = suggestion['relevant_file']
-            relevant_lines_start = suggestion['relevant_lines_start']
-            relevant_lines_end = suggestion['relevant_lines_end']
+            body = suggestion["body"]
+            relevant_file = suggestion["relevant_file"]
+            relevant_lines_start = suggestion["relevant_lines_start"]
+            relevant_lines_end = suggestion["relevant_lines_end"]
      if not relevant_lines_start or relevant_lines_start == -1:
          get_logger().exception(
              f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
          continue
-
      if relevant_lines_end < relevant_lines_start:
-                get_logger().exception(f"Failed to publish code suggestion, "
-                                  f"relevant_lines_end is {relevant_lines_end} and "
-                                  f"relevant_lines_start is {relevant_lines_start}")
+                get_logger().exception(
+                    f"Failed to publish code suggestion, "
+                    f"relevant_lines_end is {relevant_lines_end} and "
+                    f"relevant_lines_start is {relevant_lines_start}")
          continue
+            if mode != PERSISTENT_MODE_OFF:
+                marker = generate_marker(suggestion.get("original_suggestion") or suggestion)
+                if marker:
+                    body = append_marker(body, marker)
+                    marker_hash = marker[len(MARKER_PREFIX):-len(MARKER_SUFFIX)]
+                    existing = find_comment_by_location(
+                        existing_index.get(marker_hash, []),
+                        relevant_file,
+                        relevant_lines_start,
+                        relevant_lines_end,
+                    )
+                    if existing is not None:
+                        if mode == PERSISTENT_MODE_SKIP:
+                            if resolve_outdated and (
+                                existing.get("is_resolved")
+                                or RESOLVED_BODY_MARKER in (existing.get("body") or "")
+                            ):
+                                if self.edit_review_comment(existing.get("id"), body):
+                                    reused_comment_ids.add(existing.get("id"))
+                                    if existing.get("is_resolved"):
+                                        self.unresolve_review_thread(existing)
+                                    continue
+                                get_logger().info(
+                                    f"persistent_inline_comments=skip: reopen failed for {existing.get('id')}; "
+                                    f"falling back to create-new"
+                                )
+                            else:
+                                reused_comment_ids.add(existing.get("id"))
+                                get_logger().info(
+                                    f"persistent_inline_comments=skip: existing comment {existing.get('id')} "
+                                    f"on {relevant_file}; not re-posting")
+                                continue
+                        else:
+                            # mode == update
+                            if self.edit_review_comment(existing.get("id"), body):
+                                reused_comment_ids.add(existing.get("id"))
+                                # If we previously auto-resolved this thread but the
+                                # suggestion is back, unresolve it.
+                                if resolve_outdated and existing.get("is_resolved"):
+                                    self.unresolve_review_thread(existing)
+                                continue
+                            get_logger().info(
+                                f"persistent_inline_comments=update: edit failed for {existing.get('id')}; "
+                                f"falling back to create-new")
+                    elif mode == PERSISTENT_MODE_SKIP:
+                        # No same-location match, so allow a new inline comment and
+                        # let the outdated pass resolve any stale thread with this marker.
+                        pass
+
      if relevant_lines_end > relevant_lines_start:
          post_parameters = {
              "body": body,
Evidence
PR Compliance ID 4 requires each function to have a single, well-defined responsibility. The changed
publish_code_suggestions includes dedup marker generation, fetching existing comments,
editing/reopening logic, and a separate outdated-resolution pass, indicating multiple distinct
concerns in one function.

Rule 4: Single Responsibility for Functions
pr_agent/git_providers/github_provider.py[712-842]
pr_agent/git_providers/gitlab_provider.py[764-882]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish_code_suggestions` has grown to handle multiple concerns (publish + dedup + edit/skip + outdated thread resolution), making it harder to test and maintain.
## Issue Context
This PR introduces persistent inline-comment dedup and outdated-thread resolution; these can be factored into helpers/classes to keep provider publish methods focused.
## Fix Focus Areas
- pr_agent/git_providers/github_provider.py[712-842]
- pr_agent/git_providers/gitlab_provider.py[764-882]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. New tests use single quotes 📘 Rule violation ⚙ Maintainability
Description
New/modified Python code introduces inconsistent string quoting (single quotes) against the
repository preference for double quotes. This reduces formatting consistency and may conflict with
Ruff-enforced style.
Code

tests/unittest/test_add_docs_inline_dedup.py[R8-12]

+    add_docs = PRAddDocs.__new__(PRAddDocs)
+    add_docs.git_provider = MagicMock()
+    add_docs.git_provider.publish_code_suggestions = MagicMock(return_value=True)
+    add_docs.dedent_code = MagicMock(return_value='"""Explain foo."""\nfoo()')
+
Evidence
PR Compliance ID 11 requires following repository Python style, including preferring double quotes.
The newly added test/code uses single-quoted string literals in multiple places, introducing
inconsistent style in touched lines.

AGENTS.md
tests/unittest/test_add_docs_inline_dedup.py[8-12]
pr_agent/tools/pr_add_docs.py[131-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Touched/added Python lines use single quotes, conflicting with the repo preference for double quotes.
## Issue Context
Keeping string quoting consistent reduces churn and aligns with Ruff formatting expectations.
## Fix Focus Areas
- tests/unittest/test_add_docs_inline_dedup.py[8-12]
- pr_agent/tools/pr_add_docs.py[131-134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Inline-dedup docs mismatch 🐞 Bug ⚙ Maintainability
Description
The new inline-dedup docs claim (a) improved_code normalization “collapses internal whitespace runs”
and (b) upstream line drift “does not split”, but normalize_code() does not collapse internal
whitespace and the tested provider behavior for same-marker/different-location is to post a new
comment and resolve the old one. This inconsistency will mislead operators about what dedup will do
across whitespace-only edits or rebases/line shifts.
Code

docs/docs/tools/improve.md[R362-377]

+Two inline comments are considered the same suggestion when the marker matches. The marker is a short hash computed as follows:
+
+- **Structured (preferred):** When the suggestion has an `improved_code` field (i.e., the model proposed replacement code), the hash covers `(file, normalized improved_code)`. Wording changes in the suggestion's prose do **not** affect the key; `label` is **not** part of the key either — the edit itself is the identity.
+- **Prose fallback:** When no `improved_code` is present, the hash falls back to `(file, label, normalized prose prefix)`.
+
+Normalization of `improved_code` expands tabs, strips trailing whitespace, drops leading/trailing blank lines, removes the longest common leading indent, and collapses internal whitespace runs — so reindentation of the same proposed edit does not split comments.
+
+### Strict behaviour
+
+This is intentionally a strict rule: when a suggestion has `improved_code`, its prose is never consulted for dedup. Two suggestions at the same spot with identical prose but **different** proposed edits are treated as distinct and remain as two separate inline comments. We'd rather under-merge (and show two comments) than over-merge two genuinely different fixes into one.
+
+### What's invariant across runs
+
+- Prose paraphrase of the same finding (same proposed edit) — does **not** split.
+- Reindentation or whitespace variation in the proposed edit — does **not** split.
+- Upstream commits that push the target line up or down in the file — do **not** split. Line numbers are not part of the key.
Evidence
Docs promise whitespace-collapsing normalization and no-split across line drift, but the code’s
normalize_code omits internal whitespace collapsing, and the unit tests explicitly validate the
“different location => create new + resolve old” behavior that contradicts a straightforward reading
of “do not split.”

docs/docs/tools/improve.md[362-377]
pr_agent/algo/inline_comments_dedup.py[59-77]
pr_agent/algo/inline_comments_dedup.py[163-187]
tests/unittest/test_github_inline_dedup.py[435-454]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new inline-comment dedup documentation describes normalization and “line drift doesn’t split” behavior that isn’t implemented (and is contradicted by unit tests).
### Issue Context
- `normalize_code()` does tab expansion, rstrip, blank-line trimming, and dedent — but does **not** “collapse internal whitespace runs”.
- The provider flow uses a marker-hash index but still requires an exact location match to *edit-in-place*; tests explicitly validate that when the same marker appears at a different location, the system creates a new comment and resolves the old one.
### Fix Focus Areas
- docs/docs/tools/improve.md[362-377]
- pr_agent/algo/inline_comments_dedup.py[59-77]
- pr_agent/algo/inline_comments_dedup.py[85-93]
- tests/unittest/test_github_inline_dedup.py[435-454]
### Suggested fix
Update the docs (and the inline_comments_dedup module comment) to accurately describe the current behavior:
- Remove/adjust the claim about collapsing internal whitespace runs (or implement that behavior if truly desired).
- Clarify that when a suggestion’s marker matches but inline coordinates don’t (e.g., due to line drift), the agent posts a new inline comment and resolves the old thread (instead of editing the original in place).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Opt-out semantics undocumented 🐞 Bug ⚙ Maintainability
Description
Docs state that manually unresolving an auto-resolved thread opts it out of future auto-resolution
via a body marker, but the tested implementation removes RESOLVED_BODY_MARKER when the suggestion is
re-emitted and refreshed. After that refresh, the “marker remains” premise is no longer true, so the
opt-out is not persistent across re-emissions as currently described.
Code

docs/docs/tools/improve.md[R312-313]

+        <td><b>resolve_outdated_inline_comments</b></td>
+        <td>When dedup is enabled (<code>persistent_inline_comments != "off"</code>), automatically resolve inline-comment threads whose suggestion was not re-emitted on the latest run; the thread body gets a short auto-resolve note. Default is true. Has no effect when <code>persistent_inline_comments = "off"</code>. Reviewers can manually unresolve an auto-resolved thread to opt it out of future auto-resolution &mdash; the bot detects the prior resolution marker in the body and respects it.</td>
Evidence
The documentation promises a marker-based opt-out (‘marker remains in the body and is respected’),
while the provider implementation overwrites the body on re-emit and the unit test asserts the
marker is removed, implying the opt-out only applies until the next refresh/re-emission.

docs/docs/tools/improve.md[311-313]
pr_agent/git_providers/github_provider.py[769-778]
tests/unittest/test_github_inline_dedup.py[190-213]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The docs describe a manual-unresolve opt-out mechanism based on `RESOLVED_BODY_MARKER` remaining in the body, but the implementation intentionally removes that marker when refreshing/reopening a previously auto-resolved suggestion.
### Issue Context
- Outdated-pass skipping logic uses `RESOLVED_BODY_MARKER in body` as an idempotency/opt-out signal.
- The refresh path edits the comment with a newly generated suggestion body that does not include `RESOLVED_BODY_MARKER`.
- The unit test explicitly asserts the marker is not present after refresh.
### Fix Focus Areas
- docs/docs/tools/improve.md[311-313]
- pr_agent/git_providers/github_provider.py[769-778]
- tests/unittest/test_github_inline_dedup.py[190-213]
### Suggested fix
Pick one and make docs+code consistent:
1) **Docs-only**: explicitly state that manual unresolve prevents auto-resolution only until the next time the suggestion is re-emitted/refreshed (at which point the marker is cleared).
2) **Code change** (if you want opt-out to persist across re-emissions): preserve the opt-out marker (possibly without the visible resolved note) when refreshing the body, and update tests accordingly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 0dc5adf

Results up to commit N/A


🐞 Bugs (4) 📘 Rule violations (2) 📎 Requirement gaps (0)


Action required
1. resolve_outdated bool-cast misparses📘 Rule violation ☼ Reliability
Description
resolve_outdated_inline_comments is coerced via bool(raw) which treats any non-empty string
(e.g., "false") as True, causing silent misconfiguration. This violates the requirement to
validate/normalize configuration inputs at boundaries.
Code

pr_agent/git_providers/github_provider.py[R723-728]

+        mode = normalize_persistent_mode(
+            get_settings().pr_code_suggestions.get("persistent_inline_comments", PERSISTENT_MODE_OFF)
+        )
+        resolve_outdated = bool(
+            get_settings().pr_code_suggestions.get("resolve_outdated_inline_comments", True)
+        )
Evidence
Compliance ID 24 requires config inputs be validated/normalized; the new code uses
bool(get_settings()...get(...)), which is not a safe normalization for string-sourced config
values and can invert intended behavior without warning.

pr_agent/git_providers/github_provider.py[723-728]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolve_outdated_inline_comments` is normalized with `bool(raw)`, which misparses string config values (e.g., env var `"false"` becomes `True`).
## Issue Context
This flag is user/operator-controlled configuration and should accept common boolean representations with targeted warnings/errors on invalid values.
## Fix Focus Areas
- pr_agent/git_providers/github_provider.py[723-728]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. edit_review_comment() called with None📘 Rule violation ☼ Reliability
Description
The code uses existing.get("id") / c.get("id") without validating presence/type before editing
comments or tracking IDs, but upstream API fields like GitHub databaseId can be missing (None).
This can lead to invalid API calls (e.g., /pulls/comments/None) and incorrect dedup/outdated
behavior.
Code

pr_agent/git_providers/github_provider.py[R644-808]

+                        out.append({
+                            "id": c.get("databaseId"),
+                            "thread_id": thread_id,
+                            "body": c.get("body") or "",
+                            "path": c.get("path"),
+                            "line": c.get("line"),
+                            "start_line": c.get("startLine"),
+                            "is_resolved": is_resolved,
+                        })
+                cursor = page_info.get("endCursor")
+                if not page_info.get("hasNextPage") or not cursor:
+                    break
+            return out
+        except Exception as e:
+            get_logger().warning(f"Failed to list GitHub review comments via GraphQL: {e}")
+            return []
+
+    def edit_review_comment(self, comment_id, body: str) -> bool:
+        try:
+            body = self.limit_output_characters(body, self.max_comment_chars)
+            self.pr._requester.requestJsonAndCheck(
+                "PATCH",
+                f"{self.base_url}/repos/{self.repo}/pulls/comments/{comment_id}",
+                input={"body": body},
+            )
+            return True
+        except Exception as e:
+            get_logger().warning(f"Failed to edit GitHub review comment {comment_id}: {e}")
+            return False
+
+    _RESOLVE_THREAD_MUTATION = """
+    mutation($threadId:ID!) {
+      resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } }
+    }
+    """
+
+    _UNRESOLVE_THREAD_MUTATION = """
+    mutation($threadId:ID!) {
+      unresolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } }
+    }
+    """
+
+    def _run_thread_mutation(self, query: str, comment: dict) -> bool:
+        thread_id = comment.get("thread_id")
+        if not thread_id:
+            return False
+        try:
+            _, data = self.pr._requester.requestJsonAndCheck(
+                "POST",
+                self._graphql_url(),
+                input={"query": query, "variables": {"threadId": thread_id}},
+            )
+            if not data or data.get("errors"):
+                get_logger().warning(
+                    f"GitHub thread mutation errors for {thread_id}: {(data or {}).get('errors')}"
+                )
+                return False
+            return True
+        except Exception as e:
+            get_logger().warning(f"GitHub thread mutation failed for {thread_id}: {e}")
+            return False
+
+    def resolve_review_thread(self, comment: dict) -> bool:
+        return self._run_thread_mutation(self._RESOLVE_THREAD_MUTATION, comment)
+
+    def unresolve_review_thread(self, comment: dict) -> bool:
+        return self._run_thread_mutation(self._UNRESOLVE_THREAD_MUTATION, comment)
+    def publish_code_suggestions(self, code_suggestions: list) -> bool:
+        """
+        Publishes code suggestions as review comments on the PR.
+
+        When `pr_code_suggestions.persistent_inline_comments` is 'update' (default)
+        or 'skip', a stable marker is embedded in each body so subsequent runs
+        can recognize and update (or skip) the existing comment rather than
+        creating duplicates.
+        """
     code_suggestions_validated = self.validate_comments_inside_hunks(code_suggestions)
+        mode = normalize_persistent_mode(
+            get_settings().pr_code_suggestions.get("persistent_inline_comments", PERSISTENT_MODE_OFF)
+        )
+        resolve_outdated = bool(
+            get_settings().pr_code_suggestions.get("resolve_outdated_inline_comments", True)
+        )
+
+        existing_index: dict[str, list[dict]] = {}
+        if mode != PERSISTENT_MODE_OFF:
+            try:
+                existing_index = build_marker_index(self.get_bot_review_comments())
+            except Exception as e:
+                get_logger().warning(f...

Comment thread pr_agent/algo/ai_handlers/litellm_ai_handler.py
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 30, 2026

Persistent review updated to latest commit 9089f58

Comment thread pr_agent/git_providers/github_provider.py
Comment thread pr_agent/git_providers/github_provider.py
Comment thread pr_agent/git_providers/github_provider.py Outdated
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented May 1, 2026

Persistent review updated to latest commit 568eb42

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented May 1, 2026

Persistent review updated to latest commit 0dc5adf

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.

Support Claude Sonnet extended thinking models

1 participant