fix(linearbot): stop working an issue that is taken back - #1560
Open
0xAlcibiades wants to merge 3 commits into
Open
fix(linearbot): stop working an issue that is taken back#15600xAlcibiades wants to merge 3 commits into
0xAlcibiades wants to merge 3 commits into
Conversation
Un-delegating an issue does nothing. parseIssueAssignmentWebhook returns null unless the issue is currently assigned or delegated to the bot, so the payload that says "stop" is exactly the one it discards, and nothing else watches for it. A turn still waiting to start runs anyway, minutes later; a turn already streaming keeps its sandbox and its comment to the end, on an issue somebody has visibly taken back. Add parseIssueReleaseWebhook for the mirror case -- dropped as assignee AND as delegate, with updatedFrom naming the bot as the previous value, since "not assigned to the bot" otherwise describes almost every issue in the workspace. Losing one field while still holding the other is not a release. Neither is the bot releasing the issue itself, which is how an agent normally signs off. Handling splits on whether the turn has started. One that has not is dropped where it stands: nothing has been posted and no sandbox exists, so never starting is the whole of the cleanup. One that has is interrupted through api-rs, ending the execution and letting the sandbox fall to the usual idle reclaim instead of holding a fleet slot to the end of work nobody wants. A thread this process has no record of is interrupted too -- a turn that outlived a restart is exactly the case where the issue looks busy and no local state explains it. The issue's status is left alone. Whoever took it back decides where it belongs.
0xAlcibiades
force-pushed
the
alcibiades/release-undelegated-issues
branch
from
August 30, 2026 17:37
45e7e57 to
8b38f8e
Compare
Contributor
Author
|
Folded into #1537 — the release path and the start stagger are the same handoff path, and reviewing them apart meant reading the same call site twice. |
Contributor
Author
…is stale The release path decided whether to interrupt from the per-thread pending map, and skipped the interrupt when the entry read started: false. That entry is overwritten by a newer assignment, so at release time it can say "not started" while a turn from the evicted, earlier handoff is still streaming on the issue that was just taken back -- and the skip let that older turn keep working. Interrupt unconditionally: it is a no-op when nothing is running, and the pending.released flag already keeps a still-queued turn from ever starting. Split the action into applyRelease so the unconditional interrupt is tested.
…ment A take-back webhook redelivered out of order after the issue is re-delegated reads the *newer* assignment's pending entry, so an unconditional interrupt kills the legitimate re-delegated turn and a released-mark drops it before it starts. Guard on the issue updatedAt (Linear advances it on every change): if the release strictly predates the assignment that owns the current entry, skip both the interrupt and the released-mark. With no entry -- or an unparseable timestamp -- the interrupt still runs, so a real take-back always reaches a pre-restart turn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1559
Change
parseIssueReleaseWebhookis the mirror ofparseIssueAssignmentWebhook, andhas to be a separate parser because that one returns
nullthe moment the botis no longer on the issue — the payload that says "stop" is exactly the one it
discards.
A release is the bot being dropped as assignee and as delegate, with
updatedFromnaming the bot as the previous value. That last part is loadbearing: "not assigned to the bot" describes almost every issue in a workspace,
so without it every unrelated edit anywhere would qualify.
Two cases deliberately do not count:
the assignee is still meant to be working.
turn is how an agent signs off; acting on it would interrupt the turn that
just did the work.
Handling splits on whether the turn started
A handoff turn does not begin the instant its webhook lands — it waits its place
among concurrent handoffs, then spends seconds fetching issue context and
spawning a sandbox. So
pendingAssignmentstracks, per thread, whether the turnhas started, because the two cases need different things:
sandbox exists, so never starting is the whole of the cleanup.
POST /api/session/{thread_key}/interrupt,which ends the execution and lets the sandbox fall to the normal idle reclaim
instead of holding a slot against
--session-sandbox-running-limitfor therest of a turn nobody wants.
A thread this process has no record of is interrupted too. A turn that outlived
a restart is exactly the case where the issue looks busy and no local state
explains it, and
interruptreportsinterrupted: falseharmlessly when thereis nothing running.
The map is per process, like the work it tracks. A restart loses the entries,
which costs nothing: it also loses the turns.
What this does not do
It does not touch the issue's status. Whoever took the issue back decides where
it belongs, and moving it here would fight them.
Testing
Nine parser tests: dropped as delegate; dropped as assignee; handed to someone
else outright; still held by the other field (no fire); an unrelated edit on
someone else's issue (no fire); no
updatedFromat all (no fire); the botreleasing it itself (no fire); a
create(no fire); andupdatedFromnestedunder
data.bun testinservices/linearbot: 117 pass.bun run check:typesclean.