1. Executive Summary
mini-AGI is a byte-level language model that trains from scratch on
one 8 GB card and never stops training. It is MIT-licensed, about 10,300
lines of Python across 31 files, and its git history begins on 19
September 2026 — three days before this pin — while
runs/samples.txt carries 855 evaluations over 409 million
characters, so the tree is considerably older than the repository that
holds it.
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.
That makes it the second system here whose memory is weights, after Second Me — and a stricter case than that one, because Second Me 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 nothing routed to it lately, which is a statement about capacity and not about truth.
The surprise is that the machinery around that store is memory-system machinery, built carefully and for the right reasons. There are three tiers with an LRU between them; a retrieval step that scores a per-expert key vector against the hidden states of the text just read, with hysteresis so the working set does not churn; a growth rule with five brakes, all five wired and all five engaged by the shipped config; and an eviction rule that deliberately refuses to read the gate, with the reason written down — the smallest gates belong to the busiest experts, so a gate test deletes the sink and spares the dead. 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 calls
build_paged(wdir, device, args.resident, args.ram_capacity, args.context)
positionally, so read_only takes its default of
False whatever --save says — while
build_paged's own docstring warns that "paging an
expert in marks it dirty whether or not anything touched it, so a plain
read would otherwise write expert files back under the run." 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", and the file goes on to record
that an earlier, denser variant cost about +0.65 nats on held-out
against +0.02 for real documents of the same length, and that whether
the current regime still costs anything is "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: running the trunk at 0.1x the experts' learning
rate takes the damage from reading half a million characters of one
subject from +2.2300 nats on the seven unread subjects to +0.0067. The
second is expert pruning, which deletes a file from
weights/experts/ when nothing has admitted it to the card
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 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 nobody routed to; 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["Resident experts mutated<br/>in VRAM"]
VRAM --> DIRTY["Displaced expert parked<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{"nothing routed here<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: 45 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,
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:48
(Tiers): disk holds the whole pool, an
OrderedDict keeps ram_cache experts as CPU
tensors with true LRU eviction, and resident slots hold the
working set on the card. 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 (907 lines) is a
Flask app on 127.0.0.1 with a server-sent-events chat endpoint.
train.py (2,294 lines) carries four subcommands, of which
read is the one the project runs and stream is
the same mechanism pointed at a packed corpus. minagi/ is
the model: paged.py (1,124) is the pool and the paging,
pool.py (823) the dense pool and AutoGrow,
recur.py (655) the recurrent block and the loaders,
stream.py (491) the corpus reader, store.py
(451) persistence, plasticity.py (358) the learning-rate
controller, live.py (190) the live learner,
ingest.py (151) the file walker.
4. Essential Implementation Paths
A chat turn becomes memory.
serve.py:383 api_chat builds the prompt from
the message list, streams the reply under a process-wide
LOCK, and on completion calls serve.py:343
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 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 resident
expert 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:311, if val < best); the live and
read paths write on a timer.
A file read. train.py:557
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:900
prune(step, survival, protect) walks the pool, keeps
anything resident, anything younger than survival and
anything addressed 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, the
segment router 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 eight parallel tensors beside
it: use, age, born,
gate_seen, last_seen, ever,
since, admits, plus uid and a
keys matrix. born is the step an expert
joined; last_seen is the segment anything last wanted it;
since is when it was admitted to the card;
ever is whether it has been resident 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 artifact is
runs/expert_history.jsonl, appended once per checkpoint by
train.py:935 log_history with the pool's full
telemetry, and runs/history.jsonl, appended by the
stream subcommand with typed events — start,
step, val, pruned,
grew (carrying AutoGrow's refusal reason),
saved, done. Both are genuinely append-only
and both are about capacity. A reader with either in hand can say when
the pool grew and why growth was held; neither 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, so neither file is in
the tree to inspect.
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.
Before each chunk the pool is asked what the text wants.
paged.py:479 demand(x, sites, top_k) scores
the whole pool, and its docstring names the mistake it exists to avoid:
scoring the routers on raw character embeddings "looks reasonable
and is useless: an embedding carries no context, so every subject asks
for very nearly the same experts." So demand is scored on the
hidden states the call sites actually routed on while reading the
previous chunk, sampled as they went and one chunk
stale by construction — a read cannot see the text it is about to
predict.
Two terms are summed. The router term applies each call site's router
weights to those states plus the depth embedding, softmaxes, and
accumulates the top-k mass. The key term is a cosine similarity:
keys holds one normalised vector per expert, the hidden
states are normalised, and softmax(|h·k| * 8.0) is top-k'd
into a second tally. The key term is scaled by what an expert has earned
— its gate relative to the largest, floored at key_floor —
before the per-character choice rather than after, because
"discounting the tally would still let an untrained expert win the
choice and only then be marked down." An expert inside its trial
window is exempted from that handicap, since the gate it would be judged
on only moves when it is chosen. Both terms are normalised separately so
that "where the router has no opinion the key term still
speaks."
choose_by_demand turns the score into a working set with
two brakes that any retrieval cache would recognise: a candidate must
beat the weakest evictable resident by margin (0.10) to
displace it, and a newly admitted expert is safe for
dwell_chars (2,048). One mechanism is deliberately not
demand-driven — a terminating cold-start sweep admits exactly one
never-resident expert per chunk, in index order, until every expert has
had a turn, and then stops for good, because an expert that has never
been resident has never trained, so its router row is noise and it could
never be wanted.
While generating, serve.py re-chooses every
reselect_chars (64) and the comment records that the call
used to pass a single character — "harmless by accident", since
demand ignores the tensor it is handed whenever it has
collected states, which during a reply it always has.
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 really does gate weights_store.save,
growth and pruning. It does not gate the tier writeback.
cmd_read calls build_paged with five
positional arguments (train.py:645) and never passes
read_only, which defaults to False at
train.py:1799. Every time the working set changes, the
displaced expert is handed back with
tiers.put(..., dirty=True) (paged.py:744), and
Tiers._trim writes a dirty entry to disk whenever the RAM
cache exceeds ram_capacity. With the shipped
pool.ram_cache: 96 this is inert on a fresh 64-expert model
and live on the published one, which the README puts at 174–175 experts.
The condition is pool larger than the RAM cache, and on the
project's own model it holds.
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 resident by uid,
their normalised gates, 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 — 551 lines of
question-and-answer pairs about the model's own architecture, templated
with live values ({ram_cache}, {resident}),
built into a training lane that the project raised from 0.25% to 12.5%
of reading. When the served model answers "how do you decide which
experts to use?", it is reciting a corpus, not introspecting. This
is worth naming precisely because it is the shape a reader of a memory
atlas will misread: 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 four things: 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 two 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.
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, that a denser earlier variant cost +0.65 nats, that 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 genuinely careful. Adam
moments travel in the expert's own file, so a paged expert never
inherits a stranger's momentum. Residency is matched by identity rather
than by slot position, so a re-sorted demand list does not churn the
card. 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
wanted 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 really is
a named append-only event record with typed kinds and refusal reasons,
and every event it records is a mutation of capacity, with no
path recording 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 already been
changed and does not gate the save; and the surface a person actually
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,
and it is worth stating against a codebase whose comments repeatedly
cite tests as guarantees — serve.py:313 says
"tests/test_sample_routing.py is the guard" over a tree with
zero files under tools/ or tests/.
The same applies to the analysis tooling. Five distinct
tools/*.py are cited in source comments —
birth_probe.py, plot_progress.py,
capture_routing.py, chess_legality.py,
plot_routing.py — and the README's data pipeline cites a
sixth, tools/expand_corpus.py. None are committed. The
consequence is specific and it bears on the project's headline result:
the forgetting table that the README calls "the measurement the
whole design rests on" — +2.5871 nats with the working set frozen,
+2.2300 with swapping at full trunk rate, +0.0067 at 0.1x — is published
as prose and a PNG, and the script that produced it is not in
the tree at this commit. The raw numbers behind
assets/mitigations.png and
assets/probe_massed.png are not committed either. A reader
cannot recompute any of it from this repository.
What is committed is runs/samples.txt, 102,872
lines covering the whole run: per-round held-out loss with its standard
error, per-subject breakdown, repeat rate, expert count, and two
readings of nine fixed prompts. It is an unusually honest evaluation
artifact — 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. The
README's variance discipline is the same: it states that the same
configuration run twice lands about 0.014 apart because expert dispatch
is non-deterministic on CUDA, and that "about 0.03" is the
threshold for a real difference rather than the printed standard
error.
There is no paper. arxiv, bibtex,
@article, Citation and doi across
the tree return a ## Citation block naming the repository
itself and a seventeen-entry reference list of prior work — Shazeer 2017
for the expert pool, PonderNet for adaptive depth, RoFormer,
ZeRO-Offload, Chinchilla — which is attribution, not a result. I
ran nothing. Every claim on this page is read from source at
the pinned commit.
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
segments 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.
Publish the arms that lost. The forgetting table carries three configurations, two of which are bad, and the README says which single config value separates 50.68% from 99.84%. A memory system that claims a consolidation or decay policy helps should be able to show the same table.
Make the protection terminate. The cold-start sweep admits one never-resident expert per chunk until every expert has had one turn, then stops for good — "no randomness, and no permanent tax." Exploration reserves in retrieval usually run forever and cost forever.
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 general shape — a writer and a reader composing
a safety property separately — is worth checking for 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 tests that are not committed. Five
tools/*.py and one tests/*.py are named in
source comments as guards, and none exist at this commit. A comment that
names a test 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. It is worth reading 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 published model.
cmd_readnever passesread_only, so a pool larger thanram_cachewrites trained expert files back under a command that prints "not saved". - Secrets are read as text. No filter excludes
.env,id_rsa,credentialsor.netrcfromtrain.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, on a tree whose comments cite them.
Nothing establishes that pruning keeps what it should, that the margin
prevents churn, or that a reload after a save produces the same model —
the last of which
live.pyrecords as a bug class that has already 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." Withsurvival_charsat 100 million and the run at 409 million characters, the window is wide — but rarity and deadness are the same signal to this rule. - The headline result is not reproducible from the tree, because the script that produced it is not committed.
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 genuinely 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. The growth brake with five conditions, each producing a one-line refusal reason that is logged, is exactly what a memory system's write admission control should look like and almost never does. And the discipline of publishing the losing arms beside the winning one 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 — then the part that lives in weights needs to be the part nobody will ask you to delete, and you 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.pysays it is unknown. The instrument already exists —cmd_read's before/after mixture scoring — and pointing it at a chat session rather than a directory would answer it. - What does the dry-read writeback actually change?
The condition is established from the code; the magnitude is not. A run
on the published 175-expert model with
ram_cacheat 96 would show how many expert files a no-save read rewrites. - Is the cold-start sweep reached before the trial ends on a
large pool?
choose_by_demandadmits one never-resident expert per chunk, andprunepromises a newborn "one fair turn" insidesurvival. At 397 experts and one admission per 2,048 characters the sweep takes about 800,000 characters, well inside a 100-million-character survival window — but the two rates are not coupled anywhere, and nothing asserts the relationship holds if either config value moves. - What happens to
runs/history.jsonlon the path the project runs? The typed event log lives only instream;readwrites periodic telemetry snapshots instead. Whether that is deliberate is not recorded. - Where did the tree come from? 16 commits since 19
September 2026 against a sample log covering 855 evaluations. The code
predates its history, and the
tools/directory the comments reference presumably exists somewhere it has not been published.
15. Appendix: File Index
| Path | Lines | What it holds |
|---|---|---|
train.py |
2294 | Four subcommands; cmd_read is the documented path,
cmd_stream the one with the typed event log |
minagi/paged.py |
1124 | Tiers, PagedPool, demand,
choose_by_demand, prune, flush —
the whole paging and deletion mechanism |
serve.py |
907 | Flask chat server, remember, the resident-expert view,
the long-term-memory counter |
minagi/pool.py |
823 | The dense pool and AutoGrow with its five brakes and
refusal reasons |
minagi/recur.py |
655 | The recurrent block, load_any, load_recur,
choose_for |
minagi/stream.py |
491 | Corpus reading, interleaving, the held-out evaluator |
minagi/store.py |
451 | 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 |
190 | LiveLearner, exchange_text, and the
unmeasured-risk header |
minagi/ingest.py |
151 | The file walker and the text/binary decision; no secret filter |
config.yaml |
161 | Every threshold named above, all five growth brakes engaged |
corpora/self_knowledge.yaml |
551 | The model's account of itself, as training data |
runs/samples.txt |
102872 | The whole run's evaluation history — the one committed result artifact |
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` divergence reload, and nothing else).
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 in a comment, against what is committed (5 cited, 0 present).
grep -rnoE "tools/[A-Za-z_]+\.py" --include='*.py' --include='*.md' . | sort -u
git ls-files | grep -c '^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' \) -print
# No paper: the only Citation block names this repository (17 arXiv links are prior work).
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' .
History
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.