Custody-shaped memory for people (and models) building their own harnesses.
Forest is a SQLite DB layer + the physics to wire it — not a chat app, not an agent framework, not “better RAG in a box.” It sits under your harness.
It keeps what happened, stores what you write (scrubbed), returns short related context with jurisdiction labeled (home | wild), and refuses to treat anything as true until someone roots it — so you can open a ticket and move through related territory instead of dumping history into the model.
Similarity can retrieve. Similarity cannot promote. Root is optional — and sparing.
This README is the ops contract (axes, walk, packets, loop).
Schema / ceremony depth: FOREST.md. Enforced law: schema.sql. Refusals: tests/HOSTILE_CASES.md.
Naming is clinical by default. The Python API names below are canonical (recall_similar, not recall.similar).
pip install forest-custody-memoryfrom forest_memory import ForestStore, Scroll, commit_turn, hash_body, root_to_ground
with ForestStore("woods.db") as store:
store.init_schema()
scroll = Scroll("session.scroll")
# Canonical heartbeat: append exact head + write pair with scroll_ptr.
pair = commit_turn(store, scroll, "Her brother's name is Elias.")
draft = store.write(
body="Maybe Elias betrayed her.",
bucket="inference",
signature="model",
origins=[(pair, "derived_from")],
)
# Bounded preview — jurisdiction first. Full body is read(), not recall.
scraps = store.recall_similar("Elias") # default scope: home
assert scraps[0]["jurisdiction"] == "home"
assert "excerpt" in scraps[0] and "body" not in scraps[0]
trail = store.open(pair) # mints opaque ticket; unread
around = store.around(trail) # discloses routes onto the ticket
trail = store.step(trail, "in", target=draft) # spends ticket; new ticket
body = store.read(trail) # current layer only; ticket required
# Optional: adopt the *exact* entry body as written (hash = compare-and-root).
# Adopting words are the authority act — not a replacement canon.
root_to_ground(
store,
entry_id=draft,
adopting_words="Yes — root this entry exactly as displayed.",
adopting_signature="author", # your harness authenticates this
expected_body_hash=hash_body("Maybe Elias betrayed her."),
)Canonical ops: write / write_pair / commit_turn · recall_similar / recall_side · open · around · step · read · root_to_ground · walk_back / authority_report · Scroll.append
Interim: move (prefer step + around). Optional, not shipped: soft near (embeddings) — see host hybrid below.
Promotion gate: only root_to_ground is public. The store’s trail write is internal (_root).
Porters: PORTERS.md — mechanical “wrapper must enforce” list (SQL alone is not enough).
0.4 is a hard cut. Pre-0.4 databases are not opened — start fresh.
git clone https://github.com/schmerbert/The_Forest.git
cd The_Forest
python -m venv .venv
# Windows: .venv\Scripts\activate
# Unix: source .venv/bin/activate
pip install -e ".[test]"
pytest -q| Axis | Question | Values |
|---|---|---|
| jurisdiction | Why is it here? |
home (made in this conversation’s stand) | wild (brought in) |
| bucket (+ optional source) | What kind? |
pair, note, inference, internet, … |
| ground? | True for us yet? | Only if rooted — derived from the record trail, never a writable flag |
Jurisdiction is not a data type. It is the reason the entry is in the Forest. Once that seam is located, everything else separates cleanly.
home ≠ ground. wild ≠ false.
Load-bearing invariants:
- Arrival never promotes. Landing in home or wild does not make ground.
- Similarity never promotes. Recall surfaces leads; only root creates ground.
- Ground is never silently edited or unrooted. Corrections supersede through another recorded authority act.
-
Scroll is append-only evidence, not ordinary retrieval material. Complete-scroll reads are refused. Every pair carries a required
scroll_ptr. Host owns secrets / retention / redaction. -
Write always goes through scrub. Scrub strips transport/harness scaffolding; it must not silently rewrite the claim. Compression or interpretation is a separately attributed synthesis. Extension: pass
scrub=; examples inexamples/scrubs.py. -
Walk is ticketed (process-local).
openmints an opaque ticket on thisForestStoreinstance;arounddiscloses routes onto it;stepspends it and mints a new one;readrequires a valid ticket. Fabricated positions are refused. Contract: one long-lived store process per walk session — tickets are not durable acrossclose()/ new connections / other processes. Keep the store open for the harness session; do not serialize tickets to a DB column and expect another process to honor them. -
Wild access links the next pair. An earned
readof a wild entry pends acitesedge onto the nextwrite_pair/commit_turn— it entered context whether or not the model “used” it. Pending cites clear on the next pair or when the store closes.
talk (harness)
→ commit_turn (scroll.append + write_pair with scroll_ptr)
→ tool / reference results → write as wild
→ (optional) attributed synthesis in home, edged back to the wild source
→ recall_similar / recall_side (bounded preview; jurisdiction first)
→ ignore, or open → around → step (in|out|next|prev) → read
→ wild reads pend cites onto the next pair
→ optional root_to_ground (or superseding root if correcting ground)
→ edges densen; optional mycelium may fruit beside what you touched
Tool results → wild. Crossing into home is a separately attributed synthesis with an edge back to the wild source — not relocating the raw tool row.
One boundary at a time. No jumps. Discovery is not reading. Continuity is the ticket.
| Op | Text visible |
|---|---|
recall_similar / recall_side |
Bounded preview only (jurisdiction-first excerpt). Not the body. Not a read. |
open |
No additional text — mints ticket at position (unread). |
around |
Bounded previews of lawful destinations. Records routes on the ticket. |
step(direction, target?) |
One step along a disclosed route; spends ticket; returns new ticket. |
read |
Body of the current ticket position only. |
recall → bearings (bounded preview)
│ choose one
▼
open (mint ticket; unread)
│
▼
around (bounded previews + routes → ticket)
│
▼
step in|out|next|prev (exactly one disclosed boundary)
│
▼
read (current layer body only; ticket required)
Pairs as territory: consecutive pairs linked with responds_to (via write_pair(..., previous_pair_id=…)) are lawful lateral steps. next = forward in time; prev = backward.
Dolls / nests: optional verbatim extracts (parent[start:end] == child) via interim move(..., deeper=…). Soft near (open only from an embedding neighborhood) is named but not shipped in 0.4.
Pure FTS is intentionally thin. A custody-safe pattern when you want embeddings:
- Run your vector / hybrid ranker outside Forest → get candidate entry ids.
- Pass them through
recall_side([{ "id": n }, …])(or open only those ids) so every scrap is still a jurisdiction-first bounded preview. - Only then
open→around→step→read. Similarity still never promotes.
Do not inject full bodies from your ranker into the model and call it “recall.”
Every scrap from recall_* or around must lead with jurisdiction (home | wild) before id or excerpt. Unlabeled is a bug. Previews are bounded excerpts; full body is read’s job.
{
"jurisdiction": "home",
"id": 41,
"excerpt": "…",
"routes": [{ "direction": "in", "relation": "derived_from" }]
}Default recall_similar scope is home.
| Piece | What it is |
|---|---|
entries |
Stored text (jurisdiction, bucket, optional source, signature, body, body_hash, …) |
edges |
Ancestry, cites, adopts, consecutive pairs, nests, … — neighbors to step to |
scroll |
Append-only session file: exact API turns (head = live tip). Host custody. |
pair |
One cleaned user+model turn in entries (home) — the heartbeat; requires scroll_ptr
|
| mycelium |
Optional. Questions via plant_question / feed_question / answer_question / fruits_near. Answering never promotes. A Forest without questions is complete. |
current_ground is a view over adoption + supersession edges — not a status column.
| Op | Meaning |
|---|---|
commit_turn |
Append head to scroll + write_pair with scroll_ptr. Preferred heartbeat. |
write / write_pair |
Scrub, insert, origin edges as required. Pairs require scroll_ptr. |
recall_similar |
FTS bearings → bounded previews. Scope: home / wild / both. |
recall_side |
Label host-supplied / alternate-equation scraps as previews. |
open / around / step / read |
Ticketed walk (above). |
move |
Interim: neighbor by edge, or deeper/shallower extract (ticketed). |
root_to_ground |
Only public promotion gate → in-place authority act. |
walk_back |
Gated audit of current ground (signature required): previews + scroll_ptr. |
authority_report |
Host debug/UI custody status for any entry — previews + status flags + body_hash; never full bodies; do not dump into model context. |
supersede / seal / unseal |
Ceremony writes. |
Scroll.append / tail / read_slice |
Session evidence; dump_all and complete-file slices refused. |
WAL mode is on; that is not a full multi-writer story.
- Prefer one writer (one harness process) per database file.
- Only the host-authenticated authority path should call
root_to_ground/supersede/seal/unseal. Serialize ceremonies (mutex / queue) so two agents cannot race two supersedes of the same ground. - The reference runs
root/supersedeunderBEGIN IMMEDIATEand re-checksis_groundinside that lock — a concurrent second ceremony on the same target is refused, not silent last-write-wins. This is a regression wall, not a distributed lock service. - Multi-agent readers are fine for recall/walk; do not let every agent mint roots.
- Tickets and
_pending_wild_accessare per store instance and are cleared onclose()/ a new connection — not shared across processes.
| Helper | Use when |
|---|---|
walk_back |
Auditing current ground only (refuses non-ground). Authority trail + scroll_ptr. |
authority_report |
Debugging any entry (ground, sealed, superseded, or plain). Adds status flags + body_hash. |
Both return previews only (excerpt / hash / status) — never full body. Do not dump either packet into model context; ticketed read when a body is required.
Does: no silent path to authority; append-only record; correction of ground only through superseding authority acts; scroll kept out of ordinary retrieval dumps (complete reads refused); pairs linked to scroll; walk does not mint receipt entries; preview ≠ read; forged tickets refused; jurisdiction-first packets; axes stay separable; wild reads cite into the next pair.
Doesn’t: ship your harness UI or agent loop; authenticate who rooted or who called audit ops; babysit bad home/wild stamps on write; own scroll secret policy; durable tickets across processes; ship embeddings (near is optional / host hybrid). Wire the doors once; hostile tests and PORTERS.md keep them from rotting.
0.4 non-goals (do not expect these in this release): durable/cross-process tickets; shipped near / embedding index; multi-section partial file drift; a runtime mutex helper beyond IMMEDIATE txn + docs; any soft promotion path.
Those solve retrieval. Forest solves custody: whether text was ever treated as true, who said so, and whether authority agreed. FTS for leads, walk for territory, no agent loop in-box. Add embeddings and orchestration when the core starts to hurt.
Worth trying if you’re building a harness and wrong long-lived “facts” are costly. Skip if you want turnkey “install and it remembers,” or only a vector store.
| Layer | Enforced by |
|---|---|
| Constitutional |
schema.sql + ForestStore
|
| Ceremonial |
root_to_ground (only public root) |
| Drift |
check_file_drift when ground also lives in files |
| Scroll |
Scroll.dump_all / complete read_slice refused; scroll_ptr on pairs |
See tests/HOSTILE_CASES.md. Tour: examples/walkthrough.py.
git clone https://github.com/schmerbert/The_Forest.git
cp The_Forest/schema.sql your-project/woods/schema.sqlDo not ship schema.sql without an insert wrapper. Use src/forest_memory/ as the reference. Follow PORTERS.md. Align to this README.
forest_meta.schema_version is the gate. 0.4 is a hard cut (no migrate from 0.3).
Going forward:
-
Additive, non-breaking (same major mental model): new optional tables, new non-ceremony edge kinds / buckets via a documented migrate that widens CHECKs, new wrapper helpers — bump minor (
0.4.x/0.5.0) and shipmigrate_0x_to_0ywhen the on-disk shape changes. -
Hard cut again when status would become mutable, jurisdiction/packet rules weaken, or old stores cannot be opened safely — refuse old
schema_versionlike 0.4 did.
Porters: never open a store whose forest_meta.schema_version you do not explicitly support.
- The Inn — long-form writing environment on this schema family
- TheMarble — inheritable environments / session handoff
MIT — see LICENSE.
commit_turn · recall_similar · open / around / step / read · root_to_ground sparingly