Description
Housekeeping, bundled deliberately so it costs you one notification instead of seven. Nothing here
is urgent and none of it is a crash on SurrealDB — close any line you disagree with and we will not
raise it again.
A read of v3.0.3 (ac20df41) turned up seven things too small for their own issues. Each is
stated with the command that shows it. None is a crash on the SurrealDB backend; two are wrong
answers, two are interface gaps of the #139 family, three are documentation.
1. The Tool Stats date filter in the dashboard is a no-op
GET /api/dashboard/tool-stats?days=N returns a byte-identical summary for days=7, 30
and 90. The route takes days: int = Query(default=30, ge=1, le=365) and the daily series
honours it, but the summary comes from:
$ grep -n "def get_tool_stats" src/surreal_memory/storage/surrealdb/tool_events.py
205: async def get_tool_stats(self, brain_id: str) -> dict[str, Any]:
There is no days parameter, so the summary is all-time by construction. The UI shows a working
filter that does not exist. Fixing it means changing the signature, not the call site.
Verified against a brain with 406 rows in tool_events, so the identical responses are not an
artefact of empty data — the three responses hash the same under SHA-256.
2. get_tool_stats / get_tool_stats_by_period exist only on the SurrealDB mixin
Same function, the other half of the problem. Both are defined only in
storage/surrealdb/tool_events.py — nothing in storage/base.py or the in-memory store — and
both are called without a guard:
$ git grep -n "get_tool_stats(" v3.0.3 -- src/
src/surreal_memory/mcp/lifecycle_handler.py:305: await storage.get_tool_stats(brain.id) # type: ignore[attr-defined]
src/surreal_memory/server/routes/dashboard_api.py:1543: await storage.get_tool_stats(brain.id) # type: ignore[attr-defined]
On any backend other than SurrealDB that is an AttributeError. This is the family #139 moved
onto the interface for pinning and #145 for watch state; the ratchet missed this pair.
3. The _query unwrap heuristic is a trap the next SELECT VALUE walks into
# src/surreal_memory/storage/surrealdb/store.py:686
return result[0] if isinstance(result[0], list) else result
This is the mechanism behind the first bug #143 fixed: SELECT VALUE <array-field> returns a
list of arrays, the heuristic collapses it to the first row, and the caller then iterates that
row's id strings character by character. #143 fixed the one call site that was exposed; the
heuristic is unchanged.
We checked every SELECT VALUE under storage/surrealdb/:
$ grep -rn "SELECT VALUE" src/surreal_memory/storage/surrealdb/ | wc -l
6
Of the six, one is a comment (pinning.py:113, the warning #143 left behind), four are nested
subqueries the unwrap never touches (retrieval_trace.py:177, typed_memory.py:392 and :409,
store.py:1751). Exactly one is top-level — store.py:2712 in get_connected_neuron_ids — and
it is safe only because in/out are scalar record links rather than arrays. Copy that line
onto an array field and #143 happens again.
It also contradicts _query's declared return type of list[dict], which is why mypy is quiet
about it.
4. smem doctor / /health — schema_version went 40 → 9 without a line in the migration guide
#134 was right to fix it: 2.x reported the SQLite constant 40 unconditionally, including on
SurrealDB installs, and 3.x reports the active backend's 9 via _active_schema_version(). But:
$ grep -c -i "schema.version" docs/guides/migrating-to-3.0.md
0
Any monitoring that parses schema_version sees 40 → 9 and reads it as a regression. One
sentence in the guide would cover it, ideally noting that the field to check for a version is
version, not schema_version.
5. The brain-files panel shows SurrealDB-only brains a path to a file that is not there
#144 made brains that live only in SurrealDB visible, which is the right fix. The files panel
then builds a path for each of them regardless:
# src/surreal_memory/server/routes/dashboard_api.py:830-843
db_path = Path(cfg.get_brain_db_path(name))
size = 0
if db_path.exists():
size = db_path.stat().st_size
For a SurrealDB-only brain the row shows a path to a file that does not exist and
size_bytes: 0. What makes it worse than merely blank: if a stale .db from a 2.x install
happens to still be on disk, that one row looks real while its neighbours do not, and there is
no way for the user to tell the column is fiction.
6. aiosqlite is still a hard dependency — and one helper's name says the wrong thing
$ grep -n "aiosqlite" pyproject.toml
42: "aiosqlite>=0.19.0",
This is not a leftover to delete — db_introspector.py:385 uses it for smem train-db
against external SQLite files, which is a product feature, and doctor and the test suite use it
too. The most that seems right is moving it to optional-dependencies.
Separately: utils/sandbox.py::ensure_aiosqlite_or_exit_cli reads as though it checks the
aiosqlite package. Its own docstring says otherwise — "SQLite (stdlib sqlite3) is the
minimum requirement" — and the body checks sqlite3. Worth renaming.
7. AGENTS.md asks for the AI attribution that its own Hard Rule #2 forbids
AGENTS.md:27-46 (Hard Rule #2) says:
Do not put AI attribution in commits, PR descriptions, or the CHANGELOG. No
Co-Authored-By: ... trailer, no Built with: … footer, no "Generated with …" line.
[...] Earlier revisions of this file asked for the opposite. That guidance is withdrawn.
AGENTS.md:105-127 (PR Template), in the same file, says a PR body must contain:
## Verified by
@your-github-handle — built with <agent name + version>.
The second block looks like a leftover of the revision the first block withdraws. It is not
academic: an agent reading the file top to bottom is told to add exactly the footer the hard
rule prohibits, and the local no-ai-attribution hook in .pre-commit-config.yaml will reject
the resulting commit message.
For what it is worth we resolved it in your favour: ## Verified by kept, tool name dropped, on
the grounds that Hard Rule #2 is explicitly the newer of the two and that none of your own merged
PRs carries a tool attribution. We mention it only because we had to pick one and would rather you
knew which — a one-line edit would spare the next contributor the same guess. Raising it rather
than patching it, since AGENTS.md is on the list of files agents are told not to touch without
asking first (AGENTS.md:134).
To Reproduce
Each finding above carries the command that shows it, run on a clean v3.0.3 checkout
(ac20df41) with the package installed. The short version:
# 1 and 2 - the summary has no days parameter, and the method exists on one backend only
grep -n "def get_tool_stats" src/surreal_memory/storage/surrealdb/tool_events.py
git grep -n "get_tool_stats(" v3.0.3 -- src/
# 3 - the unwrap heuristic and the single top-level SELECT VALUE it does not protect
sed -n '686p' src/surreal_memory/storage/surrealdb/store.py
grep -rn "SELECT VALUE" src/surreal_memory/storage/surrealdb/
# 4 - the migration guide never mentions the field that changed
grep -c -i "schema.version" docs/guides/migrating-to-3.0.md
# 5 - the panel builds a path whether or not a file is there
sed -n '830,843p' src/surreal_memory/server/routes/dashboard_api.py
# 6 - a hard dependency, and a helper whose name says the wrong thing
grep -n "aiosqlite" pyproject.toml
sed -n '15,22p' src/surreal_memory/utils/sandbox.py
# 7 - the two blocks of AGENTS.md that contradict each other
sed -n '27,46p;105,127p' AGENTS.md
Expected Behavior
- (1)
?days=N either filters the summary or is not offered on it.
- (2)
get_tool_stats / get_tool_stats_by_period live on NeuralStorage, as pinning did after
#139 and watch state after #145.
- (3)
_query either honours its declared list[dict] return type or exposes a separate method
for scalar projections, so the next SELECT VALUE cannot repeat #143.
- (4) One sentence in the migration guide about
schema_version going 40 → 9.
- (5) A SurrealDB-only brain shows no file path rather than one that does not exist.
- (6)
aiosqlite sits under optional-dependencies, and the helper's name matches what it checks.
- (7)
AGENTS.md asks for one thing about AI attribution, not two opposite things.
Actual Behavior
- (1) identical
summary for days=7, 30 and 90.
- (2)
AttributeError on any backend other than SurrealDB, hidden behind
# type: ignore[attr-defined].
- (3) the heuristic that caused
#143 is unchanged; one top-level call site is safe only by
accident of field type.
- (4)
grep -c -i "schema.version" docs/guides/migrating-to-3.0.md -> 0.
- (5) a path to a missing file with
size_bytes: 0, and one accidentally-real row next to it.
- (6) hard dependency at
pyproject.toml:42; ensure_aiosqlite_or_exit_cli checks sqlite3.
- (7) an agent following the file top to bottom writes a footer its own hard rule forbids, and
the repo's no-ai-attribution hook then rejects the commit.
Environment
- OS: Linux (x86-64)
- Python version: 3.12.13
- Surreal-Memory version: 3.0.3 (
ac20df41)
- Installation method: source,
pip install -e ".[dev,server,surrealdb]"
- Backend: SurrealDB 3.2.0
Additional Context
We are happy to send patches for any of these. (1) and (2) are one change to a signature and its
two call sites; (4) is a sentence; (6) is a rename plus a dependency move. (3) and (5) look like
they want a decision from you first.
Description
Housekeeping, bundled deliberately so it costs you one notification instead of seven. Nothing here
is urgent and none of it is a crash on SurrealDB — close any line you disagree with and we will not
raise it again.
A read of
v3.0.3(ac20df41) turned up seven things too small for their own issues. Each isstated with the command that shows it. None is a crash on the SurrealDB backend; two are wrong
answers, two are interface gaps of the
#139family, three are documentation.1. The Tool Stats date filter in the dashboard is a no-op
GET /api/dashboard/tool-stats?days=Nreturns a byte-identicalsummaryfordays=7,30and
90. The route takesdays: int = Query(default=30, ge=1, le=365)and thedailyserieshonours it, but the summary comes from:
There is no
daysparameter, so the summary is all-time by construction. The UI shows a workingfilter that does not exist. Fixing it means changing the signature, not the call site.
Verified against a brain with 406 rows in
tool_events, so the identical responses are not anartefact of empty data — the three responses hash the same under SHA-256.
2.
get_tool_stats/get_tool_stats_by_periodexist only on the SurrealDB mixinSame function, the other half of the problem. Both are defined only in
storage/surrealdb/tool_events.py— nothing instorage/base.pyor the in-memory store — andboth are called without a guard:
On any backend other than SurrealDB that is an
AttributeError. This is the family#139movedonto the interface for pinning and
#145for watch state; the ratchet missed this pair.3. The
_queryunwrap heuristic is a trap the nextSELECT VALUEwalks intoThis is the mechanism behind the first bug
#143fixed:SELECT VALUE <array-field>returns alist of arrays, the heuristic collapses it to the first row, and the caller then iterates that
row's id strings character by character.
#143fixed the one call site that was exposed; theheuristic is unchanged.
We checked every
SELECT VALUEunderstorage/surrealdb/:Of the six, one is a comment (
pinning.py:113, the warning#143left behind), four are nestedsubqueries the unwrap never touches (
retrieval_trace.py:177,typed_memory.py:392and:409,store.py:1751). Exactly one is top-level —store.py:2712inget_connected_neuron_ids— andit is safe only because
in/outare scalar record links rather than arrays. Copy that lineonto an array field and
#143happens again.It also contradicts
_query's declared return type oflist[dict], which is why mypy is quietabout it.
4.
smem doctor//health—schema_versionwent 40 → 9 without a line in the migration guide#134was right to fix it: 2.x reported the SQLite constant40unconditionally, including onSurrealDB installs, and 3.x reports the active backend's
9via_active_schema_version(). But:$ grep -c -i "schema.version" docs/guides/migrating-to-3.0.md 0Any monitoring that parses
schema_versionsees 40 → 9 and reads it as a regression. Onesentence in the guide would cover it, ideally noting that the field to check for a version is
version, notschema_version.5. The brain-files panel shows SurrealDB-only brains a path to a file that is not there
#144made brains that live only in SurrealDB visible, which is the right fix. The files panelthen builds a path for each of them regardless:
For a SurrealDB-only brain the row shows a path to a file that does not exist and
size_bytes: 0. What makes it worse than merely blank: if a stale.dbfrom a 2.x installhappens to still be on disk, that one row looks real while its neighbours do not, and there is
no way for the user to tell the column is fiction.
6.
aiosqliteis still a hard dependency — and one helper's name says the wrong thingThis is not a leftover to delete —
db_introspector.py:385uses it forsmem train-dbagainst external SQLite files, which is a product feature, and doctor and the test suite use it
too. The most that seems right is moving it to
optional-dependencies.Separately:
utils/sandbox.py::ensure_aiosqlite_or_exit_clireads as though it checks theaiosqlitepackage. Its own docstring says otherwise — "SQLite (stdlibsqlite3) is theminimum requirement" — and the body checks
sqlite3. Worth renaming.7.
AGENTS.mdasks for the AI attribution that its ownHard Rule #2forbidsAGENTS.md:27-46(Hard Rule #2) says:AGENTS.md:105-127(PR Template), in the same file, says a PR body must contain:The second block looks like a leftover of the revision the first block withdraws. It is not
academic: an agent reading the file top to bottom is told to add exactly the footer the hard
rule prohibits, and the local
no-ai-attributionhook in.pre-commit-config.yamlwill rejectthe resulting commit message.
For what it is worth we resolved it in your favour:
## Verified bykept, tool name dropped, onthe grounds that
Hard Rule #2is explicitly the newer of the two and that none of your own mergedPRs carries a tool attribution. We mention it only because we had to pick one and would rather you
knew which — a one-line edit would spare the next contributor the same guess. Raising it rather
than patching it, since
AGENTS.mdis on the list of files agents are told not to touch withoutasking first (
AGENTS.md:134).To Reproduce
Each finding above carries the command that shows it, run on a clean
v3.0.3checkout(
ac20df41) with the package installed. The short version:Expected Behavior
?days=Neither filters the summary or is not offered on it.get_tool_stats/get_tool_stats_by_periodlive onNeuralStorage, as pinning did after#139and watch state after#145._queryeither honours its declaredlist[dict]return type or exposes a separate methodfor scalar projections, so the next
SELECT VALUEcannot repeat#143.schema_versiongoing 40 → 9.aiosqlitesits underoptional-dependencies, and the helper's name matches what it checks.AGENTS.mdasks for one thing about AI attribution, not two opposite things.Actual Behavior
summaryfordays=7,30and90.AttributeErroron any backend other than SurrealDB, hidden behind# type: ignore[attr-defined].#143is unchanged; one top-level call site is safe only byaccident of field type.
grep -c -i "schema.version" docs/guides/migrating-to-3.0.md->0.size_bytes: 0, and one accidentally-real row next to it.pyproject.toml:42;ensure_aiosqlite_or_exit_clicheckssqlite3.the repo's
no-ai-attributionhook then rejects the commit.Environment
ac20df41)pip install -e ".[dev,server,surrealdb]"Additional Context
We are happy to send patches for any of these. (1) and (2) are one change to a signature and its
two call sites; (4) is a sentence; (6) is a rename plus a dependency move. (3) and (5) look like
they want a decision from you first.