Skip to content

feat(review): preserve findings across review reruns - #2722

Open
yefuyou wants to merge 6 commits into
The-PR-Agent:mainfrom
yefuyou:feature/review-finding-lifecycle
Open

feat(review): preserve findings across review reruns#2722
yefuyou wants to merge 6 commits into
The-PR-Agent:mainfrom
yefuyou:feature/review-finding-lifecycle

Conversation

@yefuyou

@yefuyou yefuyou commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve structured review findings across persistent /review reruns
  • reconcile findings across runs using existing fingerprint primitives
  • persist active and resolved finding state in the persistent review comment
  • resolve findings only after complete and valid full-scope reviews
  • preserve active findings during incremental and token-limited reviews
  • avoid duplicate persistent comments when lifecycle state updates fail

Design notes

Resolution is conservative:
findings are only marked resolved after complete valid reviews.
Incremental reviews, partial analysis, malformed predictions, and provider failures do not trigger negative state transitions.

Validation

  • focused lifecycle/provider tests — 310 passed
  • pytest tests/unittest -q — 1960 passed, 1 skipped, 1 xfailed
  • git diff --check
  • flake8 validation for new files

AI disclosure

This PR was developed with assistance from Codex and validated with targeted tests by the contributor.

Closes #2453

@github-actions github-actions Bot added the feature 💡 label Aug 20, 2026
Comment thread pr_agent/algo/review_finding_state.py Fixed
@yefuyou
yefuyou force-pushed the feature/review-finding-lifecycle branch from 75348a5 to d9a235a Compare August 20, 2026 13:15
@yefuyou
yefuyou marked this pull request as ready for review August 20, 2026 13:33
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Preserve review findings across reruns via persistent comment state

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Persist structured review findings across repeated /review runs.
• Reconcile findings by fingerprint; resolve only after complete full-scope reviews.
• Prevent duplicate persistent comments when stateful updates fail.
Diagram

graph TD
  A["/review run (PRReviewer)"] --> B["Load settings"] --> C["Fetch prior review comment"] --> D["Parse hidden state marker"] --> E["Reconcile findings"] --> F[("Persistent review comment")]
  A --> G["Publish review (stateful)"] --> F
  C --> H["Git provider: get_issue_comments"]
  G --> I["Git provider: edit_comment"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Store state outside the PR comment (labels/check-run/DB)
  • ➕ Avoids embedding JSON in markdown and marker parsing
  • ➕ Potentially cleaner separation of display vs state
  • ➕ Can support richer querying/analytics
  • ➖ Requires provider-specific APIs and permissions
  • ➖ Harder to make portable across GitHub/GitLab/etc.
  • ➖ May not be available in self-hosted or restricted environments
2. Resolve via provider-native threads / inline comment lifecycle
  • ➕ Aligns with platform UX (resolved threads, per-line context)
  • ➕ No custom schema/versioning needed
  • ➖ Findings are summary-level today; mapping reliably to threads is non-trivial
  • ➖ Provider capabilities differ significantly; portability suffers
  • ➖ Doesn’t address rerun stability for summary-only findings
3. Persist only active findings; omit resolved history entirely
  • ➕ Simpler state model and less comment bloat
  • ➕ Avoids retention policies and resolved rendering
  • ➖ Loses useful audit trail and reopen signals
  • ➖ Harder to explain why a finding disappeared after a full review

Recommendation: The PR’s approach (hidden, versioned state marker inside the persistent review comment) is the best portability/complexity tradeoff for cross-provider support. The conservative resolution gate (only on complete full-scope reviews) and fail-closed parsing mitigate incorrect state transitions and duplicate-comment risks.

Files changed (7) +971 / -10

Enhancement (2) +463 / -8
review_finding_state.pyAdd versioned persistence + reconciliation for review findings +296/-0

Add versioned persistence + reconciliation for review findings

• Introduces a deterministic, versioned state marker stored in the persistent review comment. Normalizes findings, fingerprints them, reconciles active/resolved/reopened lifecycle, and renders a collapsed 'Resolved findings' section while retaining bounded history.

pr_agent/algo/review_finding_state.py

pr_reviewer.pyWire finding lifecycle into /review generation and publishing +167/-8

Wire finding lifecycle into /review generation and publishing

• Loads prior persistent review state, validates structured findings, and reconciles lifecycle using fingerprints. Publishes even when the textual review has no suggestions if the finding state changed, and disables fallback publishing when updating the persistent comment with state.

pr_agent/tools/pr_reviewer.py

Bug fix (1) +4 / -2
git_provider.pyAdd no-fallback mode for persistent comment updates +4/-2

Add no-fallback mode for persistent comment updates

• Extends persistent comment publishing to optionally skip fallback comment creation on update errors. This prevents duplicate persistent review comments when stateful lifecycle updates rely on editing the existing comment.

pr_agent/git_providers/git_provider.py

Tests (3) +503 / -0
test_pr_reviewer_finding_state.pyAdd PRReviewer integration tests for stateful publishing behavior +125/-0

Add PRReviewer integration tests for stateful publishing behavior

• Validates that resolved findings render into the review output and that state transitions trigger publishing even when 'No major issues detected'. Also verifies fail-closed behavior when state is blocked.

tests/unittest/test_pr_reviewer_finding_state.py

test_review_finding_persistence.pyTest persistence gating and no-fallback publishing semantics +87/-0

Test persistence gating and no-fallback publishing semantics

• Covers fail-closed handling for malformed structured findings, ensures no fallback comment is created after edit failures in stateful mode, and verifies the feature is disabled for generic persistent publishers.

tests/unittest/test_review_finding_persistence.py

test_review_finding_state.pyAdd unit tests for reconciliation, parsing, and retention rules +291/-0

Add unit tests for reconciliation, parsing, and retention rules

• Exercises identity stability under normalization, conservative resolution rules, reopen metadata, deterministic marker round-tripping, invalid marker fail-closed behavior, and resolved retention limits while preserving active findings.

tests/unittest/test_review_finding_state.py

Other (1) +1 / -0
configuration.tomlAdd persistent_finding_state configuration flag +1/-0

Add persistent_finding_state configuration flag

• Adds a pr_reviewer setting to enable/disable persisting review finding lifecycle across reruns. Defaults to enabled alongside persistent_comment.

pr_agent/settings/configuration.toml

@qodo-code-review

qodo-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (3) 📜 Skill insights (0)

Grey Divider


Action required

1. Gitea drops persistent reviews ✓ Resolved 🐞 Bug ≡ Correctness
Description
The invalid-marker recovery path forces fallback_on_error=False, but Gitea's
get_issue_comments() returns dictionaries while the generic persistent publisher dereferences
comment.body. On any PR with an existing Gitea comment this raises, is swallowed by
publish_persistent_comment_full, and returns without editing or creating the review, so the
lifecycle update and the current review are lost.
Code

pr_agent/tools/pr_reviewer.py[R226-233]

+                persistent_args = dict(
+                    initial_header=f"{PRReviewHeader.REGULAR.value} 🔍",
+                    update_header=True,
+                    final_update_message=False,
+                    fallback_on_error=False,
+                    **review_thread_kwargs,
+                )
+                self.git_provider.publish_persistent_comment_full(pr_review, **persistent_args)
Relevance

●●● Strong

Concrete provider response-shape bug risking silent loss of persistent review updates; similar
lifecycle bugs fixed before.

PR-#2404
PR-#2599

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new branch invokes the generic full publisher with fallback disabled. Gitea declares its issue
comments as dictionaries, whereas the generic publisher reads an object body; its broad exception
handler returns None when fallback is disabled, making the failure silent and preventing
publication.

pr_agent/tools/pr_reviewer.py[226-233]
pr_agent/git_providers/gitea_provider.py[625-637]
pr_agent/git_providers/git_provider.py[372-400]

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 stateful persistent-review path uses `fallback_on_error=False`, but the generic publisher assumes object-style comments. Gitea supplies mapping records, causing the update to fail and publish nothing.

## Issue Context
Gitea is considered state-capable because its provider reports `get_issue_comments` support and overrides the persistent publisher, but its returned comments must be normalized or handled by a Gitea-specific persistent update implementation.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[226-233]
- pr_agent/git_providers/git_provider.py[372-400]
- pr_agent/git_providers/gitea_provider.py[625-637]

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


2. State block causes duplicate comments ✓ Resolved 🐞 Bug ☼ Reliability
Description
When review finding state is blocked (malformed marker or read error), PRReviewer.run() publishes a
new non-persistent comment instead of updating the persistent review comment, leaving the malformed
marker in place. This creates repeated duplicate review comments across reruns and prevents the
system from self-healing by overwriting/removing the bad marker.
Code

pr_agent/tools/pr_reviewer.py[R214-219]

+            if state_blocked:
+                get_logger().warning(
+                    "Review finding state is unavailable; publishing this review without persistent state"
+                )
+                self.git_provider.publish_comment(pr_review, **review_thread_kwargs)
+            elif get_settings().pr_reviewer.persistent_comment and not self.incremental.is_incremental:
Relevance

●●● Strong

Persistent-comment fallback paths causing duplicate comments were explicitly accepted as reliability
fixes.

PR-#2404

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR introduces a state-blocked branch that explicitly publishes non-persistently, and
_load_review_finding_state sets the block flag when the marker is invalid or unreadable—so the
invalid marker is never overwritten and the behavior repeats every run.

pr_agent/tools/pr_reviewer.py[214-233]
pr_agent/tools/pr_reviewer.py[263-280]
PR-#2404

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

### Issue description
If persistent finding state cannot be parsed/loaded, the code sets `_review_state_blocked=True` and then `run()` publishes via `publish_comment()` (non-persistent). Because the persistent comment is never edited, the malformed marker remains forever and every rerun continues posting new comments.

### Issue Context
The intended behavior (per PR description) is to avoid duplicate persistent comments when lifecycle state updates fail. The current blocked-state branch does the opposite by always creating new comments and never clearing the invalid marker.

### Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[214-233]
- pr_agent/tools/pr_reviewer.py[263-280]

### Proposed fix
- In `run()`, when `state_blocked` is True:
 - Still publish using the persistent mechanism (`publish_persistent_comment_full` or `publish_persistent_comment`) **without** appending any state marker/section.
 - This will overwrite the malformed marker (self-heal) and prevent duplicate review comments.
- Keep lifecycle reconciliation disabled for that run (i.e., don’t compute state transitions), but do not switch to non-persistent publishing.

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


3. Loads stale review state ✓ Resolved 🐞 Bug ≡ Correctness
Description
_load_review_finding_state() returns the first matching review comment from get_issue_comments(),
which can select an older/stale review when multiple PR-Agent review comments exist, causing
reconciliation to use the wrong baseline state. This can incorrectly resolve/reopen findings and
keep lifecycle state out of sync with the latest persistent review comment.
Code

pr_agent/tools/pr_reviewer.py[R266-270]

+            for comment in self.git_provider.get_issue_comments():
+                body = getattr(comment, "body", "")
+                if not isinstance(body, str) or not body.startswith(header):
+                    continue
+                parsed = parse_review_state(body)
Relevance

●●● Strong

A recent precedent explicitly accepted reversing issue-comment iteration to select the newest review
state.

PR-#2381
PR-#2599

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new state loader returns the first header-matching comment in provider iteration order, which is
commonly oldest-first; past bugs show this pattern picks stale reviews when multiple exist.

pr_agent/tools/pr_reviewer.py[263-276]
PR-#2381

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

### Issue description
`PRReviewer._load_review_finding_state()` scans `get_issue_comments()` in forward order and returns on the first match. If multiple PR-Agent review comments exist (e.g., from prior failures/duplicates), this can load an older comment’s embedded state and reconcile against stale data.

### Issue Context
This PR introduces persistent finding lifecycle state that depends on reading the latest persistent review comment’s marker. Picking an older match can regress state and produce wrong resolved/reopened transitions.

### Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[263-281]

### Proposed fix
- Iterate comments in reverse chronological order when searching for the persistent review comment:
 - Prefer `for comment in reversed(list(self.git_provider.get_issue_comments())):` (or select the max by a timestamp field when available).
- Keep the existing header filter, but ensure the *newest* matching comment is parsed and used as the previous state baseline.

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


View high (2)
4. Gitea comments shape unchecked ✓ Resolved 🐞 Bug ☼ Reliability
Description
GiteaProvider.get_issue_comments() now only checks for None; if the API returns a non-list
truthy payload (e.g., an error dict), callers that reverse/iterate the result will mis-handle it and
may skip updating the persistent comment or publish duplicates.
Code

pr_agent/git_providers/gitea_provider.py[R639-641]

+        if comments is None:
            self.logger.error("Failed to get comments")
-            return []
+            raise RuntimeError("Failed to get comments")
Relevance

●● Moderate

Similar Gitea payload-shape validation issues were accepted before, but this exact check-only-None
pattern is untested.

PR-#2569
PR-#2142

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
get_issue_comments() now raises only for None and otherwise returns the raw payload. Persistent
publishing logic immediately materializes and reverse-iterates the returned value, which will behave
incorrectly if it’s not a list of comments (e.g., iterating dict keys). This is a known Gitea
integration failure mode in this codebase.

pr_agent/git_providers/gitea_provider.py[631-643]
pr_agent/git_providers/git_provider.py[377-382]
PR-#2569

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

### Issue description
`GiteaProvider.get_issue_comments()` returns `comments` without validating its type. Downstream code assumes an iterable of comment objects/dicts; if Gitea returns an error object (dict) or other unexpected payload, the persistent update logic can iterate keys instead of comments and fail to locate/update the existing persistent review.

### Issue Context
This repo has had prior issues where Gitea endpoints returned unexpected payload shapes and required explicit type validation.

### Fix Focus Areas
- pr_agent/git_providers/gitea_provider.py[631-643]

### Suggested fix approach
- Change the guard to:
 - `if comments is None: raise ...`
 - `if not isinstance(comments, list): raise RuntimeError(f"Unexpected comments payload type: {type(comments)}")`
- Optionally filter/validate each element is a dict-like comment (or tolerate both dict/object shapes consistently).
- Add/extend a unit test that simulates `list_all_comments` returning a dict error payload and assert `get_issue_comments()` raises.

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


5. Gitea edit failures bypass fallback ✓ Resolved 🐞 Bug ☼ Reliability
Description
When editing a dict-shaped Gitea comment fails, GiteaProvider.edit_comment catches the exception
and returns without raising, while publish_persistent_comment_full treats the call as successful
because it does not inspect the return value. As a result, normal persistent updates cannot fall
back to publishing a new comment after an edit failure, so the review update is lost.
Code

pr_agent/git_providers/gitea_provider.py[R359-362]

+        if isinstance(comment, dict):
+            comment_id = comment.get("comment_id") or comment.get("id")
+        else:
+            comment_id = getattr(comment, "id", None)
Relevance

●● Moderate

PR intentionally disables fallback on stateful updates to avoid duplicate comments; edit failures
already surfaced via tests/logging.

PR-#2492
PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed Gitea code extracts a dictionary comment ID and then catches API failures, while the
generic publisher only falls back on an exception; therefore a failed dict-comment edit is returned
as if successful.

pr_agent/git_providers/gitea_provider.py[359-367]
pr_agent/git_providers/gitea_provider.py[363-375]
pr_agent/git_providers/git_provider.py[379-408]
tests/unittest/test_review_finding_persistence.py[158-176]

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

## Issue description
Gitea edit failures are swallowed by `GiteaProvider.edit_comment`, so `GitProvider.publish_persistent_comment_full` cannot detect the failure and execute its fallback publishing path.

## Issue Context
The generic publisher only enters fallback handling when `edit_comment` raises. The changed dict-ID path is used for Gitea comments represented as dictionaries, and the existing persistence tests cover exception propagation with a mock but not Gitea's swallowed API exception.

## Fix Focus Areas
- pr_agent/git_providers/gitea_provider.py[359-362]
- pr_agent/git_providers/gitea_provider.py[363-375]
- pr_agent/git_providers/git_provider.py[380-408]
- tests/unittest/test_review_finding_persistence.py[158-176]

Make failed Gitea edits propagate an exception (or otherwise provide an explicit failure signal that the generic publisher handles), preserving the no-fallback behavior when `fallback_on_error=False` and the fallback behavior when it is true. Add a regression test using `GiteaProvider.edit_comment` with a failing API call and verify the generic publisher publishes a replacement only when fallback is enabled.

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



Remediation recommended

6. Long HTML row line 📘 Rule violation ⚙ Maintainability
Description
The new persistent_finding_state documentation row is a single physical line exceeding the
120-character maximum. This can violate repository style/lint expectations and reduces readability
of docs diffs.
Code

docs/docs/tools/review.md[63]

+        <td>If set to true, PR-Agent persists structured review finding state across complete review runs, so findings can be resolved and reopened. Incremental and partial reviews do not resolve absent findings. Default is true.</td>
Relevance

●●● Strong

Recent precedent accepts line-length fixes in modified documentation/Python code.

PR-#2318
PR-#2212

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2694690 requires keeping modified source file lines at or under 120 characters. The
added <td>...</td> line for persistent_finding_state is a long single-line HTML table cell in
the modified docs section.

Rule 2694690: Enforce maximum line length of 120 characters
docs/docs/tools/review.md[63-63]

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

## Issue description
A newly added documentation line exceeds the 120-character maximum line length requirement.

## Issue Context
The `persistent_finding_state` configuration row in `docs/docs/tools/review.md` is written as a single long HTML line.

## Fix Focus Areas
- docs/docs/tools/review.md[61-64]

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


7. Single quotes in data.get 📘 Rule violation ⚙ Maintainability
Description
The newly added code uses a single-quoted string literal ('review') where double quotes are
required by the Python string literal convention. This can cause style/lint failures or inconsistent
formatting.
Code

pr_agent/tools/pr_reviewer.py[472]

+        if not isinstance(data.get('review'), dict):
Relevance

●●● Strong

Trivial deterministic double-quote style fix; matches accepted quoting-style precedent for changed
code.

PR-#2679

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2694657 requires double quotes for Python string literals in changed code. The newly
added data.get('review') uses a single-quoted string literal without any apparent need to avoid
escaping.

Rule 2694657: Use double quotes for all Python string literals
pr_agent/tools/pr_reviewer.py[472-472]

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

## Issue description
A newly added Python string literal uses single quotes where the project requires double quotes.

## Issue Context
In `_prepare_pr_review`, the new `data.get('review')` uses a single-quoted string literal.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[472-472]

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


8. Non-imperative review state docs ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Newly added documentation text is written descriptively instead of using imperative phrasing (e.g.,
the module docstring and a comment explaining the generic publisher behavior). This violates the
required docstring/comment style convention and reduces consistency across the codebase.
Code

pr_agent/algo/review_finding_state.py[1]

+"""Persistent state helpers for cross-run review findings."""
Relevance

●●● Strong

Recent precedent explicitly accepted imperative phrasing changes for newly added comments.

PR-#2661

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires imperative phrasing for new/modified docstrings and behavior-describing
comments. The new module docstring is a descriptive noun phrase, and the added comment about the
generic publisher is also descriptive rather than imperative.

Rule 2694688: Docstrings and comments must use imperative phrasing
pr_agent/algo/review_finding_state.py[1-1]
pr_agent/tools/pr_reviewer.py[249-252]

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

## Issue description
New docstrings/comments are not written in imperative mood.

## Issue Context
The repo requires newly added/modified docstrings and behavioral comments to be written as commands (e.g., "Return ...", "Handle ...") rather than descriptive statements.

## Fix Focus Areas
- pr_agent/algo/review_finding_state.py[1-1]
- pr_agent/tools/pr_reviewer.py[249-252]

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


View medium (4)
9. _retained_findings return line too long ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The return statement in _retained_findings exceeds the 120-character limit required by the repo’s
Ruff configuration. This can cause lint failures and makes the code harder to read and review.
Code

pr_agent/algo/review_finding_state.py[176]

+    return sorted(active + resolved[:max(0, max_resolved_findings)], key=lambda finding: finding["finding_id"])
Relevance

●●● Strong

Recent precedent accepted fixing overlong Python lines under the repository’s 120-character limit.

PR-#2318
PR-#2381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires a 120-character max line length in Python files. The new
review_finding_state.py contains a long return statement that exceeds this limit.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
pr_agent/algo/review_finding_state.py[169-177]

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

## Issue description
A newly added Python line exceeds the 120-character max line length.

## Issue Context
Ruff is configured to enforce a 120 character limit; violating lines may fail linting and reduce readability.

## Fix Focus Areas
- pr_agent/algo/review_finding_state.py[169-177]

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


10. persistent_finding_state not in .pr_agent.toml ✓ Resolved 📘 Rule violation ≡ Correctness
Description
The new pr_reviewer.persistent_finding_state flag is present in
pr_agent/settings/configuration.toml but is not reflected in the root .pr_agent.toml. This
violates the configuration sync requirement and can lead to inconsistent defaults/configuration
sources.
Code

pr_agent/settings/configuration.toml[110]

+persistent_finding_state=true # Persist review finding state across complete review runs.
Relevance

●●● Strong

Configuration consistency and single-source-of-truth fixes are repeatedly accepted for newly added
settings.

PR-#2528
PR-#2598

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires keeping behavior-related configuration keys consistent between
.pr_agent.toml and pr_agent/settings/*.toml. The new persistent_finding_state key exists in
the settings configuration but is not present under [pr_reviewer] in .pr_agent.toml.

Rule 2694685: Keep .pr_agent.toml and pr_agent/settings/*.toml configuration in sync on behavior changes
pr_agent/settings/configuration.toml[107-112]
.pr_agent.toml[6-10]

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

## Issue description
A new behavior/config flag was added in `pr_agent/settings/` but not mirrored in `.pr_agent.toml` as required.

## Issue Context
The compliance checklist requires keeping `.pr_agent.toml` and `pr_agent/settings/*.toml` aligned for behavior-related configuration keys.

## Fix Focus Areas
- pr_agent/settings/configuration.toml[107-112]
- .pr_agent.toml[6-10]

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


11. persistent_finding_state undocumented in docs ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
A new user-facing configuration option pr_reviewer.persistent_finding_state was added, but the
review tool documentation’s configuration options list does not mention it. This makes the feature
difficult for users to discover or configure correctly.
Code

pr_agent/settings/configuration.toml[110]

+persistent_finding_state=true # Persist review finding state across complete review runs.
Relevance

●●● Strong

Recent documentation precedents accept documenting newly added user-facing configuration and
behavior.

PR-#2528
PR-#2491

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires updating README/docs when user-facing behavior changes. The PR introduces
persistent_finding_state in configuration.toml, but the review tool docs’ configuration table
does not include that option.

Rule 2694680: Update docs when user-facing behavior changes
pr_agent/settings/configuration.toml[107-112]
docs/docs/tools/review.md[52-86]

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

## Issue description
A new configuration option was introduced but the user-facing documentation for the `review` tool configuration options was not updated.

## Issue Context
The docs page `docs/docs/tools/review.md` contains the authoritative list of `pr_reviewer` configuration options.

## Fix Focus Areas
- pr_agent/settings/configuration.toml[107-112]
- docs/docs/tools/review.md[52-86]

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


12. Marker text breaks parsing 🐞 Bug ☼ Reliability
Description
append_review_state() renders resolved finding bodies verbatim, so if any finding body contains
the marker namespace string (<!-- pr-agent-review-state), the next run’s parse_review_state()
will see multiple namespaces and mark state invalid, disabling lifecycle reconciliation.
Code

pr_agent/algo/review_finding_state.py[R304-308]

+    human_body = "\n\n".join(
+        section
+        for section in (body, _render_resolved_section(state))
+        if section
+    )
Relevance

●● Moderate

Valid edge case but marker-poisoning issues from user content are subtle; no exact precedent found.

PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The resolved findings section prints finding["body"] verbatim, and append_review_state() appends
that section alongside the hidden marker. parse_review_state() declares the state invalid whenever
the comment contains the marker namespace substring more than once, so a single finding body
containing that substring will poison future parsing.

pr_agent/algo/review_finding_state.py[267-291]
pr_agent/algo/review_finding_state.py[294-325]
pr_agent/algo/review_finding_state.py[135-143]

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

### Issue description
Persisted review state can become permanently "invalid" if a finding body (shown in the resolved section) happens to contain the marker namespace string `<!-- pr-agent-review-state`. This makes `parse_review_state()` treat the comment as having multiple markers and fail closed, blocking lifecycle updates.

### Issue Context
- `_render_resolved_section()` includes `finding["body"]` directly.
- `parse_review_state()` uses a raw substring count (`body.count(_STATE_MARKER_NAMESPACE)`) to validate there is exactly one marker namespace.

### Fix Focus Areas
- pr_agent/algo/review_finding_state.py[135-143]
- pr_agent/algo/review_finding_state.py[267-291]
- pr_agent/algo/review_finding_state.py[303-325]

### Suggested fix approach
- Prefer removing the `namespace_count` heuristic and rely on the regex match count alone (i.e., `len(matches)`), or
- Escape/sanitize the namespace string when rendering human-visible bodies (e.g., replace `<!-- pr-agent-review-state` with an entity-encoded or zero-width-joiner variant) so it can never appear verbatim outside the real marker.
- Add a unit test where a finding body contains `<!-- pr-agent-review-state` and ensure parsing still succeeds and state round-trips.

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



Informational

13. Unsorted imports in review_finding_state 📘 Rule violation ⚙ Maintainability
Description
The standard-library imports in the new module are not alphabetically ordered per isort section
ordering. This can trigger lint/formatting failures and causes unnecessary diff churn.
Code

pr_agent/algo/review_finding_state.py[R6-9]

+import json
+import re
+from dataclasses import dataclass
+from datetime import datetime, timezone
Relevance

● Weak

Recent precedent rejected an isort ordering complaint in a modified import block.

PR-#2661

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires Python imports to be grouped and alphabetically ordered. In the new
module, standard-library imports are not ordered (e.g., from dataclasses ... appears after `import
re`).

Rule 2694656: Group Python imports according to isort sections and order
pr_agent/algo/review_finding_state.py[5-13]

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

## Issue description
Imports are not ordered/grouped according to isort expectations (standard library imports should be alphabetized).

## Issue Context
The repo requires Python imports to follow isort sectioning and ordering.

## Fix Focus Areas
- pr_agent/algo/review_finding_state.py[5-13]

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


14. pr_reviewer imports out of order ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The newly added review_finding_state import is not placed in alphabetical order within the
local-import section. This violates the repo’s isort-style import ordering and may cause
formatting/lint failures.
Code

pr_agent/tools/pr_reviewer.py[R19-22]

+from pr_agent.algo.review_finding_state import (
+    append_review_state,
+    parse_review_state,
+    reconcile_review_findings,
Relevance

● Weak

Recent, closely matching precedent rejected a requested import-ordering correction in modified
Python code.

PR-#2661

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires local imports to be alphabetically ordered. The new `from
pr_agent.algo.review_finding_state ... import is inserted before from pr_agent.algo.pr_processing
...`, breaking alphabetical ordering.

Rule 2694656: Group Python imports according to isort sections and order
pr_agent/tools/pr_reviewer.py[9-26]

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

## Issue description
A newly added import is placed out of order within the local (first-party) imports.

## Issue Context
Imports must be grouped and alphabetically ordered per isort-style conventions.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[9-26]

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


  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Context sources
✅ Compliance rules (platform): 34 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 2ca9a24

Comment thread pr_agent/git_providers/gitea_provider.py
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 0b66cd9

Comment thread pr_agent/git_providers/gitea_provider.py Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 53f75f8

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit c04cc62

@IsmaelMartinez IsmaelMartinez 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.

Thanks for taking #2453 on, and for turning the Qodo findings around so fast. Four notes inline, two with suggestions; the suite stays green with both applied.

"""Serialize state deterministically so repeated updates are diffable."""
if not _is_valid_state(state):
raise ValueError("Invalid review finding state")
payload = json.dumps(state, ensure_ascii=False, sort_keys=True, separators=(",", ":"))

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.

A finding body containing --> closes the marker early and the rest of the payload renders in the review, and parse_review_state still calls that state valid, so it repeats every run. Escaping both brackets fixes it without changing the decoded state.

It does not close Qodo's item 12: append_review_state prints bodies verbatim in the human section too.

Suggested change
payload = json.dumps(state, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
payload = json.dumps(state, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
# '<' and '>' occur only inside JSON strings, so escaping cannot change the decoded state
payload = payload.replace("<", "\\u003c").replace(">", "\\u003e")

Comment on lines +423 to +430
allow_resolution = (
bool(self.prediction)
and not bool(getattr(self.incremental, "is_incremental", False))
and not bool(self.remaining_files_list)
and parsed.valid
and current_findings is not None
and len(current_findings) < max_findings
)

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.

With num_max_findings at its default of 3, two findings enable resolution, and nothing checks that the commit moved. Re-running /review on an unchanged head marks a finding RESOLVED just because the model did not repeat it.

Gate on the previous SHA rather than the current one: _review_head_sha reads last_commit_id, which only GitHub and Gitea set, so testing the current SHA turns two of your own tests red.

Suggested change
allow_resolution = (
bool(self.prediction)
and not bool(getattr(self.incremental, "is_incremental", False))
and not bool(self.remaining_files_list)
and parsed.valid
and current_findings is not None
and len(current_findings) < max_findings
)
previous_head_sha = str(((parsed.state or {}).get("last_run") or {}).get("head_sha") or "")
current_head_sha = self._review_head_sha()
allow_resolution = (
bool(self.prediction)
and not bool(getattr(self.incremental, "is_incremental", False))
and not bool(self.remaining_files_list)
and parsed.valid
and current_findings is not None
and len(current_findings) < max_findings
and (not previous_head_sha or current_head_sha != previous_head_sha)
)

artifact={"error": e})
else:
get_logger().exception(f"Failed to edit github comment", artifact={"error": e})
raise

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.

Not behind persistent_finding_state, and the same line lands in azuredevops_provider.py:237 and bitbucket_provider.py:438, so edit_comment propagates on all three, including the 403 branch this code calls "usually due to polling".

Mostly a gain: a failed edit on the suggestions comment now republishes them where main drops them. It escapes the function only when the fallback write fails too, and then /improve publishes nothing. Both worth a line in the description.

)
except Exception as e:
get_logger().exception(f"Failed to edit comment, error: {e}")
raise

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.

Hard conflict with #2724, which returns False here. Worth agreeing the resolution before either merges, since as noted on github_provider.py the two are not equivalent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hard conflict with #2724, which returns False here. Worth agreeing the resolution before either merges, since as noted on github_provider.py the two are not equivalent.

Agreed. Since #2724 owns the broader Azure path and already validates the False return contract end-to-end, I’ll align #2722 with that behavior rather than keep the competing exception-propagation change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I traced the edit_comment() call sites further and narrowed #2722 so the two PRs no longer need to own the same Azure behavior.

#2722 now leaves AzureDevopsProvider.edit_comment() and the /improve path untouched. It only updates the shared persistent-comment publisher to treat an explicit False return as an edit failure, while preserving fallback_on_error=False for lifecycle updates so a failed edit cannot create a duplicate persistent review.

That means #2724 can keep the Azure True/False contract and its /improve caller handling independently.

I also added regression coverage for both paths:

  • False + fallback_on_error=False → no fallback comment
  • False + fallback_on_error=True → normal fallback

The full unit suite is green locally.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit e97e318

@IsmaelMartinez IsmaelMartinez 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.

Thanks for chasing this down, but the narrowing has not landed on this branch.

AzureDevopsProvider.edit_comment() is still modified here: head e97e3184 adds raise to its except block, which is the line the conflict was about. Merged onto today's main, this and #2724 still collide on azuredevops_provider.py and git_provider.py.

Worth knowing before you change anything: simply dropping the Azure raise would not be safe on its own. On main edit_comment returns None, not False, so the is False guard you added to publish_persistent_comment_full would never fire, and your fallback_on_error=False callers would read a failed edit as success. That only becomes safe once #2724's True/False contract is in.

So this is an ordering question. If #2724 lands first, this can drop the Azure change entirely. If this lands first, #2724 has to adopt the raise instead.

@yefuyou

yefuyou commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for chasing this down, but the narrowing has not landed on this branch.

AzureDevopsProvider.edit_comment() is still modified here: head e97e3184 adds raise to its except block, which is the line the conflict was about. Merged onto today's main, this and #2724 still collide on azuredevops_provider.py and git_provider.py.

Worth knowing before you change anything: simply dropping the Azure raise would not be safe on its own. On main edit_comment returns None, not False, so the is False guard you added to publish_persistent_comment_full would never fire, and your fallback_on_error=False callers would read a failed edit as success. That only becomes safe once #2724's True/False contract is in.

So this is an ordering question. If #2724 lands first, this can drop the Azure change entirely. If this lands first, #2724 has to adopt the raise instead.

You're right. I found the residual change: the Azure raise and its regression test were introduced in an earlier commit and survived my later scope narrowing. My last update only narrowed the newest patch, not the full PR diff against main.

I also understand why simply removing it now would be unsafe while main still returns None on that path. I'll coordinate the merge order: once the Azure True/False contract is established, I'll rebase #2722, remove the Azure-specific change and test, and verify the complete PR diff against main before updating the PR.

@IsmaelMartinez

Copy link
Copy Markdown
Collaborator

Sequencing note: #2797 rewrites the persistent-comment discovery this PR also touches and is likely to land first. Your planned rebase after #2724 would then pick both up in one pass.

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.

Preserve resolved findings across review re-runs instead of overwriting them

3 participants