Skip to content

Memory recall returns zero results when any query term is absent from the corpus: the FTS match expression is joined with AND #562

Closed

Description

@jillesmc

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...), nil

If 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 entries

The 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.

Activity

  1. added theissue type on Sep 9, 2026
  2. moved this from Todo to In Progress in CompozyOS Devon Sep 9, 2026
  3. moved this from In Progress to Done in CompozyOS Devon Sep 9, 2026
  4. jillesmc commented on Sep 9, 2026

    @jillesmc
    Author

    Independent validation on main at 744a29ac, built locally with make build-go, run before
    beta.24 ships 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:

    query beta.23 main
    zx57269 1, score 0.954 1, score 0.900
    zx57269 kind 1, score 0.969 1, score 0.990
    zx57269 banana 0 1, score 0.992
    zx57269 banana laranja 0 1, score 0.993
    kind distinct count zx57269 0 1, score 0.994
    banana laranja abacaxi 0 0

    The 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 OR did not turn recall into "return everything".

    I also confirmed the native tool now passes AllowTrivialQuery: true, which closes the asymmetry I had
    mentioned as a side note in the report.

    Method note: every run used a throwaway COMPOZY_HOME. The store that backs the original report was
    copied first and left untouched, so the evidence still exists if you ever want more from it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions