Skip to content

Query Patterns

This guide covers the two read operations in mempill: query_memory (current belief and time travel) and query_audit (ledger forensics). For the write path see Quickstart (Python) and the language guides.

query_memory computes the canonical belief for a (subject, predicate) pair from the full claim history at read time. The fold is deterministic (I8) — given the same claim history, it always returns the same result.

The most common query: what does the engine believe right now?

Python — current belief
result = engine.query_memory({
"agent_id": "my-agent",
"subject": "user",
"predicate": "city",
# as_of_tx_time omitted = now
})
belief = result["belief"]
print(belief["status"]) # "Resolved" | "TimingUncertain" | "Contested" | "NoBelief"

Read the engine’s belief as it was at a past transaction time. Useful for auditing, debugging “what did the agent think yesterday?”, or reproducible report generation:

Python — time travel
result = engine.query_memory({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"as_of_tx_time": "2024-01-01T00:00:00Z", # ISO-8601 UTC
})

as_of_tx_time rewinds the transaction-time axis — “what did the engine know as of this timestamp?” To independently select by the real-world validity window (“what was true at this point in time?”), use the separate valid_at parameter, available as of mempill 0.3.0. The two parameters can be supplied together for a full bi-temporal point-in-time query — see Point-in-time valid-time query.

Pattern 3 — everything known about a subject (query_subject)

Section titled “Pattern 3 — everything known about a subject (query_subject)”

query_memory requires knowing the predicate upfront. When the goal is instead “fetch everything known about this subject” without an existing predicate list, use query_subject — it enumerates every predicate that has ever been asserted for the subject and returns the resolved belief for each, in one call:

Python — enumerate all beliefs about a subject
result = engine.query_subject({
"agent_id": "my-agent",
"subject": "acme:ceo",
# Optional, same as query_memory:
# "valid_at": "2021-06-01T00:00:00Z",
# "as_of_tx_time": "2024-01-01T00:00:00Z",
})
for entry in result:
print(entry["predicate"], entry["value"], entry["status"])

Entries are sorted by predicate (lexicographic) for stable, deterministic output. Each entry mirrors a query_memory primary shape: predicate, value, status, valid_from_display, valid_until_display, provenance, claim_ref, conf. A Contested predicate has value: None in its entry — the caller should surface that the same way it would surface a Contested result from query_memory.

The belief is a nested structure. The deep path is: belief → primary → fact → value

Python — extract value
belief = result["belief"]
status = belief["status"]
if status in ("Resolved", "TimingUncertain"):
primary = belief["primary"]
value = primary["fact"]["value"] # the authoritative value
conf = primary["confidence"]["value_confidence"]
vt = primary.get("valid_time") or {}
vt_start = vt.get("start") # ISO-8601 string or None
vt_end = vt.get("end") or "open"
claim_ref = primary["claim_ref"] # UUID string of the winning claim
print(f"{value!r} conf={conf} [{vt_start} .. {vt_end}]")
elif status == "Contested":
# No primary — all candidates are in alternatives.
for alt in belief.get("alternatives") or []:
print(alt["fact"]["value"])
elif status == "NoBelief":
print("No claim found for this (subject, predicate)")
Status primary alternatives Meaning
"Resolved" present empty Single winner after valid-time fold
"TimingUncertain" present empty Only one claim; no valid_time supplied — ordered by tx-time
"Contested" None 2+ entries Multiple claims; engine cannot pick a winner
"NoBelief" None empty No claim exists for this (subject, predicate)

When status == "Contested", primary is None. Both (all) values are preserved in alternatives. Do not pick one silently — surface the uncertainty to the user or trigger oracle resolution.

Python — Contested handling
if belief["status"] == "Contested":
alts = [a["fact"]["value"] for a in belief.get("alternatives") or []]
# Option A: surface to user
print(f"Uncertain: {alts}")
# Option B: trigger reconciliation
engine.reconcile({
"agent_id": "my-agent",
"subject_lines": [("user", "city")],
})

After reconcile, query_memory again. If an oracle is wired and resolved the conflict, status will be "Resolved". See Writing an Oracle.

When you ingested claims with explicit valid_time, the fold selects the claim whose window contains the query instant. Use this for temporal succession queries:

# "Who is CEO today?"
q_now = engine.query_memory({"agent_id": "a", "subject": "acme:ceo", "predicate": "held_by"})
print(q_now["belief"]["primary"]["fact"]["value"]) # Bob (current)
# "Who was CEO in February 2022?"
q_past = engine.query_memory({
"agent_id": "a",
"subject": "acme:ceo",
"predicate": "held_by",
"valid_at": "2022-02-01T00:00:00Z",
})
print(q_past["belief"]["primary"]["fact"]["value"]) # Alice (past)

See Concepts: Valid-Time Succession.

Updating a fact — closing open-ended claims

Section titled “Updating a fact — closing open-ended claims”

When you record an open-ended fact (no end date) and later learn when it ended, use end_fact to explicitly close it and enable succession:

# DON'T DO THIS:
# Original claim (open-ended):
r1 = engine.ingest_claim({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"value": "Alice",
"valid_time": {
"start": "2020-01-01T00:00:00Z",
# no "end" — open-ended
...
},
...
})
# WRONG: writing a bounded copy of the same value
r_copy = engine.ingest_claim({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"value": "Alice", # same value as r1!
"valid_time": {
"start": "2020-01-01T00:00:00Z",
"end": "2023-03-14T23:59:59Z", # now with an end
...
},
...
})
# Result: TWO overlapping claims for Alice with the same value.
# This creates a line conflict (Contested) until resolved by oracle.
# Note (0.4.0): A retroactive write into an ended window is also contested —
# the engine checks against both live and explicitly ended claims.

end_fact is a module-level function (from mempill import end_fact), not a method on engine:

from mempill import end_fact
# Step 1: Record open-ended
r1 = engine.ingest_claim({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"value": "Alice",
"valid_time": {
"start": "2020-01-01T00:00:00Z",
# no "end" — open-ended
...
},
...
})
# Step 2: Later, explicitly end it
end_fact(
engine,
agent_id="my-agent",
subject="acme:ceo",
predicate="held_by",
at="2023-03-14T23:59:59Z",
provenance=mempill.ProvenanceLabel.external_first_hand(),
)
# Step 3: Ingest successor (non-overlapping)
r2 = engine.ingest_claim({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"value": "Bob",
"valid_time": {
"start": "2023-03-15T00:00:00Z",
# no "end" — open-ended
...
},
...
})
# Result: Clean succession. No conflict. History shows:
# Alice [2020, 2023-03-14] — status: Superseded
# Bob [2023-03-15, ∞) — status: Current

Why this works:

  • end_fact is not a write to the original claim; it’s a validity assertion — a ledger entry applied at read time.
  • Once bounded via end_fact, the incumbent is no longer live, so it reports Superseded — the same status as a claim closed by a non-overlapping successor. Ended is reserved for a claim whose own window expires naturally with no explicit bound and no live successor.
  • The original claim’s storage is unchanged; a new bounded window is computed on the fly.
  • When Bob’s claim arrives non-overlapping, it folds as Succession (no oracle needed).
  • The line stays clean: never two claims with the same value on the same line.

history() returns every claim ever ingested for a (subject, predicate) subject-line, ordered oldest→newest. Use it to inspect how a belief evolved over time — who held the role before, when each value was valid, and which entry is current.

Python — reading the claim timeline
from mempill import open_in_memory, remember, history, recall, RememberOptions
engine = open_in_memory()
agent = "my-agent"
# Ingest a succession: Alice → John → Bob (non-overlapping windows)
remember(engine, agent, "acme:ceo", "held_by", "Alice",
RememberOptions(valid_from="2018-01-01", valid_until="2021-01-01"))
remember(engine, agent, "acme:ceo", "held_by", "John",
RememberOptions(valid_from="2021-01-01", valid_until="2024-01-01"))
remember(engine, agent, "acme:ceo", "held_by", "Bob",
RememberOptions(valid_from="2024-01-01"))
# Retrieve the full timeline
h = history(engine, agent, "acme:ceo", "held_by")
for e in h:
print(e.value, e.status, e.valid_from, "→", e.valid_until or "open")
# Alice Superseded 2018-01-01T00:00:00Z → 2021-01-01T00:00:00Z
# John Superseded 2021-01-01T00:00:00Z → 2024-01-01T00:00:00Z
# Bob Current 2024-01-01T00:00:00Z → open
# For honest, pre-rendered display strings (no fabricated precision), use
# .valid_from_display / .valid_until_display instead of the raw RFC3339 fields:
for e in h:
print(e.value, e.status, e.valid_from_display, "→", e.valid_until_display or "open")
# Alice Superseded 2018-01-01 → 2021-01-01
# John Superseded 2021-01-01 → 2024-01-01
# Bob Current 2024-01-01 → open

The current() helper returns the single Current entry when one exists, and is guaranteed to agree with recall() — both use the same canonical fold. current() returns None when the subject-line is empty, all claims have been superseded, the line is Contested (see HistoryEntryStatus::Contested), or the only live claim’s own window has closed without a live successor (see HistoryEntryStatus::Ended).

h = history(engine, agent, "acme:ceo", "held_by")
current_entry = h.current() # HistoryEntry or None
recall_result = recall(engine, agent, "acme:ceo", "held_by")
if current_entry is not None:
# When Current exists: both agree
assert current_entry.value == recall_result.as_str() # "Bob" == "Bob"
else:
# When None (contested, ended, or empty): recall also returns None
assert recall_result is None
Field Type Description
value any The asserted value for this claim
status "Current" | "Superseded" | "Contested" | "Ended" Entry status; see status vocabulary below. Consumers should handle unknown values gracefully.
valid_from string | None RFC 3339 start of the valid-time window, or None
valid_until string | None Effective end of the slot, or None (open-ended)
valid_from_granularity (0.4.0) "year" | "month" | "day" | "instant" | None Precision of valid_from, taken verbatim from this claim
valid_until_granularity (0.4.0) "year" | "month" | "day" | "instant" | None Precision of valid_until — from whichever endpoint supplied it: the claim’s own end_granularity if using its explicit end, the successor’s start_granularity if narrowed by non-overlapping succession, or the bounding instant’s granularity if ended by an adjudication or end_fact (the winning challenger’s start_granularity for adjudication, or the date precision as given for end_fact); None when open-ended, when the successor’s ordering key fell back to transaction-time, for a Deny adjudication bound, or when the claim has no explicit end and no successor
valid_from_display (0.4.0, Python only) string | None Pre-rendered valid_from at its recorded precision
valid_until_display (0.4.0, Python only) string | None Pre-rendered valid_until at its (derived) recorded precision
provenance string Human-readable label, e.g. "External/UserAsserted"
value_confidence float Confidence in this claim’s value (0.0–1.0)
claim_ref string (UUID) Identifies the underlying claim
  • Current — live (in the fold’s live set), unconflicted (no fold-wide conflict flag), and its valid-time window contains the query instant.
  • Superseded — not live; closed by a non-overlapping successor claim.
  • Contested — live and part of an unresolved conflict: either the fold-wide conflict flag, or a pairwise valid-time overlap detected against an adjacent live entry in the timeline (never silently narrowed or picked).
  • Ended — still live per the ledger (never explicitly bounded), but its own valid-time window has naturally expired with no live successor covering the query instant. Distinguishable from Superseded because the ledger contains no explicit successor.

The HistoryEntryStatus enum is non-exhaustive — consumers should handle unknown values gracefully.

history() — honest date precision (0.4.0)

Section titled “history() — honest date precision (0.4.0)”

Since 0.4.0, every HistoryEntry also carries its recorded date-precision, rendered identically to query_memory / query_subject — no fabricated precision is ever inserted. valid_until_granularity is derived from whichever endpoint supplies valid_until:

  • If using the claim’s own explicit valid_time.end: the claim’s own end_granularity.
  • If narrowed by a non-overlapping successor: the successor’s start_granularity.
  • Otherwise (open-ended or no successor): None.

See Changelog: Date granularity on history() for the full rule and the Python/Rust code samples above.

history() always returns the full timeline as it exists now — it has no valid_at parameter. For point-in-time valid-time queries (“who was CEO as of 2022-06-01 according to what we knew then?”), use query_memory with both valid_at and as_of_tx_time, available as of mempill 0.3.0: valid_at independently selects by the real-world validity window, and as_of_tx_time independently rewinds the transaction-time axis.

The audit ledger is an append-only record of every disposition event. Use query_audit for provenance forensics, compliance, and debugging.

Python — AuditQueryRequest
audit = engine.query_audit({
"agent_id": "my-agent", # required
"claim_ref": None, # str | None — filter to a specific claim UUID
"from_tx_time": None, # str | None — ISO-8601 UTC lower bound
"limit": 100, # int — max entries to return
})
audit = engine.query_audit({
"agent_id": "my-agent",
"claim_ref": None,
"from_tx_time": None,
"limit": 500,
})
for entry in audit["entries"]:
print(entry["event_kind"], entry["recorded_at"], entry["claim_ref"][:8])
# First get the claim_ref from an ingest or query response:
resp = engine.ingest_claim({...})
claim_ref = resp["claim_ref"]
audit = engine.query_audit({
"agent_id": "my-agent",
"claim_ref": claim_ref,
"from_tx_time": None,
"limit": 50,
})
from datetime import datetime, timezone, timedelta
one_hour_ago = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
audit = engine.query_audit({
"agent_id": "my-agent",
"claim_ref": None,
"from_tx_time": one_hour_ago,
"limit": 200,
})

from_tx_time is an inclusive lower bound, so a paged walk (using each page’s last recorded_at as the next from_tx_time) must de-duplicate by entry_id, and pages should be large enough to hold all entries sharing one timestamp.

Event Meaning
ClaimCommitted Claim written and committed
ValidityAsserted Valid-time bounds recorded or updated
AdjudicationRequested Oracle was called; pending row created
AdjudicationResolved submit_adjudication resolved the conflict
RecallReEntryDetected Amplification Guard caught a RecallReEntry echo
Quarantined Claim quarantined (provenance or amplification guard)
DependentFlaggedPendingReview Claim whose source was invalidated
AdjudicationExpired Pending adjudication TTL expired; returned to Contested

Audit entries carry claim_ref but not subject, predicate, or value — the ledger is a disposition event log, not a content store. Correlate back to ingestion records in your application using the claim_ref returned from ingest_claim.

The mempill-demo adapter (memory_mempill.py) keeps a session-local registry dict mapping claim_ref → ClaimMeta for exactly this purpose.

How do I update a fact that’s already been recorded?

Section titled “How do I update a fact that’s already been recorded?”

Use end_fact(engine, agent_id, subject, predicate, at, ...) (a module-level function, from mempill import end_fact) to explicitly close the incumbent open-ended claim, then ingest a new claim starting after that instant. See Updating a fact — closing open-ended claims above.

What if I don’t know the exact end date of an open-ended fact?

Section titled “What if I don’t know the exact end date of an open-ended fact?”
  • Option 1: Set valid_time_confidence lower (< 0.7) on the successor; it will be Contested until resolved by oracle.
  • Option 2: Use end_fact with your best-guess date; if you later learn a different date, call engine.assert_validity({"target": claim_ref, "assertion": {"type": "Reopen"}, ...}) directly to undo the bound (end_fact itself has no reopen parameter — it only binds), then call end_fact again with the correct date.
  • Option 3: Route via oracle — escalate the conflict and let the oracle decide.

No — mempill facts are immutable once written. To represent a belief change, ingest a new claim with a non-overlapping valid_time window (temporal succession) or let the new claim conflict and resolve via oracle. Re-writing the same claim with a different value creates two claims on the line, which is Contested until resolved. Note (0.4.0): a claim written retroactively into a window that was explicitly ended (via end_fact or affirmed adjudication) is now contested against the ended incumbent instead of being committed silently — the engine checks all live claims AND explicitly ended claims whose narrowed window overlaps the new claim’s window.

What if I have multiple agents writing to the same subject-line?

Section titled “What if I have multiple agents writing to the same subject-line?”

Each agent has a separate write lock: per-agent SQLite files (agent_<id>.db) or per-agent Postgres advisory locks. Agent isolation is guaranteed by the engine’s single-writer protocol. If you need cross-agent shared beliefs, use a shared Postgres database and call query_memory from any agent to see the union of all beliefs.

Yes — use the as_of_tx_time parameter to rewind the transaction-time axis (when the engine recorded the claim) and valid_at to filter by the real-world validity window. Both are independent:

# "What did the engine believe on 2023-01-01 (tx-time) about the CEO in 2021-06-01 (valid-time)?"
result = engine.query_memory({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"as_of_tx_time": "2023-01-01T00:00:00Z", # what the engine knew on this date
"valid_at": "2021-06-01T00:00:00Z", # filtered to this real-world date
})

What does “Ended” status mean in history?

Section titled “What does “Ended” status mean in history?”

Ended means the claim’s own valid-time window expired with no live successor covering the query instant. It differs from Superseded (which means an explicit successor closed the line). Both are closed, but Ended indicates natural expiration while Superseded indicates a deliberate succession.