TenseMem gives AI agents memory that understands time

Most agent memory systems treat facts as a flat pile. A user says they live in Delhi, then later says they moved to Bangalore. Similarity search returns both statements and the model guesses which one is current. TenseMem takes a different approach: it stores one current value per predicate slot, tracks freshness on every read, and refuses to learn from its own output.

How the engine works

The system centers on a simple loop. An extractor (LLM or rule-based) proposes typed operations from user messages. A pure resolver applies deterministic rules to those ops. The result writes to a single SQLite file inside your process. No server required unless you want one.

Facts are a rebuildable projection of an append-only episode log. You can swap extractors or upgrade models and rebuild state from scratch. The engine decides what sticks, not the model.

from tensemem import FakeExtractor, Memory

mem = Memory(llm=FakeExtractor())
mem.register_predicate("lives_in")
added = mem.add("u1", "I live in Delhi")
ctx = mem.context(user_id="u1", min_seq=added.seq, wait=1.5)
print(ctx.text)        # fenced block the model can read
print(ctx.freshness.is_current)  # True once extract finished

FakeExtractor is rule-based and needs no API key. The same API works with tensemem[openai], tensemem[anthropic], tensemem[ollama], tensemem[litellm], or Gemini (GEMINI_API_KEY in the process environment).

One current truth per slot

Each predicate holds exactly one active value. When a new fact arrives, it supersedes the old one. The old value moves to history with a SUPERSEDED tag. You can still ask "where did I live before?" and get the previous answer.

mem.add("u1", "I live in Delhi")
mem.set(user_id="u1", predicate="lives_in", value="Bangalore")
print(mem.state("u1").values)
# {"lives_in": "Bangalore"}

history = mem.history("u1", predicate="lives_in")
# ACTIVE Bangalore, SUPERSEDED Delhi

bangalore = next(f for f in history.facts if f.object == "Bangalore")
mem.correct(bangalore.id)  # that move was never true
print(mem.state("u1").values)
# {"lives_in": "Delhi"}

Corrections restore displaced values and survive rebuild with a newer model. The system does not auto-register unknown predicates, skip keep_ops, or teach the engine from fenced context text. Those invariants are locked by tests.

Freshness on every read

Every context call returns freshness metadata: as_of_seq, pending, is_current. You can wait for a message to be applied or see that it hasn't been incorporated yet. The recalled context block is fenced so the extractor never sees its own output as user input. Extraction requires a verbatim quote from the user.

Benchmarks against common baselines

The project includes TenseBench: 101 conversations scored with FakeExtractor (no network calls).

| System                | Current-state accuracy | Stale-fact leak | Freshness honesty | Contamination |
| TenseMem              | 1.00                   | 0.00            | 1.00              | 0.00          |
| Concatenated transcript | 0.37                 | 1.00            | 0.00              | n/a           |
| Token-overlap retrieval | 0.37                 | 1.00            | 0.00              | n/a           |

Run tensemem bench to reproduce.

CLI and local Studio

Command-line tools let you inspect state without writing code:

tensemem inspect --user u1
tensemem history --user u1 lives_in
tensemem search --user u1 "where did I live"

TenseMem Studio is a local Next.js inspector that runs over tensemem serve. It shows a board, memories table, playground, compare view, and echo log. It is not a chatbot. Secrets stay in the serve process environment — never in the Token field. Studio lives in the git checkout, not on PyPI.

git clone https://github.com/tensemem/tensemem.git
cd tensemem
pip install "tensemem[server]"
tensemem --db ./studio.db serve --port 8080   # Terminal 1
cd studio && npm install && npm run dev        # Terminal 2

Integration extras

  • tensemem[openai] / [anthropic] / [ollama] / [litellm] — hosted or local extract
  • tensemem[server] — REST API and Studio backend
  • tensemem[mcp] — stdio or HTTP MCP
  • tensemem[langgraph] — TenseMemStore with put/search
  • tensemem[llamaindex] — TenseMemChatMemory with put/get
  • tensemem[local] — FastEmbed vectors for search

Gemini extract uses GEMINI_API_KEY or GOOGLE_API_KEY in the process that runs extract. No extra package beyond the server if you use the built-in Gemini adapter. Optional bearer auth via --token or TENSEMEM_TOKEN. Bind stays loopback unless you put a TLS proxy in front.

Status and contributing

Version 0.1.0 alpha. Python API, CLI, REST, MCP, and Studio all work on a single SQLite file. Default store is SQLite; a second SQL dialect is not shipped. Multi-tenant hosted Studio is not in this tree.

Default tests need no API keys and never call a model:

uv sync
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run pytest

Studio changes require cd studio && npm ci && npm run typecheck && npm run build. Live extract against a real model runs only when both TENSEMEM_LIVE=1 and OPENAI_API_KEY are set.

Model keys belong in the process that runs extract (your script or the tensemem serve terminal). They are never valid values for Studio Token.

Main is protected. Every change is a pull request. Merges wait until CI, PR quality, and CodeQL are green.