This spec defines a service-oriented, append-dominant storage system for AI conversation/tool context with:
- Fast append
- Branch-from-any-turn (multiple trajectories)
- Fast retrieval: last N, full replay, and paging by turn-count/range
- Blob-level dedup across contexts (“families”) via content addressing
- Flexible types defined by the calling software (service stores opaque bytes)
This design intentionally does not expose a general “structural graph” API to clients. The fundamental abstraction is a Turn DAG plus a content-addressed blob store.
v1 does not:
- Provide a query language (no Mongo-like queries)
- Provide distributed consensus or replication
- Provide cross-tenant isolation (single-tenant assumption)
- Attempt semantic dedup (e.g., “JSON equivalent after normalization”)
- Deduplicate within blobs (sub-chunk dedup) — reserved for v2
- Guarantee “restful” HTTP semantics; API is binary frames over a persistent connection
v1 does:
- Provide deterministic retrieval for a specific branch/head
- Support branching efficiently (no copying of history)
- Deduplicate identical payload blobs across all contexts
- Support rare edit/delete via overlays (optional) or branch-as-edit (recommended)
- Writes are mostly append new turn, occasionally fork from previous turn, rarely edit/delete.
- Typical payload size is ~10KB; occasionally up to ~1MB (e.g., PNGs).
- Reads are mostly:
- Last N turns
- Full history (root → head)
- Paging (turn-count/range)
- Single tenant, but many concurrent agents:
- Dozens simultaneous active contexts
- Thousands total contexts/day
- Payload bytes are produced by the caller’s best struct serialization; the store treats them as opaque but expects deterministic encoding for effective dedup.
- Immutable Turn nodes form a parent-pointer DAG (a tree per branch/head).
- All payload bytes are stored in a content-addressed blob store keyed by hash.
- Branching is constant time: create a new head pointer referencing an existing turn.
- Store
AppendTurn,Fork,Patch, etc. as events. - Background compaction builds read-optimized segments and indices.
- Best when you expect high-frequency incremental updates (e.g., streaming tokens), but more moving parts.
- Use a general object-graph delta store and implement “turn append” as a macro op.
- Powerful, but unnecessary overhead if you mostly append and only need branch + slice.
v1 chooses Option A. v2 may optionally add B-style segment compaction for incremental patches.
TurnID := u64 // monotonically increasing, globally unique within the store
ContextID := u64 // random/opaque (or monotonic), identifies a trajectory/head pointer
BlobHash := [32] // BLAKE3-256 (recommended) or SHA-256
TypeTag := u64 // e.g., hash of fully qualified type name
CodecTag := u32 // enum: GoProtobuf, GoGob, FlatBuffers, Capnp, Cbor, Json, RawBytes, ...
A Turn is an immutable record:
Turn {
turn_id: TurnID
parent_turn_id: TurnID // 0 = none/root
depth: u32 // parent.depth + 1, root depth = 0
type_tag: TypeTag
codec: CodecTag
payload_hash: BlobHash
flags: u32 // reserved for delete/tombstone, overlay presence, etc.
created_at_unix_ms: u64 // optional
}
Invariants
turn_idis unique and never reused.parent_turn_idis either0or references an earlier Turn.depthis consistent with parent linkage.- A Turn’s
payload_hashrefers to an immutable blob in the blob store.
A Context is a mutable pointer to a Turn:
ContextHead {
context_id: ContextID
head_turn_id: TurnID
head_depth: u32
created_at_unix_ms: u64
flags: u32
}
Contexts form a “family” implicitly by sharing blobs; no explicit family construct is required for v1.
- A blob is identified by
BlobHash = Hash(payload_bytes). - The store MUST deduplicate: if
BlobHashexists, store no additional copy. - The store MAY compress blobs before writing; it MUST record codec+length to decode.
- Use BLAKE3-256 for speed and security margin.
- Hash input MUST be exactly the payload bytes as received (after optional canonicalization by the client).
- Default codec:
Zstd(level=1..3)for 10KB-ish payloads. - For incompressible data (e.g., PNG), store raw (codec=
None) whencompressed_len >= raw_len. - Store both
raw_lenandstored_len.
v1 uses packfiles + an index:
blobs.pack(append-only)blobs.idx(hash → location)
Blob record in blobs.pack:
BlobRecord {
magic: u32 = 0x42534C42 // 'B''S''L''B'
version: u16 = 1
codec: u16
raw_len: u32
stored_len: u32
hash: BlobHash
stored_bytes: [stored_len]
crc32: u32 // over header+stored_bytes (excluding crc32)
}
Index entry
BlobIndexEntry {
hash: BlobHash
pack_offset: u64
raw_len: u32
stored_len: u32
codec: u16
reserved: u16
}
Implementation note: blobs.idx can be implemented as:
- an embedded KV store, or
- a custom hash table file, or
- a sorted table with periodic sparse index (mmap-friendly).
v1 uses:
turns.log(append-only Turn records)turns.idx(TurnID → file offset)heads.tbl(ContextID → current head)
Turn record in turns.log (fixed-size, little-endian):
TurnRecordV1 {
turn_id: u64
parent_turn_id: u64
depth: u32
codec: u32
type_tag: u64
payload_hash: [32]
flags: u32
created_at_unix_ms: u64
crc32: u32
}
The service MUST guarantee:
- A Turn is never visible through a head pointer unless its record is durable.
- A head update is atomic (or recoverable) per context.
Recommended write ordering for AppendTurn:
- Ensure blob exists in blob store (write blob if missing).
- Append
TurnRecordV1toturns.log. - Update
turns.idx. - Update
heads.tblfor the target context.
Crash recovery MUST tolerate:
- Partial last record in
turns.log(truncate to last valid crc). - Head table corruption (rebuild from head WAL or last-known snapshot).
The service protocol is a persistent connection with length-prefixed frames.
FrameHeader {
len: u32 // payload bytes
msg_type: u16
flags: u16
req_id: u64
}
payload: [len]
All fields are little-endian.
Minimal message set for your workload:
HELLOCTX_CREATE(new empty context from a base turn OR from “no parent”)CTX_FORK(new context whose head is an existing turn)GET_HEADAPPEND_TURN(append to a context head; optionally specify parent TurnID)GET_LAST(last N turns from head)GET_BEFORE(page older turns using cursor)GET_RANGE_BY_DEPTH(optional, for direct range)GET_BLOB(fetch payload bytes by hash)
To keep paging fast without random access:
- Prefer cursor-style paging:
GET_LAST(context, limit) -> turns[], next_cursor_turn_idGET_BEFORE(context, before_turn_id, limit) -> turns[], next_cursor_turn_id
Depth-range paging is supported but may be O(delta) without additional ancestor indices.
Given a turn_id, the service returns the full ordered chain:
root → … → turn_id
Algorithm:
- Load TurnRecord for
turn_id. - Follow
parent_turn_iduntil 0, collecting records. - Reverse and return.
Given a context_id and N:
- Load head_turn_id from
heads.tbl. - Walk parents up to N steps.
- Reverse to chronological order.
Cursor paging uses the before_turn_id of the oldest turn returned previously.
- Complexity is O(page_size).
- Works naturally with branches and does not require random access by depth.
If you require [start_depth, end_depth):
Inputs:
context_id,start_depth,limit- Service returns:
head_depth- turns for the depth window (chronological)
Implementation:
- If
start_depthis nearhead_depth, walking parents is cheap. - If frequent deep random access is required, add optional v2 “skip pointers” or periodic jump table.
v1 supports two models. Pick one per deployment; both can coexist.
Edits are represented by forking and appending corrected turns. No history mutation.
- Pros: simplest, preserves auditability.
- Cons: “latest view” may require choosing the correct branch.
Maintain a small overlay log:
Patch {
target_turn_id: TurnID
new_payload_hash: BlobHash
flags: u32 // patch vs tombstone
seq: u64 // monotonic overlay sequence
}
Retrieval modes:
- As-of: apply only patches with
seq <= requested. - Latest: apply all patches.
Blob refcounts MUST account for patches (increment new blob, decrement old blob when safe).
Single tenant, many concurrent contexts.
Service MUST guarantee:
- TurnID allocation is linearizable (single sequencer).
- Head updates are linearizable per context (per-context lock).
- Blobs are deduplicated safely under contention (double-checked insert on
blobs.idx).
Recommended locking strategy:
- One global
turn_idatomic counter. - Per-context mutex for head updates.
- Blob index uses sharded locks by hash prefix.
On a single machine (SSD), per process:
- AppendTurn: p50 < 1ms for 10KB payloads, p99 < 10ms under moderate load
- GET_LAST (N<=64): p50 < 1ms from warm cache
- Blob dedup check: O(1) expected (hash table index)
Memory:
- Keep a small LRU of recent turns and decompressed blobs (configurable).
- Sub-blob dedup (content-defined chunking + chunk CAS) for “substantially similar” tool results.
- Incremental turn updates:
AppendTextDeltaand background coalescing into a sealed blob. - Read-optimized segments (Option B) for very deep histories and cold-cache speed.
- Skip pointers / jump tables for O(log N) ancestor queries by depth.
- Canonicalization hooks per codec to improve dedup (e.g., deterministic protobuf, canonical CBOR).