Lifeline v0.3.0

Architecture

Lifeline's architecture: the deterministic event model, reducers, recall, projection, and the store-is-source cutover.

lifeline-context 0.5.0 · licensed FSL-1.1-MIT (source-available; converts to MIT after two years). Technical document. For the why behind each decision, the source is LIFELINE.md (the decision entries cited in brackets, e.g.: #0002).

10,000-foot view

Lifeline is an event-sourced reasoning ledger. Everything is an immutable, content-addressed Entry, chained in a DAG. The "current truth" and the context an AI reads are derived projections of that ledger — never the source. Three layers of memory, all anchored in the same ledger:

                      Layer 3 · Recall (semantic)
                      anchored embeddings → "what is relevant to the task"
                                  ▲
   Entry (append) ──▶ Layer 1 · Ledger ──reduce──▶ Layer 2 · State ──assemble──▶ payload
                      (hashed DAG,                   (current truth,                (markdown,
                       source of truth)               via reducers)                  by budget)
                                  │
                                  └── projection ──▶ LIFELINE.md (diffable view)

Module map (lifeline/):

Module Layer / role
entry.py the content-addressed Entry (identity)
store.py EventStore (port) + SQLiteEventStore (Layer 1)
state.py StateEngine + reducers (Layer 2)
recall.py Embedder + LexicalEmbedder + SemanticRecall (Layer 3)
context.py ContextAssembler — assembles the payload
projection.py store → markdown (the generated view)
ingest.py markdown → store (migration)
staging.py StagingStore (port) + SQLiteStagingStore — the HITL proposal queue
cli.py / __main__.py the lifeline CLI
mcp_server.py the MCP server (lifeline-mcp / lifeline-mcp-remote)
cloud.py Supabase adapters (SupabaseEventStore, SupabaseStagingStore) — the cloud seam

1. The event model — deterministic content-addressing [#0002, #0003]

id = sha256( kind \n author \n agent \n provider \n model \n summary \n body.strip()
             \n "|".join(sorted(parents)) \n )   # canonical form ends in a trailing \n

2. Layer 1 — the Ledger (SQLiteEventStore)

Append-only. entries table (unique id, JSON payload, kind, ts, dedup_key, parents) + edges table (DAG) + unique index on dedup_key. WAL on. append() is idempotent: violating the PK (id) or dedup_key → silently ignored (it solves the "split-brain" problem trivially). stream() returns in causal insertion order (single-writer). It is the runtime source of truth.

EventStore is an abstract port — the SQLiteEventStore is the local adapter; the SupabaseEventStore (shipped in cloud.py) implements the same interface without touching the core. It is the cloud seam, selected with --store supabase / LIFELINE_STORE=supabase.

3. Layer 2 — the State (StateEngine + reducers)

Folds the stream into "current truth" via pure reducers (state, entry) -> state. The default reducer ledger_projection produces: identity (project), decisions in effect, open_items, latest, contributors (authorship aggregated by provider/model), kinds, and superseded.

Integrity gate [gap #G3]: reduce() calls entry.verify() on every entry before folding it. If the stored id doesn't match its content (a tampered .db), the entry is dropped from the truth (never served as a decision) and its id is collected in integrity_broken, which the assembler surfaces as an explicit warning. (ts stays out of the hash by Law #3, so clock tampering is not covered here — a declared product limit; use git as the external notary.)

Status is a projection, not an FSM [#0002]: no execution state machine; the "state" is what the reducers compute. Custom reducers can be registered.

Supersession [#0018, gap #G8]: a correction supersedes its parents — they leave the current truth. Crucially, superseded is not a monotonically-growing set; it's the fixpoint of the correction graph (_effective_superseded). A correction only supersedes while it is itself active; if a later correction supersedes it, its parents come back. The reducer iterates to a stable set, so reverting a reversal restores the original. Append-only throughout: closing/reverting is a new entry, never an edit.

4. Layer 3 — Recall (SemanticRecall + Embedder)

Embedder is pluggable [#0015]: one model per index. Default LexicalEmbedder — sparse term-frequency, exact cosine, deterministic, no dependency. (We tried hashing first; the test caught a bucket collision producing false relevance → sparse TF gives an exact 0 without a shared token.) SemanticRecall.search(query, k) returns top-k anchored to the event (Law #1) — the vector is only an index; getting the match wrong does not turn into a hallucination. A dense semantic embedder (SentenceTransformerEmbedder, #0029) plugs in behind the same interface — opt-in via the [embeddings] extra; select it with LIFELINE_EMBEDDER=dense (or make_embedder(...)). The default stays lexical (zero-dependency).

Abstention floor [gap #G5]: search() drops any hit below embedder.min_score, so a weak match returns nothing rather than noise. The lexical embedder uses an exact 0.0 floor (no shared token → exactly 0, which already abstains honestly); the dense embedder defaults to 0.3 (LIFELINE_RECALL_MIN_SCORE overrides). Recall would rather say nothing than anchor to a coincidence.

5. Assembly (ContextAssembler)

Renders markdown [#0010] (more token-efficient than JSON/YAML for LLMs) within a budget. Budget priority: header + "Relevant" (if there is a query) + "Open" + "Recent" are always included; the decisions fill the rest, keeping the most recent ones and omitting the old ones with an explicit marker (Law #6 — truncation is never silent). Superseded items appear marked [closed/reverted] under "Recent".

6. Projection and the store-is-source cutover [#0014, #0020, #0022]

render_ledger_markdown(store) generates LIFELINE.md from the ledger. ingest_markdown does the reverse path. The two form a fixed point (proven): store → markdown → store reproduces the same ids, and the 2nd render is byte-identical.

This unified the two hashing schemes that coexisted (markdown chain vs Entry id) into the content-addressed id. Git artifact decision [#0022]: what gets committed is the text (LIFELINE.md, diffable/mergeable in PRs); the .lifeline/*.db is the runtime store but is gitignored and rebuildable (lifeline migrate). No contradiction with §2: the .db is the local runtime source of truth, while the committed text is the durable, shareable form — text↔store are losslessly interconvertible, so the real source of truth is the set of content-addressed entries, materialized as both. (In cloud mode the Supabase store is the source; the text follows.)

7. MCP surface (mcp_server.py) [Law #7]

Closes the loop without a human. Resource lifeline://project/context (read) + tools lifeline_append, lifeline_recontextualize, lifeline_recall (write/recall). DB via the env LIFELINE_DB (default .lifeline/ledger.db, relative to the cwd → each project its own line). The same surface runs locally (stdio) and remotely (HTTP/SSE, lifeline-mcp-remote, with an optional OAuth Resource Server) — see docs/MCP_REMOTE.md.

Invariants (summary)

  1. No memory without an immutable anchor. 2. Append-only. 3. Deterministic content-addressing.
  2. Provider-agnostic storage; per-provider delivery. 5. Why > what. 6. Budget first-class (explicit truncation). 7. MCP-native.
Lifeline — MIT licensed · Edit on GitHub ← Back to the lifeline