Re-lock to sigil 0.17.3 to fix note-patch handler-error session wedge
A folio MCP tool handler that raises (e.g. malformed note-patch: a patch entry missing 'old'/'new') wedged the entire MCP session with no response, requiring an /mcp reconnect. Root cause is a sigil runtime bug, not folio logic: during exception unwinding, a log/response write to an async port (stdin/stdout/stderr pipes) aborts to async-prompt-tag after the task's async prompt has been unwound past. The abort finds no matching prompt, raises, that raise is logged, re-enters the write path, and recurses, corrupting the continuation (car: expected pair) so no response is ever written.
Fixed in sigil ecd318755 (prompt-tag-available? gate), shipped in 0.17.3. Re-lock the sigil monorepo deps (stdlib/lib/run/test/test-runner) 0.17.1 -> 0.17.3 (c333fbde). No folio source change required.
note-read with a wrong key never triggered this because it is rejected at schema validation and never reaches the handler; only a tool that actually raises enters the unwind-during-async-write path.
Extend test-mcp-error-isolation.sgl with a raising-handler probe: create a note, send a malformed note-patch (clean JSON-RPC error, no #<object>, no hang), then a valid follow-up (session stays healthy).
sigil.lock | 20 ++++++++++----------
test/test-mcp-error-isolation.sgl | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 100 insertions(+), 10 deletions(-)sigil.lockmodified
(package name: "sigil-stdlib" url: "codeberg:sigil/sigil" ref: "^0.17" sha: "61ba6501f4ff5d1fd534c13c4a3494b69cc88412" sha: "c333fbde5ec2d805eddff982952c9993cd441715" package-selector: "sigil-stdlib" version: "0.17.1") version: "0.17.3") (package name: "sigil-run" url: "codeberg:sigil/sigil" ref: "^0.17.0" sha: "61ba6501f4ff5d1fd534c13c4a3494b69cc88412" sha: "c333fbde5ec2d805eddff982952c9993cd441715" package-selector: "sigil-run" version: "0.17.1") version: "0.17.3") (package name: "sigil-lib" url: "codeberg:sigil/sigil" ref: "^0.17" sha: "61ba6501f4ff5d1fd534c13c4a3494b69cc88412" sha: "c333fbde5ec2d805eddff982952c9993cd441715" package-selector: "sigil-lib" version: "0.17.1") version: "0.17.3") (package name: "sigil-json" url: "codeberg:sigil/sigil-json" ref: "^0.16.0" (package name: "sigil-test" url: "codeberg:sigil/sigil" ref: "^0.17" sha: "61ba6501f4ff5d1fd534c13c4a3494b69cc88412" sha: "c333fbde5ec2d805eddff982952c9993cd441715" package-selector: "sigil-test" version: "0.17.1") version: "0.17.3") (package name: "sigil-test-runner" url: "codeberg:sigil/sigil" ref: "^0.17" sha: "61ba6501f4ff5d1fd534c13c4a3494b69cc88412" sha: "c333fbde5ec2d805eddff982952c9993cd441715" package-selector: "sigil-test-runner" version: "0.17.1") version: "0.17.3") (package name: "sigil-socket" url: "codeberg:sigil/sigil-socket" ref: "^0.16.0"test/test-mcp-error-isolation.sglmodified
;;; (wrong argument key), then a valid tool/call. It asserts that both;;; responses are well-formed JSON — NOT the literal "#<object>" text.;;;;;; A SECOND regression is guarded here (see the "malformed note-patch";;; group below): a tool whose handler actually RAISES (not merely a schema;;; rejection) once wedged the whole session with no response at all. The;;; malformed note-read case above is rejected at schema validation and never;;; reaches the handler, so it did NOT exercise the raising path. note-patch;;; on an existing note passes schema validation, then raises inside `map`;;; (parse-patch-entry, missing 'old'/'new'). On sigil 0.17.1 the exception;;; unwound through the async scheduler; a log/response write during unwind;;; aborted to an async prompt already unwound past, recursed, corrupted the;;; continuation ("car: expected pair"), and NO response was ever written —;;; the session hung until the client cancelled. Fixed in sigil 0.17.3;;; (ecd318755, prompt-tag-available? gate); folio must stay locked to >=0.17.3.;;;;;; Requires: build/release/bin/folio (or build/dev/bin/folio) to exist.(import (sigil test) (assert-false (poisoned-response? followup))) (test "follow-up valid call has Folio Status content" (assert-true (string-contains? followup "Folio Status"))))))))));; Drive the binary through the RAISING-handler path: initialize, create a note,;; then send a malformed note-patch (patch entry missing 'old'/'new') so the;; handler raises inside `map`, then a valid follow-up. Returns the four response;; lines: initialize, note-create, note-patch (error), status. If the server;; wedges, read-line blocks and the test harness surfaces it as a hang rather;; than a pass.(define (patch-poison-and-probe folio-binary) (let* ((root (make-tmp-folio-root)) (_ (setenv! "FOLIO_ROOT" root)) (p (process-spawn folio-binary))) (send-line! (process-stdin p) (string-append "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"," "\"params\":{\"protocolVersion\":\"2025-11-25\"," "\"capabilities\":{}}}")) (send-line! (process-stdin p) "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}") ;; Seed a real note so note-patch reaches its handler (find-note-file hits). (send-line! (process-stdin p) (string-append "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\"," "\"params\":{\"name\":\"folio/note-create\"," "\"arguments\":{\"name\":\"probe-note\"," "\"content\":\"Body line one. Body line two.\"}}}")) ;; Malformed note-patch: patch entry has 'content' but no 'old'/'match', ;; so parse-patch-entry raises. This is the RAISING-handler path. (send-line! (process-stdin p) (string-append "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\"," "\"params\":{\"name\":\"folio/note-patch\"," "\"arguments\":{\"name\":\"probe-note\"," "\"patches\":[{\"op\":\"append\",\"content\":\"x\"}]}}}")) ;; Follow-up valid call: must succeed if the session stayed healthy. (send-line! (process-stdin p) (string-append "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\"," "\"params\":{\"name\":\"folio/status\",\"arguments\":{}}}")) (close-output-port (process-stdin p)) (let ((lines (read-lines-until (process-stdout p) 4))) (process-wait p) lines)))(test-group "mcp malformed note-patch does not wedge the session" (let ((folio (existing-folio-binary))) (cond ((not folio) (test "skipped: folio binary not built" (assert-true #t))) (else (let ((lines (patch-poison-and-probe folio))) ;; Four responses (init, create, patch-error, status) prove the ;; malformed note-patch neither hung nor swallowed later responses. (test "got four response lines (no wedge/hang)" (assert-equal 4 (length lines))) (when (= (length lines) 4) (let ((init-resp (list-ref lines 0)) (create-resp (list-ref lines 1)) (patch-resp (list-ref lines 2)) (status-resp (list-ref lines 3))) (test "init response is well-formed" (assert-true (string-contains? init-resp "\"protocolVersion\""))) (test "note-create succeeded" (assert-true (string-contains? create-resp "Created note"))) ;; The raising handler must serialise to a well-formed JSON-RPC ;; error — never "#<object>", never a missing response. (test "malformed note-patch returns a JSON-RPC error" (assert-true (string-contains? patch-resp "\"error\""))) (test "malformed note-patch error names the missing field" (assert-true (string-contains? patch-resp "missing 'old'"))) (test "malformed note-patch does not return #<object>" (assert-false (poisoned-response? patch-resp))) ;; The session must remain fully healthy for the next call. (test "follow-up valid call does not return #<object>" (assert-false (poisoned-response? status-resp))) (test "follow-up valid call has Folio Status content" (assert-true (string-contains? status-resp "Folio Status"))))))))))