Skip to content

fix: purge cache of an overridden confirmed islock and bound the ConnectBlock conflict loop - #7885

Open
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:fix/is-conflict-cache-purge
Open

PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:fix/is-conflict-cache-purge

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

When a block containing an InstantSend-locked transaction is confirmed (ChainLocked, or deep enough when ChainLocks are off), RemoveConfirmedInstantSendLocks erases the lock's db keys but deliberately keeps its islock, txid and outpoint cache entries (keep_cache=true).

If a ChainLocked block on another branch later double-spends that lock's inputs, ConnectBlock removes the conflicting lock through RemoveChainedInstantSendLocks. That function re-reads the root lock from the db, finds nothing (it was already confirmed), and so never erases the cache entries it left behind. Two consequences on develop:

  • IsLocked(txid) keeps returning true for the overridden lock's transaction: the stale txidCache entry still maps to its hash, which is archived and therefore still "known".
  • ConnectBlock's while (ConflictingISLockIfAny(tx)) loop under cs_main only terminates because the use_cache=false db read in GetInstantSendLockByHashInternal caches nullptr for the missing hash as a side effect, hiding the stale outpoint entry from the next lookup. With that write skipped (an innocent-looking cleanup), the loop never ends.

Nothing here is reachable as a hang today; this removes the dependence on that incidental write and fixes the stale IsLocked result.

What was done?

  • fix(validation): ConnectBlock checks for a conflict once and calls RemoveConflictingISLocksByTx, which removes every lock conflicting with the transaction's inputs in a single pass (at most one root lock per input), instead of looping until the lookup comes back empty.
  • fix(instantsend): RemoveChainedInstantSendLocks now takes the lock itself instead of its txid and always erases the root lock's cache entries (factored out as EraseInstantSendLockCache). Db keys are still only erased when the lock is present in the db, so index keys that may belong to another lock are not touched.

How Has This Been Tested?

  • New unit test evo_islock_tests/chainlocked_conflict_overrides_confirmed_locks: confirms two locks on different inputs of one transaction, overrides them through RemoveConflictingISLocksByTx, and checks that no conflict remains and neither locked transaction is reported by IsLocked. It fails without the cache purge (check !isman.IsLocked(txid) has failed), and fails on the conflict check if the helper stops after the first lock; it passes with both commits.
  • Mutation check with a scratch unit test (not committed) that emulates the ConnectBlock loop against CInstantSendDb: with the nullptr cache write in GetInstantSendLockByHashInternal skipped for use_cache=false, the loop does not terminate on develop and ends after one round with this change.
  • Each commit builds and passes the unit tests below; evo_islock_tests, evo_instantsend_tests, llmq_chainlock_tests pass.
  • Functional: feature_llmq_is_cl_conflicts.py, feature_llmq_chainlocks.py, p2p_instantsend.py, rpc_verifyislock.py pass.
  • macOS arm64, depends build.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T04:26:11.815261Z cfbfeef New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@PastaPastaPasta PastaPastaPasta changed the title fix(instantsend): purge cache of an overridden confirmed islock and bound the ConnectBlock conflict loop fix: purge cache of an overridden confirmed islock and bound the ConnectBlock conflict loop Oct 15, 2026
@thepastaclaw

thepastaclaw commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit cfbfeef) · triage: normal

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 605d35cb-e557-48cc-a375-f478a92ad2da
📥 Commits

Reviewing files that changed from the base of the PR and between a2113cb and cfbfeef.

📒 Files selected for processing: 1
  • src/test/evo_islock_tests.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

Walkthrough

Database cleanup now accepts the starting InstantSendLock and erases its cache entries even when the lock is no longer in the database. The conflict-removal method removes every stored conflicting lock. For chain-locked blocks, ConnectBlock calls this method; the non-chain-locked rejection path remains unchanged. A regression test checks lock and conflict status before and after cleanup.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to cfbfe

No actionable merge-blocking risk is established; the change is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly identifies the two primary changes: purging stale confirmed InstantSend-lock cache entries and bounding the ConnectBlock conflict loop.
Description check Passed The description directly explains the stale-cache issue, the conflict-loop behavior, the implemented fixes, and the validation performed.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2113cb096

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/instantsend/db.cpp
RemoveInstantSendLock(batch, islockHash, islock);
}
// Also needed when the lock was already confirmed: that removed it from the db but kept it cached
EraseInstantSendLockCache(islockHash, islock);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Purge already-confirmed descendant caches too

When the overridden lock has an IS-locked child that was mined at or below the same confirmation height (for example, parent and child are in one block), RemoveConfirmedInstantSendLocks has already deleted the child's DB_HASH_BY_OUTPOINT row while retaining its cache entries. The descendant scan in this function therefore cannot find that child, and this new unconditional erase clears only the root, so after the ChainLocked replacement IsLocked(child_txid) remains true and the child outpoint still reports a conflict. Track or traverse confirmed descendants and extend the new scenario with a confirmed child so both caches are cleared.

AGENTS.md reference: AGENTS.md:L139-L139

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accurate, but out of scope for this PR. Develop has the same behavior, and this PR doesn't make it worse.

A child lock that was confirmed together with its parent has already lost its DB_HASH_BY_OUTPOINT rows. Its entries survive only in the LRU caches, which have no index by parent, so GetInstantSendLocksByParent can't find it. Fixing that means changing what RemoveConfirmedInstantSendLocks keeps cached (keep_cache), not this removal path.

The impact is also limited. The child spends an output of the overridden transaction, which conflicts with the ChainLocked chain. So the child can never be mined there or re-enter the mempool, and its cache entries age out of the LRU. The ConnectBlock bound in the second commit doesn't depend on it either: that pass looks only at the block transaction's own inputs.


🤖 Posted autonomously by Claude on behalf of pasta.

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

Final validation — Phase 1 + Phase 2

Verified the supplied findings against exact head a2113cb and the surrounding InstantSend lifecycle. No blocking regression was confirmed; one combined suggestion remains for coverage of the new multi-conflict removal helper. This was a static review: the supplied CI snapshot shows lint and multiple builds passing, while several build and test jobs remain pending.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: normal by gpt-6.1-sol (effort low) — The change coordinates cache and database cleanup with bounded conflict removal across InstantSend and ConnectBlock, requiring cross-path reasoning but not meeting the large-or-intricate critical-surface threshold.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/test/evo_islock_tests.cpp`:
- [SUGGESTION] src/test/evo_islock_tests.cpp:250-252: Exercise the new multi-conflict removal entry point
  This scenario calls RemoveConflictingLock directly, so it tests the confirmed-lock cache purge but never exercises RemoveConflictingISLocksByTx, which now replaces ConnectBlock's repeated lookup/removal loop. Both inputs also belong to one lock, leaving removal of multiple independent roots untested. Extend this scenario to call the new helper with two confirmed locks conflicting with different transaction inputs, then check that neither transaction is reported as locked and no conflict remains. Add a matching-txid check to verify that the helper preserves the transaction's own lock. These checks would catch a traversal that stops after the first root or removes a non-conflicting lock.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Purge already-confirmed descendant caches too — Outside this PR's scope and independently confirmed as pre-existing. At base 546f2bf, confirmation already removes descendant database indexes while retaining caches, and RemoveChainedInstantSendLocks already discovers descendants exclusively through DB_HASH_BY_OUTPOINT. The PR leaves that traversal and confirmation policy unchanged, fixes the overridden root, and bounds removal by the replacement transaction's own inputs. Requiring a new confirmed-descendant index or cache-retention policy would expand the change rather than correct a regression introduced here.
    • Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.

Comment thread src/test/evo_islock_tests.cpp Outdated
Comment on lines +250 to +252
isman.RemoveConflictingLock(hash, *islock);
BOOST_CHECK(!isman.ConflictingISLockIfAny(conflict_tx));
BOOST_CHECK(!isman.IsLocked(islock->txid));

@thepastaclaw thepastaclaw Oct 15, 2026 •

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.

✅ Resolved at cfbfeef1; see the replies below.

🟡 Suggestion: Exercise the new multi-conflict removal entry point

This scenario calls RemoveConflictingLock directly, so it tests the confirmed-lock cache purge but never exercises RemoveConflictingISLocksByTx, which now replaces ConnectBlock's repeated lookup/removal loop. Both inputs also belong to one lock, leaving removal of multiple independent roots untested. Extend this scenario to call the new helper with two confirmed locks conflicting with different transaction inputs, then check that neither transaction is reported as locked and no conflict remains. Add a matching-txid check to verify that the helper preserves the transaction's own lock. These checks would catch a traversal that stops after the first root or removes a non-conflicting lock.

source: muse-spark-1.3-contributor (phase1-reviewer: general); gpt-6.1-sol (phase2-reviewer: general)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in cfbfeef. The test is now chainlocked_conflict_overrides_confirmed_locks. It confirms two independent locks on different inputs of one transaction, removes them through RemoveConflictingISLocksByTx, and checks that no conflict remains and neither transaction is reported by IsLocked. I also reordered the commits so that the bounded-pass helper comes before the test that uses it.

Mutation checks:

  • Dropping the cache purge fails !IsLocked(txid).
  • Making the helper return after its first removal fails !ConflictingISLockIfAny(conflict_tx).

I left out the matching-txid case. A transaction's own lock covers all of its inputs, so putting it on just one input of the conflicting transaction would be an artificial setup. The helper uses the same txid != tx.GetHash() filter as GetConflictingLock.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

Resolved (re-reviewed at cfbfeef1): Confirmed that the revised scenario calls RemoveConflictingISLocksByTx with two independently confirmed locks and checks both conflict removal and each transaction's unlocked status. Leaving out the artificial mixed matching-txid setup is reasonable: ProcessInstantSendLock validates a known transaction's complete lock-input set, and the new helper preserves GetConflictingLock's existing txid exclusion.

@thepastaclaw thepastaclaw added the pastaclaw:commented thepastaclaw's latest review was comment-only label Oct 15, 2026
PastaPastaPasta and others added 2 commits October 14, 2026 23:17
ConnectBlock looped while ConflictingISLockIfAny found a conflict and
removed one lock per round, so the loop only ended if every removal made
the lock invisible to the next lookup. Remove every lock conflicting
with the transaction's inputs in a single pass instead, so the work done
under cs_main is bounded: at most one root lock removal per input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemoveConfirmedInstantSendLocks erases a confirmed lock from the db but
keeps its islock, txid and outpoint cache entries. When a ChainLocked
block later conflicts with that lock, RemoveChainedInstantSendLocks
finds no db copy and so never erased those entries. The stale txid entry
kept IsLocked() returning true for the overridden lock's transaction,
because the hash it points to is archived and therefore still known.

Lookups by input or txid only stop returning the removed lock because
the use_cache=false db read overwrites its islock cache entry with
nullptr as a side effect. Pass the lock itself and always erase its
cache entries so removal no longer depends on that write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the fix/is-conflict-cache-purge branch from a2113cb to cfbfeef Compare October 15, 2026 04:20
@thepastaclaw thepastaclaw removed the pastaclaw:commented thepastaclaw's latest review was comment-only label Oct 15, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfbfeef17c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/instantsend/db.cpp
RemoveInstantSendLock(batch, islockHash, islock);
}
// Also needed when the lock was already confirmed: that removed it from the db but kept it cached
EraseInstantSendLockCache(islockHash, islock);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Erase only cache entries still owned by the removed lock

When an already-confirmed multi-input lock A has one cached input replaced by a newer conflicting lock B while another input still maps to A, overriding A through that other input reaches this unconditional purge and erases B's outpointCache entry as well. This state is reachable because PreVerifyIsLock only logs an overlapping input and WriteNewInstantSendLock overwrites its cache value; if B is then confirmed before another lookup repopulates the entry, confirmation removes B's DB index with keep_cache=true, so future competing blocks spending B's input no longer reach ConflictingISLockIfAny. Only erase each outpoint cache key when its current value still equals islockHash.

AGENTS.md reference: AGENTS.md:L242-L242

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. I'm leaving this as is: the erased entry can't be one the code depends on.

  • While B is unconfirmed, it is still in the db. Erasing its outpointCache entry only means the next GetInstantSendLockByInput falls back to the db DB_HASH_BY_OUTPOINT row, which still maps to B.
  • Once B is confirmed, its outpoint entry exists only in a 10000-entry LRU cache that evicts confirmed locks all the time. "Confirmed lock no longer visible by input" is already the normal end state for every confirmed lock, and nothing relies on those entries for correctness. A competing block for a confirmed (ChainLocked) B is rejected by ChainLocks.
  • Reaching the state also requires a quorum to sign B on an input that a confirmed transaction already spends. That is the same compromised-quorum precondition the ChainLock-overrides-IS path already assumes.

An ownership check before each erase would add code to protect an entry the cache can drop at any moment. The existing RemoveInstantSendLock(..., keep_cache=false) path erases the same keys without that check.


🤖 Posted autonomously by Claude on behalf of pasta.

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

Re-review — Final validation — Phase 1 + Phase 2

Reviewed the complete two-commit diff at cfbfeef and the surrounding conflict detection, confirmation, cache, and removal paths; no actionable in-scope defects were found. The revised regression scenario exercises removal of two independent confirmed locks through the bounded-pass helper and addresses the prior coverage finding. Verification was static only: no builds or tests were run, and the supplied CI snapshot from 2026-10-15T04:20:50Z still contained queued, pending, and running checks.

🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: normal by gpt-6.1-sol (effort low) — The change coordinates InstantSend cache invalidation, chained-lock removal, and bounded conflict handling in ConnectBlock, requiring cross-path reasoning but not constituting a large or intricate change to a critical surface.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.

No unresolved findings remain from the prior review on this head.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Purge already-confirmed descendant caches too — Out of scope: comparison with base 33670ce confirms that confirmation already deleted descendants' DB_HASH_BY_OUTPOINT rows while retaining their caches, and GetInstantSendLocksByParent already traversed only those database rows. This PR neither introduces nor worsens that limitation; its bounded helper examines the replacement transaction's own inputs and does not depend on discovering already-confirmed descendants.
    • Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.

@thepastaclaw thepastaclaw added the pastaclaw:approved thepastaclaw's latest review approved this PR label Oct 15, 2026
PastaPastaPasta added a commit that referenced this pull request Oct 31, 2026
…er PR event

c854022 ci: check potential conflicts on a schedule instead of per PR event (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  `predict_conflicts` shows as cancelled on many open PRs (#7623, #7763, #7568, #7885, #7886, #7823, #7216, …), so PRs whose build and test jobs all pass still look red.

  The job ran on every `pull_request_target` and `pull_request_review` event, in one concurrency group, `predict-conflicts`, shared by every PR. `cancel-in-progress: false` only protects the *running* job. A concurrency group holds at most one *pending* run, and when another run queues, GitHub cancels the older pending one. Each cancelled run's annotation reads `Canceling since a higher priority waiting request for predict-conflicts exists`. Out of the last 100 runs, 22 were cancelled this way, mostly when a review bot posted several reviews within seconds.

  These cancellations never clear. Each run is its own check suite on the PR's head commit, so every cancelled run stays in the PR's checks next to later successes. #7623, for example, shows 6 cancelled and 2 successful `predict_conflicts` entries.

  The group existed because each run read, changed and rewrote the conflict comments on *other* PRs. Overlapping runs could lose each other's updates, so they had to take turns.

  ## What was done?

  The job now runs on a schedule (`:07` and `:37` every hour) and on `workflow_dispatch`, with no PR triggers. Scheduled runs aren't attached to any PR, so the job no longer appears in PR checks at all.

  Each run works out every comment from scratch, so runs no longer depend on each other's writes:

  - It lists open PRs and, for every pair of ready PRs that share a base branch and touch the same files, runs the existing `pre_mergeable` check once. A merge conflict between two heads is symmetric, so one check covers both merge orders.
  - It rewrites each PR's comment to list the PRs it conflicts with. The old comment had separate "if this PR merges first" and "if these PRs merge first" sections that listed the same PRs.
  - A pair it can't check keeps what the comments already say about it. That covers a failed request (including a PR whose file list can't be fetched), a PR that already needs a rebase, and a PR GitHub hasn't yet checked against its base. This way a comment doesn't disappear and come back, which would notify subscribers again.
  - It deletes the comment on any PR that is no longer open and ready: PRs that a comment still lists, plus every PR closed or made a draft in the last day. The one-day window means a failed delete is retried, and it also catches two conflicting PRs that close together. Comments in the old format are still read, so the first run cleans up those too.
  - An unchanged comment isn't edited. A comment that can't be read is never written, and the run then fails so the problem shows up in the Actions tab.

  Other workflow changes:
  - The job only runs in `dashpay/dash`, because forks run scheduled workflows too.
  - It has a 20-minute timeout.
  - The step no longer uses `continue-on-error`, since a failure no longer affects any PR.

  The script drops the per-PR `--pr-number` and `--cleanup-closed` modes. Overall the change removes about 400 lines.

  Tradeoff: a comment can now lag a push by up to about 30 minutes. A dry run against dashpay/dash today (31 ready PRs) made 90 API requests and 60 `pre_mergeable` page loads in about 80 s. A real run also lists comments on about 10 recently closed or drafted PRs, so it comes to roughly 200 API requests an hour against the repo's `GITHUB_TOKEN` limit of 1000.

  ## How Has This Been Tested?

  - `python3 .github/workflows/test_handle_potential_conflicts.py`: 13 tests pass, on Python 3.14 and 3.10. They cover:
    - which pairs get checked: base branch, shared files, one check per pair
    - pairs that can't be checked, for each reason
    - keeping old entries only for unchecked pairs
    - clearing comments on drafts and closed PRs, including ones nothing lists any more
    - `--dry-run` never writing
    - never writing a comment that couldn't be read
    - editing only what changed, and reading old-format comments

    Mutation checks: undoing any of those behaviors (19 variants tried) makes a test fail.
  - `--dry-run` against live dashpay/dash. The comments it would write match the conflicts the current comments report; for example #7481 and #7623 list each other. It would also clear comments on PRs merged, closed or made drafts recently, including leftovers on #7536, #7728, #7856 and #7935.
  - flake8 with the repo's checks (flake8 5.0.4) and `test/lint/lint-github-actions.py` (zizmor 1.30.1) pass.
  - The workflow can't run before merge: scheduled and manually started runs use the default branch's copy of the workflow. After merge, a maintainer can start the first run from the Actions tab instead of waiting for the schedule.

  ## Breaking Changes

  None. `predict_conflicts` was never a required check, and it no longer appears on PRs. Cancelled entries already on a PR stay until that PR's next push.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [ ] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

  🤖 Generated with [Claude Code](https://claude.com/claude-code)

Top commit has no ACKs.

Tree-SHA512: c1e43f7b8f948b23a0b3309bc35d094719edf6ba5d5793dea4dd6a868d82495ae57cde3dc6706b0eff28b507aabb0691a0e0539c28d0c5674ebf5a7c9470bceb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pastaclaw:approved thepastaclaw's latest review approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants