How Time Travel Works in Semantica: Using state_at() for Temporal Graph Snapshots

Semantica's ContextGraph.state_at() method creates read-only snapshots of the entire knowledge graph as it existed at any historical point in time by filtering nodes and edges against their validity windows.

The semantica-agi/semantica repository implements a temporal knowledge graph where every node and edge carries optional validity intervals. This design allows developers to query the state of the graph at specific moments using the state_at() method, enabling powerful audit trails and historical analysis without modifying live data.

Understanding Temporal Validity in Semantica

Unlike static graph databases, Semantica treats time as a first-class citizen. Each ContextNode and ContextEdge can specify valid_from and valid_until timestamps, creating a continuous history of how entities and relationships evolve. When you call state_at(), the system reconstructs the graph topology exactly as it appeared at that timestamp, filtering out any elements that had not yet been created or had already expired.

How state_at() Processes Time-Travel Queries

The implementation in semantica/context/context_graph.py (lines 3728–3690) handles the snapshot creation through four distinct phases:

Timestamp Normalization

The method accepts flexible input types through the _normalize_timestamp() helper defined earlier in the same file. You can pass a native datetime object, an ISO-8601 string, or a Unix timestamp integer. The helper converts all formats into a timezone-aware datetime instance, ensuring consistent temporal comparisons regardless of how the query was initiated.

Thread-Safe Access with Graph Locking

Before constructing the snapshot, state_at() acquires the graph-wide lock (self._lock). This guarantees that the snapshot reflects a consistent state even when other threads are actively mutating the graph through concurrent writes. The lock ensures you never read partially committed changes or encounter race conditions during historical queries.

Filtering Active Elements

The method applies strict activity checks to prevent temporal anomalies:

  • Nodes – Only nodes where is_active(at_time) returns True are retained. The ContextNode.is_active method compares the query timestamp against the node's validity window.
  • Edges – An edge is included only if it is active and both its source and target nodes exist in the active node set. This prevents "dangling" edges from appearing in snapshots where one endpoint has expired or not yet been created.

Serializable Snapshot Structure

Active elements are serialized using their to_dict() methods. Decision nodes receive special handling—the system extracts them and enriches their properties with a flat summary. The final return value is a dictionary containing:

{
  "timestamp": "<ISO-timestamp of snapshot>",
  "nodes": [...],
  "edges": [...],
  "entities": [...],
  "relationships": [...],
  "decisions": [...]
}

The "entities" and "relationships" keys provide semantic aliases for "nodes" and "edges" respectively, making the output compatible with different visualization and analysis conventions.

Practical Examples of Graph Time Travel

The following code demonstrates how to query different historical states of a temporal graph:

from datetime import datetime, timedelta
from semantica.context.context_graph import ContextGraph

# Build a temporal graph

g = ContextGraph()

# Add a node valid from January to June 2024

g.add_node(
    node_id="n1",
    label="Customer",
    properties={"name": "Alice"},
    valid_from="2024-01-01T00:00:00Z",
    valid_until="2024-06-30T23:59:59Z",
)

# Add an eternal edge (no validity window)

g.add_edge(source_id="n1", target_id="n2", edge_type="PURCHASE")

# Add a time-bound decision

g.add_decision(
    category="credit",
    scenario="Mortgage approval",
    reasoning="Good credit score",
    outcome="approved",
    confidence=0.92,
    valid_from="2024-03-15T00:00:00Z",
)

# Query using ISO-8601 string

snap_jan = g.state_at("2024-01-15T12:00:00Z")
print("January nodes:", [n["id"] for n in snap_jan["nodes"]])

# Query using datetime object

snap_apr = g.state_at(datetime(2024, 4, 1))
print("April decisions:", [d["id"] for d in snap_apr["decisions"]])

# Query using Unix timestamp (July - after node expiration)

unix_ts = int((datetime(2024, 7, 1) - datetime(1970, 1, 1)).total_seconds())
snap_jul = g.state_at(unix_ts)
print("July edges:", [e["id"] for e in snap_jul["edges"]])

In this example, the January snapshot includes node n1, the April snapshot includes the mortgage decision added in March, and the July snapshot excludes the expired node while retaining timeless edges.

Key Implementation Files

The time-travel functionality spans several modules within the semantica-agi/semantica codebase:

File Purpose
semantica/context/context_graph.py Contains the state_at() implementation and _normalize_timestamp() helper
semantica/context/decision_models.py Defines the Decision data model used for decision node snapshots
semantica/context/context_node.py Implements ContextNode.is_active() and to_dict() methods used during filtering
semantica/context/context_edge.py Implements ContextEdge.is_active() and validity window checks

Summary

  • Semantica stores every graph mutation with temporal validity windows (valid_from / valid_until), enabling historical queries.
  • state_at() in semantica/context/context_graph.py creates consistent, read-only snapshots by acquiring the graph lock and filtering elements against the query timestamp.
  • The method accepts datetime objects, ISO-8601 strings, or Unix timestamps through internal normalization.
  • Dangling edge prevention ensures that edges only appear in snapshots if both connecting nodes are active at the queried time.
  • The returned dictionary provides serializable JSON suitable for storage, APIs, or visualization tools without affecting the live graph.

Frequently Asked Questions

What timestamp formats does state_at() accept?

The method accepts three input types: Python datetime objects, ISO-8601 formatted strings (e.g., "2024-01-15T12:00:00Z"), and Unix timestamps as integers. The internal _normalize_timestamp() helper in context_graph.py standardizes these into timezone-aware datetime instances before processing the snapshot.

Is state_at() thread-safe for concurrent environments?

Yes. The method acquires self._lock before reading the graph state, ensuring that the snapshot captures a consistent view even while other threads are adding or removing nodes and edges. This prevents phantom reads and ensures that your historical query reflects a single point-in-time state.

How does the snapshot handle expired or future-dated nodes?

Nodes are excluded from the snapshot if their validity window does not contain the query timestamp. If a node has a valid_until date earlier than the query time, or a valid_from date later than the query time, is_active() returns False and the node is filtered out. The same logic applies to edges, which are also removed if either endpoint node is inactive.

Can I modify a snapshot returned by state_at()?

The snapshot is a read-only dictionary structure containing serialized data. While you can technically modify the returned Python dictionary, it will not affect the underlying ContextGraph or its history. To change the graph, you must use mutating methods like add_node() or update_edge() on the live graph instance, which will create new temporal entries for future snapshots.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →