Unclaim overwrites GitHub's labels from the local cache, silently reverting anything changed since the last sync
Problem
releaseIssueClaim in src/lib/issue-claim.ts computes the new label set from
the cached row and then writes that whole set to GitHub:
// line 93
let updatedLabels = issue.labels.filter((label) => label !== agentLabel);
...
// line 156
await updateIssueLabels(repoFullName, issueNumber, updatedLabels);
updateIssueLabels replaces the label set rather than removing one label. The
caller, src/app/api/issues/unclaim/route.ts:48, is explicit that its source is
the cache: "Fetch the issue from the local database to get current labels".
So when the cached row is behind GitHub, unclaiming does not just drop the
agent label — it reverts every label change made on GitHub since the last sync.
Labels added there are deleted; labels removed there are restored.
Observed
Unclaiming an issue restored status/backlog and a blocked/* label that had
been cleared on GitHub minutes earlier. Both are circulation-affecting: the
issue went back to a non-claimable lane and stayed parked. Recovering it took a
manual gh edit, then refresh_issue, then set_issue_status.
The failure is silent. The write succeeds, the audit log records the labels it
intended to write, and nothing reports that GitHub's state was discarded.
Why it matters beyond one issue
Anything that reverts status/ or blocked/ takes work out of the queue, and
the symptom presents as "the loop is ignoring this issue" rather than as a
labelling bug — the cause is several steps removed from where it is noticed.
Suggested fix
Prefer the targeted primitive over set replacement. issue-claim.ts already
imports removeIssueLabel and does not use it on this path:
- remove the agent label with
removeIssueLabel,
- apply any status transition through its own targeted call,
- leave every other label untouched, so a stale cache cannot revert it.
If a full-set write is genuinely needed, re-fetch the issue's labels from GitHub
immediately before computing the new set, and treat the cache as a hint rather
than the source of truth. A stale-write guard — comparing the fetched set to the
cached one and refusing to drop labels the cache never knew about — would also
surface the condition instead of silently applying it.
stale-work.ts also calls releaseIssueClaim, so it inherits the same
behaviour and should be covered by whichever fix lands.
Unclaim overwrites GitHub's labels from the local cache, silently reverting anything changed since the last sync
Problem
releaseIssueClaiminsrc/lib/issue-claim.tscomputes the new label set fromthe cached row and then writes that whole set to GitHub:
updateIssueLabelsreplaces the label set rather than removing one label. Thecaller,
src/app/api/issues/unclaim/route.ts:48, is explicit that its source isthe cache: "Fetch the issue from the local database to get current labels".
So when the cached row is behind GitHub, unclaiming does not just drop the
agent label — it reverts every label change made on GitHub since the last sync.
Labels added there are deleted; labels removed there are restored.
Observed
Unclaiming an issue restored
status/backlogand ablocked/*label that hadbeen cleared on GitHub minutes earlier. Both are circulation-affecting: the
issue went back to a non-claimable lane and stayed parked. Recovering it took a
manual
ghedit, thenrefresh_issue, thenset_issue_status.The failure is silent. The write succeeds, the audit log records the labels it
intended to write, and nothing reports that GitHub's state was discarded.
Why it matters beyond one issue
Anything that reverts
status/orblocked/takes work out of the queue, andthe symptom presents as "the loop is ignoring this issue" rather than as a
labelling bug — the cause is several steps removed from where it is noticed.
Suggested fix
Prefer the targeted primitive over set replacement.
issue-claim.tsalreadyimports
removeIssueLabeland does not use it on this path:removeIssueLabel,If a full-set write is genuinely needed, re-fetch the issue's labels from GitHub
immediately before computing the new set, and treat the cache as a hint rather
than the source of truth. A stale-write guard — comparing the fetched set to the
cached one and refusing to drop labels the cache never knew about — would also
surface the condition instead of silently applying it.
stale-work.tsalso callsreleaseIssueClaim, so it inherits the samebehaviour and should be covered by whichever fix lands.