Skip to content

[BUG] Temporal edge deduplication collapses disjoint occurrences and skips interval finalization #1865

Description

@thantiklermcirony

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions