Integrate reviewed reliability and hot-path fixes - #256
Merged
hamzamerzic merged 21 commits intoJul 27, 2026
Conversation
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Steering a message into a live Claude turn made the assistant's output paint twice for the rest of that turn: everything streamed inside a multi-second window appeared once in the sealed pre-steer message and again at the head of the continuation. The bad boundary replayed out of the event log, so it reproduced on every reconnect. `steered_into_turn` is the client's only "seal the live stream here and re-base it" instruction. On the Claude path the steer route published it the moment the HTTP request arrived, but `ActiveClaudeClient.steer()` merely BUFFERS the request there; the runner performs the real transcript split much later, at its next interrupt boundary. Every block emitted inside that window was persisted into the sealed pre-steer message AND retained at the head of the client's freshly cleared stream. The same 202 also reported an optimistically emptied queue on that path, so the steered row left the tray while the server still held it. Codex was never affected: `turn.steer()` has no interrupt boundary, so for Codex the route genuinely is the split point. Regression from the change that moved the split to the runner and left the signal behind at the route. - One builder for the event, `steered_into_turn_event()` in `app/chat.py`, so the two publishers cannot drift on the wire shape. - The route publishes the cut only on the non-deferred (Codex) path, where the split really happens. Codex behaviour is unchanged. - The Claude cut is published by `_seal_steer_split()` itself, immediately after `split_for_steer` commits, on the sink's own broadcast, with no `await` between the split and the publish. A split with no resolvable broadcast still commits, and logs loudly. Announcing the cut can never raise out of the seal: the turn-end `finally` awaits it before unregistering the handle, so an escaping publish error would strand a live handle and leave the chat looking permanently busy. - The 202 now reports the pending queue as it actually is and marks the deferred case with `cut_deferred: true`. The client keeps the row in the tray until the cut retires it, and resolves only its own row by cid, so the 202 and the cut may land in either order. - `interrupt()` (Stop) now drops the buffered transcript rows as well as the provider-facing text: Stop's clear-and-resend path owns those rows from that point, and leaving them buffered let the dying turn's seal append the very row the client was re-sending. Tests: backend/tests/test_chats_stream_steer.py 31 passed (26 before); frontend `npm test` 1976 passed, 0 failed (1967 before). New `steerCutBoundary.test.js` pins the client half of the contract; three deferred-window ordering cases added to `usePendingQueue.test.js`. Mutation-checked: re-introducing the arrival-time publish, or letting a publish error escape the seal, each fail a named test. Backend changes require a server restart to take effect. Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
The "tap to answer" cue for a pending question could stick on screen while the question card was fully visible and — because the turn is parked waiting on the answer — never clear on its own. Only a reload cleared it. The resume cue had the identical shape. `useOffscreenNudge` resolved its observed target with a one-shot `querySelector` inside its own layout effect, while callers passed a hand-maintained list of rebind triggers. The pending question card genuinely changes DOM node mid-turn: it is rendered by the live streaming surface first and by the durable message row after promotion. Any commit that performed that handoff without one of the enumerated triggers changing left the `IntersectionObserver` bound to a node React had already detached, so it reported nothing ever again. With the turn parked awaiting an answer, nothing re-renders to rebind it. Whether a refresh landed inside that particular commit was a race, which is why it presented as intermittent. The hook now takes the ELEMENT rather than a finder plus a trigger list, so node identity IS the effect dependency and a stale observer becomes unrepresentable. Identity is published by `useNudgeTargetRef()`, a `useState`-backed callback ref that is stable across renders, threaded to `QuestionCard` through both render paths, so the live-to-durable handoff reaches the observer as an ordinary node swap. Only the card that actually blocks the turn publishes: the unanswered tail question, and the tail resumable note. The tail gate lives at the publication site because `MsgContent` renders a Resume button on every resumable block of the last message and one shared callback ref has exactly one slot — the same `.pop()` semantics the old `querySelectorAll` lookup had, now expressed where it cannot drift from what arms the cue. Tests: frontend `npm test` 1977 passed, 0 failed (1967 before). Nine new `useOffscreenNudge` cases cover a card remounting onto a new node, the live-to-durable handoff (including a batched one, which must never blank the cue), a handoff onto an already-visible row, the cue retiring when the pending state ends, ref stability across renders so memoized rows keep publishing, and observer release on unmount. `resumeAffordance.test.js` pins that both nudges observe a node published by the card rather than a lookup. The hook test shim gained real effect cleanup and `unmount()` semantics, without which a leaked observer is indistinguishable from a released one. Mutation-checked: restoring the lookup-based bind fails a named test. Frontend only; no restart required. Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
…ged merge as error Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
… split Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.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
This integration PR lands six independently reviewed, individually green changes with one final combined CI run. The repository requires a branch to be current with
mainbefore every merge and has no merge queue; integrating the exact reviewed heads avoids six serial update-and-retest cycles while still validating their interactions beforemainmoves.Included, unchanged as ancestors of this branch:
#245 was merged normally immediately before this integration branch was cut. The companion Memory producer, mobius-os/app-memory#14, is already merged and green on its own
main.Review and integration checks
Deliberately excluded
#241 is not included. Its mobile viewport lock removes native page zoom from ordinary chat text and shell controls, and the branch does not yet provide an equivalent reading-size affordance. That remains an explicit accessibility/product decision rather than being smuggled into an integration merge.