Summary
TranscriptIndex.lookup nulls _last_search_summary at entry and only assigns it deep in the computation. The query cache returns above that assignment. So on a cache hit the retrieval returns byte-identical content while the summary stays None — and any caller that reads the summary to report how much was retrieved reports zero.
The count is bound to a cache-miss side effect rather than to the retrieval.
Mechanism
src/synapt/recall/core.py:
1858 self._last_search_summary = None # reset at entry
1875 cached = self._query_cache.get(cache_key)
1876 if cached is not None:
1877 return cached # early return, above the assignment
3578 self._last_search_summary = SearchResultSummary(...) # only reached on a miss
3587 self._last_search_summary = SearchResultSummary(...)
The cache is per-instance (_query_cache, max 32 entries), so this affects any caller that holds one index across repeated queries.
Reproduction
Runs against this package alone; no other dependency.
import tempfile
from pathlib import Path
from synapt.recall.core import TranscriptChunk, TranscriptIndex
TOPICS = ["a bicycle repair", "a garden fence", "a kitchen faucet", "a laptop battery",
"a bookshelf", "a car alternator", "a roof gutter", "a violin bridge",
"a camera lens", "a door hinge", "a water heater", "a bathroom tile",
"a lawn mower", "a window latch", "a stair rail"]
chunks = [
TranscriptChunk(
id=f"s{i:02d}:t0",
session_id=f"{i:08d}-0000-0000-0000-000000000000",
timestamp=f"2026-08-{6 + i:02d}T12:00:00+00:00",
turn_index=0,
user_text=f"doc-{i:02d} maintenance notes for {t}.",
assistant_text=f"Recorded maintenance notes for {t}.",
)
for i, t in enumerate(TOPICS)
]
with tempfile.TemporaryDirectory() as d:
index = TranscriptIndex(chunks, cache_dir=Path(d))
for i in range(12):
result = index.lookup("maintenance", max_chunks=5, max_tokens=2400)
summary = getattr(index, "_last_search_summary", None)
print(i + 1, len(result), "None" if summary is None else summary.chunk_blocks)
Observed:
call len(result) _last_search_summary
1 1686 10
2 1686 None
3 1686 None
...
12 1686 None
len(result) is identical on every call — the retrieval is correct and unchanged throughout. Only the reported summary disappears.
Why it matters
Any surface that renders "N passages retrieved" from this summary shows a correct result body beside the number 0, on every repeat of a query. A caller cannot distinguish "nothing matched" from "cached hit" without reaching further into private state, and the two mean opposite things.
Suggested fix
Either cache the summary alongside the result and restore it on a hit, or move the summary assignment above the cache return so it describes what is being returned in both paths. The first keeps the cache's benefit; the second is simpler if recomputing the summary is cheap.
A regression test wants two calls, not one: a single-call test passes under every implementation, because the defect only appears from the second identical query onward.
Summary
TranscriptIndex.lookupnulls_last_search_summaryat entry and only assigns it deep in the computation. The query cache returns above that assignment. So on a cache hit the retrieval returns byte-identical content while the summary staysNone— and any caller that reads the summary to report how much was retrieved reports zero.The count is bound to a cache-miss side effect rather than to the retrieval.
Mechanism
src/synapt/recall/core.py:The cache is per-instance (
_query_cache, max 32 entries), so this affects any caller that holds one index across repeated queries.Reproduction
Runs against this package alone; no other dependency.
Observed:
len(result)is identical on every call — the retrieval is correct and unchanged throughout. Only the reported summary disappears.Why it matters
Any surface that renders "N passages retrieved" from this summary shows a correct result body beside the number 0, on every repeat of a query. A caller cannot distinguish "nothing matched" from "cached hit" without reaching further into private state, and the two mean opposite things.
Suggested fix
Either cache the summary alongside the result and restore it on a hit, or move the summary assignment above the cache return so it describes what is being returned in both paths. The first keeps the cache's benefit; the second is simpler if recomputing the summary is cheap.
A regression test wants two calls, not one: a single-call test passes under every implementation, because the defect only appears from the second identical query onward.