fix(protocol): Migrate to JSON framing with newline delimiters (resolves desync)
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 2s
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 2s
This commit is contained in:
@@ -138,29 +138,29 @@ A robust utility to read a framed message from a stream. It enforces the determi
|
|||||||
|
|
||||||
#+begin_src lisp :tangle ../src/communication.lisp
|
#+begin_src lisp :tangle ../src/communication.lisp
|
||||||
(defun read-framed-message (stream)
|
(defun read-framed-message (stream)
|
||||||
"Reads a hex-length prefixed message from the stream securely. Skips leading whitespace."
|
"Reads a hex-length prefixed JSON message from the stream."
|
||||||
(let ((length-buffer (make-string 6)))
|
(let ((length-buffer (make-string 6)))
|
||||||
(handler-case
|
(handler-case
|
||||||
(progn
|
(progn
|
||||||
;; 0. Skip leading whitespace (newlines, spaces, etc.)
|
;; Skip leading junk (newlines, etc.)
|
||||||
(loop for char = (peek-char nil stream nil :eof)
|
(loop for char = (peek-char nil stream nil :eof)
|
||||||
while (and (not (eq char :eof)) (member char '(#\Space #\Newline #\Tab #\Return)))
|
while (and (not (eq char :eof)) (not (digit-char-p char 16)))
|
||||||
do (read-char stream))
|
do (read-char stream))
|
||||||
|
|
||||||
;; 1. Read the 6-char hex length
|
|
||||||
(let ((count (read-sequence length-buffer stream)))
|
(let ((count (read-sequence length-buffer stream)))
|
||||||
(when (< count 6) (return-from read-framed-message :eof))
|
(if (< count 6) :eof
|
||||||
(let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
|
(let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
|
||||||
(unless len (error "Invalid protocol header: ~a" length-buffer))
|
(if (not len) :error
|
||||||
|
(let ((msg-buffer (make-string len)))
|
||||||
;; 2. Read exactly LEN bytes
|
(read-sequence msg-buffer stream)
|
||||||
(let ((msg-buffer (make-string len)))
|
(let ((msg (cl-json:decode-json-from-string msg-buffer)))
|
||||||
(read-sequence msg-buffer stream)
|
;; Convert JSON alist back to plist for kernel compatibility
|
||||||
(let ((*read-eval* nil) (*print-pretty* nil))
|
(let ((plist nil))
|
||||||
(let ((msg (read-from-string msg-buffer)))
|
(dolist (pair msg)
|
||||||
(validate-communication-protocol-schema msg)
|
(push (intern (string-upcase (string (car pair))) :keyword) plist)
|
||||||
msg))))))
|
(push (cdr pair) plist))
|
||||||
(error (c)
|
(let ((final (nreverse plist)))
|
||||||
(harness-log "PROTOCOL READ ERROR: ~a" c)
|
(validate-communication-protocol-schema final)
|
||||||
:error))))
|
final)))))))))
|
||||||
|
(error (c) (harness-log "PROTOCOL ERROR: ~a" c) :error))))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ The CLI actuator writes the agent's response back to the client's network stream
|
|||||||
(handler-case
|
(handler-case
|
||||||
(if (and stream (open-stream-p stream))
|
(if (and stream (open-stream-p stream))
|
||||||
(progn
|
(progn
|
||||||
(format stream "~a" (frame-message (format nil "~s" (list :TYPE :CHAT :TEXT text))))
|
(format stream "~a" (frame-message (list :TYPE :CHAT :TEXT text)))
|
||||||
(finish-output stream)
|
(finish-output stream)
|
||||||
(format stream "~a" (frame-message (format nil "~s" '(:TYPE :STATUS :SCRIBE :idle :GARDENER :sleeping))))
|
(format stream "~a" (frame-message '(:TYPE :STATUS :SCRIBE :IDLE :GARDENER :SLEEPING)))
|
||||||
(finish-output stream))
|
(finish-output stream))
|
||||||
(harness-log "CLI ERROR: No active or open reply stream for signal."))
|
(harness-log "CLI ERROR: No active or open reply stream for signal."))
|
||||||
(error (c) (harness-log "CLI ACTUATOR ERROR: ~a" c)))))
|
(error (c) (harness-log "CLI ACTUATOR ERROR: ~a" c)))))
|
||||||
@@ -77,7 +77,7 @@ Handles an individual TCP connection. It reads lines until the connection is clo
|
|||||||
"Handles TUI slash commands by returning structured Lisp s-expressions."
|
"Handles TUI slash commands by returning structured Lisp s-expressions."
|
||||||
(cond
|
(cond
|
||||||
((string= cmd "/status")
|
((string= cmd "/status")
|
||||||
(format stream "~a" (frame-message (format nil "~s" '(:TYPE :STATUS :SCRIBE :idle :GARDENER :sleeping))))
|
(format stream "~a" (frame-message '(:TYPE :STATUS :SCRIBE :IDLE :GARDENER :SLEEPING)))
|
||||||
(finish-output stream))
|
(finish-output stream))
|
||||||
((string= cmd "/exit")
|
((string= cmd "/exit")
|
||||||
(prin1 '(:TYPE :info :TEXT "Goodbye!") stream)
|
(prin1 '(:TYPE :info :TEXT "Goodbye!") stream)
|
||||||
@@ -93,9 +93,9 @@ Handles an individual TCP connection. It reads lines until the connection is clo
|
|||||||
(handler-case
|
(handler-case
|
||||||
(progn
|
(progn
|
||||||
;; 1. Send Handshake
|
;; 1. Send Handshake
|
||||||
(format stream "~a" (frame-message (format nil "~s" (make-hello-message "0.1.0"))))
|
(format stream "~a" (frame-message (make-hello-message "0.1.0")))
|
||||||
(finish-output stream)
|
(finish-output stream)
|
||||||
(format stream "~a" (frame-message (format nil "~s" '(:TYPE :STATUS :SCRIBE :idle :GARDENER :sleeping))))
|
(format stream "~a" (frame-message '(:TYPE :STATUS :SCRIBE :IDLE :GARDENER :SLEEPING)))
|
||||||
(finish-output stream)
|
(finish-output stream)
|
||||||
|
|
||||||
;; 2. Communication Loop
|
;; 2. Communication Loop
|
||||||
|
|||||||
@@ -67,28 +67,28 @@
|
|||||||
:capabilities '(:auth :swank :org-ast))))
|
:capabilities '(:auth :swank :org-ast))))
|
||||||
|
|
||||||
(defun read-framed-message (stream)
|
(defun read-framed-message (stream)
|
||||||
"Reads a hex-length prefixed message from the stream securely. Skips leading whitespace."
|
"Reads a hex-length prefixed JSON message from the stream."
|
||||||
(let ((length-buffer (make-string 6)))
|
(let ((length-buffer (make-string 6)))
|
||||||
(handler-case
|
(handler-case
|
||||||
(progn
|
(progn
|
||||||
;; 0. Skip leading whitespace (newlines, spaces, etc.)
|
;; Skip leading junk (newlines, etc.)
|
||||||
(loop for char = (peek-char nil stream nil :eof)
|
(loop for char = (peek-char nil stream nil :eof)
|
||||||
while (and (not (eq char :eof)) (member char '(#\Space #\Newline #\Tab #\Return)))
|
while (and (not (eq char :eof)) (not (digit-char-p char 16)))
|
||||||
do (read-char stream))
|
do (read-char stream))
|
||||||
|
|
||||||
;; 1. Read the 6-char hex length
|
|
||||||
(let ((count (read-sequence length-buffer stream)))
|
(let ((count (read-sequence length-buffer stream)))
|
||||||
(when (< count 6) (return-from read-framed-message :eof))
|
(if (< count 6) :eof
|
||||||
(let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
|
(let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
|
||||||
(unless len (error "Invalid protocol header: ~a" length-buffer))
|
(if (not len) :error
|
||||||
|
(let ((msg-buffer (make-string len)))
|
||||||
;; 2. Read exactly LEN bytes
|
(read-sequence msg-buffer stream)
|
||||||
(let ((msg-buffer (make-string len)))
|
(let ((msg (cl-json:decode-json-from-string msg-buffer)))
|
||||||
(read-sequence msg-buffer stream)
|
;; Convert JSON alist back to plist for kernel compatibility
|
||||||
(let ((*read-eval* nil) (*print-pretty* nil))
|
(let ((plist nil))
|
||||||
(let ((msg (read-from-string msg-buffer)))
|
(dolist (pair msg)
|
||||||
(validate-communication-protocol-schema msg)
|
(push (intern (string-upcase (string (car pair))) :keyword) plist)
|
||||||
msg))))))
|
(push (cdr pair) plist))
|
||||||
(error (c)
|
(let ((final (nreverse plist)))
|
||||||
(harness-log "PROTOCOL READ ERROR: ~a" c)
|
(validate-communication-protocol-schema final)
|
||||||
:error))))
|
final)))))))))
|
||||||
|
(error (c) (harness-log "PROTOCOL ERROR: ~a" c) :error))))
|
||||||
|
|||||||
Reference in New Issue
Block a user