Bi-Temporal Versioning
Bi-Temporal Versioning & Time-Travel Queries
SmartMemory tracks two independent time dimensions for every memory item, so you can answer both "what did we believe was true on date X?" and "what did the system actually have stored on date X?".
The two times
| Dimension | Semantics |
|---|---|
| Valid time | When the fact is/was true in the real world |
| Transaction time | When the system recorded the version |
Each version of an item carries four timestamps:
@dataclass
class TemporalVersion:
item_id: str
content: str
metadata: dict
valid_time_start: datetime | None
valid_time_end: datetime | None
transaction_time_start: datetime | None
transaction_time_end: datetime | None
version: intSource: smart-memory-core/smartmemory/temporal/queries.py.
A version's transaction-time interval [transaction_time_start, transaction_time_end) is closed on the left and exclusive on the right. The
current version has transaction_time_end = None. The same shape applies to
valid time: an open valid_time_end means "still believed to be true."
How versions are created
Every update() on an existing item produces a new version. The previous
version's transaction_time_end is set, a new row is written with a fresh
transaction_time_start = now(), and the version counter increments. Deletes
become a tombstone version with change_type = "deleted".
In FalkorDB this is stored on the node properties — there's no separate
HAS_VERSION edge in the current implementation. VersionTracker
(smartmemory/temporal/version_tracker.py) is the read+write mechanism;
TemporalQueries is the user-facing query layer.
The query API
Service routes live under /memory/temporal/* and are wired in
smart-memory-service/memory_service/api/routes/temporal.py. The JS SDK
mirrors them on client.memories.*:
| Endpoint | SDK method | Returns |
|---|---|---|
GET /memory/temporal/{id}/history | getHistory(id, opts) | All versions of id |
GET /memory/temporal/at/{timestamp} | timeTravel(ts) | The state of every item at ts |
GET /memory/temporal/{id}/at/{timestamp} | getItemAtTime(id, ts) | The version of id valid at ts |
GET /memory/temporal/{id}/changes | getChanges(id, opts) | Change events between since and until |
POST /memory/temporal/{id}/compare?v1=&v2= | compareVersions(id,…) | Field-level diff between two versions |
POST /memory/temporal/{id}/rollback | rollback(id, opts) | Replay a past version as the new HEAD |
GET /memory/temporal/{id}/audit | (REST only) | Full audit trail with provenance |
GET /memory/temporal/search/during | (REST only) | All items valid during a window |
GET /memory/temporal/compliance/report | (REST only) | Aggregate retention / change report |
GET /memory/temporal/relationships/{rel_id}/history etc. | (REST only) | Bi-temporal queries on edges |
Worked examples
History of a single item
history = memory.temporal.get_history("item123")
for v in history:
print(f"v{v.version} valid={v.valid_time_start.date()}—"
f"{v.valid_time_end and v.valid_time_end.date() or 'now'} "
f"recorded={v.transaction_time_start.isoformat()}")"What did we know on Sept 1?"
state = memory.temporal.at_time("2024-09-01")
# → list of TemporalVersion, one per item that existed at that transaction timeThe same operation in the JS SDK:
const past = await client.memories.timeTravel('2024-09-01T00:00:00Z', {
limit: 100,
});Compare two versions
const diff = await client.memories.compareVersions('item123', 3, 5);
// diff.changed_fields → ["content", "metadata.tags"]Rollback
await client.memories.rollback('item123', { toVersion: 3 });
// or
await client.memories.rollback('item123', { toTime: '2024-09-15T12:00:00Z' });A rollback writes a new version with the chosen prior content — it does not delete the intervening versions, so the audit trail stays intact.
Use cases
- Audit & compliance — answer "what did this user see on date X" exactly.
- Backdated facts — set
valid_time_startin the past when ingesting historical records (e.g. importing a CRM snapshot from last quarter). - Belief revision — when a fact changes, the old version stays queryable; models can still be replayed against the prior state.
- Diff / blame —
compareVersionsshows which field changed between any two versions.
Caveats
- Time-travel queries iterate version chains in FalkorDB — for items with
hundreds of versions, prefer
getChanges()with a tightsince/untilwindow overgetHistory()with no bound. valid_time_endis set when a successor version'svalid_time_startis written — backfilling old facts requires explicitly setting the prior version'svalid_time_end.- Rollback is best-effort for relations: edges follow the same bi-temporal model but rollback only restores node properties, not edge fan-out from evolvers that ran in the meantime.
Key code surfaces
smartmemory/temporal/queries.py—TemporalQueries(user-facing API)smartmemory/temporal/version_tracker.py— version writessmartmemory/temporal/relationships.py— bi-temporal edgesmemory_service/api/routes/temporal.py— REST endpoints