Skip to content

feat(mcp): add temporal_mode to search_memory_facts - #1804

Open
choi138 wants to merge 4 commits into
getzep:mainfrom
choi138:feat/upstream-temporal-mode
Open

choi138 wants to merge 4 commits into
getzep:mainfrom
choi138:feat/upstream-temporal-mode

Conversation

@choi138

@choi138 choi138 commented Aug 28, 2026 •

Copy link
Copy Markdown

Summary

Graphiti's bi-temporal model invalidates facts instead of deleting them ("Query what's true now, or what was true at any point in time"). The MCP search_memory_facts tool exposed date ranges but had no way to request facts that are true now.

This PR adds an opt-in temporal_mode parameter:

  • temporal_mode="current" applies as-of-now interval semantics:
    • (valid_at <= now OR valid_at IS NULL)
    • (invalid_at > now OR invalid_at IS NULL)
    • expired_at IS NULL
  • explicit valid_at_after / valid_at_before bounds further restrict the current interval; a future upper bound is capped at the internally captured UTC now
  • temporal_mode="all" or omitted preserves the existing full-history behavior
  • "current" combined with invalid_at_after / invalid_at_before raises ValueError

The reference instant is captured internally with timezone-aware UTC; no additional MCP argument is exposed.

Motivation (measured)

On a production graph (78,515 edges, 60 real user queries, top-24 candidates each): 52.7% of fact-search candidates were invalidated facts (per-turn median 54%, p90 71%). Clients that only want currently true facts can otherwise have historical facts crowd live ones out of the candidate window.

Changes

  • mcp_server/src/utils/type_config.py: build the as-of-now valid-time and transaction-time filters for temporal_mode="current"
  • mcp_server/src/graphiti_mcp_server.py: expose and accurately document the mode
  • graphiti_core/search/search_filters.py: give every dated condition a unique parameter across outer OR groups for valid_at, invalid_at, created_at, and expired_at
  • mcp_server/tests/test_core_parity.py: cover future valid_at, future invalid_at, null endpoints, explicit range intersection, and UTC capping
  • tests/utils/search/test_search_filters.py: cover the parameter-collision regression across all four date fields

Default result semantics remain unchanged for callers that omit temporal_mode. The core query parameter names are internal and now remain unique for multi-group filters.

Testing

  • uv run pytest tests/test_core_parity.py -q → 33 passed
  • CI-equivalent root unit suite → 374 passed, 11 skipped
  • focused search-filter/security tests → 19 passed
  • root pyright ./graphiti_core → 0 errors
  • changed MCP source Pyright → 0 errors
  • root and MCP Ruff check/format → passed
  • git diff --check → passed

@zep-cla-assistant

zep-cla-assistant Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@choi138

choi138 commented Aug 28, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA behalf on myself, e-mail: kidjustinchoi@gmail.com

@choi138

choi138 commented Aug 28, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

zep-cla-assistant Bot added a commit that referenced this pull request Aug 28, 2026
Graphiti's bi-temporal model invalidates facts instead of deleting
them. The MCP tool exposed only invalid_at date-range parameters,
preventing clients from requesting only currently true facts. Core
already supports this via ComparisonOperator.is_null.

On a production graph, 52.7% of the top 24 fact-search candidates
were invalidated facts crowding out live ones. temporal_mode='current'
exposes the existing core capability while preserving default behavior.
@choi138
choi138 force-pushed the feat/upstream-temporal-mode branch from cff7ebf to 857deb9 Compare August 28, 2026 01:03

@linhongyu510 linhongyu510 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking temporal-semantics gap: temporal_mode="current" is documented as returning facts that are currently true, but the implementation requires both invalid_at IS NULL and expired_at IS NULL and does not bound valid_at. Graphiti itself describes facts as valid between valid_at and invalid_at (graphiti_core/search/search_helpers.py), so a fact whose invalid_at is tomorrow is true now yet is dropped, while a fact whose valid_at is tomorrow and invalid_at is null is admitted. The helper tests only compare the constructed SearchFilters, so they lock in this mismatch rather than exercise boundary behavior. Please implement as-of-now interval semantics (including null endpoints), or rename/document the mode as active/open-ended history if that narrower behavior is intentional, and add regressions for future valid_at and future invalid_at. Independent validation at 857deb9c: mcp_server/tests/test_core_parity.py 32 passed; Ruff check/format and git diff --check passed.

Use interval-aware current fact filtering, preserve explicit valid-time bounds, and prevent date filter parameter collisions across OR groups.
@choi138

choi138 commented Aug 31, 2026

Copy link
Copy Markdown
Author

Thanks @linhongyu510 — the interval-semantics gap is addressed in 3911eef.

temporal_mode="current" now captures a timezone-aware UTC now internally and generates:

  • (valid_at <= now OR valid_at IS NULL)
  • (invalid_at > now OR invalid_at IS NULL)
  • expired_at IS NULL

Explicit valid_at_after / valid_at_before bounds further restrict that interval, and a future upper bound is capped at now. No public reference-time argument was added.

While adding the boundary regressions, I also found and fixed an existing core query-construction issue: dated filters in separate outer OR groups reused the same parameter name and overwrote one another. Parameters are now unique across OR groups for valid_at, invalid_at, created_at, and expired_at.

Regression coverage now includes future valid_at, future invalid_at, null endpoints, explicit range intersection, UTC capping, and multi-OR-group parameter binding across all four date fields.

Local verification:

  • MCP parity tests: 33 passed
  • CI-equivalent root unit suite: 374 passed, 11 skipped
  • focused search-filter/security tests: 19 passed
  • root and changed-MCP Pyright: 0 errors
  • root and MCP Ruff check/format: passed
  • git diff --check: passed

@choi138
choi138 requested a review from linhongyu510 August 31, 2026 01:28

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants