Skip to main content
The Memory Ledger (app/ledger/) is Haki’s durable capture layer: it writes events (the raw proof) and manages facts (what is true), behind a deliberately small interface — write an event, read an object, list a timeline, apply a versioned status mutation.

Event (Event)

An event is the proof: “this message was said on this date.” Table events (app/models/event.py), append-only — business content is never modified after insert. The only write tolerated afterwards is the derived embedding (episodic memory, computed once by the Consolidator).
string
required
The event’s full scope.
string
default:"user"
Type of the subject (customer, user…).
string
required
Stable identity of the subject. Required — validated by the Ledger (typed missing_scope error), not by Pydantic, so the error message can carry the exact index of the offending event in the batch.
string | null
Provenance: who/what produced the event, in which conversation thread, in which run.
string
required
Free-form type, e.g. conversation.message, conversation.turn, agent.observation. No closed enum server-side.
datetime
required
Business time — when the event actually happened.
datetime
System time — when Haki recorded it. Generated server-side (server_default=func.now()).
object
required
Free-form content (JSONB).
string[]
Free-form tags, e.g. ["customer-data"].
string
sha256: + canonical hash of the business content (org/project/subject/ kind/occurred_at/payload, sorted keys). Computed server-side, never sent by the client.

Bitemporality

Haki systematically separates two time axes: This separation answers two different questions: “what did we know at date X?” (system time) and “what was true at date X?” (business time) — without ever conflating them. A deleted fact (terminal) gets recorded_to at the moment of the transition; valid_to, on the other hand, marks the end of business validity (set by the Consolidator at supersession time).

Write-time idempotency

write_events (app/ledger/core.py) batch-inserts with ON CONFLICT DO NOTHING on the unique constraint (project_id, idempotency_key). Each event’s effective key is chosen in this order:
  1. the batch idempotency_key (CaptureRequest.idempotency_key), namespaced by each event’s content hash (f"{batch_key}:{content_hash}") — several events in one batch never collide with each other;
  2. else the event’s own idempotency_key (EventIn.idempotency_key);
  3. else a fallback derived from the content hash + subject_id.
A network retry that replays the exact same batch with the same key therefore never creates a duplicate: POST /v1/capture returns the same event ids, with deduplicated: true on the ones already known.

Timeline

GET /v1/timeline?project_id=...&subject_id=... (both parameters are required — never a cross-subject timeline) returns events ordered by (occurred_at, recorded_at). It is the “raw proof” view used by the console and by the MCP haki_inspect flow.

API reference — Capture

Exact schema of POST /v1/capture, request/response examples.