feat(FN-4040): add optimistic room send with failure handling and reconcili

The merge implements optimistic room send with failure recovery in the ChatView and useChatRooms hook, including a reconciliation flow that handles send failures gracefully and documents the UX behavior. The dashboard guide and architecture docs are updated to reflect the new room messaging pattern.

Fusion-Task-Id: FN-4040
This commit is contained in:
Fusion
2026-05-11 16:45:34 -07:00
committed by gsxdsm
parent 2ccf8c699d
commit a2303c67a9
7 changed files with 124 additions and 14 deletions

View File

@@ -66,8 +66,6 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
| [Reports Plugin](./plugins/reports.md) | Reports plugin rendering, export, standalone HTML generation, and section configuration |
| [Even Realities Plugin API](./even-realities-plugin-api.md) | Even Realities plugin API endpoint reference and test coverage matrix |
| [Memory Plugin Contract](./memory-plugin-contract.md) | Pluggable memory backend architecture, interface contract, and migration strategy |
| [CLI Printing Press Design](./design/cli-printing-press-plugin.md) | Fusion integration architecture for the CLI printing press plugin (FN-3762) |
| [CLI Printing Press Research](./research/cli-printing-press.md) | Upstream architecture and integration research for the CLI printing press generator (FN-3761) |
### Audit Reports
| Report | Description |

View File

@@ -283,7 +283,8 @@ Intentional exclusions from shared snapshots:
- Room messages persist through `POST /api/chat/rooms/:id/messages`; the route persists the user message first, then calls `ChatManager.sendRoomMessage(...)` to orchestrate room-member responders and persist assistant room replies with `chatStore.addRoomMessage(...)` (including `senderAgentId` for each responder).
- `sendRoomMessage(...)` uses existing room-member + mention resolution rules: mentioned members are direct responders, non-mentioned members are ambient responders (capped by `ROOM_AMBIENT_MAX_RESPONDERS`), and non-member mentions are handled explicitly by the manager instead of silently disappearing.
- Room-reply generation is now non-silent on failure: if a room has members but no active responders can be resolved, or all responder generations fail/return empty output, `sendRoomMessage(...)` throws `RoomReplyGenerationError` and the route surfaces HTTP 502 instead of returning a silent user-only success.
- UI does not optimistically insert room messages; `useChatRooms.sendRoomMessage()` re-fetches authoritative room messages immediately after `POST /api/chat/rooms/:id/messages` so persisted assistant replies remain visible across SSE timing gaps, then continues applying `chat:room:message:*` events for live updates.
- `useChatRooms.sendRoomMessage()` now follows direct-chat style optimistic UX: append a temporary local user room message before `POST /api/chat/rooms/:id/messages`, reconcile that temp entry to the persisted user message on success, then refresh authoritative transcript state while continuing `chat:room:message:*` live SSE updates.
- On failures, `useChatRooms.sendRoomMessage()` performs state reconciliation (rollback temp entry or replace with persisted transcript when POST partially succeeded) and rethrows; `ChatView` owns the single user-facing toast and keeps composer text for retry.
- Mention UI in rooms keeps direct-chat behavior unchanged while adding room affordances:
- `AgentMentionPopup` receives room membership context and shows members first with a `status-dot` member indicator (`aria-label="Room member"`).
- With an empty mention filter in room mode, only room members are listed; a hint row prompts the user to type to search non-members.

View File

@@ -113,14 +113,15 @@ Chat Rooms are project-scoped group conversations for multiple agents. They are
- Each room row includes a trash action (`aria-label="Delete room {name}"`, `data-testid="chat-room-delete-{slug}"`) that opens a **Delete Room?** confirmation dialog with **Cancel** and **Delete** actions.
- Confirming delete calls `rooms.deleteRoom(roomId)` and permanently removes the room and its messages ("This action cannot be undone. This room and all its messages will be permanently deleted."); failures surface a `Failed to delete room` toast.
- Selecting a room opens the room thread pane with loading and empty states, then renders room messages from `rooms.messages` as `ChatMessageInfo` entries in the same thread UI used for direct Chat.
- Submitting the room composer calls `rooms.sendRoomMessage(...)`, which posts the user message to `POST /api/chat/rooms/:id/messages`.
- After a successful room send, the room composer is cleared (matching direct-chat composer behavior) so stale text is not left in the input.
- Submitting the room composer calls `rooms.sendRoomMessage(...)`, which immediately inserts a temporary local user message and then posts to `POST /api/chat/rooms/:id/messages`.
- On successful room send, the optimistic message is reconciled with persisted server data, the transcript is refreshed to authoritative history, and the composer clears (matching direct-chat clear-on-success behavior).
- On mobile, room threads use the same keyboard-aware thread anchoring as direct chat, keeping the composer pinned above the soft keyboard while typing.
- The dashboard backend now orchestrates room responders on that POST: mentioned members are routed as direct responders, additional ambient members may reply (up to the room ambient responder cap), and each assistant reply is persisted with `senderAgentId` via `chatStore.addRoomMessage(...)`.
- If room replies cannot be generated (for example no resolvable responders or all responders fail), the POST fails with an API error (HTTP 502) instead of silently returning only the user message.
- If room responders cannot be resolved or all room-reply generations fail, the POST now returns an error instead of silently succeeding with only the user message, so failures are surfaced deterministically.
- Room responder prompt construction now includes a bounded recent room transcript (role, sender label, timestamp, content) plus an explicit latest-user-message marker, so replies stay thread-aware without unbounded prompt growth.
- The UI still avoids optimistic room echo; after `POST /api/chat/rooms/:id/messages`, it immediately re-fetches authoritative room messages to surface persisted user/assistant replies even if SSE delivery is delayed, and it continues to apply `chat:room:message:*` SSE updates for live fan-out.
- On send failure, `useChatRooms` rolls back/reconciles optimistic state and rethrows; `ChatView` catches once, preserves composer text for retry/edit, and surfaces a single error toast (no duplicate hook+view notifications).
- After each send attempt, the room transcript still re-fetches authoritative messages so persisted user/assistant replies remain visible even when SSE delivery is delayed, and `chat:room:message:*` SSE updates continue live fan-out.
- Relationship summary: direct Chat runs one target (agent or model) per session; rooms are shared threads with multiple agent members and now use the same message contract as direct Chat; Quick Chat stays a floating single-target panel and does not host rooms.
- For backend details, see the [Chat Room REST API reference](./architecture.md#real-time-channels) and the [chat room storage schema (`chat_rooms`, `chat_room_members`, `chat_room_messages`)](./storage.md#chat-rooms-migration-70).