Skip to content

Treat memory tag filters as literal LIKE substrings - #355

Merged
tylerreckart merged 4 commits into
mainfrom
fix/memory-tag-like-escape
Sep 21, 2026
Merged

tylerreckart merged 4 commits into
mainfrom
fix/memory-tag-like-escape

Conversation

@cursor

@cursor cursor Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Problem

EntryFilter.tag (/mem entries tag= and GET /v1/memory/entries?tag=) wraps the tag in LIKE %"tag"% against the serialized tags JSON. %, _, and \\ were not escaped, so they were SQL wildcards.

A filter for foo_bar also matched fooXbar. A filter for 100% also matched 1000. Snake_case tags are common; this is a hard WHERE in browse mode, so the wrong entries are returned (and in FTS mode the wrong rows get the tag boost).

This is the same LIKE-literal class as lesson search, but a separate filter and SQL site. Not SQL injection (the pattern is bound).

Fix

Escape \\, %, and _ in the tag and pair both the browse AND tags LIKE ? predicate and the FTS tag-boost CASE WHEN e.tags LIKE ? with ESCAPE '\\'. Unescaped ASCII tags without those characters are unchanged. JSON quote wrapping around the token is unchanged.

Helper is named escape_like_wildcards (local to the memory-entry TU) so it does not collide with the lesson-search helper on #353.

Tests

unit_memory_entries — foo_bar does not match fooXbar; 100% does not match 1000; a JSON-encoded backslash still matches when escaped. Suite 25/25 locally (142 assertions) + ASan/UBSan.

Independently mergeable against main (0456350). Code hunks auto-merge with #321–#354, including #330 (exact_conversation in list_entries) and #353 (like_literal + search_lessons). CHANGELOG [Unreleased] sibling-conflicts after the first of those merges, same as the rest of the series.

Backward-compatible for tags without % / _ / \\. Callers that relied on _ as “any character” will see fewer rows — that was the bug.

Open in Web View Automation 

Note

Low Risk
Localized SQL filter fix with tests and docs; behavior change only for tags containing %, _, or \.

Overview
Memory tag= filters now match literal tag substrings, fixing false positives where SQLite LIKE treated %, _, and \ as wildcards.

/mem entries tag= and GET /v1/memory/entries?tag= build a %"tag"% pattern against serialized tags JSON. Unescaped patterns let foo_bar match fooXbar and 100% match 1000. The change adds escape_like_wildcards / tag_like_pattern in tenant_store.cpp and applies LIKE … ESCAPE '\' in both browse hard filters and FTS tag-boost CASE WHEN clauses.

Docs and changelog note literal semantics; unit_memory_entries covers underscore, percent, and backslash cases. Tags without those characters behave as before; callers who accidentally relied on _ as “any character” will see fewer rows.

Reviewed by Cursor Bugbot for commit 3e06630. Bugbot is set up for automated code reviews on this repo. Configure here.

EntryFilter.tag was wrapped in LIKE %"tag"% without escaping, so
foo_bar also matched fooXbar and 100% also matched 1000 on /mem
entries tag= and GET /v1/memory/entries?tag=. Escape \, %, and _
and pair the browse filter and FTS tag-boost predicate with ESCAPE '\'.

Co-authored-by: Tyler Reckart <tylerreckart@users.noreply.github.com>
@tylerreckart
tylerreckart marked this pull request as ready for review September 21, 2026 12:58
@tylerreckart
tylerreckart enabled auto-merge (squash) September 21, 2026 12:58
@tylerreckart
tylerreckart merged commit 78d78d6 into main Sep 21, 2026
1 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants