Skip to content

feat(live): the placement interview, per-node prefetch and the rich bounded close (Lunaris Live, Phase 2c) - #210

Merged
PouyanJay merged 10 commits into
mainfrom
live-placement-close
Aug 19, 2026
Merged

feat(live): the placement interview, per-node prefetch and the rich bounded close (Lunaris Live, Phase 2c)#210
PouyanJay merged 10 commits into
mainfrom
live-placement-close

Conversation

@PouyanJay

Copy link
Copy Markdown
Owner

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.

T1 Walking skeleton: POST /api/live/sessions {topic} opens a placing session with the compile attached and the interviewer's first question; the web entry is session-first
T2 The interview: bounded, resumable, over both transports; warming and POST /{id}/advance (200 / 202); the compile that fails, and the one lost to another process
T3 Priors from the interview, credited then verified at the boundary before anything is built on them; the learner's profile briefs the tutor
T4 Per-node material prefetched one node ahead, kept per learner and map (live_materials), consumed once; the root prefetched while the learner is still being interviewed
T5 The close is a ceremony: the tutor's recap over the record (deterministic fallback), and the mastery delta from the beliefs stamped at open
T6 The spaced schedule: a review ladder written at close, shown to the learner, honoured by the next session's director; a finished map reopened before its day is a 409 naming the day
T7 The web ceremony over both surfaces: a real ending that names the review day, the interview labelled, the panel rebuilt when teaching begins
T8 Admission and metering: one reading of the ceiling for the turn and the prefetch, a topic-open that spends a compile slot and an opening or neither, the interview bound as a setting
T9 Every move and every close covered; the keyed eval extended to a real placement; and a claim credits the chain beneath it

What it does

  • A session opens on a topic. The map compiles behind it while the interviewer asks what the learner already knows and wants; the mapper turns the answers into claims (never evidence) and a profile; the director credits a claimed chain to its boundary and checks the deepest claim with a graded retrieval before building on it. If the compile is slow the session warms and the surface advances it; if the compile fails the session closes and says so.
  • Material is asked for one node ahead of the learner, under the session's own ceiling and keys, and kept per learner and map; a first turn on a concept whose material is there asks the tutor for words only. Later passes always generate fresh.
  • The close carries the tutor's recap over what was covered, a meter that reads as movement (recall beside where the concept stood at open), and each concept's next review day. Every graded concept lands on a ladder (a day, then ×2.5 per rung, seven rungs); a session opened on or after that day retrieves it first. Both surfaces say "Come back on Thursday 20 August for Prior and Update."
  • No new cost subject: interview, mapper, prefetch and recap are the session's money under LIVE_SESSION and the $2 ceiling binds them all, on both transports.

The decisions worth arguing with

  • A claim credits the chain beneath it, and is checked before anything is built on it (AD27). The keyed eval's first run put claims on the gradient, the chain rule and backpropagation and none on "slope and derivative", which nobody had asked about, and the director introduced the root. Now a credible claim credits what it stands on, the check is the claim that implies it, and a demonstration credits downward the same way (a MET on top of a chain is evidence for the chain). A hesitant claim still credits nothing.
  • The schedule is a ladder, not a forgetting curve. Beliefs still do not decay across sessions; the due date is the only cross-session pull. Days are named in UTC on every surface; the learner's timezone is a later item.
  • A review answered mid-session moves to tomorrow, not to nothing, so a session that never closes cannot drop a concept off the ladder.
  • The panel is rebuilt across the placing → teaching boundary: its thread opens on the turn it mounted with, and the first lesson arrives through the host's poll.

The eval ran

test_a_fluent_learner_is_placed_past_the_root and test_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 lint clean · 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.status widened with placing and warming; live_knowledge.prior; live_materials (RLS select-own); live_knowledge.review_stage (CHECK 0..7) and due_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=true on dev, VITE_COPILOT_URL set 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 in documentation/deployment.md.

…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).
@PouyanJay
PouyanJay merged commit 6624018 into main Aug 19, 2026
9 checks passed
@PouyanJay
PouyanJay deleted the live-placement-close branch August 19, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant