Skip to content

Integrate reviewed reliability and hot-path fixes - #256

Merged
hamzamerzic merged 21 commits into
mobius-os:mainfrom
hamzamerzic:integrate/reviewed-fixes-20260727
Jul 27, 2026
Merged

Integrate reviewed reliability and hot-path fixes#256
hamzamerzic merged 21 commits into
mobius-os:mainfrom
hamzamerzic:integrate/reviewed-fixes-20260727

Conversation

@hamzamerzic

Copy link
Copy Markdown
Collaborator

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 main before every merge and has no merge queue; integrating the exact reviewed heads avoids six serial update-and-retest cycles while still validating their interactions before main moves.

Included, unchanged as ancestors of this branch:

  • #246 — classify self-requested Codex transport stops as interruptions while retaining warning-level forensics
  • #253 — publish the steering cut at the transcript’s actual split
  • #254 — observe the exact question/resume node published by its rendering surface
  • #247 — quiesce the idle shell frame pipeline and keep status cues non-colour-dependent
  • #255 — reconcile divergent platform histories with one bounded merge
  • #234 — render structured Memory recall outcomes and exact note citations

#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

  • every included PR head had privacy, backend, frontend-unit, and e2e green before integration
  • each exact reviewed head is an ancestor of this branch; no change was recreated or silently rewritten
  • all six merges completed without a manual conflict resolution
  • the complete combined diff was rechecked for whitespace, source-only scope, private paths/data, commit attribution, and the Möbius co-author trailer
  • 426 focused backend integration tests passed across steering, terminal handling, runner contracts, platform reconciliation, Memory parsing/persistence, transcript compaction, and tool-output handling
  • the complete frontend suite passed on the combined tree
  • the production frontend build passed on the combined tree

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.

hamzamerzic and others added 21 commits July 26, 2026 11:29
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>
@hamzamerzic hamzamerzic added the maintenance Internal cleanup, dependency, or upkeep work label Jul 27, 2026
@hamzamerzic
hamzamerzic merged commit 3b8ac61 into mobius-os:main Jul 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Internal cleanup, dependency, or upkeep work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant