Keep the running footer until a chat's messages are current (#9170)

* fix(app): change the turn footer together with its messages

The footer followed the agent directory while the messages followed the
timeline catch-up, so a chat returning to a turn that ended in the
background applied the idle snapshot first: the running footer vanished
or a completed footer sat under stale messages until the catch-up landed.

The timeline now holds the turn its messages were captured during, and
releases it in the same store write as the rows that bring the chat
current. While held, the footer shows an updating state in the running
footer's slot. The sidebar, tab and composer keep reading the true status.

Losing the connection does not release the hold. No catch-up can run or
fail until the host returns, and releasing it would put a completed
footer under messages known to be behind.

* test(app): expect the updating flag on submission-only activity

* fix(app): keep a failed catch-up from restoring the updating footer

A subscription failure could land while the chat's cache read was still
pending. The failure released the held turn, then the read restored the
turn saved with the messages, and no catch-up remained to release it, so
the chat showed a sync error beside "Updating...".

The replica writes the held turn from the cache, so it now also hears
when a catch-up settles or is owed again, and a read in flight respects it.

* fix(app): keep the running footer until a chat's messages are current

A separate catching-up footer added a visible state nobody asked for. A
chat that is behind now keeps showing the running turn it already shows,
so the idle status changes nothing on screen until the messages land with
the completed footer.

The held turn carries its start time, and saves it with the cached
messages, so the elapsed timer keeps counting from the turn's real start
through the hold and on first paint from cache.

* fix(app): let a newer turn's timer replace a held one on hosts without turn ids

Old hosts report a null turn id for every turn, so comparing ids alone kept the older turn's start time and the held footer counted from it until the chat caught up.
This commit is contained in:
Mohamed Boudra
2026-11-09 15:47:29 +07:00
committed by GitHub
parent 180f07587a
commit b2b82e5cb0
12 changed files with 912 additions and 17 deletions
+15
View File
@@ -112,6 +112,21 @@ offline, then Updating messages until authoritative catch-up completes. Socket c
certify that the displayed conversation is current. The timeline owner publishes freshness; the
view renders it without a toast timer or a separate resume workflow.
The turn footer in a chat follows its messages, not the agent status. Turn activity comes from the
agent directory and messages from the chat's own catch-up, and either can land first. So the timeline
owner keeps an unsettled turn per chat: the turn its messages were captured during. It is set when a
catch-up becomes owed while the turn is open, or when cached messages are painted that were saved
while one was, and it is saved with those messages in the replica. It carries the turn's start time.
While it is set and the agent reads idle, the chat keeps showing that running turn, with its loader
and elapsed timer unchanged, and there is no separate catching-up footer. The commit that brings the
chat current clears it, in the same store write as the rows, so the final messages and the completed
footer land together. A failed catch-up clears it too, and the footer takes the true status. Losing
the connection is not a failed catch-up: none can run or fail until the host returns, so the running
footer stays beside "Reconnecting to host" and the catch-up on reconnect resolves it. Releasing it
offline would put a completed footer under messages known to be behind. Surfaces outside the timeline (the sidebar, tabs, the composer's stop and send controls) read the true status
and never see this state. Live delivery needs no hold because `applyStreamEvent` commits queued rows
before it applies turn liveness.
The draft-create handoff has the same lifetime: the viewed-timeline owner releases it when the sync
stops owing that chat a catch-up, not when the first authoritative page lands. Releasing it at the
first page leaves a chat this client just created looking hydrated but still catching up, which