Clear AI messages abandoned in processing - #5071
Open
elias-ba wants to merge 1 commit into
Open
Conversation
Security Review ✅
|
elias-ba
force-pushed
the
timeout-failure-taxonomy
branch
from
August 15, 2026 22:50
3683332 to
5a28e60
Compare
elias-ba
force-pushed
the
timeout-stuck-message-reaper
branch
from
August 15, 2026 22:50
01753c3 to
55042f3
Compare
elias-ba
force-pushed
the
timeout-failure-taxonomy
branch
from
August 15, 2026 23:40
5a28e60 to
1813938
Compare
elias-ba
force-pushed
the
timeout-stuck-message-reaper
branch
from
August 15, 2026 23:40
55042f3 to
3abf115
Compare
elias-ba
force-pushed
the
timeout-failure-taxonomy
branch
from
August 17, 2026 02:53
1813938 to
d16eb79
Compare
elias-ba
force-pushed
the
timeout-stuck-message-reaper
branch
from
August 17, 2026 02:53
3abf115 to
d0b1213
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## timeout-failure-taxonomy #5071 +/- ##
==========================================================
- Coverage 90.5% 90.5% -0.0%
==========================================================
Files 421 422 +1
Lines 20024 20044 +20
==========================================================
+ Hits 18131 18148 +17
- Misses 1893 1896 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
elias-ba
force-pushed
the
timeout-failure-taxonomy
branch
from
August 17, 2026 04:10
d16eb79 to
50ae521
Compare
elias-ba
force-pushed
the
timeout-stuck-message-reaper
branch
from
August 17, 2026 04:10
d0b1213 to
8d50a20
Compare
elias-ba
force-pushed
the
timeout-failure-taxonomy
branch
from
August 17, 2026 09:24
50ae521 to
d340e01
Compare
elias-ba
force-pushed
the
timeout-stuck-message-reaper
branch
from
August 17, 2026 09:24
8d50a20 to
abe0cd6
Compare
This was referenced Aug 17, 2026
processing_started_at has been written on every message since it started processing and read by nothing. This reads it. A message is left processing forever when its job dies without emitting telemetry. Attaching Oban's stop event covers most of that, but not the case it cannot reach: when a job outlives the drain window, Oban stops the producer first and kills the task after, so there is nothing left to report it. Until something clears the row the panel stays locked for everyone in that session, and nothing raises. Two guards decide what to reap, because neither is sound alone. Age is vulnerable to clock skew and to a queue that was paused. A live Oban job for the same message is the cross-node authority. The live set is read after the candidates, never before - a job starting in between then appears in it and is skipped, where the other order could reap a message whose job had only just begun. The write is a guarded update rather than a read-modify-write, so a job that finishes between the select and the write keeps its own result. The grace period sits well clear of the longest a run can legitimately take, so a slow answer is never mistaken for an abandoned one, and a partial index keeps the every-five-minutes scan off the table. The sweep is batched. Each message reaped loads its whole session to tell the panel, and the backlog this worker exists for is exactly when that would be largest; the oldest go first and the rest wait five minutes. The broadcast now carries the message id. Without it a listener has to guess, and it guessed by taking the newest - so clearing something stranded several exchanges back marked an unrelated later message as failed. The channel uses the id when it is given one and keeps the old guess only for broadcasts that do not carry it.
elias-ba
force-pushed
the
timeout-failure-taxonomy
branch
from
August 18, 2026 07:29
d340e01 to
e815b6a
Compare
elias-ba
force-pushed
the
timeout-stuck-message-reaper
branch
from
August 18, 2026 07:29
abe0cd6 to
43d8b43
Compare
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.
Description
processing_started_athas been written on every message since it started processing, and read by nothing. This reads it.A message is left processing forever when its job dies without emitting telemetry. Attaching Oban's stop event in #5069 covers most of that, but not the case it cannot reach: when a job outlives the drain window, Oban stops the producer first and kills the task after, so there is nothing left to report it. Until something clears the row the panel stays locked for everyone in that session, and nothing raises.
Two guards decide what to reap, because neither is sound alone. Age is vulnerable to clock skew and to a queue that was paused. A live Oban job for the same message is the cross-node authority. The live set is read after the candidates, never before: a job starting in between then appears in it and is skipped, where the other order could reap a message whose job had only just begun.
The write is a guarded update rather than a read-modify-write, so a job that finishes between the select and the write keeps its own result. The grace period sits well clear of the longest a run can legitimately take, so a slow answer is never mistaken for an abandoned one, and a partial index keeps the every-five-minutes scan off the table. The sweep is batched at 200, since each message reaped loads its whole session to tell the panel and the backlog this exists for is exactly when that would be largest. Oldest go first and the rest wait for the next pass.
Reaping something also meant telling the panel which message it was about, which is the second half of this PR.
The broadcast now carries the message id. Without it a listener has to guess, and it guessed by taking the newest, so clearing a message stranded several exchanges back marked an unrelated later message as failed.
broadcast_status/3carries the id,broadcast_message_error/2takes it, and the channel resolves it. Broadcasts that do not carry an id keep the old guess, so nothing else changes behaviour.Closes #4260
Validation steps
Lightning.AiAssistant.StuckMessageReaper.perform(%Oban.Job{})The panel should unlock and the message show as failed.Additional notes for the reviewer
The second half changes which message the client is told failed. Broadcasts carrying no id keep the old "newest user message" guess, so only the reaper's path behaves differently.
AI Usage
Please disclose whether you've used AI anywhere in this PR (it's cool, we just want to know!):
You can read more details in our Responsible AI Policy
Pre-submission checklist
/reviewwith Claude Code):owner,:admin,:editor,:viewer)