Summary
Assorted error-handling and input-validation gaps that turn recoverable conditions into crashes, invalid output, or silent data loss.
Findings
memory_stats emits invalid JSON on a fresh session (medium)
hexus/writer.py:144-149 returns float("nan") for p50/p95 when no writes have been processed; mcp_server/tools.py:944 embeds it and json.dumps emits the bare token NaN — invalid JSON that breaks strict parsers (JS JSON.parse, most LLM-host tool-result parsers).
Async-writer failures logged at DEBUG only (medium)
hexus/__init__.py:1068-1075 — DB-down/insert errors log at DEBUG, so in production every mirrored write can drop with no visible signal (contradicts writer's "log once" contract). _maybe_embed (:1840-1844) warns exactly once ever (_embed_warned never resets), after which a dead embedder writes NULL-embedding rows forever, invisibly.
Unguarded env-var parsing (medium)
mcp_server/tools.py:246-254 — float(HEXUS_DECAY_HALF_LIFE_DAYS) / recall_boost_weight are read outside the try/except; a non-numeric value turns every recall into a hard error instead of a graceful {"error": …}.
Inconsistent validation across parallel tools (medium)
memory_recall_turns (tools.py:389) doesn't clamp min_similarity to [0,1] (others do). top_k/limit caps disagree between MCP schema docs (200) and implementation (100). Empty agent_identity scoping differs by tool (also tracked in the security issue).
Embedding failures recorded as success (medium)
mcp_server/tools.py:699-702 — memory_record_delegation swallows EmbeddingError, sets vec = None, still inserts, and returns the same {"id": …} success shape — the row is silently un-searchable with no signal to the caller.
Unbounded memory_retain payload (medium)
tools.py:132-172 — no cap on list length or per-item size before embedding; a huge contents loads everything into one list, embeds sequentially, and (per the webhook issue) can spawn a thread per item.
on_session_end JSON serialization is unguarded (medium)
hexus/__init__.py:1219 — json.dumps(messages) (type List[Dict[str, Any]]) can raise TypeError on datetimes/tool-call objects and propagate into the host's teardown path; everything else in the class is defensively wrapped.
Suggested direction
Emit null/0 instead of NaN; log write/embed failures at WARNING (rate-limited, resettable); guard env parsing with fallback; unify min_similarity/limit clamping and the schema caps; signal un-embedded inserts to the caller; bound memory_retain size; wrap on_session_end serialization with default=str.
Filed from the 2026-07 codebase review.
Summary
Assorted error-handling and input-validation gaps that turn recoverable conditions into crashes, invalid output, or silent data loss.
Findings
memory_statsemits invalid JSON on a fresh session (medium)hexus/writer.py:144-149returnsfloat("nan")for p50/p95 when no writes have been processed;mcp_server/tools.py:944embeds it andjson.dumpsemits the bare tokenNaN— invalid JSON that breaks strict parsers (JSJSON.parse, most LLM-host tool-result parsers).Async-writer failures logged at DEBUG only (medium)
hexus/__init__.py:1068-1075— DB-down/insert errors log at DEBUG, so in production every mirrored write can drop with no visible signal (contradicts writer's "log once" contract)._maybe_embed(:1840-1844) warns exactly once ever (_embed_warnednever resets), after which a dead embedder writes NULL-embedding rows forever, invisibly.Unguarded env-var parsing (medium)
mcp_server/tools.py:246-254—float(HEXUS_DECAY_HALF_LIFE_DAYS)/recall_boost_weightare read outside thetry/except; a non-numeric value turns every recall into a hard error instead of a graceful{"error": …}.Inconsistent validation across parallel tools (medium)
memory_recall_turns(tools.py:389) doesn't clampmin_similarityto [0,1] (others do).top_k/limitcaps disagree between MCP schema docs (200) and implementation (100). Emptyagent_identityscoping differs by tool (also tracked in the security issue).Embedding failures recorded as success (medium)
mcp_server/tools.py:699-702—memory_record_delegationswallowsEmbeddingError, setsvec = None, still inserts, and returns the same{"id": …}success shape — the row is silently un-searchable with no signal to the caller.Unbounded
memory_retainpayload (medium)tools.py:132-172— no cap on list length or per-item size before embedding; a hugecontentsloads everything into one list, embeds sequentially, and (per the webhook issue) can spawn a thread per item.on_session_endJSON serialization is unguarded (medium)hexus/__init__.py:1219—json.dumps(messages)(typeList[Dict[str, Any]]) can raiseTypeErroron datetimes/tool-call objects and propagate into the host's teardown path; everything else in the class is defensively wrapped.Suggested direction
Emit
null/0instead of NaN; log write/embed failures at WARNING (rate-limited, resettable); guard env parsing with fallback; unifymin_similarity/limit clamping and the schema caps; signal un-embedded inserts to the caller; boundmemory_retainsize; wrapon_session_endserialization withdefault=str.Filed from the 2026-07 codebase review.