The pool's model road and the dispatch model
On the default children road, each session's first prompt launches one Haiku pick-reader per store file to decide which entries bear on the task. The model road puts a small ranker in their seat. It's trained on the store's own earlier pools, picks from the same barrier rows, and writes the same picks file.
The prose stays the source of truth. The model only chooses which LIVE rows to surface, and every pick is a verbatim barrier line that the hot file still owns. SPENT, FREED and EXPIRED rows never reach it, and tombstones keep working.
Always on, always learning. On AudioPlugin-p4: pool-road: model, pool-audit: 5, dispatch-model: on.
- On by default once trained. With no setting named, the road reads
modelonce a pool ranker exists, and the dispatch model readsononce it exists. - Every session teaches it. The operator, already launched for the pool, reviews the model's picks on a sheet with four runners-up per file, keeping, dropping or adding. Its reviewed picks are what the pool uses, and they become a label.
- One session in five is audited. The Haiku readers also run in the background. Their picks are kept as labels from readers that never saw the model's choices, a check on the operator agreeing with the model.
- It updates itself. The turn close trains the first model once the store holds enough (5 labelled pools, or 5 judged repeats), then retrains in the background whenever new labels land.
Neither model learns from its own unreviewed output.
Where it sits
modelpicks (model) — data-store.md, 1 of 369 entries bear on the task
data-store.md:62 || bears: model 0.64; shares: toolbar, latency, start, page
What ships
| Repository | Path | What it is |
|---|---|---|
| prompty-prompter-pioneer-puppeteer | vlds/scripts/pick_model.py | pairs, eval, train, picks and shadow, using only the standard library |
| vlds/scripts/test_pick_model.py | Acceptance tests run against a throwaway p4 layout | |
| phi.py, vlds_hooks.py | The model and model-shadow roads accepted, and the read: line credits model | |
| hooks/pool-prompt.md | The two roads, step by step, for the operator | |
| vlds/scripts/dispatch_model.py | The dispatch barrier's model: rows, eval, train, judge, report | |
| hooks/vlds_hooks.py | dispatch-model: off | shadow | on at the prompt hook; the turn close retrains a model the store has outgrown | |
| vlds/MODEL-ROAD.md | This guide, in the repository | |
| phi | phi_prune/gaps.py, delta.py | Fibonacci gap codes and the .phid delta format that records a model round |
| phi_prune/p4.py | A store read as training records: read_p4, split, dispatch_rows, pick_pairs | |
| .docs/p4/continual_p4.ipynb | Continual training of a small text model on a store, in rounds | |
| AudioPlugin-p4 | models/pick-model.json, dispatch-model.json | The pool ranker (21 labelled pools) and the dispatch model (232 resembling rows) |
Merged into each repository's default branch: phi and prompty-prompter-pioneer-puppeteer on main, AudioPlugin-p4 on master.
Set it up
Run these from the project root. Any session's store works for pairs, eval and train, because the barrier reads every session.
PICK=<plugin root>/scripts/pick_model.py
STORE=.claude/p4/sessions/<s8>/vlds
# 1. What the store can teach: one line per labelled pool
python $PICK --store $STORE pairs
# 2. Measure first: leave-one-pool-out against term overlap alone
python $PICK --store $STORE eval --verbose
# 3. Train: writes .claude/p4/models/pick-model.json (and a round delta after the first)
python $PICK --store $STORE train --now "<the latest now:>"
# 4. Try a task, printed rather than written
python $PICK --store $STORE picks --session <s8> --task "<one-line task>" --now "<the latest now:>"
Then choose the road in the ## recall section of .claude/p4/vindex/phi-index.md. An edit there is a ruling, so it's the owner's to make.
## recall
pool-road: model-shadow
The three settings
Start here
The children road runs unchanged. The model's picks are written beside the children's, and pick_model.py shadow appends one row to models/shadow.tsv. Only the children's picks are folded. Each shadow session also leaves a new labelled pool.
The default once trained
The model proposes picks and a review sheet. The operator, running in the foreground, keeps, drops or adds picks, and its reviewed picks are what the pool uses. One session in five also runs the children as an audit. With no model trained, it falls back to the children road.
Roll back any time
The default until a model is trained. The roads share everything below the picks file, so switching back changes nothing else.
# models/shadow.tsv
# time session children model both recall precision
2026-10-28 09:14 4f2a91c0 14 12 6 0.429 0.500
That row is an example of the format, not a measurement.
Switching is live. Every reader looks up the road at each call, never caching it, so an edit to vindex/phi-index.md, or a model landing in or leaving models/, takes effect at the next prompt with no restart. scripts/test_roads_live.py walks one running store through children → model-shadow → model → children → model by default → children once the model is removed, and the dispatch model through on → off → shadow → on.
Retraining at the turn's close
Once a store opts in, the Stop hook retrains on its own. Under pool-road: model or model-shadow, it retrains the pool ranker when a labelled pool exists that the model hasn't seen. Under dispatch-model: shadow or on, it retrains the dispatch model once 20 more judged rows exist than it trained on.
Training runs detached in the background, so the turn close isn't held. Output goes to models/train.log, and a lock allows at most one start every 15 minutes. On the default settings nothing is trained.
The dispatch model
When an earlier message in the session reads like a new one, the prompt hook leaves the row open and the operator judges FRESH, ECHO or SUPERSEDED. That's one operator launch each time, though most such messages are fresh asks that only read alike. The dispatch model learns from rows the operator already judged and answers one question: is this message so plainly fresh that the operator doesn't need to be asked?
- It never proposes a repeat. A wrong ECHO drops the owner's request, so the model only ever takes the safe side.
- A rule sits over it. FRESH is never written for a message identical to an earlier row, or while a resembling row is still open, whatever the model scores. The tests caught the case that needed this: an exact repeat of an open message, weeks later, scored as fresh.
- Its threshold sits under every repeat it trained on, at 0.4 of the lowest repeat's score.
## recall
dispatch-model: shadow # off (default) | shadow | on
- shadow: the hook's line names the proposal and logs it to
models/dispatch-shadow.tsv; the operator still judges every row.dispatch_model.py reportjoins the log to the operator's final states. - on: the hook writes
state: FRESHitself when the model is sure, and says so in one line. Everything else goes to the operator.
| AudioPlugin-p4, leave-one-session-out | Rows | Written FRESH by the model |
|---|---|---|
| Fresh messages that resembled an earlier one | 207 | 28 (14%) |
| Real repeats (ECHO or SUPERSEDED) | 25 | 0 |
Of 2,243 judged turn rows, 232 resembled an earlier row across 40 sessions; 89 of those fall under the rule. The model saves launches and never makes a judgment call.
How it learns
- Pairs. For each session's
recall-pool.md, the candidates are the barrier rows timed no later than itspooled:line, and the labels are the rows its steering section picked. Matching is on file, time and head, because picks-file line numbers move as sessions are added. On AudioPlugin-p4, 265 of 271 steering lines match, across 21 pools. - Features. Term overlap between the task and the row, over the whole entry and over the head a child reads; the row's rank within its file; and its file, kind, state and age. Adding the rows' own words as features (
--tokens) scored worse here. - Model. Logistic regression with AdaGrad, positives weighted up, and a pick threshold chosen for mean F1. The model is a JSON file of hashed weights.
- Rounds. Each retrain writes
pick-model.rNN.phid: the changed weights' positions as Fibonacci gap codes and their values as Fibonacci-coded levels. That's thephi_prune.deltaformat, and phi's decoder reads it back in its own tests.
Measured on AudioPlugin-p4
| Ranker | R-precision | Recall in top 20 | Picks: precision / recall |
|---|---|---|---|
| Model, round 0 | 0.391 | 0.477 | 0.308 / 0.388 |
| Term overlap alone | 0.332 | 0.408 | — |
Leave-one-pool-out over 21 pools, 2026-10-27. R-precision is the share of a pool's n picks found in the model's top n. The model averages 15.5 picks a pool against the children's 12.6.
Better than plain overlap, but it recovers fewer than half of the children's picks.
The children are Haiku readers, not ground truth, and there's no measurement of how often two children agree with each other. That's why the road starts in shadow.
Limits
- 21 pools is small. The model mainly learns overlap and file-rank weighting; it can't read meaning. A task phrased in words the entries don't use will miss.
- The
bears:clause is mechanical. It names the score and the shared terms, not how the entry constrains the task. The main session still judges. - The dispatch model only saves launches. With 25 repeats to learn from, it clears 14% of plainly fresh messages and leaves every judgment call to the operator. It can't find a repeat the hook's overlap rule missed.
- Retraining is mechanical. A retrain picks up new labels; nothing checks that the new model is better than the last. Run
evalafter a retrain to see.
The phi side: continual training
.docs/p4/continual_p4.ipynb trains a small character-level GPT on a store in rounds and ships each round as a sparse .phid delta. Two smoke runs (300 base steps, 150 per round, held out from 2026-10-26), round 1, in bits per character:
| Kept | Before | Dense | Dense, cut | Sparse-trained | Position bits: Fibonacci / Elias-gamma |
|---|---|---|---|---|---|
| 1% | 3.513 | 3.282 | 3.496 | 3.501 | 6.55 / 7.46 |
| 10% | 3.513 | 3.282 | 3.422 | 3.461 | 4.19 / 4.17 |
- Cutting an update to 10% or less loses most of what the round learned.
- Training only the probed positions did no better than cutting a dense update, so the notebook ships dense-then-cut by default.
- Fibonacci gap codes win at 1% kept and tie Elias-gamma at 10%.
These are one seed and short runs. They show what the pipeline measures, not final sizes.