No reviewers
Labels
No labels
action
backlog
blocked
bug-report
cannot-reproduce
in-progress
in-triage
needs-triage
prediction/actioned
prediction/dismissed
prediction/unreviewed
priority
rejected
reproduced
tech-debt
underspecified
vision
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: disinto-admin/disinto#1076
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/issue-1026"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #1026
Changes
AI Review
Summary
This PR adds WebSocket streaming to replace the one-shot
claude --printpattern. The diff wires up a new/chat/wsCaddy route, a raw asyncio-over-TCP WebSocket implementation inserver.py, and a JS-side WebSocket client. The approach has several critical defects that prevent it from working at all.Findings
blocker
docker/chat/ui/index.html:453-464+server.py:568: Frontend-backend protocol mismatch — the feature cannot deliver tokens. The frontend callstryWebSocketSend(message)which returnstrueandsendMessage()returns immediately (no HTTP POST is sent). The client sends the chat message as a JSONchat_requestover the WebSocket. But the server'shandle_connection()loop treats allOPCODE_TEXTframes as heartbeat acks and doespass. Claude is never invoked via this path, so the queue is never populated, so no tokens are streamed. The feature cannot work as written. The fix must either (a) make the server handle thechat_requestWS frame and invoke Claude within the WS coroutine, or (b) have the frontend send an HTTP POST in parallel with the WS connection (matching the server's model where POST triggers Claude and WS receives tokens).blocker
server.py:1279_WebSocketHandler.accept_connection(): Re-reads HTTP headers already consumed byBaseHTTPRequestHandler.do_GET()is called after the framework has already parsed the full HTTP request.accept_connection()then calls_read_line()byte-by-byte to reconstruct theGETline and headers — but those bytes are gone. The asyncio reader wrapping the raw socket will block indefinitely waiting for data that never arrives; the 101 handshake cannot complete. Fix: useself.headers(already populated by the framework) to validate and respond, bypassingaccept_connection()entirely.blocker
server.py:1273,1293,1318ws_writer = transport:asyncio.Transportused asasyncio.StreamWriter.create_connection()returns a(transport, protocol)pair;transportis anasyncio.BaseTransportwhich haswrite()but notdrain()orwait_closed(). The ping keepalive at line 1293 (await ws_writer.drain()) and the cleanup at line 1318 (await ws_writer.wait_closed()) will both raiseAttributeErrorat runtime. Fix: wrap withasyncio.StreamWriterviaasyncio.open_connection(sock=sock)which returns a proper(reader, writer)pair.medium
server.py:437,447,454,460_decode_frame:StreamReader.read(N)doesn't guarantee N bytes.asyncio.StreamReader.read(n)returns up to n bytes; frame parsing (2-byte header, extended lengths, mask key, payload) all useread(). Partial reads will silently corrupt frame parsing. Usereadexactly(n)for all fixed-length reads.medium
server.py:1256_websocket_queues[user]: Queue never removed on disconnect. When a connection closes (normally or with error),_websocket_queues[user]is overwritten on reconnect but neverdel``d. A user who connects, disconnects, then makes an HTTP POST chat request will have tokens pushed into a stale or new queue, but the queue consumer is gone. Over time this leaks memory. Fix:del _websocket_queues[user]in thefinallyblock ofhandle_ws()`.low
server.py:548_send_close: Invalid WebSocket close code.b"\x03\x00"encodes the 16-bit big-endian integer 768. RFC 6455 §7.4.2 prohibits close codes 0–999. The normal-closure code is 1000 (struct.pack(">H", 1000)=b"\x03\xe8").low
server.py:543,556_send_close/_send_pong:drain()called withoutawaitin sync methods. Both sync methods callself.writer.drain()withoutawait, which returns a coroutine that is never scheduled. The close/pong frame may not be flushed before the connection is dropped.Verdict
REQUEST_CHANGES — Frontend sends chat request exclusively via WebSocket, but server ignores all incoming WS text frames — Claude is never invoked; feature is broken end-to-end
Reviewed at
0daf26f| AGENTS.mdAI Review: REQUEST_CHANGES — Frontend sends chat request exclusively via WebSocket, but server ignores all incoming WS text frames — Claude is never invoked; feature is broken end-to-end
0daf26f6d9to01f7d061bcAI Re-review (round 2)
Previous Findings
_handle_chat_requestadded to_WebSocketHandler;handle_connectionnow dispatcheschat_requestframes to it directly.accept_connectionre-read HTTP headers from consumed socket → FIXED: signature changed to takesec_websocket_key/sec_websocket_protocolas params;handle_websocket_upgradepassesself.headersvalues.asyncio.Transportused asStreamWriter(nodrain()/wait_closed()) → FIXED: replaced withasyncio.open_connection(sock=sock)returning a proper(reader, writer)pair.read(N)used instead ofreadexactly(N)in frame parsing → FIXED: all fixed-length reads converted toreadexactly.del _websocket_queues[self.user]inhandle_connectionfinallyblock.struct.pack(">H", 1000).drain()called withoutawaitin sync_send_close/_send_pong→ FIXED: both methods are nowasyncwithawait self.writer.drain().New Issues
medium
server.py:574_handle_chat_request: blockingproc.stdout.readlinein asyncio coroutine. Thesend_streamtask (1-second ping keepalive) cannot execute whilereadlineis blocked waiting for Claude output. On responses longer than the proxy keepalive window (Caddy default: 120s) the WS connection may be dropped mid-stream. Tokens still flow because eachawait writer.drain()yields briefly, but the periodic ping is suppressed for the full generation duration. Fix:await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline).medium
server.py:1297-1307+server.py:513-515: WS path bypasses per-message rate limiting and skips conversation history._check_rate_limit/_record_requestare called once at connection establishment — each subsequentchat_requestframe on the same connection is uncounted._write_messageand_record_tokensare also not called from_handle_chat_request, so token quotas are not enforced and conversation history is not saved for WS-originated messages. The HTTP POST path does all three. Recommend tracking as tech-debt.low
server.py:1207:proc.stdout.getvalue()fallback is dead code.subprocess.PIPEreturns aBufferedReaderwhich never has.getvalue(); the condition is alwaysFalse. The oldresponse = raw_outputwas also a no-op fallback, so behavior is unchanged — but the comment implies it does something it doesn't.Verdict
APPROVE — All 7 previous findings fixed; new mediums are design gaps in the WS path, not regressions from the fix
Reviewed at
01f7d06| Previous:0daf26f| AGENTS.mdAI Re-review (round 2): APPROVE — All 7 previous findings fixed; new mediums are design gaps in the WS path, not regressions from the fix