Parse note-patch entries via direct recursion, not map
A malformed note-patch (patch entry missing old/new) made the folio MCP drop its JSON-RPC error response on the native (release) binary: the client never got an answer for that request id, though the session stayed healthy. It only reproduced on native; the bytecode (dev) bundle serialized the error fine, so the earlier re-lock alone (verified on dev) missed it.
Root cause is a sigil native-backend codegen bug: an exception propagating through map's CPS frames still corrupts call-with-prompt's return context on native. That is the exact VM bug sigil-mcp server.sgl:272-279 documents and whose call/cc-escape workaround was removed as fixed in v0.16.0, but the fix only landed for bytecode, not native. make-tool-note-patch raised inside (map parse-patch-entry ...); the corrupted continuation then threw car: expected pair while building the error response (and a trailing type-error at shutdown), so the response was never written.
Fix: parse patch entries with a direct-recursion helper so the raise stays on ordinary call frames and never crosses map's CPS frames. The dispatcher guard then serializes a clean -32603 error. Verified against the release/native binary with the 4-message probe (init, note-create, malformed note-patch, status): id3 = clean JSON-RPC error, session healthy; test-mcp-error-isolation passes 4/4 on release; full suite 148 pass. All other folio raise sites go through direct frames and were already native-safe.
The underlying native codegen bug is broad (any native raise through map inside a prompt) and should be fixed in sigil; tracked separately.
src/folio/tools.sgl | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)src/folio/tools.sglmodified
(error "Patch entry missing 'new' (or 'content') field")) (else (cons old-str new-str))))) ;; Parse a list of patch objects via direct recursion rather than `map`. ;; parse-patch-entry raises on a malformed entry; `map` (a higher-order ;; stdlib procedure) propagates that exception through its internal CPS ;; frames, which the native backend mis-unwinds — the raised error corrupts ;; the dispatch continuation so the JSON-RPC error is never serialised and ;; the response is silently dropped (car: expected pair / type-error on the ;; native release binary; harmless on bytecode). Direct recursion keeps the ;; raise on ordinary call frames so the dispatcher's guard serialises a ;; clean error. See test/test-mcp-error-isolation.sgl and ;; investigations/folio-mcp-tool-error-wedge-async-log-unwind. (define (parse-patch-entries entries) (if (null? entries) '() (let ((head (parse-patch-entry (car entries)))) (cons head (parse-patch-entries (cdr entries)))))) (define (make-tool-note-patch store idx) (lambda (args) (let* ((name (require-arg args name: "name")) (file-path (find-note-file store name))) (if (not file-path) (format "Note '~a' not found." name) (let ((patches (map parse-patch-entry (array->list patches-raw)))) (let ((patches (parse-patch-entries (array->list patches-raw)))) (let-values (((old-body new-body) (note-patch! file-path patches))) (index-note! idx file-path) (string-append