Skip to content

Duplicate issue detection

New issues keep receiving the existing general research response from claude-issue-summarize.yml. Label triage and the interactive @claude responder also keep their existing behavior. Duplicate detection runs independently, so research does not have to wait for search or stop when a potential match exists.

claude-dedupe-issues.yml uses anthropics/claude-code-action@v1 and the search method from Anthropic's dedupe command: five parallel searches with different approaches, followed by a separate agent that reads and verifies the candidates. Our .claude/commands/dedupe.md adapts the criteria to dismech's research-notebook use: related diseases, mechanisms, papers, and follow-up ideas are not automatically duplicates.

The agent has a read-only GitHub token and returns structured candidates. A separate job checks that the issues are older and still open, and that no human has added context while the search ran, then posts up to three links with explanations and adds duplicate-pending. Unusable or repeated candidates are discarded; valid remaining matches can still be posted. If the notice fails, no label is added; if labeling fails, the notice is rolled back for a retry. It records the oldest match as the canonical issue. The model is selected through .github/agent-config.yaml (Sonnet by default); manual dispatch accepts an issue number and optional model override.

The upstream three-day closure pattern is implemented by .github/scripts/issue-duplicates.js using GitHub's REST API:

  • The bot's duplicate notice starts a three-day objection window.
  • Any subsequent human comment, or any human 👎 on the notice, prevents closure. Bot comments, including the normal research response, do not cancel it.
  • On the next sweep, objections, edits, and reopening also remove duplicate-pending, even before the three days expire. Missing or invalid notices are removed from the queue too. Dry runs only report this cleanup.
  • Removing duplicate-pending also prevents closure. A rerun does not restore a removed label or reopen the objection window.
  • Edited, locked, or reopened issues are left open. The canonical issue must still be open when the closure runs. Existing labels are preserved.
  • A daily sweep closes eligible issues with GitHub's native duplicate reason and canonical issue ID. This happens on the first sweep after the full three days, not at an exact wall-clock deadline.
  • Each sweep attempts at most five closures by default. Manual dispatch accepts max_closures from 1 to 50; failed closure requests also consume that budget. Excess candidates stay queued, while cancellation cleanup continues. An API failure on one issue is logged as a warning and does not stop the other issues.
  • Only notices carrying our marker and posted by github-actions[bot] count. Rerunning detection does not create another notice or restart the clock. Existing duplicate labels or comments from other workflows cannot trigger this closure process.

The sweep paginates open issues labeled duplicate-pending, plus their comments, reactions, and timeline events directly through the API. This avoids scanning every open issue's discussion and does not use GitHub's search index to find pending closures. Candidate discovery remains agentic GitHub search, supplemented by a list of recent issues. There is no local embedding index or hosted database.

The sweep's daily schedule is managed by .github/cron-profiles.yaml; the off profile disables it. Manual dispatch defaults to a dry run, logging which issues would close and which labels would be cleared. After merging, run a preview over the first real proposals before their three-day windows expire:

gh workflow run auto-close-duplicates.yml -f dry_run=true -f max_closures=5

Tests run offline with node --test tests/js/issue_duplicates.test.mjs and are included in the existing just test-search CI check. These exercise the lifecycle rules; they do not measure the model's duplicate-detection accuracy.