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
- A dream's subject is invented rather than drawn from a real row. (Fence: every candidate is a store row or a canon file; no candidates → the old parity rule, and the note's missing
curiositykey is the record that the engine did not choose.) - The same subject is chosen on consecutive dreams for no reason. (Fence: novelty decays as 1/(1+visits), visits read back from the store — there is no second ledger to drift.)
- The engine raising stops a dream. (Fence: caught; the error string lands in the note as
curiosity_error; the dream fires on the fallback.) - The score moves on a quantity nobody measured. See the weight table — one row is a priority, not a measurement, and is labelled as such.
Candidates — each is a gap with a measurable size
| kind | what it is | size is | closed by |
|---|---|---|---|
overdue | an open expectation past its settles_by | hours past the deadline | a look, then a settlement |
open | an open expectation with no deadline — settles by event | hours since it was opened | a look, then a settlement |
unverified | a live belief with confidence NULL | hours since written | an expectation that would test it |
unread | a canon post no dream has read | hours since the file changed | reading it |
thin | a writable StateFrame dimension with the fewest live rows | capped — see §Boundaries | a 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
| control | went red because | fix |
|---|---|---|
empty store → only thin candidates, identity never among them | — | passed first time; listed so the reader knows it exists |
| a 3-day-old unverified belief beats a 1-hour-old post | 12 empty dimensions each took the 168 h cap at full weight and drowned it | thin weight 1 → 0.25; the cap is a default age, not a measured one |
| overdue reach counts the belief it hangs on | reach was keyed by belief id against a subject-keyed counter — always 0 | id → subject lookup |
| second dream does not repeat the first by accident | — | novelty 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
| kind | candidates |
|---|---|
| unread | 850 |
| open | 23 |
| thin | 2 |
| unverified / overdue | 0 |
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
- This feeds the subject problem; it does not solve it. A scorer over existing gaps cannot want something the store has never mentioned. Aspiration — a subject at a timescale longer than one dream — remains the largest gap in the design and is said so in the objective file.
thinhas no measured age. A never-written dimension takes the cap by default. That is why its weight is lowest, and it is the one place the score is not fully measured.- Novelty is per key, not per subject. Two canon posts about the same word are two candidates. Nothing here clusters.
- Canon ordering is by file mtime, which is when the mirror wrote the file, not when the post was made.
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