fix(mcp): stop memory tool hangs from s-expression parser infinite loop#1421
Merged
Conversation
memory_add could hang forever, permanently pinning a tokio worker at 100% CPU. After every add, the related-memories lookup feeds the raw memory content back through recall_fulltext, which interpolates it into a '(fulltext ?content "...")' bind expression escaping only double quotes. The s-expression parser had no escape support, so the string token ended at the first embedded quote — and once the leftover text started with ')', parse_s_expression_args consumed zero bytes per iteration and looped forever. Content containing a quote followed by a paren (common for code-flavored memories) wedged the server; each occurrence ate one runtime worker until the whole MCP server starved. Fixes: - parse_s_expression_args / parse_s_expression_arg: a token that consumes no input is now a ParseError, guaranteeing termination on any malformed input - string literals honor backslash escapes (\" and \\) throughout: quoted_string_end/unescape_string in filter_sexpr, the sexpr tokenizer, and find_matching_paren; find_matching_bracket is now also quote-aware - recall_fulltext escapes backslashes before quotes so content round-trips through the parser - MemoryStore::initialize tolerates losing the create_ledger race (concurrent first-burst tool calls returned 'Ledger already exists' errors) Diagnosed on a live wedged v4.1.1 serve process via sample/lldb: the spinning thread was re-trimming the same 231-byte content slice starting with ')'. Reproduced end-to-end over MCP stdio with a single add whose content contains '") '; regression tests cover the parser and the recall path.
aaj3f
approved these changes
Jul 5, 2026
aaj3f
left a comment
Contributor
There was a problem hiding this comment.
Grateful for this -- was going to investigate why I still saw the issue after the other PR merged, but this makes sense
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
memory_add(andmemory_recall) on the fluree MCP server could hang forever — the tool call never responds and one tokio worker gets permanently pinned at 100% CPU. Bursts of 3+ concurrent adds made this near-certain, and each occurrence eats another runtime worker until the whole server starves. PR #1384's lock work is sound but was unrelated: all memory locks are 30s-bounded and can't produce an unbounded hang.Root cause
Diagnosed on a live wedged v4.1.1
mcp serveprocess (12.7h of accumulated CPU) viasample/lldb: the spinning thread was re-trimming the same content slice starting with)insideparse_s_expression_args.The chain:
recall_fulltextinterpolates it into a(fulltext ?content "...")bind expression, escaping only"→\".),parse_s_expression_argsfinds the delimiter at position 0, consumes zero bytes, and loops forever.Memory content containing a double quote followed by a paren — routine for code-flavored insights — reliably wedges every v4.1.1 install. Reproduced end-to-end over MCP stdio with a single add.
Fixes
ParseError, guaranteeing the parse loop terminates on any malformed input (parse_s_expression_args,parse_s_expression_arg).\"and\\throughout —quoted_string_end/unescape_stringinfilter_sexpr, the sexpr tokenizer, andfind_matching_paren;find_matching_bracketis now also quote-aware. Unrecognized\xsequences are preserved verbatim.recall_fulltextescapes backslashes before quotes so arbitrary content round-trips through the parser.MemoryStore::initializetolerates losing thecreate_ledgerrace — concurrent first-burst tool calls previously failed withLedger already exists: __memory:main.Testing
stray ')' errors instead of looping, escaped quotes with parens, hostile fulltext round-trip)recall_fulltextwith quote+paren+backslash contentfluree-db-querylib suite (1,177),fluree-db-memorysuite (64), andgrp_queryintegration suite (299) pass; clippy clean