fix(kg): accept ISO datetimes for temporal inputs - #1417
Conversation
|
Thanks for tackling #1374 — the test coverage and backward-compat wrapper are well done. However, I found critical correctness bugs before merge. The problemExpanding accepted formats from 1 to 6 (date, datetime with/without TZ, T vs space separator, fractional seconds) introduces silent data loss and wrong query results. The KG uses lexicographic TEXT comparison in SQL: AND (t.valid_from IS NULL OR t.valid_from <= ?) AND (t.valid_to IS NULL OR t.valid_to >= ?)This only works when all temporal values share the same format. Before this PR that was guaranteed (only Reproduced bugsBug 1 - Data loss for legacy date-only facts (CRITICAL) kg.add_triple("Alice", "ate_at", "Cafe", valid_from="2026-05-06", valid_to="2026-05-06")
result = kg.query_entity("Alice", as_of="2026-05-06T15:00:00Z")
# Expected: 1 fact. Actual: 0 factsCause: Bug 2 - Mixed timezones → false positives (CRITICAL) kg.add_triple("Bob", "works_at", "Globex", valid_from="2026-05-06T20:30:00-05:00") # = 01:30Z next day
result = kg.query_entity("Bob", as_of="2026-05-07T01:00:00Z") # 30 min BEFORE fact exists
# Expected: 0 facts. Actual: 1 factThe KG returns facts that don't exist at the queried time. Bug 3 - Mixed timezones → false negatives (CRITICAL) kg.add_triple("Carol", "is_in", "NYC", valid_from="2026-05-07T01:23:00+05:00") # = 20:23Z
result = kg.query_entity("Carol", as_of="2026-05-06T20:30:00Z") # 7 min AFTER fact starts
# Expected: 1 fact. Actual: 0 factsThe KG silently drops facts that should match. Bug 4 - Valid intervals rejected (HIGH) kg.add_triple("Eve", "is_in", "London",
valid_from="2026-05-06T15:00:00", # T
valid_to="2026-05-06 20:00:00") # space
# Raises: "valid_to is before valid_from"This is a valid 5-hour interval. The inverted-interval check at Suggested fixesOption A (recommended): Restrict to one canonical format — accept only Option B: Normalize on store. Convert all inputs to canonical UTC form before persisting. Loses verbatim round-trip but fixes comparisons. Option C: Revert and split. Keep storage at date-only, accept datetime only in Please add tests for mixed-format scenarios — current tests only exercise consistent formats, which is why these bugs slip through. Happy to help draft the fix if useful. |
|
Very good catch and excellent feedback @lealbrunocalhau Working on it. Thank you. |
|
Thanks for the detailed review — you were right. The first version accepted too many temporal shapes for a KG that stores and compares temporal values as TEXT. I updated the PR to use the safer canonical approach:
I also added regression tests for the mixed-format cases you called out, including the legacy date-only fact queried with a datetime @lealbrunocalhau please check it out? |
There was a problem hiding this comment.
Pull request overview
Expands MemPalace KnowledgeGraph temporal handling to accept canonical UTC ISO datetimes (YYYY-MM-DDTHH:MM:SSZ) in addition to full dates (YYYY-MM-DD), while preserving correct TEXT-based ordering semantics and improving temporal validation at the KG core layer.
Changes:
- Added
sanitize_iso_temporal()(with calendar validation) and keptsanitize_iso_date()as a backward-compatible wrapper. - Updated MCP KG tools and KG core (
KnowledgeGraph) to validate temporal inputs and support legacy date-only facts with datetimeas_ofqueries (start-of-day / end-of-day semantics). - Added regression tests across config, MCP server, and KG temporal querying/invalidation.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
mempalace/config.py |
Introduces sanitize_iso_temporal() and updates sanitize_iso_date() to wrap it. |
mempalace/knowledge_graph.py |
Validates temporals at the KG layer and adjusts temporal comparisons for date-only vs datetime compatibility. |
mempalace/mcp_server.py |
Switches KG tool call sites from sanitize_iso_date() to sanitize_iso_temporal() and updates tool schema descriptions. |
tests/test_config.py |
Adds unit tests for accepted/rejected temporal formats and wrapper behavior. |
tests/test_mcp_server.py |
Adds MCP-level regression tests for datetime acceptance and rejection of non-canonical forms. |
tests/test_knowledge_graph.py |
Adds KG-core tests for datetime/date-only compatibility and temporal validation errors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -171,10 +219,17 @@ def add_triple( | |||
| add_triple("Max", "does", "swimming", valid_from="2025-01-01") | |||
| add_triple("Alice", "worried_about", "Max injury", valid_from="2026-01", valid_to="2026-02") | |||
| @@ -313,15 +389,13 @@ def query_relationship(self, predicate: str, as_of: str = None): | |||
| JOIN entities o ON t.object = o.id | |||
| WHERE t.predicate = ? | |||
| """ | |||
| params = [pred] | |||
| if as_of: | |||
| query += " AND (t.valid_from IS NULL OR t.valid_from <= ?) AND (t.valid_to IS NULL OR t.valid_to >= ?)" | |||
| params.extend([as_of, as_of]) | |||
|
|
|||
| results = [] | |||
| with self._lock: | |||
| conn = self._conn() | |||
| for row in conn.execute(query, params).fetchall(): | |||
| for row in conn.execute(query, [pred]).fetchall(): | |||
| if as_of and not _triple_valid_at(row["valid_from"], row["valid_to"], as_of): | |||
| continue | |||
|
|
|||
|
Concerns from review Copilot's perf comments on Pulling the temporal predicate out of SQL means every row for the entity (or every row for the predicate) is materialized in Python and then filtered. For an entity with many facts, or a popular predicate like It also discards the benefit of A SQL-side normalization keeps correctness while letting SQLite filter before transfer: CASE WHEN length(valid_to) = 10 THEN valid_to || 'T23:59:59Z' ELSE valid_to END...with the symmetric one for Stale docstring
Those are now rejected by
Worth either normalizing CI Linux 3.13 and macOS jobs hang about 90 minutes after the test suite reports The failing test is But the 233 Worth investigating before merge since it blocks two of six matrix legs. |
|
@igorls would appreciate another review, you or Copilot. Could you please trigger it? The failing test was I can make the following changes:
What do you think? |
|
Your diagnosis matches the symptoms exactly. The 233 All three of your proposed changes look right to me:
Please go ahead with all three. One follow-up thought, not blocking this PR: Canceling the currently-pending CI run since it will hit the same hang. Push the fix and trigger a fresh one. |
…k subprocess state
… lock subprocess state
|
test-macos and test-linux3.13 are now passing, but lint is failing.
But it does not say which line would be formatted. I tried several formatting and pushed a formatting-only follow-up for @igorls I need help here, perhaps could you please run I also updated the PR description to reflect the current implementation: canonical UTC datetime support, |
|
Pushed the CI/process-isolation fix Igor agreed with. What changed:
I also verified that the Copilot SQL-filtering concern is addressed in the current branch: both |
…etimes # Conflicts: # tests/test_palace_locks.py
Copilot review on PR #1434 caught that the existing 3.3.5 entry described the validator as it was authored under #1167 — accepting ``YYYY``/``YYYY-MM``/``YYYY-MM-DD`` and rejecting ISO datetimes — but PR #1417 (closes #1374) merged into develop on 2026-05-10 and inverted that: ``sanitize_iso_temporal()`` now rejects partial dates and accepts canonical UTC datetimes (``YYYY-MM-DDTHH:MM:SSZ`` / ``+00:00``). ``sanitize_iso_date()`` is kept as a backwards-compat wrapper. Update the bullet to describe the *shipped* behavior, name both functions, list both accepted and rejected forms, and call out the 3.3.4 → 3.3.5 behavior change for partial-date inputs that now error. Reference both #1167 (original) and #1374/#1417 (the expansion).
What does this PR do?
Fixes #1374.
This expands KG temporal validation so MemPalace accepts canonical UTC ISO datetimes in addition to full dates, while preserving safe TEXT-based temporal comparisons in the knowledge graph.
What changed
sanitize_iso_temporal().sanitize_iso_date()as a backward-compatible wrapper.YYYY-MM-DDYYYY-MM-DDTHH:MM:SSZYYYY-MM-DDTHH:MM:SS+00:00, normalized toZ2026-02-31are rejected.KnowledgeGraphto validate temporal inputs at the core layer, not only the MCP layer.as_ofqueries:valid_fromcompares as start-of-dayvalid_tocompares as end-of-dayCASEexpressions, so SQLite filters rows before Python materializes them.sanitize_iso_temporal()for:as_ofvalid_fromvalid_toended+00:00normalization toZas_ofvalid_tointerval semanticsquery_entity()andquery_relationship()temporal filteringmcp_serverjust to clear cachesKnowledgeGraphsupports explicit context-manager cleanupspawnto avoid inherited fork statefinallyso failed tests cannot hang CIWhy
#1374 reports that KG/MCP temporal inputs reject legitimate sub-day timestamps such as:
The KG stores temporal values as TEXT, so accepting many ISO-like formats would be unsafe: mixed formats can sort incorrectly and silently return wrong facts.
This PR accepts only canonical UTC second-level datetimes plus full dates, while preserving backward compatibility for existing date-only KG facts.
How to test
Checklist
python -m pytest tests/ -v)ruff check .)