Fix/clean 102 chat message reliability - #95
Merged
maksymhryzodub-prog merged 10 commits intoSep 18, 2026
Merged
Conversation
Spec 015: lost, duplicated and misordered chat messages, delivery state, timestamps, scroll on send, agent status consistency. Research records the causes found in code and the four defects reproduced against the local hub with the scripted socket probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…issed events (CLEAN-102) The hub kept one socket per identity and every Owner/Admin chats as `admin`, so the last view to connect took every event: the tab that asked got nothing and another tab showed the answer. A conversation now holds all its sockets and fans out to them; the sender's other views get a `user_message` echo. Sends are acknowledged through the socket.io ack when the browser mints a `clientMessageId`: accepted / rejected (AGENT_OFFLINE, ATTACHMENT_FAILED, SHARE_REJECTED, EMPTY), the id travels to the agent as the message id, and a resend of an id already handed over is not forwarded twice. Callers that send no id (embed widget) keep the synthetic "Agent is not connected" reply. Routed events carry a per-conversation `seq` and are kept in a bounded buffer, also while no socket is connected; a reconnect with `lastSeq` gets what it missed before live traffic. Verified against the local stack with specs/015-chat-message-reliability/probe.mjs (steal, gap). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…N-102) The runtime stores one assistant event per turn; its text is every bubble of the turn concatenated, so a reload showed "Проверю:Ха! Работает!" in a single bubble. When the event carries the bubbles (`data.messages`, written by the runtime from CLEAN-102 on), the reader replays those under their wire ids. Older events and any malformed `messages` array fall back to the whole text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…102) The drift sweep moves agents to `failed` / `unreachable` with no pod or hub event behind the change (the 5-minute startup timeout), and GET /agents/:id reconciles on read. Neither reached `/agents/status/stream`, so an open agents list kept showing `deploying` until somebody opened that agent. Every status write in AgentStatusService now goes through one method that also emits a `modified` frame; the controller's sync-on-read notifies the same way. docs/state.md records the client-side rule this belongs to: one record per entity in its store, fetches upsert, pushes patch, components render by id. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The agents list, the agent page and the chat page each held their own copy of an agent, so a status could differ between them. The store is now the single source: fetchById/create/update/restart upsert into `agents`, components render `byId(id)`, and the optimistic restart flip goes through `patch()` with a rollback. A failed poll no longer wipes the record the open page renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cts (CLEAN-102) Order: the timeline was sorted by `ts`, mixing the browser's clock (own messages) with the agent's (replies), so with skew a reply rendered above its question. Items now get a per-conversation `seq` on arrival and the flow is built by buildChatFlow; `ts` is display only. Delivery: a send is acknowledged by the hub. Messages carry sending / slow (5 s) / delivered / failed (30 s, rejected, offline) and the state persists with the conversation, so a reload shows an undelivered message as such, with Resend (same id) and Discard. A follow-up can be sent while the agent is still answering. Reconnects: the channel is reference-counted with a grace close, so moving from the landing chat to the agent page keeps the socket and the running turn. `lastSeq` rides the handshake and replayed events are applied once; a transient disconnect no longer ends the turn; if the watchdog still expires the transcript tail is consulted and the person is told when the turn really did not finish. Also: time under every message, day separators, scroll to bottom on send while incoming content only follows a reader already at the bottom, and the question asked in another tab shows up here too. Strings via en.json + i18n:sync. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ivery state (CLEAN-102) The bridle store was one global message list and socket shared by every chat, so the Rancher panel and an agent's Chat tab wrote into the same state and a mount-time clear wiped whatever the other one held. State now lives in a record per "<agentId>:<channel>" with its own socket, acquired and released by the provider; loading the transcript merges instead of replacing. On top of that, the same fixes as the console: order by `seq` instead of mixed-clock `ts`, acknowledged sends with sending / slow / delivered / failed, a per-device outbox for messages that never arrived (merged under the transcript on load, Resend with the same id, Discard), `lastSeq` replay after a reconnect, turns that survive a transient disconnect, time under every message, day separators, and scroll to bottom on send. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s the UI (CLEAN-102) The agents list showed "Deploying" next to a header saying "Failed". Three causes: the same agent lived in several independent copies (list array, a fetchById copy per component, the status store's own map, a local optimistic ref); the status stream mapper read `type` off the API's response envelope and decoded every frame to null, so no live update ever applied; and the REST mapper turned `unreachable` into `pending`. An agent now lives once in agentStore: fetches upsert, the stream's snapshot and events write into the same collection, components render byId(id), and optimistic status flips go through patch() with a rollback. The list row shows the failure reason. Rule recorded in docs/state.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
research.md gains the after-the-fix results (scripted hub probe, headless Chrome checks under a +2 min page clock) and an explicit list of what is still unverified. The browser checks live in specs/015-chat-message-reliability/e2e and need no dependency in the repo; message bubbles get data-message-id / data-role / data-delivery as hooks for them. The contract records where the implementation refines it, tasks.md is ticked, and CLAUDE.md points at docs/state.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <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.
No description provided.