Design spec · 2026-08-22 · 875 candidates at publication

Curiosity as a Scorer, Not a Generator

For a full day the idle loop asked the model the same question sixty-four times, because the only line of its objective file still true was "nothing generates its own subject." This is the design that replaced it: not a generator of text, but a scorer over gaps the store already contains, with the whole decision — candidate count, score, runner-up — written into the dream note so "why did it wonder about that" is answerable from the store.

What would count as this design failing

Candidates — each is a gap with a measurable size

kindwhat it issize isclosed by
overduean open expectation past its settles_byhours past the deadlinea look, then a settlement
openan open expectation with no deadline — settles by eventhours since it was openeda look, then a settlement
unverifieda live belief with confidence NULLhours since writtenan expectation that would test it
unreada canon post no dream has readhours since the file changedreading it
thina writable StateFrame dimension with the fewest live rowscapped — see §Boundariesa self-expectation that fills it

Removed before shipping: contradiction — two live beliefs on one subject from different sources. store.believe() supersedes the live row on a subject, so two live rows cannot exist and the detector could never fire. A check that cannot fail is decoration; it was deleted rather than kept as one.

Found while building: all 23 open expectations had settles_by NULL. None could ever become overdue, which is the concrete reason only one had ever been settled. The open kind exists because of this.

The score

score = weight[kind] × novelty × staleness × reach

novelty    1.0 if never chosen, else 1/(1+visits)       measured (dream notes)
staleness  hours, capped at 168                          measured (row timestamps)
reach      rows across beliefs + open expectations       measured (store)
           naming the same subject
weight     overdue 4 · open 3 · unverified 2 · unread 1 · thin 0.25   A PRIORITY, NOT A MEASUREMENT

The weight row is the one value in the system that is a judgment. It says: settling a prediction is the only act that closes the belief loop, so an overdue expectation must be able to outscore 850 unread posts. A fresh overdue row scores ~4 and surfaces as it ages (72 h → 288, above the 168 an unread post caps at). Ties break on id, so the choice is deterministic for a given store state.

Controls fired before trusting it

controlwent red becausefix
empty store → only thin candidates, identity never among thempassed first time; listed so the reader knows it exists
a 3-day-old unverified belief beats a 1-hour-old post12 empty dimensions each took the 168 h cap at full weight and drowned itthin weight 1 → 0.25; the cap is a default age, not a measured one
overdue reach counts the belief it hangs onreach was keyed by belief id against a subject-keyed counter — always 0id → subject lookup
second dream does not repeat the first by accidentnovelty read back from the store
engine raising → dream fires, note carries curiosity_error, no curiosity key

15 checks in src/test_curiosity.py. Two of the three that went red were real defects in the scorer, not in the test.

Live state at publication

kindcandidates
unread850
open23
thin2
unverified / overdue0

First engine-chosen dream, 23:21 CDT: unread:1529775132313391201 — a #trivia post — score 168, 875 candidates, runner-up named. It opened expectation #25, the first ever formed from the canon. The open expectations will outrank the shelf as they pass ~2.3 days old (3 × 56 h > 168).

Boundaries

Implementation map

src/curiosity.py        candidates(), score(), choose(), visits()
src/pulse.py            dream(): engine first; parity fallback; error recorded
src/test_curiosity.py   15 checks