Observation parser is case-sensitive on XML wrapper tags; local models that emit `<Concepts>`/`<Facts>` silently lose facts, concepts and files
Version: 13.25.1 (also reproduced on 13.24.23)
Provider: CLAUDE_MEM_PROVIDER=openrouter pointed at a local OpenAI-compatible llama.cpp-based server, model gemma-4-12b-it-Q6_K, thinking disabled
Related: #3606 (plan-18 Observer Response Pipeline), #3351 (unstructured observation blocks discarded), #3830 (concepts-only observations)
Summary
The observation/summary parser extracts every field with new RegExp(<${tag}>([\s\S]*?)</${tag}>) and no i flag. Smaller local models, given the stock code mode prompt at realistic context lengths (4–9k input tokens), frequently capitalize the wrapper tags — <Concepts>, <Facts>, <Files_read>, <Files_modified> — while leaf tags (<narrative>, inner <fact>, <concept>) stay lowercase. The parser then returns an empty list for every capitalized wrapper. The observation is still stored (narrative and title parse fine), so nothing errors and nothing is logged: the row just has facts: [], concepts: [], files_modified: [].
Evidence
Fingerprint across 439 observations generated by this model before any workaround:
| field | rows populated |
|---|---|
| narrative | 439 / 439 |
| files_read | 201 |
| facts | 177 |
| concepts | 31 |
| files_modified | 12 |
Same DB, same prompt, Haiku 4.5 (5,100 rows): facts 100%, concepts 94%. So this is not the model failing to produce the fields.
Raw model output tail from the production prompt (synthetic transcript, ~4.6k input tokens), reproduced 3/3 runs:
<narrative>…</narrative>
<Concepts>
<concept>security_note</concept>
<concept>gotcha</concept>
</Concepts>
<Files_read>
<file>terraform/task_definition</file>
</Files_read>
<Files_modified>
<file>issues/418</file>
</Files_modified>
</observation>
Everything under <Concepts> and <Files_*> is dropped by the current parser.
Proposed fix
- Match tags case-insensitively in the observation and summary parsers (the
Ka/rC-style helpers):new RegExp(<${tag}>([\s\S]*?)</${tag}>, "i")for the wrapper and inner tags. Tolerating whitespace inside the angle brackets (<\s*${tag}\s*>) would cover the sibling failure I also saw in JSON experiments with the same model (" concepts"with a leading space). - Log a
PARSERwarning when a wrapper tag is present in any casing but yields zero children, so this class of loss is visible instead of silent.
Both are non-breaking for outputs that already parse.
Workaround that confirms the diagnosis
With the local server accepting a raw llama.cpp GBNF grammar that pins lowercase tags (observation | summary | <skip_summary … /> | empty; content unconstrained), the same model over the next 25 observations produced 0 empty facts and 0 empty concepts, with no change in output token count (~650–730 per batch either way). Grammar attached below in case it is useful to others running local servers; it is not a substitute for the parser fix since most providers cannot take a grammar.
root ::= ws (observations | summary | skip | "") ws
observations ::= observation (ws observation)*
observation ::= "<observation>" ws "<type>" ws otype ws "</type>" ws "<title>" text "</title>" ws ("<subtitle>" text "</subtitle>" ws)? "<facts>" ws ("<fact>" text "</fact>" ws)+ "</facts>" ws "<narrative>" text "</narrative>" ws "<concepts>" ws ("<concept>" ws concept ws "</concept>" ws)+ "</concepts>" ws ("<files_read>" ws ("<file>" text "</file>" ws)* "</files_read>" ws)? ("<files_modified>" ws ("<file>" text "</file>" ws)* "</files_modified>" ws)? "</observation>"
otype ::= "bugfix" | "feature" | "refactor" | "change" | "discovery" | "decision" | "security_alert" | "security_note" | "sensitive"
concept ::= "how-it-works" | "why-it-exists" | "what-changed" | "problem-solution" | "gotcha" | "pattern" | "trade-off"
summary ::= "<summary>" ws "<request>" text "</request>" ws "<investigated>" text "</investigated>" ws "<learned>" text "</learned>" ws "<completed>" text "</completed>" ws "<next_steps>" text "</next_steps>" ws ("<notes>" text "</notes>" ws)? "</summary>"
skip ::= "<skip_summary" (" reason=\"" [^"]* "\"")? " />"
text ::= [^<]*
ws ::= [ \t\n]*
Environment
macOS client, worker runtime (not server), CLAUDE_MEM_MODE=code, default prompts. Happy to test a patch build.
Source: thedotmack/claude-mem