Repository navigation
Memory recall returns zero results when any query term is absent from the corpus: the FTS match expression is joined with AND #562
Description
CompozyOS version
compozy 0.3.0-beta.23
Operating system
Linux (x86_64)
Area
Core / daemon
ACP agent
Not agent-specific. It happens through the CLI and through compozy__memory_search alike.
Model / provider
Not applicable. Recall here is deterministic and does not go through a model.
Steps to reproduce
1. compozy memory write --scope workspace --type project \
--name "sonda-zx00841" \
--content "A contagem distinta de kind na particao zx00841 e 29."
2. compozy memory search "zx00841" --scope workspace
-> 1 result, score 0.954
3. compozy memory search "zx00841 banana" --scope workspace
-> 0 results
banana does not appear anywhere in the corpus. zx00841 is a unique exact match. Step 3 returns
nothing.
What happened, and what did you expect?
Context, since this arrives without one. I was running a structured comparison of
agent-orchestration tools, and CompozyOS was one of them. That is why the numbers below are exact, why the
arrangement sometimes looks artificial, and why several reports from me land close together: they came out
of one measurement pass on a dedicated Linux box, not from a production incident. The note in the steps below is a throwaway I wrote just to test recall. Happy to re-run any
of it with a different arrangement if that would be more useful to you.
Adding one word that is absent from the corpus takes recall from a 0.95 score to zero.
Measured on 0.3.0-beta.23, same note, same store, same moment:
| query | results | score |
|---|---|---|
zx00841 |
1 | 0.954 |
particao zx00841 |
1 | 0.962 |
zx00841 kind |
1 | 0.969 |
kind zx00841 |
1 | 0.974 |
zx00841 banana |
0 | n/a |
zx00841 banana laranja |
0 | n/a |
kind distinct count zx00841 |
0 | n/a |
contagem distinta de kind na particao zx00841 |
1 | 0.779 |
The pattern is not about length or language. kind zx00841 works and zx00841 banana does not. It is
about whether every term exists.
The cause is one line. The FTS match expression joins every query term with AND:
// internal/memory/catalog_document.go:40-50
func buildCatalogMatchQuery(query string) (string, error) {
terms, err := searchQueryTerms(query)
…
return strings.Join(quoted, " AND "), nil
}And that single expression is used for both FTS tables, so there is no second chance:
// internal/memory/recall_source.go:164-193
match, err := buildCatalogMatchQuery(query.QueryText)
…
unicodeCandidates, err := queryRecallFTS(ctx, db, …, "memory_chunks_fts", match, …)
trigramCandidates, err := queryRecallFTS(ctx, db, …, "memory_chunks_fts_trigram", match, …)
return append(unicodeCandidates, trigramCandidates...), nilIf the AND expression matches nothing, both tables return nothing, Candidates returns an empty slice,
and the ranking stage never sees the note. The scorer downstream is well built. It fuses unicode,
trigram, recency and signal weights, and it never runs, because retrieval already returned zero.
What I expected: a query with one unknown term should degrade recall, not annihilate it. The
obvious shapes: OR with score fusion (which the ranker is already designed for), or an AND attempt
with an OR fallback when it yields nothing, or dropping terms that have zero corpus frequency before
building the match.
Why this is worse than it looks. This is the recall path an agent uses through
compozy__memory_search, and an agent writes natural-language queries. A query like "what was the
distinct kind count for the events partition" contains several words that will not be in a short note,
so it returns nothing, and the agent concludes the memory does not exist. Meanwhile
compozy memory list shows it, which is exactly how I first noticed: the listing has it, the search
does not.
I originally believed the derived catalog had failed to index the entry. It had not. The entry was
findable by a narrower query at the same moment. The index is fine; the query composition is the
defect.
One related asymmetry, while you are in this code. The API/CLI path sets AllowTrivialQuery: true
(internal/api/core/memory.go:180) and the native tool
(internal/daemon/native_tool_workspace_memory_calls.go:167-169) does not. So through the native tool a
single-word query is discarded before retrieval, with the reason trivial_query recorded
(internal/memory/recall/recall.go:194-196) where the agent cannot see it. Combined with the AND
behaviour above, an agent gets zero results for both too few and too many words.
Still present on main: internal/memory/catalog_document.go is byte-identical between
v0.3.0-beta.21 and main (checked 2026-09-09).
Logs
# 0.3.0-beta.23, one note, one store, consecutive calls
$ compozy memory write --scope workspace --type project --name "sonda-zx00841" \
--content "A contagem distinta de kind na particao zx00841 e 29."
(applied)
$ compozy memory list --scope workspace -o json | grep -c sonda-zx00841
1
$ compozy memory search "zx00841" --scope workspace -o json
1 entry why_recalled: unicode=1.000 trigram=1.000 recency=1.000 signal=0.535 score=0.954
$ compozy memory search "zx00841 banana" --scope workspace -o json
0 entries
$ compozy memory search "kind distinct count zx00841" --scope workspace -o json
0 entriesThe same behaviour through the native tool, from inside a managed session:
TOOL compozy__memory_propose {"scope":"workspace","type":"project","operation":"add",
"name":"sonda-zx00841","content":"A contagem distinta de kind …"}
-> {"applied":true, "frontmatter":{"filename":"project_sonda-zx00841.md", …}}
TOOL compozy__memory_list {"scope":"workspace","type":"project","limit":200}
-> the entry is present
TOOL compozy__memory_search {"scope":"workspace","query":"contagem distinta de kind"}
-> found, why_recalled: ["unicode=0.500","trigram=0.000","recency=1.000","score=0.425"]Final checks
- I searched existing issues and this is not a duplicate.
- I can reproduce this on the latest release.
Duplicate search performed (title + body, open and closed): memory search (vazia) ·
memory recall (no hits) · recall · extractor · reindex · fts. The only neighbour is #395
(Memory extractor persists forbidden operational chatter…, closed), which is about what gets
persisted, not about retrieval.
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fieldsDone
Activity
jillesmc commentedon Sep 9, 2026
Independent validation on
mainat744a29ac, built locally withmake build-go, run beforebeta.24ships in case any of it is useful to you.Validated by measurement. Fresh store, memory enabled, one note written with a runtime-generated
token, then the same six queries I used in the report:
zx57269zx57269 kindzx57269 bananazx57269 banana laranjakind distinct count zx57269banana laranja abacaxiThe three that returned nothing now return the note, and the last row is the control: a query with no
matching term still returns zero, so
ORdid not turn recall into "return everything".I also confirmed the native tool now passes
AllowTrivialQuery: true, which closes the asymmetry I hadmentioned as a side note in the report.
Method note: every run used a throwaway
COMPOZY_HOME. The store that backs the original report wascopied first and left untouched, so the evidence still exists if you ever want more from it.