This document describes fractal-engine storage authority, payload refs, folded views, heads, lineage, and failure semantics. It is written for public maintainers and contributors.
The v1 runtime separates durable truth from projections:
flowchart LR
SESSION["Session id"] --> EVENTS["SQLite event rows"]
EVENTS --> VIEW["Folded view"]
EVENTS --> HEADS["Immutable heads"]
EVENTS --> EDGES["Lineage edges"]
LARGE["Large values"] --> BLOBS["BlobStore bytes"]
BLOBS --> REFS["Payload refs"]
REFS --> EVENTS
HEADS --> CURRENT["current-head pointer"]
CURRENT --> RESUME["resume-session!"]
CURRENT --> ATTACH["attach-rlm source selection"]
:store :sqlite is selected.MemoryStore implements the same SessionStore contract for in-process and
test use.current-head is the authoritative continuation pointer for resume and
attach semantics.The durable SQLite layout is intentionally small: one database for session and
event rows, plus a sharded BlobStore for payload bytes. The path passed as
:store/dir chooses where those physical files live; it does not participate in
semantic identity.
All runtime storage goes through fractal.engine.store/SessionStore:
(defprotocol SessionStore
(create-session! [store session-map])
(append-event! [store sid event])
(append-events! [store sid events])
(publish-head! [store sid head])
(append-lineage-edge! [store sid edge])
(intern-payload! [store value opts])
(read-payload* [store ref])
(current-view [store sid])
(read-state [store sid])
(peek-next-id [store sid counter-key])
(notify-transient [store sid item])
(subscribe! [store sid callback])
(events-since [store sid event-id]))
Important contract points:
create-session! is idempotent. Re-creating an existing session returns the
existing slot/handle and does not clear state.create-session! can reopen a persisted session by folding its durable
event log into a fresh in-process view.append-event! stamps event id, timestamp, and any entity id assigned by the
event type.append-events! stamps consecutive ids and commits the batch atomically.publish-head! computes immutable head content under the same per-session
store lock as event appends.current-view is the strong read-your-writes runtime projection.read-state is a relaxed external-reader hook; the current implementations
return the folded cache.The view shape from empty-view is:
{:session nil
:messages []
:turns []
:steps []
:evals []
:heads []
:current-head nil
:edges []
:counters {:event 0 :message 0 :turn 0 :step 0 :eval 0}
:vars-ref nil
:compact-from-event-id nil
:error nil
:events []}
The view is not stored as a competing authority. It is rebuilt by applying
apply-event over the event stream. SQLite recovery depends on this property:
closing and reopening a store reconstructs the same session view by folding the
durable log.
Projection fields have clear roles:
:events is the ordered event stream folded so far.:counters tracks the max assigned ids for events and entities.:heads stores immutable head records.:current-head stores only the selected head id.:edges stores folded lineage edges.:vars-ref is the latest vars snapshot projection/fallback.:compact-from-event-id controls transcript pruning.Every durable state change is an event:
| Event type | Fold effect |
|---|---|
:session/started | Sets the session entity. |
:turn/started | Appends a turn entity. |
:turn/put | Replaces the matching turn by :turn/id. |
:step/started | Appends a step entity. |
:step/put | Replaces the matching step by :step/id. |
:message/appended | Appends a transcript message. |
:eval/added | Appends an eval record. |
:session/vars-snapshotted | Updates :vars-ref. |
:session/compacted | Updates :vars-ref, updates the compaction boundary, and appends one compact message. |
:head/published | Adds a head and sets :current-head. |
:lineage/edge-added | Adds a lineage edge. |
:session/stop-requested | Sets session status to :stop-requested. |
:session/stopped | Sets session status to :stopped. |
:session/error | Stores :error and sets session status to :error. |
All stamped durable events are appended to :events. .../put events are value
replacement events, not patches. Events carry results, never recipes; folding
must not replay provider calls or evals.
Transient live items such as :delta/token and :subscribe/gap are not durable
events. They do not receive :event/id and do not fold into the view.
Values are stored inline when they are small enough and content-addressed when they are large. The current inline threshold is 512 canonical bytes.
A payload ref is an opaque tagged map:
{:fractal/ref :payload
:payload/id "sha256:<hex>"
:payload/kind :message | :eval-result | :final | :vars | :code | :request
:payload/size 1234}
The hashing basis is fractal.engine.payload/canonical-bytes: deterministic
EDN bytes with sorted maps/sets and stable printing. The same value produces the
same sha256: id across MemoryStore and BlobStore.
Payload rules:
read-payload passes non-ref values through unchanged and dereferences tagged
refs.verify-no-dangling-refs.A head is an immutable content-addressed continuation boundary:
{:head/id "sha256:..."
:head/version 1
:head/session "s-..."
:head/basis nil | "sha256:..."
:head/event-range [from-event-id to-event-id]
:head/kind :turn-final | :compaction
:head/turn-id 1 | nil
:head/vars-ref <inline-value-or-payload-ref>
:head/final-ref <inline-value-or-payload-ref> | nil
:head/compact-from-event-id nil | 42}
publish-head! derives:
:head/basis from the folded current head before publication:head/event-range from the previous current head range to the requested
boundary event:head/id from the canonical content of the head without :head/idPublishing a head appends a :head/published event and sets :current-head.
The current head pointer is the mutable authority. The head content itself is
immutable.
Successful FINAL turn:
:session/vars-snapshotted.:turn/put.:turn-final head whose event range ends at the final :turn/put.Compaction:
:session/compacted event.:compaction head whose event range ends at the compaction event.Non-final terminal outcomes do not publish heads. They still append terminal turn state so the durable event stream and view represent the failure.
resume-session! for SQLite:
:head/vars-ref.:vars-ref projection only if no current head is
available.This priority is deliberate. :vars-ref is useful projection state, but it is
not a second mutable restore authority. If a later vars snapshot projection
exists after a published head, resume still prefers the current head.
attach-rlm is derivation, not in-place resume.
Source resolution:
current-head.:head/id selects that immutable head.Execution:
:head/vars-ref.FINAL.:derivation lineage edge.The source session is not advanced. The source head is immutable and remains a historical continuation boundary.
Lineage edges are durable, content-addressed facts:
{:edge/id "sha256:..."
:edge/version 1
:edge/type :invocation | :derivation
:edge/from-session "s-..."
:edge/to-session "s-..."
:edge/from-head "sha256:..."
:edge/to-head "sha256:..."
...}
rlm and map-rlm children record :invocation edges from the parent current
head to the child current head. attach-rlm records a :derivation edge from
the selected source head to the attached child's current head.
Edges fold into the caller/parent view under :edges. They should be treated as
durable graph facts, not reconstructed from directory names, child session ids,
or other physical layout details.
Compaction is a durable state transition, not a destructive rewrite.
kept-messages derives the transcript from the event stream:
:events.:message/appended and
:session/compacted.:compact-from-event-id.This is why the compact continuation frame survives pruning. The projection is
computed over events instead of mutating the historical :messages vector.
SQLite persist-before-fold:
Head publication:
:head/expected-basis is checked against the folded current head.current-head over an unexpected basis.Payload refs:
Live delivery:
events-since.:subscribe/gap marker tells subscribers to recover from durable events.Turn outcomes:
:final publishes a head.:timeout, :budget-exceeded, :error, and stopped-session results settle
the turn without publishing a new head.When changing storage, heads, compaction, resume, or recursion:
apply-event pure and deterministic.current-view read-your-writes for runtime code.current-head the sole mutable restore pointer.:vars-ref as projection/fallback state.The current storage model is exercised by:
src/fractal/engine/store.cljsrc/fractal/engine/store/memory.cljsrc/fractal/engine/store/sqlite.cljsrc/fractal/engine/store/blobstore.cljsrc/fractal/engine/payload.cljsrc/fractal/engine/payload_io.cljsrc/fractal/engine/session.cljsrc/fractal/engine/session_loop.cljsrc/fractal/engine/compaction.cljsrc/fractal/engine/recursion.cljtest/fractal/engine/store_test.cljtest/fractal/engine/store_contract_test.cljtest/fractal/engine/store_sqlite_test.cljtest/fractal/engine/blobstore_test.cljtest/fractal/engine/payload_test.cljtest/fractal/engine/payload_io_test.cljtest/fractal/engine/session_test.cljtest/fractal/engine/Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |