fix(orchestration): hydrate command read model, resolve orphaned approvals, settle turns honestly - #457
Merged
Merged
Conversation
…command read model getCommandReadModel returned messages/activities/checkpoints as empty arrays for every thread. The decider's command guards fold exactly these collections — hasOpenBlockingRequest (open approvals), threadHasQueuedTurnStart (queued sends), and branch context all read them — so after every server restart every guard operated on nothing until live events happened to repopulate it: a thread could be settled or snoozed while an approval was still open, and branching a thread copied zero messages. Hydrates from the same projection tables and with the same caps (2,000 messages/activities, 500 checkpoints) the live in-memory fold already uses, so a restarted engine and a long-running one see the same window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d approvals, clear stale pending starts
Three related fixes to the turns and pending-approvals projectors:
- A turn superseded by steering (a new active turn opening before the
provider ever completed the old one) was upserted as "completed" —
wrong history plus a bogus "Agent finished" banner for abandoned
work. Now settles as "interrupted".
- applyPendingApprovalsProjection had no thread.session-set or
thread.reverted handling: a session that ended (stop, error, restart
sweep) with an open approval left the row pending forever — the
thread's "waiting for approval" badge stuck permanently, settle and
snooze both rejected, the card unanswerable. Session end and revert
now resolve every pending row for the thread. Also folded a known
stale-detail string ("No active provider session is bound to this
thread") into the existing stale-request classifier.
- A pending turn-start row (turn_id NULL, written between
thread.turn-start-requested and turn.started adopting it) was only
ever cleared on adoption. A start that fails before adoption left the
row behind, so the next genuine send inherited a stale
messageId/proposed-plan association. Any terminal session-set now
clears it too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-session sweep A crash mid-stream left an assistant message flagged streaming forever — projected clients render an eternal "responding" shimmer on a thread the server has already declared dead. The startup sweep that interrupts stale running sessions now also finalizes any assistant message still marked streaming on that thread, in the same interrupt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
Four related orchestration-projector fixes from the 2026-08-03 server-side triage:
getCommandReadModelreturnedmessages: [], activities: [], checkpoints: []for every thread. The decider's command guards fold exactly these —hasOpenBlockingRequest(open approvals),threadHasQueuedTurnStart(queued sends), branch context — so after every server restart every guard operated on nothing until live events happened to repopulate it: a thread could be settled/snoozed while an approval was still open, and branching a thread copied zero messages. Now hydrated from the same projection tables and caps the live in-memory fold already uses.streamingforever, rendering an eternal shimmer on a thread already declared dead. Both now clear/finalize on the terminal session-set the startup sweep already writes.Area
apps/macapps/windowsapps/mobileapps/server— backend serverRelease size
size:XSsize:Ssize:Msize:Lsize:XLVerification
pnpm run verify --allafter merging current main: 5/5 steps, exit 0🤖 Generated with Claude Code