Bug Description
An incoming fact with the same text and endpoints as a historical fact can reuse that historical edge even when their supplied validity intervals are disjoint. The new occurrence is lost. The same issue exists in the semantic-duplicate result path and within-batch deduplication.
Environment
Graphiti 0.30.2 at eaa4128681bc53487138a4bbc22d58336ebe70d2, Python 3.12.14, Windows, development checkout. This tests the real resolver with supplied dates and a fixed response at the model seam; it does not evaluate natural-language extraction.
Minimal reproduction
Run with the chosen checkout first on PYTHONPATH and its dependencies installed. No graph service or model account is needed.
import os
os.environ['GRAPHITI_TELEMETRY_ENABLED'] = 'false'
import asyncio
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from graphiti_core.edges import EntityEdge
from graphiti_core.nodes import EpisodicNode
from graphiti_core.utils.maintenance.edge_operations import resolve_extracted_edge
t0 = datetime(2026, 1, 1, tzinfo=timezone.utc)
common = dict(
source_node_uuid='person', target_node_uuid='project', name='ASSIGNED_TO',
group_id='repro', fact='Alex is assigned to Payments.', created_at=t0,
)
old = EntityEdge(
uuid='old', **common, valid_at=t0,
invalid_at=t0 + timedelta(days=10), expired_at=t0 + timedelta(days=11),
)
new = EntityEdge(uuid='new', **common, valid_at=t0 + timedelta(days=20))
episode = EpisodicNode(
uuid='episode', name='Reassignment', content=new.fact, group_id='repro',
source='message', source_description='supplied fixture', valid_at=new.valid_at,
)
async def supplied_response(*args, **kwargs):
assert kwargs['prompt_name'] == 'dedupe_edges.resolve_edge'
return {'duplicate_facts': [0], 'contradicted_facts': []}
async def main():
result, _, _ = await resolve_extracted_edge(
SimpleNamespace(generate_response=supplied_response), new, [old], [], episode
)
print('resolved UUID:', result.uuid)
print('valid_at:', result.valid_at.isoformat())
print('invalid_at:', result.invalid_at.isoformat() if result.invalid_at else None)
print('historical end preserved:', old.invalid_at == t0 + timedelta(days=10))
asyncio.run(main())
Actual: UUID old, interval [2026-01-01, 2026-01-11). The January 21 recurrence disappears.
Expected, and observed with the candidate: UUID new, start 2026-01-21, no end; the old January 11 end remains unchanged. A semantic duplicate response must not override known disjoint intervals.
Proposed scope
The correction is limited to temporal bookkeeping in edge_operations.py:
- Guard exact and semantic reuse against separate intervals; retain different supplied intervals in batch deduplication. Preserve the source reference clock while both dates remain unknown.
- Resolve missing dates before historical duplicate eligibility, using the edge's own reference time and at most one timestamp attempt. Do not invent a start from an end or from ingestion time.
- Finalize
expired_at for an ended fact even when no candidates exist. Currently an unrelated candidate determines whether that metadata is recorded.
- Clip an older finite interval at a known later contradiction inside it, preserving prior transaction expiration and never extending its end. Currently an already-set
expired_at bypasses clipping.
Compatible duplicates and unknown-time controls remain covered. This is not complete interval algebra or a new storage schema. The end-only RELEASED extraction report in #1841 remains unresolved; supplied valid-start fixtures do not establish its end-to-end cause or resolution.
Proposed source · Identity regressions · Resolution regressions
Validation and limits
The two new regression modules contain 33 passing cases on the candidate, covering temporal identity, reactivation, touching/disjoint intervals, batch ordering, expiration, finite clipping and timestamp-reference compatibility. All semantic decisions are supplied fixtures.
Combined-candidate evidence, including the separate Neo4j datetime correction reported in #1864: 430 unit tests passed, 11 skipped; 26/26 declared valid-time query assertions passed across two real query paths, compared with 16/26 on baseline. These cover 13 fixture timepoints on Neo4j Community 5.26.30, not model accuracy or isolated validation of this resolver change. One planned fulltext-dependent database case remained unverified after sandbox index-creation failures; full integration is incomplete.
Bug Description
An incoming fact with the same text and endpoints as a historical fact can reuse that historical edge even when their supplied validity intervals are disjoint. The new occurrence is lost. The same issue exists in the semantic-duplicate result path and within-batch deduplication.
Environment
Graphiti 0.30.2 at
eaa4128681bc53487138a4bbc22d58336ebe70d2, Python 3.12.14, Windows, development checkout. This tests the real resolver with supplied dates and a fixed response at the model seam; it does not evaluate natural-language extraction.Minimal reproduction
Run with the chosen checkout first on
PYTHONPATHand its dependencies installed. No graph service or model account is needed.Actual: UUID
old, interval[2026-01-01, 2026-01-11). The January 21 recurrence disappears.Expected, and observed with the candidate: UUID
new, start2026-01-21, no end; the old January 11 end remains unchanged. A semantic duplicate response must not override known disjoint intervals.Proposed scope
The correction is limited to temporal bookkeeping in
edge_operations.py:expired_atfor an ended fact even when no candidates exist. Currently an unrelated candidate determines whether that metadata is recorded.expired_atbypasses clipping.Compatible duplicates and unknown-time controls remain covered. This is not complete interval algebra or a new storage schema. The end-only RELEASED extraction report in #1841 remains unresolved; supplied valid-start fixtures do not establish its end-to-end cause or resolution.
Proposed source · Identity regressions · Resolution regressions
Validation and limits
The two new regression modules contain 33 passing cases on the candidate, covering temporal identity, reactivation, touching/disjoint intervals, batch ordering, expiration, finite clipping and timestamp-reference compatibility. All semantic decisions are supplied fixtures.
Combined-candidate evidence, including the separate Neo4j datetime correction reported in #1864: 430 unit tests passed, 11 skipped; 26/26 declared valid-time query assertions passed across two real query paths, compared with 16/26 on baseline. These cover 13 fixture timepoints on Neo4j Community 5.26.30, not model accuracy or isolated validation of this resolver change. One planned fulltext-dependent database case remained unverified after sandbox index-creation failures; full integration is incomplete.