Summary
createOrUpdateDashboard() has two paths where a transient API failure produces a permanent duplicate Dependency Dashboard instead of a failed run. Both are on main as of 4d3d670.
We hit this on stacksjs/system-cleaner, which accumulated three buddy-bot dashboards (stacksjs/system-cleaner#1 opened 2026-03-30, then stacksjs/system-cleaner#7 and stacksjs/system-cleaner#8 on 2026-04-27, 1h53m apart) alongside one from Renovate. I've since closed the extras and kept one, but the cause is upstream, so it will recur.
It's rare — two misses in about six months, and the surviving dashboard has been updated in place ever since — so this reads as a latent correctness bug rather than anything urgent.
I notice the comment at src/buddy.ts:3645-3657 documents a different duplicate-dashboard cause you've already fixed (labels removed by a maintainer made a dashboard unfindable, hence the body marker). These are two more causes in that same class.
Path 1 — the lookup fails open
this.logger.info('No existing dashboard issue found')
return null
}
catch (error) {
this.logger.warn(`Failed to search for existing dashboard: ${error}`)
return null
}"Searched, found nothing" and "the search threw" return the same value. The caller at :3358 branches solely on truthiness, so it cannot tell them apart.
The only network call inside that try is getIssues(), which rethrows on any failure — src/git/github-provider.ts:2105-2108:
catch (error) {
this.logger.error('❌ Failed to get issues:', error)
throw error
}So any 5xx, timeout, socket error or secondary rate-limit on GET /repos/{owner}/{repo}/issues becomes null → "no dashboard exists" → createIssue(). The run still exits 0.
The race-check at :3376 calls the same function, so it fails open in exactly the same way and provides no protection here.
Path 2 — a failed update falls through to create, and the race-check guard is defeated
if (existingIssue) {
issue = await this.updateDashboardIssue(gitProvider, existingIssue.number, issueOptions)
}
if (!issue) {
const raceCheckIssue = await this.findExistingDashboard(gitProvider, dashboardConfig.issueNumber)
// Skip the race-check result if it is the issue we just found to be missing
if (raceCheckIssue && raceCheckIssue.number !== existingIssue?.number) { ... }
if (!issue) {
issue = await gitProvider.createIssue(issueOptions) // duplicate
}
}updateDashboardIssue() returns Promise<Issue | null> and returns null from its own catch (:3597). So when the dashboard is found but updating it fails transiently:
existingIssue= the real dashboard (say issue N),updateDashboardIssue(N)throws →null!issue→ enter the create branch- race-check finds N again — but the guard is
raceCheckIssue.number !== existingIssue?.number, i.e.N !== N→ false, so the one issue that would have prevented the create is explicitly skipped createIssue()→ duplicate
The guard's comment says "the issue we just found to be missing", but existingIssue is populated precisely when it was not missing. This path turns a failed update into a duplicate every time.
Suggested fix
The general principle: a failed run is strictly better than a duplicate dashboard.
- Make the lookup fail closed. Have
findExistingDashboarddistinguish found / absent / errored — a discriminated result, or just let the exception propagate — and havecreateOrUpdateDashboardabort onerroredrather than falling through tocreateIssue. - Don't create when an update failed. Only enter the create branch when
existingIssuewas genuinely absent. If it was found and the update threw, fail the run and let the next scheduled one retry. - Optionally, reconcile: after creating, close any older marker-bearing dashboards, so a duplicate that does slip through self-heals instead of persisting.
Happy to send a PR if the approach looks right to you.
Environment
- buddy-bot
0.10.5installed when observed; verified present onmainat4d3d670(latest releasev0.11.1) - Affected repo: stacksjs/system-cleaner