feat: sync desktop streaming with backend persistence across refreshes#45
feat: sync desktop streaming with backend persistence across refreshes#45cyzus wants to merge 2 commits into
Conversation
- Route retry and steer through background queue (/chat/send, /chat/steer-send) so streams are reconnectable after page refresh via EventBus - sessionStorage seed: save streaming parts on visibilitychange/beforeunload and restore them on reconnect, including pending approval dialogs - Persist pending tool approvals to DB (chat.config._pending_approvals) so approval state survives disconnect; clear on stream completion - Route tool approval resumes through /chat/send background queue instead of direct SSE, with current parts saved to abandonedPartsRef for visual continuity - Add /chat/steer-send endpoint that runs process_steer via background queue - Expose restorePartsFromSeed in useAGUI for seeding parts without starting stream https://claude.ai/code/session_01UYzkRjFAHjkX1PJqc2s6qi
After every tool batch completes during a streaming run, reconstruct a valid ModelResponse + ModelRequest from accumulated PartEndEvent parts and ToolReturnParts, then fire-and-forget save to agent_state in DB. This means if the stream is interrupted mid-run (disconnect, crash) the next turn resumes from after the last completed tool batch rather than from the start of the entire run. partial_history is also updated immediately so the finally-block crash recovery path has the latest state. Implementation uses pydantic-ai's PartEndEvent (complete parts, not deltas) so no lossy delta-accumulation is needed. FunctionToolCallEvent tracks in-flight count; FunctionToolResultEvent triggers the checkpoint when all tools in the batch have resolved. https://claude.ai/code/session_01UYzkRjFAHjkX1PJqc2s6qi
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bac65e9037
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const resp = await fetch(`${getApiBase()}/chat/send`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(body), | ||
| }); |
There was a problem hiding this comment.
Allow queued approval resumes to attach to live stream
When this new queued resume path is used after tool approval, useToolApproval has already set streamingChatIdRef.current = targetChatId before calling resumeViaQueue, but the /chat/live connector skips tryConnect() whenever that ref matches the mounted chat. Because this path no longer opens a direct SSE reader, approving the final pending tool posts /chat/send successfully but never attaches to the background stream, leaving the UI stuck on the pending/running tool while the backend continues unseen; the same pre-set ref pattern also affects the new retry/steer queued paths.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR updates Suzent’s streaming architecture so desktop (frontend) streaming can reconnect after refreshes by routing turns/steers through a background stream queue, seeding UI state from sessionStorage, and persisting tool-approval state server-side.
Changes:
- Add mid-run checkpointing during streaming and persist pending tool approvals to the DB so state survives disconnects/refreshes.
- Add a reconnectable background-queue steer endpoint (
POST /chat/steer-send) and wire it into the server routes. - Add frontend stream seeding (save/restore AG-UI parts) and expose
restorePartsFromSeed()fromuseAGUI.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/suzent/streaming.py |
Adds mid-run checkpoint persistence and DB persistence/cleanup for pending tool approvals. |
src/suzent/server.py |
Registers the new /chat/steer-send route. |
src/suzent/routes/chat_routes.py |
Adds /chat/steer-send background-queue endpoint and extends /chat/send to accept resume_approvals. |
frontend/src/hooks/useAGUI.ts |
Exposes restorePartsFromSeed() for restoring parts without starting a stream. |
frontend/src/components/ChatWindow.tsx |
Implements sessionStorage stream seeding and routes retry/steer/resume through the background queue. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _sync_save() -> None: | ||
| _st = serialize_state( | ||
| messages, model_id=_model_id, tool_names=_tool_names | ||
| ) | ||
| _ckpt_get_db().update_chat(chat_id, agent_state=_st) | ||
|
|
| asyncio.create_task( | ||
| _save_mid_run_checkpoint(_checkpoint) | ||
| ) |
| async def _clear_pending_approvals_task(): | ||
| try: | ||
| def _sync_clear(): | ||
| _db2 = _get_db2() | ||
| _chat2 = _db2.get_chat(chat_id) | ||
| if _chat2 is not None: | ||
| _cfg2 = dict(_chat2.config or {}) | ||
| if "_pending_approvals" in _cfg2: | ||
| del _cfg2["_pending_approvals"] | ||
| _db2.update_chat(chat_id, config=_cfg2) | ||
| await asyncio.to_thread(_sync_clear) | ||
| except Exception: | ||
| pass | ||
|
|
||
| asyncio.create_task(_clear_pending_approvals_task()) |
| stream_queue = try_register_background_stream(chat_id) | ||
| if stream_queue is None: | ||
| return JSONResponse({"error": "Chat is already streaming"}, status_code=409) | ||
|
|
so streams are reconnectable after page refresh via EventBus
and restore them on reconnect, including pending approval dialogs
approval state survives disconnect; clear on stream completion
direct SSE, with current parts saved to abandonedPartsRef for visual continuity
https://claude.ai/code/session_01UYzkRjFAHjkX1PJqc2s6qi