Metis · Features

Threads, the Personal Atlas, and the Mnemosyne Mastery Loop

A focused page within the Metis Features documentation. The full map and every sibling page live in the Features hub.

8sections13 minread1diagram1table

On this page

A V9 lesson is not supposed to dead-end. Once Prometheus has forged a gated V9LessonArtifact and the learner has played it, three subsystems decide what happens after the last beat — and they are the difference between a one-shot explainer video and a guide that carries a curious ape across days. Theia turns the answer into the next, better question and offers the thread forward (Olbers' paradox → the age of the universe → the cosmic microwave background → the Big Bang). The experience layer renders the learner's progress as a literal map of reality — concepts they have lit up shown as brightness, everything still dark shown as an inviting next star — so progress reads as a star map, not a progress bar. And the Mnemosyne glue schedules spaced retrieval at the moment recall is about to fail, projects each concept's memory strength onto a MasteryLevel, and feeds that mastery back into the very wonder-scoping that picks the next lesson. The product optimizes for durable understanding weeks later, not session length.

The reason all three are buildable rather than aspirational is the decision that runs through every V9 subsystem: compose the real engines, reinvent nothing. Theia walks the same AtlasStore graph that wonder-resolution uses (@oshun/v9-atlas); the star-map reads the same six-rung MasteryLevel ladder (@mnemosyne/core) that the rest of the platform's learning science uses; and the mastery loop delegates every schedule to the shipped FSRS-v4, SM-2, and IRT engines in libs/mnemosyne/core. The V9 packages on this page are binding glue, not new psychometrics — the header of libs/v9/mnemosyne-glue/src/index.ts says so plainly: "Reuses the real Mnemosyne FSRS/IRT engines — no new psychometrics." This page is the feature-side companion for the back half of the pipeline; the hub for the set is ../V9_features.md.

What ships, honestly#

The logic and data for all three subsystems are real and unit-tested with known-value assertions, not shape checks. Theia's thread continuation, emotional arc, and surprise-me (libs/v9/theia/src/theia.ts, tested in theia.spec.ts); the personal Atlas star-map (libs/v9/experience/src/mastery-map.ts, tested in experience.spec.ts); the FSRS knowledge-trace, the active-recall checkpoint, and the flow-channel tuner (libs/v9/mnemosyne-glue/src/*, tested in mnemosyne-glue.spec.ts); and the per-learner IRT difficulty + SM-2 retrieval checkpoint (libs/v9/lesson-explorables/src/*, tested in their *.spec.ts) all exist and assert specific numbers — FSRS initial stabilities pinned to 0.4072 and 15.4722, the Rasch calibration identity asserted to six decimals, the SM-2 1 → 6 → 16 interval ladder. A genuine constant-stub would fail these.

Two honest qualifications. First, there is no rendered V9 star-map screen in the repo. buildPersonalAtlasMap and buildWonderFrontDoor are view-models — the tested data a screen would bind to — but a grep of apps/ finds no V9 surface importing @oshun/v9-experience or @oshun/v9-theia (the StarMapCanvas.tsx that exists belongs to Nyx mobile, a different domain). Treat the star map as a shipped, tested data model whose UI is not yet in-repo. Second, the emotional arc (orchestrateEmotionalArc) is a deterministic keyword-and-position labeler over a lesson's beats, not an affect model — real and predictable, but it classifies text it is given, it does not sense emotion. Where the monolith promises verifiable credentials, that promise is backed: libs/v9/cross-cutting/src/lms-interop.ts ships a real toOpenBadgeCredential emitting an Open Badges 3.0 verifiable credential — but it lives in the cross-cutting interop module, covered by ./governance-and-boundaries.md, not in the mastery glue.

Threads — Theia, the wonder director#

libs/v9/theia/src/theia.ts is the "wonder director." It does three jobs over the real Atlas graph and the learner's mastery state, and it owns no graph store of its own — it reads AtlasStore (@oshun/v9-atlas) and the Map<V9ConceptId, MasteryLevel> the mastery loop produces.

Thread continuation — the next, better question#

nextWonders(atlas, from, max = 3) walks the concept edges leaving a node and returns the questions it opens, in a fixed priority order of edge type:

ts
const THREAD_EDGE_PRIORITY = ['enables', 'bridges', 'specializes', 'related'];

These are four of the nine edge types in the V9ConceptEdgeTypeSchema enum (libs/contracts/src/v9/concept-graph.ts: prerequisite, related, part_of, generalizes, specializes, enables, conflicts, complements, bridges). The order is the editorial claim that an enables edge ("you can now understand…") is a better thread than a merely related one, and that the cross-axis bridges edge — V9's own addition, joining e.g. cosmology to deep-time — is worth surfacing above a same-axis specialization. The function dedups by target and slices to max, and it throws on an unknown source concept rather than returning an empty thread (if (!atlas.hasNode(from)) throw). The theia.spec.ts case proves the ordering: with both an enables edge to "Age of the universe" and a related edge to "Cosmic microwave background", the first returned wonder is the enables target. These same concept ids are what the forge bakes into the artifact's thread.nextWonders (libs/contracts/src/v9/lesson.ts), which the lesson player surfaces as the end-of-lesson thread.

The emotional arc#

orchestrateEmotionalArc(beats) tags each narrative beat with one of four emotions — awe | curiosity | understanding | anticipation — to shape the science↔human arc the product analysis asks for (awe → curiosity → understanding → the next question). It is a transparent rule, not a model: the first beat (or one containing "hook") is awe; a beat mentioning "why it matters" / "connect" is understanding; a beat about "the next question" — or the final beat — is anticipation; everything else is curiosity. The test feeds it a three-beat lesson and asserts awe → understanding → anticipation. Because it is deterministic, the same lesson always arcs the same way, which is what lets the rendered player rely on it; because it is keyword-driven, it is honest about being a labeler over beats Prometheus already wrote.

"Surprise me" — the frontier of what you almost know#

surpriseMe(atlas, masteryMap, max = 1) answers a different question: not "where next from here?" but "what unexplored idea is the learner most ready for?" For every node not yet mastered, it computes a readiness = the fraction of that concept's transitive prerequisites the learner has already mastered, keeps those in (0, 1], and ranks by readiness descending. "Mastered" here is ≥ intermediate on the MASTERY_LEVELS ladder (MASTERED_FLOOR = indexOf('intermediate')), and the prerequisite set comes from the real Mnemosyne closure via atlas.prerequisitesOf — so a "surprise" is never a random concept, it is the highest-readiness star just past the learner's frontier. The test mastering "Light" surfaces "Olbers paradox" at readiness 1.0.

Note one honest duplication: the experience layer's front door has its own frontier suggester, suggestFrontier in libs/v9/experience/src/wonder-front-door.ts, which proposes concepts adjacent to the current teaching set. It is not the same function as surpriseMe (adjacency vs. readiness-ranking); the two serve different entry points (a just-resolved wonder vs. an open "surprise me" button) and are not yet unified.

The personal Atlas — progress as a star map#

buildPersonalAtlasMap(atlas, masteryMap) in libs/v9/experience/src/mastery-map.ts is the "what you know / what's next" view-model. It maps every Atlas node to a StarMapNode with three signals that make "star map, not progress bar" concrete:

  • brightness (0–1) — mastery rendered as light. brightnessOf(level) = (indexOf(level) + 1) / MASTERY_LEVELS.length, so novice0.17, intermediate = 0.5, and master = 1.0; an unreviewed concept is 0 (dark). The six rungs are novice, beginner, intermediate, advanced, expert, master (libs/mnemosyne/core/src/types.ts).
  • exploredtrue for any concept with any mastery entry, even novice. Note the deliberate asymmetry: a concept you have merely touched is "explored" (and faintly bright), but it does not count toward another concept's readiness until it reaches ≥ intermediate.
  • frontier — a still-dark concept whose every prerequisite is already mastered: the inviting next star to light. !explored && prereqs.length > 0 && prereqs.every(isMastered).

The map also rolls up totalCount, exploredCount, and frontierCount. The experience.spec.ts worked example builds a two-node Atlas — "Cosmology basics" → (prerequisite) → "Age of the universe" — masters only the basics, and asserts: basics is explored with brightness === 1; "Age of the universe" is unexplored with brightness === 0 but frontier === true, because its only prerequisite is now mastered. That is the whole product metaphor in one assertion: mastering one star lights the next as an invitation.

The mastery loop — remembering forever#

The mastery loop is where "remember it weeks later" is actually engineered. It has four moving parts, and a subtlety worth stating up front: V9 uses two different spaced-repetition engines for two different jobs.

Engine Where Job
FSRS-v4 libs/v9/mnemosyne-glue/src/knowledge-trace.ts The live mastery loop: stability, the forgetting frontier, and the MasteryLevel fed back to Atlas
SM-2 libs/v9/lesson-explorables/src/retrieval-checkpoint.ts The durable schedule.checkpointRef the forge bakes into the artifact (the G7 gate reads it)

Both are real and tested; they are simply not the same algorithm. The artifact's schedule block (checkpointRef, intervalDays, nextReviewAtIso in libs/contracts/src/v9/lesson.ts) is minted by the SM-2 path at forge time; the re-review-over-days loop below runs on FSRS. If you read only one of the two files you will think V9 "has spaced repetition" and miss that it has two seams.

1. The knowledge trace (FSRS forgetting frontier)#

A taught concept becomes an FSRS card — ConceptTraceCard = { difficulty, stability, state, reps, lapses, lastReviewAt }. reviewConcept(card, grade, now) delegates straight to fsrsReview (libs/mnemosyne/core/src/memory-science.ts) using the published FSRS-v4 default weights (FSRS_DEFAULT_PARAMETERS.w, a 17-element vector with requestRetention: 0.9). conceptRetrievability applies the FSRS forgetting curve R = (1 + elapsedDays / (9·stability))^-1, and atForgettingFrontier(card, now, requestRetention = 0.9) is the scheduling decision: a New card is always due, and a reviewed card is due once its recall probability has decayed to or below the requested retention — recall scheduled for the moment it is hardest-but-possible, which is the testing effect the product leans on. The trace test pins the engine's honesty: stabilities are strictly monotonic in grade (again < hard < good < easy) and equal 0.4072 and 15.4722 for again/easy — exactly w[0] and w[3].

conceptMastery(card) then projects the FSRS state onto a MasteryLevel by stability in days: Newnovice; Learning/Relearningbeginner; in Review, < 7d → beginner, < 30d → intermediate, < 180d → advanced, < 365d → expert, else master. A concept retained for over a year is master; one you saw once is novice. This is the single number every other subsystem on this page reads.

2. The active-recall checkpoint#

applyRetrievalCheckpoint(conceptId, card, recallScore, now) in mastery-feedback.ts is the loop's pump. It insists the checkpoint is active recall — a question the learner answers — not a re-read: a [0,1] recall score maps to an FSRS grade (retrievalScoreToGrade: ≥ 0.95 → easy, ≥ 0.75 → good, ≥ 0.5 → hard, else again; throws outside [0,1]), drives a real FSRS review, and returns the updated card, the new MasteryLevel, the next-review ISO timestamp, and the interval in days. buildAtlasMasteryMap folds a learner's whole set of traces into the Map<V9ConceptId, MasteryLevel> that resolveWonder({ learnerMastery }) consumes — and that is the feedback edge: mastered concepts drop out of the prerequisite frontier, so the next wonder is scoped to what the learner still needs.

3. Adaptive difficulty in the flow channel#

Keeping the next item in Csíkszentmihályi's flow channel — challenge matched to skill, so neither boredom nor anxiety — is done with IRT, not heuristics. flow-channel.ts defines a FlowBand of success probability { lower: 0.6, upper: 0.85 }: flowChannelFor(theta, difficultyLogit) computes the success probability with the real irt2PL and classifies > 0.85 → boredom, < 0.6 → anxiety, else flow. tuneDifficultyToFlow(history) picks the next item's difficulty: it estimates the learner's ability θ by reusing Mnemosyne's Newton-Raphson MLE (estimateAbility, via libs/v9/lesson-explorables/src/adaptive-difficulty.ts computeAdaptiveDifficulty), calibrates an item to a target success probability (default 0.75, which must lie strictly inside the flow band or the function throws), and then verifies the recommended item lands in flow. The calibration is an exact identity, not an approximation: b* = θ − logit(p*), and the adaptive-difficulty test asserts irt1PL(θ, b*) ≈ targetSuccess to six decimal places. A stronger learner gets a harder next item; targeting 90% success yields easier items than targeting 60% — both asserted.

4. The loop closes#

Put together, one lesson's checkpoint updates a trace, the trace projects to mastery, mastery rewrites the Atlas map, and the rewritten map both re-scopes the next wonder (fewer prerequisite gaps) and re-lights the star map (a new frontier star). Difficulty tuning keeps the next item in flow. The same MasteryLevel feeds Theia's surpriseMe so the "next star" is always the readiest unexplored one.

flowchart TD L[Lesson played] --> RC["Active-recall checkpoint<br/>applyRetrievalCheckpoint"] RC -->|"recall score → grade"| FSRS["FSRS review (fsrsReview)<br/>stability · forgetting frontier"] FSRS --> M["conceptMastery → MasteryLevel"] M --> MAP["buildAtlasMasteryMap<br/>Map&lt;conceptId, MasteryLevel&gt;"] MAP --> RW["resolveWonder(learnerMastery)<br/>frontier shrinks"] MAP --> SM["Theia surpriseMe<br/>readiest unexplored star"] MAP --> STAR["buildPersonalAtlasMap<br/>brightness · frontier"] RW --> NEXT[Next lesson scoped] SM --> NEXT HIST["Response history"] --> FLOW["tuneDifficultyToFlow (IRT)<br/>next item in flow band"] FLOW --> NEXT NEXT --> L

How it reaches the screen — the lesson player#

The consumer-facing binding for the back half is libs/v9/experience/src/lesson-player.ts. buildLessonPlayer(artifact) projects a gated V9LessonArtifact into player state and refuses an ungated lessonif (!isV9LessonPublishable(artifact)) throw new UngatedLessonError, proven by the test that flips G2 to failing and expects the throw. Two of its fields belong to this page directly: retrievalPrompt (from assessment.retrievalCheck.prompt) is the active-recall question the checkpoint scores, and nextWonders (from thread.nextWonders) is the thread Theia authored. So the same continuation concepts nextWonders() computes at forge time are the ones the learner taps at the end of the lesson; the same retrievalCheck the gates verified is the question that later pumps the mastery loop.

Edge cases, failure modes, and configuration#

  • Fail-loud, never fabricate. nextWonders/surpriseMe/prerequisitesOf throw on an unknown concept id; retrievalScoreToGrade throws outside [0,1]; tuneDifficultyToFlow throws when the target success sits outside the flow band; computeAdaptiveDifficulty rejects a target success ≤ 0 or ≥ 1. None of these returns a plausible-but-invented value.
  • Cold start. A brand-new concept card is New, novice, and always at the forgetting frontier (atForgettingFrontier returns true for New). An empty response history is a warm start: computeAdaptiveDifficulty([], { priorTheta: 0 }) returns θ = 0 and an adaptive difficulty of σ(−logit(0.8)) ≈ 0.2.
  • The "explored but not mastering" gap. Because explored triggers on any level but isMastered requires ≥ intermediate, a faintly-lit novice concept will not unlock its dependents as frontier stars. This is intended — touching a concept is not the same as being able to build on it — but it is a real behavior to know when reading the map.
  • Two clocks, supplied not invented. Every scheduler takes nowUnixMs from the caller (reviewConcept, applyRetrievalCheckpoint, buildRetrievalCheckpoint), so next-review dates are deterministic and testable against a fixed clock — the SM-2 test pins checkpointRef to an exact string for a fixed NOW.
  • Tunable knobs. The flow band { 0.6, 0.85 } and target success 0.75 (flow) / 0.8 (calibration) are defaults you can override per call; the FSRS requestRetention (0.9) governs how aggressively reviews are scheduled; the MASTERED_FLOOR of intermediate governs frontier/readiness, and resolveWonder's masteryFloor option (default intermediate) governs which prerequisites count as cleared.

How this connects to the rest of V9#

  • Atlas & wonder resolution. The mastery map is the input to resolveWonder's mastery-filtered prerequisite frontier, and prerequisitesOf delegates to the Mnemosyne KnowledgeGraph.getPrerequisites transitive closure over prerequisite edges only. See ./atlas-wonder-resolution.md.
  • Prometheus. The forge writes thread.nextWonders, assessment.masterySignal, and the SM-2 schedule.checkpointRef into the artifact this page's runtime then re-reads. See ./prometheus-lesson-forge.md and the gate mechanics in ./subsystem-map-and-gates.md.
  • Chiron & Hephaestus. The emotional arc and thread sit on top of Chiron's teacher persona and Hephaestus's explorables; Chiron's cross-session memory opens on what the learner struggled with last time, reading the same mastery signal. See ./chiron-and-hephaestus.md.
  • The braid & the commons. Theia is also the orchestrator of the science↔human braid, and the "surprise me" / thread objects become shareable in Agora. See ./braid-commons-and-films.md.
  • Governance. The free-tier quota that decides whether the next lesson is even served (checkBillingAccess) and the Open Badges credential that attests mastery both live in the governance/cross-cutting surface. See ./governance-and-boundaries.md.