Commitb00e3446Recorded10 Jul 2026Repositorysigil-nrepl
Drain streamed :out/:err frames in the nREPL client
Message
The server now emits incremental (response :status out/err ...) frames before the final eval response. Teach (sigil nrepl client) to skip those stream frames and return the terminal response, so nrepl-eval keeps returning the eval value (not the first output frame) for output-producing code. Adds a per-connection recv buffer so frames coalesced in one TCP segment are not lost. Keeps the MCP nREPL bridge (a wire client of this module) correct.
Changed
src/sigil/nrepl/client.sgl | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------
1 file changed, 64 insertions(+), 39 deletions(-)Diff
src/sigil/nrepl/client.sglmodified
@@ -44,13 +44,15 @@
44
;; Connection Management 45
;; ============================================================ 46
−47
;; Connection: (vector 'nrepl-client socket request-counter)+47
;; Connection: (vector 'nrepl-client socket request-counter recv-buffer)+48
;; recv-buffer holds bytes read past the end of one frame, so streamed+49
;; :out/:err frames coalesced with the final response are not lost. 50
(define (make-nrepl-connection socket)−49
(vector 'nrepl-client socket 0))+51
(vector 'nrepl-client socket 0 (make-bytevector 0))) 52
53
(define (nrepl-connection? obj) 54
(and (vector? obj)−53
(>= (vector-length obj) 3)+55
(>= (vector-length obj) 4) 56
(eq? (vector-ref obj 0) 'nrepl-client))) 57
58
(define (conn-socket conn)@@ -64,6 +66,12 @@
66
(vector-set! conn 2 n) 67
n)) 68
+69
(define (conn-buffer conn)+70
(vector-ref conn 3))+71
+72
(define (conn-buffer-set! conn buf)+73
(vector-set! conn 3 buf))+74
75
;;; Connect to an nREPL server. 76
;;; 77
;;; Returns a connection object on success, or `#f` on failure.@@ -107,41 +115,58 @@
115
(bytevector-u8-set! prefix 3 (remainder len 256)) 116
(bytevector-append prefix msg-bytes))) 117
−110
;; Read at least n bytes from socket with timeout.−111
;; Returns a bytevector or #f on timeout/error.−112
(define (read-at-least sock n acc deadline)−113
(cond−114
((<= deadline 0) #f)−115
((>= (bytevector-length acc) n) acc)−116
(else−117
(if (socket-ready? sock 100)−118
(let ((data (socket-read-bytevector sock)))−119
(cond−120
((or (not data) (eof-object? data)) #f)−121
((= (bytevector-length data) 0)−122
(read-at-least sock n acc (- deadline 1)))−123
(else−124
(read-at-least sock n (bytevector-append acc data) deadline))))−125
(read-at-least sock n acc (- deadline 1))))))−126
−127
;; Read a complete response from socket.−128
;; Protocol: 4-byte big-endian length + UTF-8 S-expression−129
(define (read-response sock)−130
;; Read at least 4 bytes for the length prefix−131
(let ((header (read-at-least sock 4 (make-bytevector 0) 50))) ;; 5s timeout−132
(if (not header)−133
#f−134
(let* ((b0 (bytevector-u8-ref header 0))−135
(b1 (bytevector-u8-ref header 1))−136
(b2 (bytevector-u8-ref header 2))−137
(b3 (bytevector-u8-ref header 3))−138
(len (+ (* b0 16777216) (* b1 65536) (* b2 256) b3))−139
(total-needed (+ 4 len)))−140
;; Read the full message (header may already contain some body)−141
(let ((full (read-at-least sock total-needed header 50)))−142
(if (and full (>= (bytevector-length full) total-needed))−143
(read-expr (utf8->string (bytevector-copy full 4 total-needed)))−144
#f))))))+118
;; Decode one framed message from a byte buffer.+119
;; Returns (message . remaining-bytes) or #f if the buffer is incomplete.+120
(define (try-decode-frame buf)+121
(if (< (bytevector-length buf) 4)+122
#f+123
(let* ((len (+ (* (bytevector-u8-ref buf 0) 16777216)+124
(* (bytevector-u8-ref buf 1) 65536)+125
(* (bytevector-u8-ref buf 2) 256)+126
(bytevector-u8-ref buf 3)))+127
(total-needed (+ 4 len)))+128
(if (< (bytevector-length buf) total-needed)+129
#f+130
(cons (read-expr (utf8->string (bytevector-copy buf 4 total-needed)))+131
(bytevector-copy buf total-needed))))))+132
+133
;; Read one complete framed message from a connection, buffering any bytes+134
;; that arrive past the frame boundary (streamed :out/:err frames can be+135
;; coalesced with the final response in a single TCP segment).+136
;; Protocol: 4-byte big-endian length + UTF-8 S-expression.+137
(define (read-frame conn)+138
(let loop ((deadline 50)) ;; ~5s+139
(let ((dec (try-decode-frame (conn-buffer conn))))+140
(if dec+141
(begin (conn-buffer-set! conn (cdr dec)) (car dec))+142
(if (<= deadline 0)+143
#f+144
(if (socket-ready? (conn-socket conn) 100)+145
(let ((data (socket-read-bytevector (conn-socket conn))))+146
(cond+147
((or (not data) (eof-object? data)) #f)+148
((= (bytevector-length data) 0) (loop (- deadline 1)))+149
(else+150
(conn-buffer-set! conn+151
(bytevector-append (conn-buffer conn) data))+152
(loop deadline))))+153
(loop (- deadline 1))))))))+154
+155
;; True for streamed output frames (not the terminal eval response).+156
(define (stream-frame? resp)+157
(and (pair? resp) (eq? (car resp) 'response)+158
(memq (response-ref (cdr resp) ':status #f) '(out err))))+159
+160
;; Read frames from a connection, skipping streamed :out/:err frames, and+161
;; return the terminal response. Non-eval ops produce no stream frames, so+162
;; the first frame is returned immediately (backward compatible).+163
(define (read-terminal-response conn)+164
(let loop ()+165
(let ((resp (read-frame conn)))+166
(cond+167
((not resp) #f)+168
((stream-frame? resp) (loop))+169
(else resp))))) 170
171
;; ============================================================ 172
;; Request/Response@@ -161,7 +186,7 @@
186
#f 187
(begin 188
(socket-write (conn-socket conn) (encode-message request))−164
(read-response (conn-socket conn)))))+189
(read-terminal-response conn)))) 190
191
;; Extract keyword argument from response plist 192
(define (response-ref resp key default)