ConceptsBitemporal model

Bitemporal model

Bitemporal columns aren’t limited to facts — they live on decisions, strategies, relations and hyperedges as well, so any of those can be superseded without losing history. Taking facts as the example, every fact carries two time axes:

  • valid_from / valid_to — business time, when the statement was true in the world.
  • created_at / updated_at — system time, when Mnemosyne learned the statement.

A fact is current when valid_to IS NULL. A fact is historical when it has a valid_to. Both stay in the database; the historical row is your audit trail.

Why two axes

Suppose the agent learned at 10:00 that the Redis version was 6.2, and at 14:00 that it was upgraded to 7.0 at 11:00. With a single time axis you have to pick:

  • Delete the 6.2 row → lose history.
  • Update the row to 7.0 → corrupt the audit trail.
  • Soft-delete with a timestamp → close, but you can no longer answer “what did the agent know at 12:00?”.

Mnemosyne writes:

fact_a: { content: "Redis 6.2", valid_from: <unknown>, valid_to: 11:00,
          created_at: 10:00 }
fact_b: { content: "Redis 7.0", valid_from: 11:00, valid_to: NULL,
          created_at: 14:00 }

You can now answer four different questions:

QuestionQuery
What is true now?valid_to IS NULL
What did we believe at 12:00?created_at <= '12:00' filtered by valid window
What was true in the world at 12:00?valid_from <= '12:00' AND valid_to > '12:00'
When did we change our mind?scan created_at history

Time travel in the API

Every read endpoint accepts an asOf parameter:

curl -X POST http://localhost:3000/v1/recall \
  -H "Authorization: Bearer mns_live_xxx" \
  -d '{
    "query": "redis version",
    "asOf": "2026-06-07T12:00:00Z"
  }'

The hits returned are the facts the agent would have surfaced at noon on that day, regardless of what’s true now.

Implementation notes

  • The PostgreSQL tstzrange is indexed with a GIST exclusion constraint — no two current rows can claim the same (workspace_id, topic_key) simultaneously.
  • Closing a fact (DELETE /v1/facts/:id) sets valid_to = now() and preserves the row.
  • Restoring is the inverse: valid_to returns to NULL and the old window is re-opened.

This model is borrowed from data warehousing’s Type-2 SCD and from XTDB. We picked it because the agent’s reasoning quality drops sharply when its memory rewrites history.