feat(live): the text session loop — director, tutor, grader and learner model (Lunaris Live, Phase 2a) - #208
Merged
Merged
Conversation
The walking skeleton for the session loop. A learner opens a session on a graph, the session is persisted, and it comes back already teaching its first turn. The director is a stub and the tutor's words are a format string — what this proves is that the path is wired, with one session_id correlating web contract, API, package and store. Three shapes worth naming, because T2-T6 build on them: - The turn is the unit, and it carries the move. The director's trace and the learner's transcript are the same sequence read two ways, so they cannot disagree about what happened. A move's reason is required: a session is dozens of choices made in seconds on someone's behalf. - The graph is referenced, never embedded. C1 grows a map at runtime, so a copy taken at session start would be stale the first time the learner asked something the map did not cover. - A session is a row, not connection state. Forty minutes of context about one learner must survive a page reload. Review caught a real authorization bug in both stores: owner_id=None skipped the owner check, so an unscoped read reached an owned session. Phase 1 had already settled this for graphs — treating None as 'see everything' is safe only while auth stays unconfigured, which is a property of two config flags agreeing, not of the store. The deeper problem was that nothing could have caught it: the API suite runs with auth off, so it cannot exercise cross-owner isolation at all. There is now a store-contract suite that went red on the real bug. Also from review: X-Session-Id now rides failures rather than only success, the router logs before letting an unexpected error become a 500, and the resume path is correlated.
…a, T2) The half of the loop the director reads and the grader writes: a belief per concept, moved by graded evidence and decayed by turns without it. Three choices worth naming: - The stored estimate is the belief at its last evidence, NOT the belief now. Now is recall_of, a decay applied at read time. A persisted number that changed meaning with every passing turn could not be compared with itself between sessions. - Decay is measured in turns, not wall time, so a session is reproducible — replaying the same answers gives the same beliefs, which is what will make the simulated-learner eval mean anything. Real cross-session forgetting happens over days and is deliberately NOT modelled; a curve invented now would be indistinguishable from a fitted one to everything downstream, which is the dangerous kind of placeholder. - Recall decays towards a floor rather than to zero: a concept demonstrated once is not the same as one never seen, and the director should prefer retrieving the first over introducing it from scratch. Knowledge is one row per concept, unlike its sibling tables. A turn produces evidence about exactly one concept, so a per-concept upsert touches one row where a document rewrite would rewrite the learner's whole history of the map on every answer. The RLS posture is sharper here than for graphs or sessions: the director gates every introduction on this number, so a learner who could write it could skip the curriculum. SELECT only, and the grader is the sole writer.
… T3) The director: a pure function over (graph, learner model, clock). A scored rule set rather than a model call, per plan section 7 — legible, deterministic, exhaustively testable without a key, and the place pedagogical iteration will happen once there is session data to iterate against. Rules run in a fixed order, and the order is the policy: the clock outranks everything; a stuck learner is not walked away from; a slipping concept interrupts new material; otherwise teach something new whose prerequisites are met; nothing left is a reason to stop rather than loop. Most of the tests put two rules in competition and pin which wins, because a director that introduces while the learner is stuck is wrong in a way no single-rule test would catch. All 20 tests passed on the first implementation, which in this journey is a warning rather than a result. Mutation testing found three real problems: - _MIN_EVIDENCE was dead code. One answer already lands below the mastery threshold, so the guard restated a rule in a second place that could drift from the first. Deleted. - The prerequisite gate was unobservable. Against a valid teaching order it is redundant, so hardcoding the branch true passed every test. It is kept — C1 grows maps at runtime and these are public functions anyone can call with a hand-built graph — and is now pinned by a test with a deliberately lying topo_order, the only state that can see it. - One test was weak twice over: asserting "not b" passes when a lowered threshold picks the other root, and asserting the node id alone passes when the same change turns the move into a retrieval. It now pins both.
A session's opening turn is now real on both halves: the director picks it from what this learner is already believed to know, and a tutor says it over the concept's authored teaching notes rather than a fixed string. The tutor is a seam with two implementations — a deterministic one for CI and keyless dev, and a Claude one whose prompt is mostly the node's own objective and misconceptions, passed verbatim. Phase 1 pays a model call per concept to author those; this is the moment they were for. Each move asks for something different: an introduction opens from something concrete, a retrieval refuses to re-explain, a remediation refuses to repeat what has already failed. A tutor that cannot speak fails the turn rather than falling back to the definition, and nothing is persisted when it does — a saved shell would be a resumable transcript with nothing in it. It surfaces as a retryable 503, told apart from storage being down. Each turn now carries its own run_id, so a line of transcript can be traced to the model calls behind it; a stored session this build cannot parse is told apart from an outage, because "try again" on an unreadable row is an invitation to reload forever. The stub compiler now authors teaching notes, so the offline path can exercise teaching at all. Also fixes two typecheck errors already on this branch in the web session client test.
The turn now stages one of the concept's do-statements, the learner answers in their own words, and a separate grader scores that answer against that statement. The belief moves, and the director reads the moved belief to decide what happens next — so a session responds to what the learner just demonstrated rather than to what they demonstrated a turn ago. The grader is deliberately not the tutor: a teacher marking its own homework would put its own bias into the model that decides what gets skipped. And a grader that cannot answer is never read as a wrong answer — an outage must not teach the system that somebody does not understand a concept. Running a whole session, rather than reading the tests, found two things the suite could not see. The director was re-introducing concepts the learner had already mastered, because recall dips below the mastery bar long before it falls far enough to be worth revisiting; what was earned is now judged on the undecayed belief and only what has faded on recall. And the tutor was repeating its previous turn word for word, because it had never been told what it had already said. Answering is a new endpoint, and a session that has run out of material closes rather than looping. The transcript is written before the belief: a crash between the two writes then under-counts evidence instead of double-counting it, which is the direction that cannot hand somebody a concept they have not earned.
…2a, T6) A session now knows when it started, so the 25-40 minute budget the plan asks for is real rather than a setting nothing could reach: the clock is wall time measured from the row, so it survives a reload instead of resetting with the tab. When it runs out, the session says so — the ending is a turn the learner can read, carrying the director's own reason, rather than a status change that leaves the transcript looking like a crash. An answer now names the turn it is answering. Pressing send twice used to grade the second copy against the question that replaced the first, filing the words under a criterion they were never written for. The sequential case is refused outright; the concurrent one cannot be settled from a snapshot, because both requests read the same head and both are right about it, so the store settles it instead: writing a turn is now conditional on the session still being the length the request read. A session written before sessions were dated is now unreadable rather than silently re-clocked, which is the honest answer for one nobody can date — a default would have handed it a fresh budget on every reload. And a host whose clock steps backwards mid-session no longer fails the turn.
The transcript and a box to answer in — the plainest thing that can carry a session, on the stack Phase 1 already proved. Each turn shows what the director chose and why, what the tutor said, what the learner was asked to demonstrate, what they answered and how it was marked. Showing the reason is deliberate: a session is dozens of choices made on somebody's behalf in seconds, and being told why is the difference between being taught and being steered. Every state a session can really be in is rendered rather than assumed — opening, waiting on the learner, marking their answer, ended, unable to open, and failing mid-session with the transcript still on screen, because a failed send is not a lost session. Their own words appear the moment they send them; the verdict waits for the server, because guessing one would be the surface marking work it never marked. Enter stays a newline and ⌘↵ sends, submit is never pre-disabled, an empty send explains itself and puts the caret back, and the transcript announces as it grows.
A session spends on every turn, so it becomes a subject the ledger knows by name: its own cost rows, keyed to the sitting rather than to the map it walks, since a map outlives every session run on it and "what did this session cost" is a question about one sitting. The ledger's key was generalised for exactly this, so all it needed was the enum value and a constraint widened — validated without blocking the writes the previous release is still making during a deploy. A tenant's session now runs on their own key. The model client is built on first use inside the tutor and reads its key off a contextvar, so resolving credentials without scoping the call would have taught their session on the platform's key, on the surface that spends most. Openings are rationed per learner per day, and a session takes one turn at a time. That second one is what makes the cost ceiling mean anything: two answers sent at once both pass every check made against the session they loaded, and both pay a grader and a tutor before either tries to write — so the refusal has to happen before the money, not after. And the ledger itself is now bounded in time as well as in failure: a store that hangs would otherwise hold the turn open with no way out, which is worse than one that simply fails.
Two halves. The deterministic one asks whether the loop is total: every teaching depth, every criterion kind, every verdict approached from both sides, maps that are one concept wide and five deep and twenty across, concepts the compiler left unspecified, and the things a learner actually types — one word, a shrug, an emoji, another script, markdown, a prompt injection, nine thousand characters. The keyed one asks whether the policy teaches, and asserts on the director's trace rather than on prose: a session is one sample of a generative system, and an assertion about how well an explanation reads would be a coin toss dressed as a test. Three simulated learners, each of which has to force a move — somebody who cannot answer must be remediated rather than marched onward, somebody fluent must be advanced, and somebody confidently wrong must not be marked correct or built upon. The wrong idea is the map's own. An earlier version hard-coded one about a concept eight turns of teaching never reached, so the learner answered everything else correctly and the test passed on an unrelated verdict; it now takes the misconception the compiler authored for the concept the session opens on. Running it, the learner insisted twice that inputs pass through a neuron unmultiplied, was refused twice, was remediated with a different explanation each time, and gave the idea up on the fifth turn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lunaris Live, Phase 2a: the thing that walks the map. Phase 1 compiled a topic into a concept
graph; this teaches from one. A learner opens a session, the director picks the next move from
(graph, learner model, clock), the tutor teaches it, the learner answers in their own words, aseparate grader scores that answer against the concept's own do-statement, the belief moves, and
the director decides again — until it closes deliberately.
Nine tasks, each test-first, reviewed by the project's agents and mutation-checked before commit.
lunaris_live.session→ Supabase, onesession_idLIVE_SESSIONspend, BYOK, caps and a per-session ceilingWhat it does
POST /api/live/sessionsopens a session on a compiled map, already teaching.POST /api/live/sessions/{id}/turnsanswers the turn in front of the learner and returns thewhole session — the answered turn changes as well as gaining a successor.
GET /api/live/sessions/{id}resumes it, because a session is a row and not connection state./livegains a session surface: the transcript, what each move was for, what the learner wasasked to demonstrate, what they said, and how it was marked.
The decisions worth arguing with
that decides what gets skipped. A grader that cannot answer is never read as a wrong answer.
the map keeps score.
Without that split, a concept sitting between the two thresholds was re-introduced from scratch to
somebody who had just mastered it — found by running a whole session, not by reading the tests.
evidence instead of double-counting it, which is the direction that cannot hand somebody a concept
they have not earned.
before either tries to write, so the cost ceiling can only mean something if the refusal happens
before the money.
The eval ran
Three simulated learners against the real tutor and grader on a really-compiled map, asserting on
the director's trace rather than on prose. The sharpest run: a learner holding the map's own
authored misconception insisted twice that a neuron's inputs pass through unmultiplied, was
refused both times despite sounding fluent, was remediated with a genuinely different explanation
each time (movie ratings → a shopping receipt → two numbers fighting for control → fill-in lines →
one input with nowhere to hide), and gave the idea up on the fifth turn.
Gates
Python 2631 passed · web 1511 passed ·
tests/db80 against real Postgres · ruff, tsc, eslint,prettier and
supabase db lintclean.One migration —
20260810000000_cost_subject_live_session.sql— widens the cost ledger'ssubject_typeCHECK to admit a session. Expand-only,NOT VALIDthenVALIDATE CONSTRAINTso thescan does not block the previous release's writes during a deploy, and verified from a full
supabase db reset. RLS, grants and policies are untouched.Not done, deliberately
something a test can discharge.
.eval/live-session/want a domain-literate read.prefetch and the rich closing ceremony are P2c.