feat(live): the placement interview, per-node prefetch and the rich bounded close (Lunaris Live, Phase 2c) - #210
Merged
Merged
Conversation
…iles (live-placement-close T1)
Lunaris Live, Phase 2c, walking skeleton. `POST /api/live/sessions` now takes `{topic}` beside
`{graphId}`: the service mints the graph id, launches the compile as a detached task through a new
`LiveGraphService.launch()` (extracted from `stream()`), and opens the session in a new `placing`
status whose first turn is the interviewer's question (`MoveKind.PLACE`, `IInterviewer`,
`StubInterviewer`, `open_placement`, `Session.topic`). The compile task inherits the session id
through contextvars, so a session and its compile are one trace.
The web `/live?topic=` opens that session at once (the compile screen, the map-then-click entry
and the compile stream client are gone); `/live?graph=<id>` keeps the map workspace as a deep
link. `useLiveSession(apiBaseUrl, opening)` is keyed on what the opening names, not the object.
Until T2 reads interview answers, a placing session refuses them with an honest 409 on both
transports, checked in `_ready` ahead of the graph read (during the compile window the graph is
not there). The two openings share one `_open_and_save` ceremony.
Migration `20260819090000_live_sessions_placing.sql` widens the status CHECK (expand-only, NOT
VALID then VALIDATE, no policy touched).
Gates: Python 2825, web 1675, tests/db 9 on real Postgres, ruff/tsc/eslint/Prettier/db lint
clean; 8/8 mutations killed; four reviewers' findings taken.
…ver comes (live-placement-close T2)
A placing session's answers now go to the interviewer: recorded on the question that asked them,
never graded, no belief moved. The interview is bounded three ways (the interviewer has enough,
four questions, the map has landed), and when the map has landed the same answer that closes the
interview is met with the first lesson through the seam `take_turn` now shares (`next_turn`). When
the interview runs out first the session WARMS (a turn that asks nothing) and the surface polls
`POST /{id}/advance` (200 with the session / 202 still warming) until teaching begins. A compile
that fails closes the session out loud with the compiler's reason, made a sentence; a compile lost
to another process is given up after the compile deadline plus a grace.
`LaunchedCompiles` (process-wide, wired from the composition root) remembers a compile only while
it runs and keeps only a failure's reason, bounded, readable until the session that owns it has
closed and said so. `ClaudeInterviewer` (strong tier, JSON contract) beside `StubInterviewer`,
keyed on `LUNARIS_PIPELINE`; an interviewer that cannot speak ends the interview, not the turn.
T1's placing refusal and its error are gone. The status migration is folded into T1's (unshipped)
file to admit `warming` too.
Web: `advanceSession`, a warming poll in `useLiveSession`, a warming footer that asks nothing.
Gates: Python 2860, web 1681, tests/db 83 from a full reset, ruff/tsc/eslint/Prettier/db lint
clean; 20/20 mutations killed; four reviewers' findings taken.
… anything is built on them (live-placement-close T3) A placement claim is never evidence. `NodeKnowledge.prior` seeds a concept with nothing demonstrated; `claim_of` is the one reader of whether a claim stands (no evidence yet), and the first evidence settles it: a MET starts from a credible claim (at or above the mastery bar) so one right answer lands it demonstrated, anything less starts from nothing. The director credits a credible claim for the frontier's purposes, RETRIEVES and grades the deepest unvouched claim the next concept stands on before introducing it, and lets a demonstrated concept vouch for the claims beneath it (found by driving the real loop). Tier 2 reads the claim as the band. `IPriorMapper` (`StubPriorMapper` by whole-word mention, `ClaudePriorMapper` on the worker tier, clamped, unknown ids dropped) runs in `settle_placement` on both seams; `Session.profile` rides the row and reaches the tutor with the director's reason as "why now". `ask_model` replaces the fourth copy of the whole-answer model call; `exchanges_of` the second copy of "what the interview was". Migration: `live_knowledge.prior` (nullable, CHECK, expand-only). Gates: Python 2899, web 1681, tests/db 85, ruff/tsc/eslint/Prettier/db lint clean; 24 mutations designed, all killed after two same-rule survivors were resolved; four reviewers' findings taken.
…rner and map, consumed once (live-placement-close T4) Plan §6, made literal: after each teaching turn the director's own prediction names the concept the learner will reach next, and its first-turn material (worked example, hint, practice) is asked for then, under the session's own cost scope on the tenant's own key, as its own run, and kept in `live_materials` (owner, map, concept). The turn that reaches the concept reads it and asks the tutor for words only: no second call, no grace to lose. A second pass over a concept generates fresh, so nothing is prefetched for a concept with evidence. The root is asked for when the map lands behind a placing session, so the first lesson is full. `open_session` is now the first `next_turn` on an empty session and returns a `TurnOutcome`, so an opening reads and lets go of kept material exactly as any turn does. `IMaterialStore` (memory + Supabase), `predict_next`, `MaterialPrefetcher` over a process-wide `PrefetchRegistry` (rendezvous for tests and a bounded drain at shutdown), `credentials_for` (one resolve-then-scope, with the compile plane's empty-vault rule: the session plane had let a tenant with no keys be taught on the platform's), `supabase_client` (the four Live stores' shared lazy client). Migration: `live_materials`, RLS owner-read / server-write, one row per learner, map and concept, an unowned row being one owner. Gates: Python 2919, web 1681, tests/db 93, ruff/tsc/eslint/Prettier/db lint clean; 14 mutations killed; four reviewers' findings taken.
…d and the mastery delta from the opening stamp (live-placement-close T5) A session's close was a fixed sentence plus the director's reason and a meter of what was demonstrated. It is now a recap in the tutor's words, briefed with a deterministic account of what the session touched and how each concept stands (demonstrated / forming / introduced), with a plain sentence when the tutor cannot speak so a close never fails on its own ceremony; and a mastery delta: the meter shows each concept's recall beside what it was when the session opened. - Session.opening_beliefs, stamped once at the two births (open_session, settle_placement after the priors are seeded), never recomputed from the live model; MeterEntry.recall_before reads it. - covered_in / Covered / CoveredOutcome: the record the recap is briefed with. recap_sentence: the deterministic fallback. ITutor.recap on the stub and Claude tutors. - One is_demonstrated predicate for the director, the recap and the meter; the meter is over evidence only, so a credited claim never checked is not shown as a regression. - Web: the delta reads "from N% ·" beside the answer count.
…er, honoured by the next session's director (live-placement-close T6) Every concept a session grades leaves the close with a place on a review ladder and a day it is due back: a concept that held climbs a rung and is due further out (a day, then x2.5 per rung, to a top of seven); one still forming is due tomorrow from the bottom; one only introduced is left to the frontier. The close shows the day on the meter and in the recap, and the next session on the map retrieves anything due before it introduces new material. Beliefs still do not decay across sessions: the ladder is the one cross-session pull the director feels. - NodeKnowledge.review_stage / due_at; live_knowledge gains both columns (expand-only, CHECK 0..7). - SessionClock.at is the turn's wall time, derived once from the session's own start plus the seconds since; nothing is due when it is unknown. Evidence moves a due review's date to tomorrow (provisional; the close writes the real one) so an unfinished session never loses a concept. - decide_move: a due review is taken after a stuck learner and before slipping, claim, frontier; longest overdue first; the reason says whether the concept was held or still forming. - next_turn returns the whole TurnOutcome, because a close moves the model. - A finished map reopened before its day is a 409 naming the day, not a 500 (both transports). - Web: the meter row reads " · review Thu 20 Aug".
…ames the review day, the interview labelled, the panel rebuilt when teaching begins (live-placement-close T7) The close's schedule reaches the learner on both surfaces: SessionEnded says "Come back on Thursday 20 August for Prior and Update." from the goodbye turn's meter, derived once by the host and handed to the transcript's footer and to the panel's composer slot. The transcript labels the director's moves in words (an interview question reads GETTING TO KNOW YOU, not PLACE). The panel waits with the shared notice while the session is warming and sends nothing into a 409, and is rebuilt across the placing → teaching boundary so it opens on the first lesson the host's poll brought rather than the last interview question. - lib/reviewSchedule.ts: nextReviewOf, UTC-named review days shared with the meter row. - SkeletonNotice + Warming extracted for both surfaces. - SurfaceCard's concept-map card renamed WhereItSits; the placement vocabulary is the interview's.
…ths: one reading of the ceiling, a topic-open that spends both gates or neither, the interview bound as a setting (live-placement-close T8) No new cost subject: the interview, the mapper, the prefetch and the recap are all the session's money under LIVE_SESSION, and the ceiling now binds them all. spent_past_ceiling is the one reader (fail-open on an unreadable ledger); the turn refuses on it with a status and the prefetch declines on it quietly, at the moment it would spend, because nothing awaits a prefetch and the turn that scheduled it may be the one that crossed the line. - A topic-open consumes a compile slot and a session opening, both or neither: the throttle checks the opening, the compile plane admits the compile, then the opening is counted. A compile refusal no longer spends an opening on nothing. - live_interview_max_questions and live_compile_grace_s are settings (LUNARIS_LIVE_INTERVIEW_MAX_QUESTIONS, LUNARIS_LIVE_COMPILE_GRACE_S), threaded from the container; the package's DEFAULT_MAX_QUESTIONS is the fallback. - Tests share one held-compile rendezvous and one AG-UI answer body (_live_stack).
…ed to placement, and a claim credits the chain beneath it (live-placement-close T9) Variant coverage: each MoveKind reached by the real path that produces it (an interview, an opening, a due review, two misses, the clock), and each close shape ending the session properly (the clock, the exhausted map, the failed compile). The keyed eval gains a real placement: a fluent simulated learner interviewed by the interviewer, placed by the mapper, and not introduced to the root; and two learners on one concept composed in different Tier 2 bands with different material. The eval's first run found the director introducing the root to a learner whose interview had put claims on the gradient, the chain rule and backpropagation and none on "slope and derivative", which nobody had asked about. A credible claim now credits what it stands on, transitively, and the check before the frontier is the claim that implies it; a demonstration credits downward the same way (a MET on the top of a chain is evidence for the chain), or the credit would vanish the moment the claim was checked and held. A hesitant claim still credits nothing. Re-run, the fluent learner opens on a retrieval at the boundary of what they claimed. - packages/live/tests/_bayes_map.py: the shared little map and the held-concept fixture. - Digest of the real placement at .eval/live-session/placement-fluent.json (gitignored).
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 2c: the placement interview, per-node material one node ahead, and the rich
bounded close. Phase 2b put the generative surfaces over an AG-UI transport; this makes a session
begin on a topic rather than on a compiled map, stay ahead of the learner, and end
deliberately: a recap, a mastery delta, a review schedule, and a next session that honours it.
Nine tasks, each test-first, reviewed by the project's agents and mutation-checked before commit.
POST /api/live/sessions {topic}opens aplacingsession with the compile attached and the interviewer's first question; the web entry is session-firstwarmingandPOST /{id}/advance(200 / 202); the compile that fails, and the one lost to another processlive_materials), consumed once; the root prefetched while the learner is still being interviewedWhat it does
LIVE_SESSIONand the $2 ceiling binds them all, on both transports.The decisions worth arguing with
The eval ran
test_a_fluent_learner_is_placed_past_the_rootandtest_two_learners_on_one_concept_get_material_in_different_bands, real interviewer, mapper and tutor: the fluent learner is interviewed, placed with 13 to 19 claims and a profile, and opens on a retrieval at the boundary of what they claimed; the same concept is composed in different Tier 2 bands for the placed and the fresh learner, with different material. Digest in.eval/live-session/placement-fluent.json.Gates
Python 1407 (packages/live + apps/api) · tests/db 97 on real Postgres · web 1696 · ruff, format, tsc, eslint, Prettier,
supabase db lintclean · every task mutation-checked (T5 15, T6 26, T7 11, T8 11, T9 6, all killed after the review rounds).Migrations (expand-only, in order)
live_sessions.statuswidened withplacingandwarming;live_knowledge.prior;live_materials(RLS select-own);live_knowledge.review_stage(CHECK 0..7) anddue_at.Rollout, and the Phase 2 exit
Merging deploys to dev. The Phase 2 exit criterion that is the operator's and the learner's, not the suite's:
LIVE_RUNTIME_ENABLED=trueon dev,VITE_COPILOT_URLset to the printed runtime URL, the SPA rebuilt; then one real browser session on/live?topic=…, interviewed, taught, closed with a day to come back, and rated. New dials are documented indocumentation/deployment.md.