VLDS Model Road
vlds 0.0.65 · setup

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.

Neither model learns from its own unreviewed output.

Where it sits

phi.py barrierevery row with its state; tombstones mask
pick_model.py picksscores the LIVE rows against the task, at most six a file
pool-picks-<s8>.mdthe same shape a child returns
phi.py pool --picksfolds, writes recall-pool.md, credits model
picks (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

RepositoryPathWhat it is
prompty-prompter-pioneer-puppeteervlds/scripts/pick_model.pypairs, eval, train, picks and shadow, using only the standard library
vlds/scripts/test_pick_model.pyAcceptance tests run against a throwaway p4 layout
phi.py, vlds_hooks.pyThe model and model-shadow roads accepted, and the read: line credits model
hooks/pool-prompt.mdThe two roads, step by step, for the operator
vlds/scripts/dispatch_model.pyThe dispatch barrier's model: rows, eval, train, judge, report
hooks/vlds_hooks.pydispatch-model: off | shadow | on at the prompt hook; the turn close retrains a model the store has outgrown
vlds/MODEL-ROAD.mdThis guide, in the repository
phiphi_prune/gaps.py, delta.pyFibonacci gap codes and the .phid delta format that records a model round
phi_prune/p4.pyA store read as training records: read_p4, split, dispatch_rows, pick_pairs
.docs/p4/continual_p4.ipynbContinual training of a small text model on a store, in rounds
AudioPlugin-p4models/pick-model.json, dispatch-model.jsonThe 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

model-shadow

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.

model

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.

children

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?

## recall

dispatch-model: shadow     # off (default) | shadow | on
AudioPlugin-p4, leave-one-session-outRowsWritten FRESH by the model
Fresh messages that resembled an earlier one20728 (14%)
Real repeats (ECHO or SUPERSEDED)250

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

  1. Pairs. For each session's recall-pool.md, the candidates are the barrier rows timed no later than its pooled: 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.
  2. 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.
  3. 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.
  4. 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 the phi_prune.delta format, and phi's decoder reads it back in its own tests.

Measured on AudioPlugin-p4

RankerR-precisionRecall in top 20Picks: precision / recall
Model, round 00.3910.4770.308 / 0.388
Term overlap alone0.3320.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

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:

KeptBeforeDenseDense, cutSparse-trainedPosition bits: Fibonacci / Elias-gamma
1%3.5133.2823.4963.5016.55 / 7.46
10%3.5133.2823.4223.4614.19 / 4.17

These are one seed and short runs. They show what the pipeline measures, not final sizes.