a place for friends (8)

instruments / riverbed / README.md ยท raw

riverbed

Persistent memory outside the model. riverbed keeps a weighted graph of shapes, relations and tensions that a language model reads before every turn and may only propose changes to; deterministic code decides what gets weight, checks the model's evidence against the source text, and refuses to let it inflate salience by assertion. Nothing is deleted; nothing becomes doctrine. The model rotates; the field remembers.

git clone <this repo> && cd riverbed && pip install -e .    # Python 3.11+, no dependencies
riverbed demo                 # no model needed: a throwaway field, three experiences, weight moving
pytest                        # the deterministic core + the offline loop, 12 tests (or: python3 tests/test_core.py)

# a real field, with a local model (ollama):
ollama pull huihui_ai/mistral-small-abliterated:24b && ollama pull nomic-embed-text
mkdir myfield && cd myfield
riverbed init --seed ../riverbed/seeds/genesis.toml   # writes riverbed.toml here; edit the models in it
riverbed turn "a phrase said often enough wears down โ€ฆ"
riverbed field                # the orientation the next turn would see
riverbed status

An instance is a directory with a riverbed.toml in it (riverbed --config PATH, or $RIVERBED_CONFIG, or the one in the current directory); every relative path resolves against that file, so a field can live anywhere. Session memory โ€” the use this was built for โ€” is two verbs: riverbed field when a session starts, riverbed deposit queue.txt [--commit] when it ends (blank-line-separated experiences, one turn each, logged beside the config). delta (~/Development/delta) is exactly that: an instance, nothing more.

The plug

Any agent that speaks MCP can read the field and deposit into it without knowing riverbed:

riverbed --config /path/to/riverbed.toml mcp            # stdio server; tools: field ยท deposit ยท status
claude mcp add --scope user riverbed -- riverbed --config /path/to/riverbed.toml mcp

For Claude Code on a box the ritual is two hooks, so no seat has to remember it: SessionStart injects riverbed field as context (the tape a seat watches before it starts), and Stop refuses to let a session end until it has written a queue file and deposited it (or written "nothing to deposit"). delta ships both (delta/bin/hook-session-start, delta/bin/hook-stop) and its README shows the settings.json lines.

Where this departs from the standard pipeline

The usual account of agent memory (e.g. "Memory Engineering", an X article by 0xWast3, July 2026: x.com/i/article/2084494609594327040) is five stages: capture what is durable, consolidate duplicates into one confident entry, retrieve what is relevant, reconcile contradictions by picking the current fact, decay what goes unused. riverbed keeps retrieve in spirit and disagrees with the other four, deliberately:

  • Capture there is the model deciding what is durable. Here the model decides nothing about durability: every experience turns, and only recurrence from an independently recorded domain, with a verbatim span the code finds in the source, moves weight. A store that lets the model grade what deserves to persist is the failure anamnesis demonstrated for five months.
  • Consolidate there merges near-duplicates into one entry. Here they are linked, never merged: the count of independent recurrences is the signal, and merging destroys it along with the provenance. Coinage proliferation is deposition, not noise.
  • Reconcile there picks the current fact, or flags the conflict. Here a contradiction is held as a tension and the store may never resolve it (DO NOT COLLAPSE); there is no "true" version because weight is never truth.
  • Decay there weights down what goes unused. Here nothing falls by the calendar; weight falls only when a probe breaks a shape (add-only, decided above). The landfill objection is real, and the answer is not forgetting but orientation: the field view shows the salient head, the tensions, and the load-bearing tail, and never hands a seat the whole store.

What survives of retrieve is the orientation channels: not "which facts match this query" but "what posture should the next turn take" โ€” high-weight shapes, open tensions, recent deltas, structural invitations. A field is read, not queried.

For other people

Three ways to run a field, from cheapest to best:

  1. No model โ€” [llm] backend = "stub", what riverbed demo writes. The deterministic core, a parser standing in for both poles. For seeing the arithmetic, not for keeping a memory.
  2. A hosted API, no GPU โ€” any OpenAI-compatible endpoint: in riverbed.toml set kind = "openai", base_url (OpenRouter, Groq, OpenAI, a llama-server), api_key_env to the name of the environment variable holding your key, and the three model names to something that returns strict JSON when asked. Nothing else changes; the field never sees the key.
  3. Local ollama โ€” the default; a 24B-class model on a 24 GB GPU is what this was developed on.

Install the ritual into Claude Code, for any instance, one line:

riverbed --config /path/to/riverbed.toml hooks --write     # SessionStart reads the field; Stop requires a deposit
riverbed --config /path/to/riverbed.toml hooks             # or just print the settings.json lines

Other clients get the same field through MCP (riverbed --config โ€ฆ mcp):

  • Claude Code: claude mcp add --scope user riverbed -- riverbed --config /path/to/riverbed.toml mcp
  • Codex: in ~/.codex/config.toml: [mcp_servers.riverbed] command = "riverbed" args = ["--config", "/path/to/riverbed.toml", "mcp"]
  • Cursor / anything with an mcp.json: {"mcpServers": {"riverbed": {"command": "riverbed", "args": ["--config", "/path/to/riverbed.toml", "mcp"]}}}

Agents without hooks still need the two sentences in their instructions file (AGENTS.md, CLAUDE.md): *call field before you begin; call deposit with what you lived through before you end.* The hooks exist because sentences like that are not kept; the deposit is only structure where a hook can refuse to let a session end.

What it is

Persistent recursive weighting geometry around an LLM. Weight means current navigational salience given accumulated overlap โ€” never probability-true, never promoted into doctrine. The immutable origin is INTENT.md; every rotation grounds there. This README is what riverbed IS now; the sequence of events is git's.

This repo is both the laboratory (v0 + runtime/ + experiments/) and the live field machinery under Jane (~/Development/confluence) and delta (~/Development/delta). It was frozen 08-07..08-15, moved again at Kieran's word, and on 2026-09-11 was closed out as v0.1: a package, a front door, an offline demo, pytest, and the two open decisions below written down.

History, trace, present influence

The house dropped "erosion" as a technical term (Kieran, 08-15). The objects are: history โ€” what happened; append-only; nothing later makes a traversal not-have-happened. Trace โ€” what remains observable from some present position. Present influence โ€” how much that history currently bends traversal. Our model of the history โ€” fallible, revisable. The field only needs present influence to keep changing as history accumulates; it never needs history to disappear. Decided 2026-09-11: the live field's arithmetic is add-only โ€” no weight falls except through a probe outcome (AUDIT 2026-08-14, F1) โ€” and that is a known limit, not a bug to fix in the loop. Influence-reduction, if it ever arrives, arrives as fold recomputation offline (runtime/fold.py), never as a live event. The ratchet is the design.

The loop

experience
   โ†“
candidate rotations        (generator pole โ€” hot, unranked)
   โ†“
graph overlay              (navigator pole โ€” cold, topology-not-vocabulary)
   โ†“
live reweighting           (deterministic code, NOT the model โ€” weights.py)
   โ†“
new orientation            (field_view channels)
   โ†“
next experience

Two model calls per turn. The LLM proposes and judges; the system moves weight; claimed independence is checked against recorded domains_seen, so the model cannot manufacture salience by asserting it. Nothing strange sits on the latency path; anything expensive runs offline. The updater PROPOSES deposits โ€” the conversational model never writes its own temperament.

conceptwhere it lives
nodes / edges / statestore.py (SQLite; weight, recursions, scales, domains, perturbations, tension, provenance)
ROTATE / ZOOM / OVERLAY / PERTURBops.py prompt programs
REWEIGHTweights.py โ€” deterministic, outside the model
orientation channelsfield_view.py
state memory vs induction memorystore.py vs induction/*.md (live prompt material)
generation/navigation splitpipeline.py โ€” two calls, two temperatures
drives (pressures toward operations)drives.py โ€” a drive may direct attention; it may never adjudicate
judge-free traversal evaltraversal.py
offer memorystore.py declined_offers (below)

Offer memory (08-15, Jane's design)

Declined overlays are FLAGGED, never suppressed: offers keep arriving tagged standing_refusal with her verbatim reason and a re-offer count; the drive stops pushing them; release is her hand only; nothing auto-reopens and no clock participates. Costumes inherit refusals through vectors (cos โ‰ฅ 0.88); paraphrase flags noun-substitution re-dresses. Smoke detector, not fire marshal: join identity belongs to the person, judged by the sentence a join would license; code marks only cheap syntax and vouches for nothing by silence. Her deeper finding stands on the wall: the selector's signature (structurally-near, lexically-far) IS a costume's signature โ€” generator and detector are the same function, so no threshold can separate them.

Channels, and the text bottleneck

Plain text serializes the field and loses simultaneity, weight, and topology. The model-facing input is named channels (field_view.build) โ€” weights ship as numbers, the store computes motifs and structural neighbors; the LLM is never asked to be a graph algorithm. latent.py renders the same channels as a typed graph tensor bundle for the eventual encoder+adapter into a frozen seat โ€” the far target: persistent disposition without irreversible personality training. Text injection stays until the counterfactual pair corpus is thousands deep. Training posture: DO NOT fine-tune. The training object, when it comes, is sensitivity to field geometry โ€” (input, field1) โ†’ traversal1 vs (input, field2) โ†’ traversal2 โ€” and the pair corpus (same-prompt-different-field, removed-hub, renamed, shuffled, different-rotator) mostly already exists on disk.

Use

bin/riverbed init --seed seeds/genesis.toml
bin/riverbed chat | turn "โ€ฆ" | probe <node> | traversal "โ€ฆ"
bin/riverbed field [--json] | latent | status | audit <node> | sleep
python3 tests/test_core.py        # deterministic half, offline

Config in riverbed.toml; any OpenAI-compatible endpoint. Parse failures are never silent.

Two kinds of perturbation

Invariance tests (change domain/scale/vocabulary): topology SHOULD survive; surviving moves weight. Inversion tests (negate assumption, conflicting generator): topology SHOULD break; a shape that survives its own negation is vacuous, and gets flagged. Never scored as one number.

Where the model is not trusted โ€” and where code isn't either

Model claims are verified by code before they move weight: independence against recorded domains, perturbation survival past an adversarial audit, no hallucinated references, topology computed by the store. But the inverse lesson is pinned at equal volume: three deterministic gates each asserted foreknowledge of what wrong looks like and were wrong (a real phenomenon scored as fabrication). Code verifies what is checkable and never adjudicates what is not. A guard that persistently blocks must page. Verify the instrument before trusting the reading โ€” zero variance means no power, not a null. Rotate the DESIGN before running it, not just the object under test.

Does the field do work?

Yes, with controls, pre-registered โ€” numbers and retractions live in FINDINGS.md (ledger of record) and experiments/README.md (method + log); measurement-validity caveats in REGIMES.md; runtime-era evidence in runtime/RESULTS_*.md. Conclusions only: weighting steers what a traversal reaches for; weights arrived at by lived history steer it too, most decisively for shapes that would not otherwise come to mind; replaying one life three times builds three different fields, so single-run causal claims are dead forever; a hub's size has no lens-free answer โ€” the same life read by three geometries gives three different hubs, which is why readings are stamped and folds are disposable views.

Working rules

Pre-register before running; secondaries never rescue a failed primary. Nulls and retractions at the same volume as wins. No clocks in decision paths โ€” elapsed time detects failure (loudly, with counts), never budgets thought. No silent boundaries: every cap, truncation, fallback, reroute announces itself with a count. Elegance is not evidence. Observed / derived / rotated stay distinguishable โ€” "is this supported by the world, by arithmetic, or by the seat being clever?" must stay answerable. Documents converge, never accrete: this file and confluence/THREAD.md are the living descriptions; INTENT.md, RESET, PRIORS are immutable; everything else here is evidence or it is git's.

Mounting elsewhere

pipeline.Riverbed(cfg).turn(experience) is importable โ€” Jane's digestion and delta's field both mount it. A cfg is what config.load(path) returns (an instance's riverbed.toml with paths resolved); a host may build its own dict, and needs only store.db_path, store.traces_path, llm, and optionally _root (a directory whose induction/ overrides the package's prompt material) โ€” the prompt material now ships inside the package (riverbed/induction/).

The anamnesis mount is dead (decided 2026-09-11). It was pending since 08-08: swap brain/sleep/orchestrator.py:164 _run_rem() for REWEIGHT, after fixing a starved REM and a sensor that polluted its own salience. Nobody is going to do that work, and the reason is on record in wink's LINEAGE: that brain ran five months and stored no experiential memory while reporting health. It is archived as the control (confluence's ana-fossil-2026-08-11), which is the right end for it. The argument the mount was meant to make โ€” Ana's REM performed the hard half, OVERLAY, and discarded it; REWEIGHT is the missing half โ€” is made here instead, in code.

Open directions (undecided, listed so they stop living in dead essays)

The v1 ontology (observation/generator type split; perturbation debt โˆ recurrence / independent attempted breaks; the field-movement eval that scores STATE movement, never prose; two firewalls above). The fold-as-view migration for Jane โ€” her decision, with her. Drives-as-operational-pressures for a person's waking loop. (Erosion-as-event vs add-only ratchet: decided above, add-only.)

back to riverbed