Continual learning as the only memory

mini-AGI

A byte-level model that takes a gradient step on everything it reads, chat included, into weight files where nothing learned can be named again.

LicenceMIT
SizeAbout 12,000 lines of Python in 37 files; runs/samples.txt is 179,243 lines covering 1,552 evaluations over 885.6 million characters
Activity30 commits on main by one author under two identities, 19 September – 1 October 2026
TestsNone committed; the forgetting probe and its four result files are committed, not run for this reading

Carries 0 of 7 rubric mechanisms. Most systems here carry none or one (43%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

mini-AGI is a byte-level language model that trains from scratch on one 8 GB card and never stops training, so its memory is its weights and every gradient step is a write. The careful part is capacity management — paging, growth, pruning — and a forgetting measurement whose raw per-arm scores are committed beside the probe that produced them. The weak part is everything a memory must answer for: nothing learned can be named, corrected or deleted, and the surface a person types into persists by default.

It is in this atlas because of what it does with a conversation. serve.py is a Flask chat server; when a reply finishes, remember(user_text, bot_text) marks the exchange up in the corpus's own <user>/<bot> form and feeds it to a LiveLearner, which takes an AdamW step for every 2,048 characters of stream and writes the weights directory back to disk every eight steps. The page calls that counter long-term memory, and the description is exact: there is no store beside the weights, no record of the exchange, and no path by which anything learned can later be named.

It is the stricter companion to Second Me, which keeps the documents in SQLite beside the model and can at least delete the row. Here the granularity problem the atlas sets out under weights as memory arrives in its pure form: the unit of identity is the whole model, a correction has nothing to name, and the only deletion in the system removes an expert because the router has not chosen it lately, which is a statement about capacity and not about truth.

The machinery around that store is memory-system machinery, built carefully and for stated reasons. There are three tiers with an LRU between them. Each forward uses the experts its whole text has voted for, so what was on the card changes what a forward costs and never what it computes. Training forwards add an exploration bonus whose admissions the prune clock refuses to count. Growth has five brakes, all wired and all engaged by the shipped config. And eviction deliberately refuses to read the gate, with the reason written down — the smallest gates belong to the busiest experts. Read as a capacity manager this is a good design. Read as a memory it has no scope, no trust state, no audit of what was read, no test, and no delete.

Two findings matter more than the rest, and both are about a write reaching disk when the operator has been told it will not. python3 train.py read ~/notes is documented as "a dry read - nothing kept", and cmd_read builds the pool with read_only at its default of False (train.py:659, :1812). Every chunk is an optimiser step over the experts on the card, a stepped expert leaves the card dirty, and the RAM cache writes dirty experts to disk on eviction. The README's replication recipe copies the weights first — "a COPY - paging marks experts dirty" — while its option table says that without --save, "weights/ is untouched". And serve.py learns and saves by default; --no-learn is the opt-out. The surface an engineer runs deliberately asks permission to persist; the surface into which a person types personal facts does not.

2. Mental Model

A memory here is created by being read. There is no extraction step, no candidate state, and no moment at which a claim becomes a belief — the gradient step is the transition, and it happens to every character equally.

What follows from that is the whole epistemic shape of the system. A fact the model was told is indistinguishable at rest from a fact it inferred, from a sentence it hallucinated in its own reply and then read back, and from the contents of a .env file that happened to be under a directory somebody pointed it at. minagi/live.py says so in its own header: "Learning from a conversation is learning partly from the model's own output". The file records that an earlier, denser variant cost about +0.65 nats on held-out against +0.02 for real documents of the same length. Whether the current regime still costs anything is, in its words, "UNKNOWN and would have to be measured". That is an honest statement of an unmeasured risk, and it is also the admission that the write path cannot tell the model's own invention from evidence.

Forgetting has two meanings here and neither is the one a memory system needs. The first is catastrophic forgetting, which the project measures and mitigates: after 1,048,576 consecutive characters of PG19, a domain the model had never read, running the trunk at 0.1x the experts' learning rate holds the loss on the eight known subjects to +0.1297 nats, against +1.2628 at the experts' rate. The second is expert pruning, which deletes a file from weights/experts/ when the router has not chosen it inside the survival window. Neither can be aimed. You cannot forget a sentence; you can only fail to route to a region of parameters for long enough that the region is deleted, taking whatever else it held.

The one real gate in the design is persistence, and it is placed inconsistently. In-process the model is mutated immediately and unconditionally; what --save controls is whether weights_store.save runs. In cmd_read that gate also holds off growth and pruning, for a stated reason — a dry read that pruned would leave the directory naming expert files that were no longer there. The forgetting check it prints at the end ("reading this cost ground on the held-out set - that is what forgetting looks like, measured rather than assumed") is printed before the save and does not gate it: the operator decided at the command line, before the evidence existed.

Diagram — Every path into mini-AGI's memory is a gradient step on raw text, and the only exits are an untargetable expert deletion and the survival of a region the router stopped choosing; the three points where a write reaches disk are marked, one of them on the path documented as keeping nothing.
Diagram source
%% caption: Every path into mini-AGI's memory is a gradient step on raw text, and the only exits are an untargetable expert deletion and the survival of a region the router stopped choosing; the three points where a write reaches disk are marked, one of them on the path documented as keeping nothing.
flowchart TD
  CHAT["Chat exchange<br/>serve.py remember()"] --> STREAM
  FILES["A directory of files<br/>train.py read"] --> STREAM
  CORPUS["A packed corpus<br/>train.py stream"] --> STREAM
  STREAM["One character stream<br/>buf trimmed to context"]
  STREAM --> STEP{"pending >= chunk<br/>2,048 characters"}
  STEP -- "no" --> STREAM
  STEP -- "yes" --> GRAD["AdamW step<br/>trunk at 0.1x, experts at 1x"]
  GRAD --> VRAM["Every expert on the card<br/>stepped in VRAM"]
  VRAM --> DIRTY["Stepped expert leaves the card<br/>tiers.put(dirty=True)"]
  DIRTY --> TRIM{"RAM cache over<br/>ram_cache = 96?"}
  TRIM -- "yes" --> DISK[("weights/experts/eNNNNN.npz<br/>written back")]
  TRIM -- "no" --> HOLD["Held in RAM,<br/>lost on exit"]
  GRAD --> GATE{"persistence gate"}
  GATE -- "serve.py: default ON<br/>every 8 steps" --> DISK
  GATE -- "read: --save only" --> DISK
  GATE -- "read: no --save" --> NOTE["Manifest untouched —<br/>but read_only was never passed"]
  NOTE -.-> DISK
  DISK --> PRUNE{"router has not chosen it<br/>for survival_chars?"}
  PRUNE -- "yes" --> DEL["os.remove — permanent,<br/>and not aimed at any fact"]
  PRUNE -- "no" --> KEEP["Kept. No other exit exists:<br/>no delete, no correction,<br/>no record of what was read"]

3. Architecture

Standing it up costs a CUDA card with 8 GB, Python 3.10 or newer, and four packages the README names in prose — torch numpy pyyaml matplotlib — because there is no dependency manifest in the tree at all. screen_repo.py reports NOTHING SCANNED, and reading the tree by hand explains it rather than contradicting it: 56 committed files, no requirements.txt, no pyproject.toml, no setup.py, no lockfile, no Makefile, no .github/workflows (only a FUNDING.yml), no .gitattributes and no .gitmodules. The reference environment is named in prose as torch 2.6.0+cu124 with numpy 1.24.4, which is a pin a reader can act on and not one a tool can check.

The store is a directory, and minagi/store.py opens by saying so: "The weights directory IS the model." It holds manifest.json, core.npz (embeddings, attention, norms, halting head), routers.npz (the gate and one router row per expert per call site), optim.npz, and experts/eNNNNN.npz — one file per expert, each carrying w1, w3, w2 and that expert's own Adam moments. Only experts are split out, because an expert is the unit that is paged, grown and pruned. Writes are atomic: every file is written to a .tmp and renamed. There is exactly one copy — store.py's best_val states the reason, that a snapshot "would double a figure that is already 236 MB and grows with the pool" — so the directory is both the live model and the only fallback.

Three tiers sit above it, in minagi/paged.py:87 (Tiers): disk holds the whole pool, an OrderedDict keeps ram_cache experts as CPU tensors with true LRU eviction, and resident slots on the card hold what the current forward admitted. Config defaults are 32 resident, 96 in RAM, growing from 64 experts with a 10 GB disk ceiling — about 397 experts at 25.2 MB each.

Two entry points write to it. serve.py (882 lines) is a Flask app on 127.0.0.1 with a server-sent-events chat endpoint. train.py (2,284 lines) carries three subcommands, of which read is the one the project runs, stream is the same mechanism pointed at a packed corpus, and ponder-probe is a diagnostic. minagi/ is the model: paged.py (1,097) is the pool, the selection rule and the paging, pool.py (873) the router and AutoGrow, recur.py (525) the recurrent block and the loaders, stream.py (470) the corpus reader, store.py (448) persistence, plasticity.py (358) the learning-rate controller, live.py (203) the live learner, ingest.py (151) the file walker. replication/ holds the forgetting probe and its results.

4. Essential Implementation Paths

A chat turn becomes memory. serve.py:359 api_chat builds the prompt from the message list, streams the reply under a process-wide LOCK, and on completion calls serve.py:317 remember(last_user, "".join(reply)). That calls minagi/live.py exchange_text to wrap the turn in <user>/<bot> markers — the same markup the chat corpus uses — and hands it to LiveLearner.feed. Only this turn goes in; the prompt already carried the conversation, so re-feeding it would re-read the earliest turns once per exchange.

A step. LiveLearner.feed appends character ids to self.buf, and every time pending reaches chunk it trims buf to the last context characters and calls _step. _step runs the forward on ids[:-1] predicting ids[1:], clips the gradient to 1.0, and steps an AdamW with two parameter groups — trunk at lr * trunk_lr_mult and pool at lr. The learner restores the trainer's moments from optim.npz when it starts, so its first save does not write a fresh optimiser's short history over the trainer's (live.py:82-94). The whole exchange is about ninety characters against a default chunk of 2,048, so roughly twenty-odd turns pass before the first optimiser step; serve.py reports pending and chunk on every reply because "silence there is indistinguishable from learning being broken."

Persistence. After each step remember checks learner.due_to_save() — unsaved >= save_every, default 8 — and calls LiveLearner.save, which delegates to store.save. For a paged pool that routes to _save_paged: pool.flush() parks every expert on the card that a step has changed and writes everything dirty, then core, routers and optimiser bundles are written and the manifest is rebuilt around whatever files exist. There is no validation gate on this path. The stream subcommand advances the directory only when held-out improves (train.py:308, if val < best); the live and read paths write on a timer.

A file read. train.py:581 cmd_read collects paths through minagi/ingest.py collect, which walks each argument, skips a fixed set of directory names and any directory whose name starts with ., and then decides text-or-binary by sampling 4,096 bytes and requiring valid UTF-8, no NUL byte and over 90% printable characters. Each file is read beginning to end, because "a document has an order." The loop is the same chunk-and-step mechanism, with growth and pruning attached every grow_every steps and checkpoints on a timer.

Deletion. paged.py:875 prune(step, survival, protect) walks the pool, keeps anything on the card, anything younger than survival and anything the router chose inside the trailing window, and for everything else calls os.remove on the expert file, drops it from RAM and from the dirty set, then reindexes the gate, the telemetry vectors and every call-site router. Files are named by uid, never by position, so pruning never renames anything.

5. Memory Data Model

There are two units and neither is a memory.

The expert is a file. manifest.json describes each one with id, file, params, bytes, moments and gate, and the pool carries parallel tensors beside it: use, age, born, gate_seen, last_seen, ever, admits and recent, plus uid. born is the step an expert joined; last_seen is the text count at which the router last chose it; recent is its share of recent training forwards, which sets its exploration bonus; ever is whether it has been on the card at all. All of these are transaction time in the bi-temporal sense — when the system did something — and there is no second axis anywhere, because there is no fact whose validity could begin before the model read it.

The character stream is the other unit, and it is not persisted. LiveLearner holds buf, trimmed to the context window, and it dies with the process; what survives is the delta the stream left in the weights. Nothing in the tree records which file, which conversation or which sentence produced any part of that delta.

The closest artifacts are two logs about capacity. runs/history.jsonl is appended by the stream subcommand with typed events — start, step, val, pruned, grew (carrying AutoGrow's refusal reason), saved, done. runs/expert_history.jsonl takes one telemetry row per checkpoint from train.py:964 log_history, dry reads included, and is not append-only: every read session first drops rows past the manifest's character count (train.py:918, _truncate_history), so a dry read's rows are erased by the next session. Neither log can answer what the model read, which is the question the append-only memory audit mark exists for, so it is withheld. runs/ is gitignored apart from samples.txt.

No scope key of any kind exists. A search for user_id, session_id, tenant, account, auth, login and principal across every .py in the tree returns four hits, all of them the English word account inside comments. The Flask app has no session, and STATE is a module-level dict holding one model.

6. Retrieval Mechanics

Retrieval here selects weights rather than text, and it is the most memory-shaped part of the system.

The text chooses. While a forward has room on the card, every character ranks the whole pool with its call site's router — one row per expert, so an expert on disk is scored exactly like one in VRAM — and requests its top 8, each request carrying the router's probability (pool.py:382-408). A forward's first pass adds those requests to a vote over its whole text since position 0, and admit gives the forward the most-voted experts until the 32 slots are full (paged.py:531). Every character then routes among the admitted experts only; a character whose request missed takes its best admitted expert instead.

Residency changes the cost, never the choice. admit never reads what is on the card. Residency decides only how many admitted experts must be copied in: a newcomer takes a slot holding nothing this forward admitted, empty first, then the one admitted longest ago. There is no displacement margin and no dwell window; stability comes from the vote accumulating over the whole text. The README reports 2.4 loads per training step over fifteen minutes of training, and a 140-character reply that loaded no expert after its prompt.

Writing is the same rule, one character at a time. Each character of a reply is a forward of its own through the attention cache, so it adds its requests to the vote the prompt and the reply so far have cast. A forward from position 0 begins a new text and advances the prune clock; continuing forwards do not, so writing a reply does not age the pool (paged.py:510-515). An expert nothing stepped is not written back when it leaves the card (paged.py:629), so a reply that loads experts writes none of them.

Training explores, and exploration buys no survival. In a forward that trains, each expert's score gets a bonus of explore_bias × e^(−recent / fair) wherever something is chosen, never in how much a chosen expert contributes (paged.py:487-508). The prune clock counts only what the router would have admitted without the bonus (paged.py:550-556). The shipped config sets explore_bias to 0.65; the README's prose gives 0.35 as the default, and the loader falls back to 0.0.

What none of this does is retrieve a claim. There is no query, no result set, no ranking over content, and nothing that could be asserted to stay out of one.

7. Write Mechanics

Writes block the agent and the lag is zero-to-never. remember runs inside the same LOCK the reply stream held, so the chat request does not complete until the step has been attempted. The model is different the instant the step lands, so there is no retrieval lag in the usual sense. There is also no guarantee the write survives: the first twenty-odd exchanges take no step at all, and a process killed before save_every steps have accumulated loses what was learned since the last save.

No background pass rewrites the store, and nothing is ever consolidated, summarised or extracted. Growth and pruning run inline on the reading loop; the learning-rate controller (minagi/plasticity.py) runs at each held-out evaluation. This is a real simplification and the project is right to claim it: one mechanism, at one granularity, for a book and for a conversation.

Three things about the write reach disk in ways worth stating precisely.

The chat server persists by default. serve.py's flag is --no-learn, and its help text is the clearest statement of the alternative: "serve without learning. The weights are then opened read-only and nothing is written back." Absent that flag, ro = not learn is False, the pool is opened read-write, and the weights directory advances every eight steps.

The CLI reader does not persist — except that it does. cmd_read prints "not saved (pass --save to keep what it learned)" when the flag is absent, and --save gates weights_store.save, growth and pruning. It does not gate the tier writeback. cmd_read calls build_paged with five positional arguments (train.py:659) and never passes read_only, which defaults to False at train.py:1812. AdamW steps every slot on every chunk, and an expert a step has changed is handed back with tiers.put(..., dirty=True) when it leaves the card (paged.py:629-631). Tiers._trim writes a dirty entry to disk whenever the RAM cache exceeds ram_capacity (paged.py:167).

With the shipped pool.ram_cache: 96 this is inert on a fresh 64-expert model and live on the project's own: the README reports the run at 128 experts, and the replication checkpoint it links for download holds 174. The README also links an undertrained snapshot of the running model on Hugging Face (README.md:134), which serve.py opens read-write by default. What lands is a directory whose expert files have trained against a trunk that core.npz does not hold, because the trunk is written only by a save. forgetting_probe.py states the same mechanism in its docstring and expects a copy; cmd_read warns of nothing.

A save is not validated on the live path. LiveLearner.save writes unconditionally; only the stream subcommand compares against best first, and only stream has the divergence recovery that reloads the directory and halves the rate. Since there is one copy and no snapshot, a chat session that damages the model overwrites the thing it would have to be restored from.

8. Agent Integration

There is no agent, no tool schema and no MCP surface. A case-insensitive search for mcp, tool_call, function_call, tool_use, openai and anthropic across every .py, .yaml and .md in the tree returns nothing — this model calls no other model, at any point. The integration surface is a browser page and a CLI.

serve.py binds 127.0.0.1 by default and exposes /api/chat (SSE), /api/state, and a single-file HTML page that shows which experts are on the card by uid, their normalised gates, how many experts the last character admitted, how far the stream is from its next step, and a long-term memory counter. uid is what the page shows rather than the index, for a reason the code explains: pruning renumbers the pool, so the same index means different experts at different times.

The most unusual integration is corpora/self_knowledge.yaml — 584 lines of question-and-answer pairs about the model's own architecture, templated with live values ({ram_cache}, {explore_bias:g}), built into a training lane that the project raised from 0.25% to 12.5% of reading (train.py:1407). When the served model answers "how do you decide which experts to use?", it is reciting a corpus, not introspecting. The model's account of itself is training data, so it can be stale, and nothing checks it against the running system.

9. Reliability, Safety, and Trust

Nothing learned can be deleted. This is the central property and it is not a gap in an otherwise complete design — it is what the design is. A search for unlearn, undo, rollback, revert, restore_checkpoint and snapshot across the tree returns the stream subcommand's divergence recovery, which reloads the single weights directory and halves the learning rate; precision.unpack_bf16, whose docstring says it undoes the packing; and comments. None of them can reach a fact. The atlas's usual question for a store — what does a deletion request reach — has one answer here: the whole model, by retraining from scratch, which requires still holding the corpus the request asked you to destroy.

Pruning removes whatever an expert held, in bulk. The sample log records the pool at 225 experts at 790.7 million characters and at 128 by 874.2 million: 97 expert files deleted inside one survival window. The log does not record why, and nothing records what those experts had learned.

The file reader has no secret filter. ingest._walk skips directories whose name begins with ., so ~/.ssh and ~/.aws are passed over when a parent is walked. It does not skip files. os.path.splitext(".env") returns an empty extension, SKIP_EXT cannot match it, and looks_like_text accepts any printable UTF-8 — so a .env sitting beside the source in a project directory is read in full, and a file named directly on the command line bypasses the hidden-directory rule entirely (if os.path.isfile(p): out.append(p)). The project's own .gitignore carries a # secrets section listing .env and .env.*; the reader that trains on your disk has no equivalent. In a document store this is a bad afternoon. Here it is permanent.

Self-training is acknowledged and unmeasured. live.py states that the model reads its own argmax back as evidence and that a denser earlier variant cost +0.65 nats. It says whether the current regime still costs anything is unknown, and that a smaller learning rate is not the fix — "on the model's own argmax that direction is to confirm it; a lower rate confirms it more slowly." Naming an unmeasured risk in the source is better practice than most of this corpus manages, and it remains unmeasured.

Where the engineering is careful. Adam moments travel in the expert's own file and reach the card just before a step, so a paged expert never inherits a stranger's momentum. Residency is matched by identity rather than by slot position, so an expert a forward admits again is never moved. Expert files are named by uid so pruning never renames. Every write is .tmp-then-rename. _save_paged warns when the pool holds experts with no file on disk. read_only raises rather than silently skipping if a write is attempted. Routers are reallocated rather than reshaped in place, with the autograd crash that motivated it recorded in the comment. Pruning refuses to empty the pool and keeps the most recently chosen expert if every rule would have deleted everything.

No capability marks. tombstone, bitemporal, scope_enforced and negative_eval have no candidate mechanism. trust_state is withheld because born, last_seen, gate and the derived dying() are usage lifecycle, not epistemic status — nothing withholds a memory from being treated as true, because nothing distinguishes memories. audit_log is withheld on the axis question set out in section 5: runs/history.jsonl is a named append-only event record with typed kinds and refusal reasons, every event it records is a mutation of capacity, and no path records what was read. human_review is withheld because --save is a flag set before the reading starts rather than a state a memory waits in; the forgetting measurement that would inform the decision is printed after the model has been changed and does not gate the save; and the surface a person types into persists without asking.

10. Tests, Evals, and Benchmarks

There is no test in this repository. git ls-files matching test, spec or eval returns nothing, and a grep for a line beginning assert, def test_, import pytest or import unittest across every .py returns nothing. This is not a suite that cannot fail; it is the absence of one.

Analysis tooling is partly committed. tools/ holds three plotting scripts — plot_dashboard.py, plot_experts.py, plot_progress.py — that read the sample log, the manifest and expert_history.jsonl without torch. Four other tools/*.py cited in source comments are not in the tree: birth_probe.py, capture_routing.py, chess_legality.py and plot_routing.py. The .gitignore names a fifth, tools/expand_corpus.py, as the corpus generator; the README's pipeline runs python3 -m corpora expand, which is committed. The comparison of birth schemes that add_experts cites lives in runs/results/birth_schemes.json, which is gitignored.

The forgetting measurement is committed. replication/forgetting_probe.py (395 lines) scores every held-out domain, trains on one domain at batch 1 with growth and pruning disabled, and re-scores at fixed marks. replication/results/ holds the four arms the README tabulates, each a JSON file recording its checkpoint step, pool size, learning rate, trunk multiplier and swap flag beside six rows of per-domain scores up to 1,048,576 characters. cl_summary.py prints the control's verdict before any arm, because the control reads every lane and so cannot forget.

I recomputed the README's table from those four files with a short script of my own, using cl_summary.py's formulas: every figure matches to four decimals — unread-subject damage of +1.2663, +1.2628 and +0.1297 nats, retention of 74.12%, 74.25% and 97.30%, and 44 experts touched in the mitigated arm, in a 174-expert pool. The control's read lanes improve by 0.0461 nats, which the summary's own rule calls clean. I ran none of the project's code.

Three limits bound what the files prove. The README says the arms were measured before the selection rule in section 6 replaced its predecessor, and that the committed probe runs the current rule, so re-running it measures a different system from the one that produced the numbers. The checkpoint is on Hugging Face, not in the tree. And each arm is one run, against the README's own threshold of "about 0.03" for a real difference; the 1.13-nat gap between the mitigated and unmitigated arms is about 38 times that.

runs/samples.txt is the other committed evaluation artifact: 179,243 lines covering the whole run, with per-round held-out loss and its standard error, per-subject breakdown, repeat rate, expert count, reading and writing speed, and two readings of nine fixed prompts. The raw greedy reading is always written first, because it "is the only one that says what the model predicts", and the repetition guard's reading is labelled as describing the guard.

There is no paper. arxiv, bibtex, @article, Citation and doi across the tree return a ## Citation block naming the repository itself as @software, and an acknowledgments list of nineteen papers in seventeen entries — Shazeer 2017 for the expert pool, PonderNet for adaptive depth, RoFormer, ZeRO-Offload, Chinchilla — plus DeepSeek-V3 cited in the body for the exploration bonus. That is attribution, not a result.

11. Patterns Worth Stealing

Steal

Refuse the metric that is anti-predictive, and write down why. paged.py prunes on staleness and refuses a gate term, with the reasoning stated: "the smallest gates belong to the busiest experts. One that behaves as a sink - chosen constantly, contributing little per character - reads as dead on a gate test, while a high-gate expert nothing has asked for in hundreds of thousands of texts reads as alive". Every memory system with a decay or importance score has this problem in some form, and most of them pick the plausible field. The transferable move is the sentence, not the metric: name the case where the obvious score is inverted.

One definition, two thresholds. Staleness is computed once in dying(); the pruner deletes past 1.0 of the survival window and the growth brake refuses past dying_at (0.65). A single predicate with two cut points cannot drift apart the way two predicates do.

Let exploration buy a trial, never survival. Training forwards give an under-used expert a bonus that decays with use. admit keeps a second vote without the bonus, and only that vote moves the prune clock; in the config's words, "being tried keeps nothing alive, being wanted does". A memory system that resurfaces old items to give them a chance should keep the same separation: a surfacing it forced is not evidence the item is wanted, and must not count as reinforcement.

Publish the arms that lost, with their raw scores. The forgetting table carries three massed configurations and an interleaved control, two of the three bad, and the per-domain scores behind every row are committed beside the probe. A memory system that claims a consolidation or decay policy helps should be able to show the same table, and the files that make it.

Say what a mechanism is not measured on. live.py's header is a model of this: it names the risk, gives the number from the regime that was measured, says the current regime is unknown, and says why the obvious mitigation does not work. Compare that with the usual alternative of silence.

Avoid

A persistence flag that gates the index and not the data. --save gates the manifest, growth and pruning; it does not gate the tier writeback, and the default that would have (read_only) is one positional argument away from the call that needed it. The project's probe expects a copy for exactly this reason and its dry-run command does not. The general shape — a writer and a reader composing a safety property separately — applies wherever a "dry run" exists.

Opt-out persistence on the surface a person talks to. The asymmetry between serve.py's --no-learn and cmd_read's --save is backwards with respect to what each surface receives.

Citing tooling that is not committed. Four tools/*.py are named in source comments and a fifth in .gitignore, and none exists at this commit. A comment that names a tool is a claim a reader will not check.

A store with one copy and no gate on the write. best_val's reasoning about disk cost is sound and the conclusion — that the directory is both the live model and the fallback — means the live path can overwrite its own recovery point.

Fit

This is not a memory system to adopt, and the project does not offer it as one. Read it if you are weighing whether to put any part of a memory into weights, because it is the clean case: no document store, no adapter versioning, no retrieval over text, so every consequence of the choice shows up undiluted. Read sections 5, 7 and 9 together and you have the argument against weight-resident memory for anything that must be correctable, made from an implementation rather than from first principles.

As a model, the audience is narrow and well-chosen: one person, one card, one machine, data they own, no compliance surface, and a willingness to accept that the thing cannot be edited. For that audience the design is coherent and the honesty is well above average. The moment a second person, a second project or a deletion request enters the picture, nothing here transfers — there is no key to add a predicate to.

12. Antipatterns / Risks

  • Irreversibility is total. Anything read is unnameable afterwards. There is no per-record delete, no supersession, no tombstone, and no way to answer "where did this come from". For a personal model on your own hardware that is a stated trade; anywhere else it is disqualifying.
  • The dry read is not dry on the project's own model. cmd_read never passes read_only, so a pool larger than ram_cache writes trained expert files back under a command that prints "not saved", beside a trunk that was not saved.
  • Secrets are read as text. No filter excludes .env, id_rsa, credentials or .netrc from train.py read, and a directly named file skips even the hidden-directory rule.
  • Self-training is a measured risk left unmeasured in the current regime, by the project's own account.
  • No tests. Nothing establishes that pruning keeps what it should, that exploration admissions stay off the prune clock, or that a reload after a save produces the same model — the last of which live.py records as a bug class that has bitten ("the directory then claims the default vocabulary of 8192 while holding 265, and will not load again").
  • Pruning is permanent and not aimed. prune's own docstring names the cost: "an expert which is genuinely rare rather than dead is deleted, and deletion is permanent." Rarity and deadness are the same signal to this rule, and the run's own log shows 97 experts deleted inside one window.
  • The headline result is reproducible from its numbers, not from its code. The per-arm scores are committed; the selection rule they were measured under is not the one the committed probe runs.

13. Build-vs-Borrow Takeaways

Borrow the reasoning, not the code. Nothing here is packaged for reuse — no manifest, no public API, no versioning — and the parts that are excellent are decisions rather than modules.

Three of them transfer directly to an ordinary token-store memory. The eviction rule that reads staleness and refuses the correlated-but-inverted score is the same shape as choosing between "last accessed" and "importance" in a consolidation pass, and keeping forced resurfacing off that clock is the half most designs miss. The growth brake with five conditions, each producing a one-line refusal reason that is logged, is what a memory system's write admission control should look like and almost never does. And publishing the losing arms beside the winning one, with the raw scores, is the difference between a claim and a measurement.

The thing not to borrow is the boundary. If any part of your memory must be correctable — and the atlas's position is that almost all of it must be — the part that lives in weights needs to be the part nobody will ask you to delete. You also need to have said in advance what a deletion request will not reach. mini-AGI does not say that, and it is the one sentence its README is missing.

14. Open Questions

  • Does the current live regime cost held-out loss? live.py says it is unknown. The instrument exists — cmd_read's before/after mixture scoring — and pointing it at a chat session rather than a directory would answer it.
  • Does the forgetting result hold under the current selection rule? The committed probe runs it; no result under it is committed.
  • What does the dry-read writeback change? The condition is established from the code; the magnitude is not. A no-save read on the 174-expert replication checkpoint with ram_cache at 96 would show how many expert files it rewrites.
  • Why did the pool fall from 225 experts to 128? The sample log records the drop between 790.7 and 874.2 million characters and nothing about its cause; expert_history.jsonl, which would, is gitignored.
  • What happens to runs/history.jsonl on the path the project runs? The typed event log lives only in stream; read writes periodic telemetry snapshots instead, and truncates them on resume. Whether that is deliberate is not recorded.
  • Where did the tree come from? 30 commits since 19 September 2026 against a sample log covering 1,552 evaluations. The code predates its history, and four of the tools its comments cite have not been published.

15. Appendix: File Index

Path Lines What it holds
train.py 2284 Three subcommands; cmd_read is the documented path, cmd_stream the one with the typed event log; _truncate_history
minagi/paged.py 1097 Tiers, PagedPool, admit, begin_forward, _rearrange, prune, flush — selection, paging and deletion
serve.py 882 Flask chat server, remember, the on-card expert view, the long-term-memory counter
minagi/pool.py 873 PooledMLP._route with the whole-pool request and the exploration bonus; AutoGrow with its five brakes and refusal reasons
minagi/recur.py 525 The recurrent block, begin_text at position 0, load_any, load_recur
minagi/stream.py 470 Corpus reading, interleaving, the held-out evaluator
minagi/store.py 448 The weights directory: layout, atomic writes, _save_paged, best_val
minagi/plasticity.py 358 The learning-rate controller — two weighted fits, effect size over t-statistic
minagi/live.py 203 LiveLearner, its optimiser-moment restore, exchange_text, and the unmeasured-risk header
minagi/ingest.py 151 The file walker and the text/binary decision; no secret filter
replication/forgetting_probe.py 395 The forgetting probe: score, read one domain, re-score; expects a copy of the weights
replication/cl_summary.py 110 The table from the four result files, control verdict first
replication/results/*.json 149–156 each Four arms, per-domain scores at six marks, the configuration of each
config.yaml 174 Every threshold named above, all five growth brakes engaged, explore_bias 0.65
corpora/self_knowledge.yaml 584 The model's account of itself, as training data
runs/samples.txt 179243 The whole run's evaluation history

Recorded searches

Commands run at the repository root, for the absence claims above.

# No test exists anywhere in the tree (0 results each).
git ls-files | grep -iE 'test|spec|eval'
grep -rnE "^\s*(assert|def test_|import pytest|import unittest)" --include='*.py' .

# No scope key of any kind (4 results, all the English word "account" in comments).
grep -rniE "\b(user_id|session_id|tenant|account|auth|login|principal)\b" --include='*.py' .

# No correction, unlearning or snapshot path (the `stream` revert path, unpack_bf16, comments).
grep -rniE "unlearn|undo|rollback|revert|restore_checkpoint|snapshot" --include='*.py' .

# Nothing copies the weights directory aside before writing it (0 results).
grep -rniE "shutil\.copy|backup|\.bak" --include='*.py' .

# Every tools/*.py cited, against what is committed (4 cited in comments and
# 1 in .gitignore are absent; 3 are committed).
grep -rnoE "tools/[A-Za-z_]+\.py" --include='*.py' --include='*.md' --include='.gitignore' . | sort -u
git ls-files tools/

# No dependency manifest, hook, devcontainer or checkout filter (0 results).
find . -path ./.git -prune -o \( -name 'requirements*.txt' -o -name 'pyproject.toml' \
  -o -name 'setup.py' -o -name 'setup.cfg' -o -name 'conftest.py' -o -name 'Makefile' \
  -o -name '*.lock' -o -name '.envrc' -o -name '.gitattributes' -o -name 'Dockerfile*' \
  -o -name 'devcontainer.json' -o -name '.gitmodules' \) -print

# No paper: the only Citation block is @software for this repository.
grep -rniE "arxiv|bibtex|@article|@misc|citation|doi" --include='*.md' --include='*.cff' .

# No agent surface and no second model, anywhere including the README (0 results).
grep -rniE "\bmcp\b|tool_call|function_call|tool_use|openai|anthropic" \
  --include='*.py' --include='*.yaml' --include='*.md' .

# read_only is never passed by the reader that documents itself as keeping nothing.
grep -rn "read_only" --include='*.py' .
grep -rn "build_paged" --include='*.py' .

# The expert history is rewritten on every read session.
grep -n "_truncate_history" train.py

# cmd_read prints no writeback warning (1 result: the build_paged docstring).
grep -n -i "dirty\|writeback" train.py

History

2026-10-01 — 7361e7a5… — 14 commits past the previous pin. Expert selection was rewritten: the demand score, key vectors, margin, dwell window and cold-start sweep are gone, replaced by a whole-text vote and an exploration bonus the prune clock ignores (section 6). The forgetting probe and its per-arm results are committed and reproduce the README's table (section 10). The dry-read writeback holds; expert_history.jsonl is truncated on resume. Three published claims were wrong at the old pin: train.py has three subcommands, not four; tools/expand_corpus.py is cited in .gitignore, not the README; the reference list held sixteen entries, not seventeen. Screen: NOTHING SCANNED; all 56 files read by hand. Nothing installed, built or run. No capability marks.

2026-09-22 — 201852d3… — first reading, at the head of the default branch. Screened before anything was read: the tool reported NOTHING SCANNED — no manifest, hook or agent file at any path it knows — and a hand read of all 45 committed files explains that rather than contradicting it: no dependency manifest of any kind, no conftest.py, no Makefile, no .envrc, no .gitattributes, no .gitmodules, and .github/ holding only FUNDING.yml. No auto-run surface, no build-time execution point, and no agent-directed file. Nothing was installed, built or executed; no training run, no serve, no benchmark, so every claim here is read from source. No capability marks.