fix(kg): reject inverted intervals in add_triple (valid_to < valid_from) - #1214
Conversation
A triple with valid_to < valid_from satisfies neither of the temporal
filter clauses in query_entity():
valid_from <= as_of AND valid_to >= as_of
so the triple is invisible to every query — silently corrupt. Reject
at write time with a clear error instead of letting bad data pile up
in the SQLite store.
The guard only fires when both bounds are present; open intervals
(only valid_from or only valid_to) are still accepted, and same-day
intervals (valid_from == valid_to, point-in-time facts) are explicitly
allowed.
|
+1 to rejecting at write time — silently invisible triples are a hard failure mode to debug after the fact. One coordination note worth surfacing: lex-string "2026-12-01" < "2026-3-01" # True — the guard would falsely reject Dec→Mar as invertedThe PR description names the dependency on #1167. Just noting that if #1167 stalls and #1214 lands first, MCP Low-risk in practice (LLM-driven callers tend to emit ISO 8601), and the right fix shape is #1167 anyway. Not blocking — just worth tagging the dependency in case the merge order goes #1214 → #1167 rather than the other way. |
|
@jphein Thanks — good call on tagging the merge-order gap explicitly. Concrete behavior if #1214 lands before #1167:
Happy with either merge order. If #1167 first, the gap closes naturally; if #1214 first, the limitation is documented and #1167 follows up. |
|
Thanks @arnoldwender — concrete on the merge-order behavior. Your read matches mine: practical exposure is small because LLM-driven callers tend to emit canonical ISO 8601, but the gap is worth the explicit docstring note in the meantime. Happy to defer to whichever of #1167 / #1214 lands first; the other will get a small rebase note if needed. |
Catches up on a heavy upstream day — 22 fixes merged in 24h plus prior backlog. Highlights pulled in: - MemPalace#1305 hooks: ~/.mempalace/ deletion is now a stable kill-switch (hooks no longer rebuild the dir hierarchy on Stop/PreCompact/SessionStart) - MemPalace#1214 KG: reject inverted intervals (valid_to < valid_from) at write time — prevents silently invisible triples - MemPalace#1067/MemPalace#1105 chroma: ChromaBackend.close_palace() now actually releases the SQLite file lock (PersistentClient.close() on evict + invalidation) - MemPalace#1215 entity_registry: atomic save (tmp+fsync+rename) — no more corruption on crash mid-write - MemPalace#1073/MemPalace#1107 mempalace compress: paginated drawer fetch — no longer trips SQLITE_MAX_VARIABLE_NUMBER on palaces >32k drawers - MemPalace#1282 stdio: Windows console UTF-8 reconfig for cli/mcp_server/hooks_cli - MemPalace#1164/MemPalace#1167 mcp KG: sanitize_iso_date() blocks malformed date strings silently producing empty result sets - MemPalace#1136/MemPalace#1160 mcp: per-path KG cache for multi-tenant hosts that rotate MEMPALACE_PALACE_PATH between tool calls - MemPalace#1286 mcp: retry _get_collection() once on transient failure - MemPalace#1138 lint cleanup, MemPalace#1019 search-crash fix - 4 new tools/ scripts (backup_claude_jsonls, find_orphan_claude_jsonls, render_jsonl, save.md) Conflict resolution (CHANGELOG.md only — code files all auto-merged): - 3.3.5 section: untouched (already merged in our prior commit; upstream added several new bug-fix entries which auto-merged cleanly) - 3.3.4 Bug Fixes: kept upstream's new MemPalace#1305 entry; preserved our richer detail on topic-tunnels (MemPalace#1194/MemPalace#1195/MemPalace#1197), HNSW-bloat (MemPalace#1191), max_seq_id (MemPalace#1135), and auto-ingest (MemPalace#1230/MemPalace#1231) — upstream's shorter topic-tunnels entry was a strict subset of ours. xdev patches preserved (still on this branch, untouched by merge): - 6ef44cb fix(hooks): route CC transcripts via convo_miner with cwd-based wings - 3fad61d fix(config): allow leading dash in wing names - 3fc821a fix(config): tighten leading-char to allow dash but not underscore Tests: 1557 passed, 1 skipped (full unit suite excluding benchmarks). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
What and Why
A triple with
valid_tobeforevalid_fromsatisfies neither of the temporal filter clauses inKnowledgeGraph.query_entity():so the triple is invisible to every query — silently corrupt. The data lives in SQLite forever but never surfaces, and the caller never sees a warning. This is a P0 data-integrity bug in a path adapters can hit easily (any caller that mixes up the two date params).
Root Cause
mempalace/knowledge_graph.py:149—add_triple()acceptsvalid_fromandvalid_toas opaque strings, validates each format independently in upstream callers (and now PR #1167 at the MCP boundary), but never checks the relationship between them.Fix
Reject at write time with a clear ValueError naming both bounds. The guard fires only when both are set:
valid_from, or onlyvalid_to) — accepted unchanged.valid_from == valid_to, point-in-time facts) — explicitly allowed (<not<=).sanitize_iso_date(PR fix(kg): validate ISO-8601 date formats at MCP boundary #1167) ensuresYYYY-MM-DDlex order matches calendar order.Test plan
test_add_triple_rejects_inverted_interval— asserts ValueError with'before valid_from'matchtest_add_triple_accepts_equal_dates— point-in-time facts passtest_add_triple_allows_only_one_bound— open intervals (one bound) still passtest_knowledge_graph.pytests greentest_backends.py::test_pin_hnsw_threads*unrelated — environmental ChromaDBconfiguration_json["hnsw"]schema drift, also fails on a cleandevelopcheckout)